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:
syuilo
2022-07-02 15:12:11 +09:00
committed by GitHub
parent ded0f6f0df
commit eccc90c843
29 changed files with 371 additions and 73 deletions

View File

@ -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();

View File

@ -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',

View File

@ -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();
});

View File

@ -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>

View File

@ -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();