feat: Log user ips (#8872)
* wip * store ip and headers * Update admin-file.vue * require admin for view ip/headers * IP (recent) 消した * admin必須 * opt in * clean ips periodically * respect logging setting in drive/files/create
This commit is contained in:
		| @@ -17,6 +17,7 @@ const accountData = localStorage.getItem('account'); | ||||
| export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null; | ||||
|  | ||||
| export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator); | ||||
| export const iAmAdmin = $i != null && $i.isAdmin; | ||||
|  | ||||
| export async function signout() { | ||||
| 	waiting(); | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <template> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer v-if="file" :content-max="500" :margin-min="16" :margin-max="32"> | ||||
| 	<MkSpacer v-if="file" :content-max="600" :margin-min="16" :margin-max="32"> | ||||
| 		<div v-if="tab === 'overview'" class="cxqhhsmd _formRoot"> | ||||
| 			<a class="_formBlock thumbnail" :href="file.url" target="_blank"> | ||||
| 				<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> | ||||
| @@ -39,6 +39,20 @@ | ||||
| 				<MkButton danger @click="del"><i class="fas fa-trash-alt"></i> {{ i18n.ts.delete }}</MkButton> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 		<div v-else-if="tab === 'ip' && info" class="_formRoot"> | ||||
| 			<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo> | ||||
| 			<MkKeyValue v-if="info.requestIp" class="_formBlock _monospace" :copy="info.requestIp" oneline> | ||||
| 				<template #key>IP</template> | ||||
| 				<template #value>{{ info.requestIp }}</template> | ||||
| 			</MkKeyValue> | ||||
| 			<FormSection v-if="info.requestHeaders"> | ||||
| 				<template #label>Headers</template> | ||||
| 				<MkKeyValue v-for="(v, k) in info.requestHeaders" :key="k" class="_formBlock _monospace"> | ||||
| 					<template #key>{{ k }}</template> | ||||
| 					<template #value>{{ v }}</template> | ||||
| 				</MkKeyValue> | ||||
| 			</FormSection> | ||||
| 		</div> | ||||
| 		<div v-else-if="tab === 'raw'" class="_formRoot"> | ||||
| 			<MkObjectView v-if="info" tall :value="info"> | ||||
| 			</MkObjectView> | ||||
| @@ -54,13 +68,15 @@ import MkSwitch from '@/components/form/switch.vue'; | ||||
| import MkObjectView from '@/components/object-view.vue'; | ||||
| import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue'; | ||||
| import MkKeyValue from '@/components/key-value.vue'; | ||||
| import FormLink from '@/components/form/link.vue'; | ||||
| import FormSection from '@/components/form/section.vue'; | ||||
| import MkUserCardMini from '@/components/user-card-mini.vue'; | ||||
| import MkInfo from '@/components/ui/info.vue'; | ||||
| import bytes from '@/filters/bytes'; | ||||
| import * as os from '@/os'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import { acct } from '@/filters/user'; | ||||
| import { iAmAdmin, iAmModerator } from '@/account'; | ||||
|  | ||||
| let tab = $ref('overview'); | ||||
| let file: any = $ref(null); | ||||
| @@ -108,7 +124,11 @@ const headerTabs = $computed(() => [{ | ||||
| 	key: 'overview', | ||||
| 	title: i18n.ts.overview, | ||||
| 	icon: 'fas fa-info-circle', | ||||
| }, { | ||||
| }, iAmModerator ? { | ||||
| 	key: 'ip', | ||||
| 	title: 'IP', | ||||
| 	icon: 'fas fa-bars-staggered', | ||||
| } : null, { | ||||
| 	key: 'raw', | ||||
| 	title: 'Raw data', | ||||
| 	icon: 'fas fa-code', | ||||
|   | ||||
| @@ -14,6 +14,18 @@ | ||||
| 					<XBotProtection/> | ||||
| 				</FormFolder> | ||||
|  | ||||
| 				<FormFolder class="_formBlock"> | ||||
| 					<template #label>Log IP address</template> | ||||
| 					<template v-if="enableIpLogging" #suffix>Enabled</template> | ||||
| 					<template v-else #suffix>Disabled</template> | ||||
|  | ||||
| 					<div class="_formRoot"> | ||||
| 						<FormSwitch v-model="enableIpLogging" class="_formBlock" @update:modelValue="save"> | ||||
| 							<template #label>Enable</template> | ||||
| 						</FormSwitch> | ||||
| 					</div> | ||||
| 				</FormFolder> | ||||
|  | ||||
| 				<FormFolder class="_formBlock"> | ||||
| 					<template #label>Summaly Proxy</template> | ||||
|  | ||||
| @@ -51,17 +63,20 @@ import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| let summalyProxy: string = $ref(''); | ||||
| let enableHcaptcha: boolean = $ref(false); | ||||
| let enableRecaptcha: boolean = $ref(false); | ||||
| let enableIpLogging: boolean = $ref(false); | ||||
|  | ||||
| async function init() { | ||||
| 	const meta = await os.api('admin/meta'); | ||||
| 	summalyProxy = meta.summalyProxy; | ||||
| 	enableHcaptcha = meta.enableHcaptcha; | ||||
| 	enableRecaptcha = meta.enableRecaptcha; | ||||
| 	enableIpLogging = meta.enableIpLogging; | ||||
| } | ||||
|  | ||||
| function save() { | ||||
| 	os.apiWithDialog('admin/update-meta', { | ||||
| 		summalyProxy, | ||||
| 		enableIpLogging, | ||||
| 	}).then(() => { | ||||
| 		fetchInstance(); | ||||
| 	}); | ||||
|   | ||||
| @@ -27,6 +27,12 @@ | ||||
| 						<template #key>ID</template> | ||||
| 						<template #value><span class="_monospace">{{ user.id }}</span></template> | ||||
| 					</MkKeyValue> | ||||
| 					<!-- 要る? | ||||
| 					<MkKeyValue v-if="ips.length > 0" :copy="user.id" oneline style="margin: 1em 0;"> | ||||
| 						<template #key>IP (recent)</template> | ||||
| 						<template #value><span class="_monospace">{{ ips[0].ip }}</span></template> | ||||
| 					</MkKeyValue> | ||||
| 					--> | ||||
| 					<MkKeyValue oneline style="margin: 1em 0;"> | ||||
| 						<template #key>{{ i18n.ts.createdAt }}</template> | ||||
| 						<template #value><span class="_monospace"><MkTime :time="user.createdAt" :mode="'detail'"/></span></template> | ||||
| @@ -92,8 +98,18 @@ | ||||
| 			<div v-else-if="tab === 'files'" class="_formRoot"> | ||||
| 				<MkFileListForAdmin :pagination="filesPagination" view-mode="grid"/> | ||||
| 			</div> | ||||
| 			<div v-else-if="tab === 'ip'" class="_formRoot"> | ||||
| 				<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo> | ||||
| 				<MkInfo v-else>The date is the IP address was first acknowledged.</MkInfo> | ||||
| 				<template v-if="iAmAdmin && ips"> | ||||
| 					<div v-for="record in ips" :key="record.ip" class="_monospace" :class="$style.ip" style="margin: 1em 0;"> | ||||
| 						<span class="date">{{ record.createdAt }}</span> | ||||
| 						<span class="ip">{{ record.ip }}</span> | ||||
| 					</div> | ||||
| 				</template> | ||||
| 			</div> | ||||
| 			<div v-else-if="tab === 'ap'" class="_formRoot"> | ||||
| 				<MkObjectView v-if="ap" tall :value="user"> | ||||
| 				<MkObjectView v-if="ap" tall :value="ap"> | ||||
| 				</MkObjectView> | ||||
| 			</div> | ||||
| 			<div v-else-if="tab === 'raw'" class="_formRoot"> | ||||
| @@ -122,6 +138,7 @@ import MkKeyValue from '@/components/key-value.vue'; | ||||
| import MkSelect from '@/components/form/select.vue'; | ||||
| import FormSuspense from '@/components/form/suspense.vue'; | ||||
| import MkFileListForAdmin from '@/components/file-list-for-admin.vue'; | ||||
| import MkInfo from '@/components/ui/info.vue'; | ||||
| import * as os from '@/os'; | ||||
| import number from '@/filters/number'; | ||||
| import bytes from '@/filters/bytes'; | ||||
| @@ -129,7 +146,7 @@ import { url } from '@/config'; | ||||
| import { userPage, acct } from '@/filters/user'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { iAmModerator } from '@/account'; | ||||
| import { iAmAdmin, iAmModerator } from '@/account'; | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	userId: string; | ||||
| @@ -140,6 +157,7 @@ let chartSrc = $ref('per-user-notes'); | ||||
| let user = $ref<null | misskey.entities.UserDetailed>(); | ||||
| let init = $ref(); | ||||
| let info = $ref(); | ||||
| let ips = $ref(null); | ||||
| let ap = $ref(null); | ||||
| let moderator = $ref(false); | ||||
| let silenced = $ref(false); | ||||
| @@ -158,9 +176,12 @@ function createFetcher() { | ||||
| 			userId: props.userId, | ||||
| 		}), os.api('admin/show-user', { | ||||
| 			userId: props.userId, | ||||
| 		})]).then(([_user, _info]) => { | ||||
| 		}), iAmAdmin ? os.api('admin/get-user-ips', { | ||||
| 			userId: props.userId, | ||||
| 		}) : Promise.resolve(null)]).then(([_user, _info, _ips]) => { | ||||
| 			user = _user; | ||||
| 			info = _info; | ||||
| 			ips = _ips; | ||||
| 			moderator = info.isModerator; | ||||
| 			silenced = info.isSilenced; | ||||
| 			suspended = info.isSuspended; | ||||
| @@ -300,7 +321,11 @@ const headerTabs = $computed(() => [{ | ||||
| 	key: 'ap', | ||||
| 	title: 'AP', | ||||
| 	icon: 'fas fa-share-alt', | ||||
| }, { | ||||
| }, iAmModerator ? { | ||||
| 	key: 'ip', | ||||
| 	title: 'IP', | ||||
| 	icon: 'fas fa-bars-staggered', | ||||
| } : null, { | ||||
| 	key: 'raw', | ||||
| 	title: 'Raw', | ||||
| 	icon: 'fas fa-code', | ||||
| @@ -362,3 +387,17 @@ definePageMetadata(computed(() => ({ | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | ||||
| <style lang="scss" module> | ||||
| .ip { | ||||
| 	display: flex; | ||||
|  | ||||
| 	> :global(.date) { | ||||
| 		opacity: 0.7; | ||||
| 	} | ||||
|  | ||||
| 	> :global(.ip) { | ||||
| 		margin-left: auto; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| import { reactive, ref } from 'vue'; | ||||
| import * as Misskey from 'misskey-js'; | ||||
| import { readAndCompressImage } from 'browser-image-resizer'; | ||||
| import { defaultStore } from '@/store'; | ||||
| import { apiUrl } from '@/config'; | ||||
| import * as Misskey from 'misskey-js'; | ||||
| import { $i } from '@/account'; | ||||
| import { readAndCompressImage } from 'browser-image-resizer'; | ||||
| import { alert } from '@/os'; | ||||
|  | ||||
| type Uploading = { | ||||
| @@ -31,7 +31,7 @@ export function uploadFile( | ||||
| 	file: File, | ||||
| 	folder?: any, | ||||
| 	name?: string, | ||||
| 	keepOriginal: boolean = defaultStore.state.keepOriginalUploading | ||||
| 	keepOriginal: boolean = defaultStore.state.keepOriginalUploading, | ||||
| ): Promise<Misskey.entities.DriveFile> { | ||||
| 	if (folder && typeof folder === 'object') folder = folder.id; | ||||
|  | ||||
| @@ -45,7 +45,7 @@ export function uploadFile( | ||||
| 				name: name || file.name || 'untitled', | ||||
| 				progressMax: undefined, | ||||
| 				progressValue: undefined, | ||||
| 				img: window.URL.createObjectURL(file) | ||||
| 				img: window.URL.createObjectURL(file), | ||||
| 			}); | ||||
|  | ||||
| 			uploads.value.push(ctx); | ||||
| @@ -86,7 +86,7 @@ export function uploadFile( | ||||
| 					alert({ | ||||
| 						type: 'error', | ||||
| 						title: 'Failed to upload', | ||||
| 						text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}` | ||||
| 						text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`, | ||||
| 					}); | ||||
|  | ||||
| 					reject(); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 syuilo
					syuilo