From fbcbb803acfc56a601b928594832fba425b04ce6 Mon Sep 17 00:00:00 2001 From: Ap_Tx <3045929398@qq.com> Date: Sun, 18 Oct 2026 11:18:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A1=A5=E5=85=A8=E7=9A=AE=E8=82=A4?= =?UTF-8?q?=E7=AB=99=E7=AE=A1=E7=90=86=E5=8F=B0=E7=94=A8=E6=88=B7=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 用户列表:搜索(用户名/邮箱/UUID)、分页、封禁状态、注册时间 - 编辑用户:用户名、邮箱、QQ、密码、权限组、封禁状态 - 绑定玩家:展示 Mojang / StarLight 来源与旧内服适配标记 - Bot 管理:查看状态、修改机器人用户名、使 Token 失效、封禁/解封 Bot 创建 - 频率限制:滑块与数字输入设置丢包频率,支持保存与取消 - 管理审计:按用户或全局查看,游标分页加载更早记录 - 邀请码:按邮箱生成邀请码并复制 - 补充对应 API 与类型定义,中英文各补 39 个文案 --- .../ui/skin-site/admin/AdminUsersPage.vue | 1305 ++++++++++++++--- .../app-frontend/src/helpers/skin-site-api.ts | 113 ++ .../app-frontend/src/locales/en-US/index.json | 41 +- .../app-frontend/src/locales/zh-CN/index.json | 41 +- 4 files changed, 1257 insertions(+), 243 deletions(-) diff --git a/apps/app-frontend/src/components/ui/skin-site/admin/AdminUsersPage.vue b/apps/app-frontend/src/components/ui/skin-site/admin/AdminUsersPage.vue index 3cf833a..bf28e4e 100644 --- a/apps/app-frontend/src/components/ui/skin-site/admin/AdminUsersPage.vue +++ b/apps/app-frontend/src/components/ui/skin-site/admin/AdminUsersPage.vue @@ -4,11 +4,22 @@ import { defineMessages, injectNotificationManager, useVIntl } from '@modrinth/u import { computed, onMounted, ref } from 'vue' import { - skinSiteAdminDeleteUser, type SkinSiteAdminPlayer, - skinSiteAdminUpdateUser, type SkinSiteAdminUser, + type SkinSiteBotInfo, + type SkinSiteUserAdminAuditInfo, + type SkinSiteUserFrequencyLimit, + skinSiteAdminBotInfo, + skinSiteAdminDeleteUser, + skinSiteAdminInvalidateBotToken, + skinSiteAdminInviteUser, + skinSiteAdminRenameBot, + skinSiteAdminSetBotCreationBanned, + skinSiteAdminUpdateUser, + skinSiteAdminUpdateUserFrequencyLimit, + skinSiteAdminUserAudit, skinSiteAdminUserCount, + skinSiteAdminUserFrequencyLimit, skinSiteAdminUserList, skinSiteAdminUserPlayers, } from '@/helpers/skin-site-api' @@ -36,10 +47,6 @@ const messages = defineMessages({ players: { id: 'app.skin-site.admin.users.players', defaultMessage: 'Players' }, updated: { id: 'app.skin-site.admin.users.updated', defaultMessage: 'User updated.' }, deleted: { id: 'app.skin-site.admin.users.deleted', defaultMessage: 'User deleted.' }, - confirmDelete: { - id: 'app.skin-site.admin.users.confirm-delete', - defaultMessage: 'Delete user “{name}”? This cannot be undone.', - }, username: { id: 'app.skin-site.admin.users.username', defaultMessage: 'Username' }, email: { id: 'app.skin-site.admin.users.email', defaultMessage: 'Email' }, qq: { id: 'app.skin-site.admin.users.qq', defaultMessage: 'QQ' }, @@ -56,6 +63,108 @@ const messages = defineMessages({ }, noPlayers: { id: 'app.skin-site.admin.users.no-players', defaultMessage: 'No linked players.' }, registered: { id: 'app.skin-site.admin.users.registered', defaultMessage: 'Registered' }, + bot: { id: 'app.skin-site.admin.users.bot', defaultMessage: 'Bot' }, + botTitle: { id: 'app.skin-site.admin.users.bot-title', defaultMessage: 'Bot management' }, + botExists: { id: 'app.skin-site.admin.users.bot-exists', defaultMessage: 'Bot created' }, + botNotExists: { id: 'app.skin-site.admin.users.bot-not-exists', defaultMessage: 'No bot yet' }, + botActive: { id: 'app.skin-site.admin.users.bot-active', defaultMessage: 'Token valid' }, + botInactive: { + id: 'app.skin-site.admin.users.bot-inactive', + defaultMessage: 'Token invalidated', + }, + botCreationBanned: { + id: 'app.skin-site.admin.users.bot-creation-banned', + defaultMessage: 'Token creation banned', + }, + botUsername: { + id: 'app.skin-site.admin.users.bot-username', + defaultMessage: 'Bot username', + }, + botSaveUsername: { + id: 'app.skin-site.admin.users.bot-save-username', + defaultMessage: 'Save bot username', + }, + botTokenHint: { + id: 'app.skin-site.admin.users.bot-token-hint', + defaultMessage: 'Token hint', + }, + botInvalidate: { + id: 'app.skin-site.admin.users.bot-invalidate', + defaultMessage: 'Invalidate token', + }, + botBan: { id: 'app.skin-site.admin.users.bot-ban', defaultMessage: 'Ban bot creation' }, + botUnban: { id: 'app.skin-site.admin.users.bot-unban', defaultMessage: 'Unban bot creation' }, + frequency: { id: 'app.skin-site.admin.users.frequency', defaultMessage: 'Rate limit' }, + frequencyTitle: { + id: 'app.skin-site.admin.users.frequency-title', + defaultMessage: 'Rate limit', + }, + frequencyHint: { + id: 'app.skin-site.admin.users.frequency-hint', + defaultMessage: 'Packets from this user will be dropped at the given rate.', + }, + frequencyNone: { + id: 'app.skin-site.admin.users.frequency-none', + defaultMessage: 'Not enabled', + }, + frequencyCurrent: { + id: 'app.skin-site.admin.users.frequency-current', + defaultMessage: 'Currently {percent}%', + }, + frequencyRange: { + id: 'app.skin-site.admin.users.frequency-range', + defaultMessage: 'Only integer values from 10% to 100% are allowed.', + }, + frequencySave: { + id: 'app.skin-site.admin.users.frequency-save', + defaultMessage: 'Save limit', + }, + frequencyClear: { + id: 'app.skin-site.admin.users.frequency-clear', + defaultMessage: 'Clear limit', + }, + audit: { id: 'app.skin-site.admin.users.audit', defaultMessage: 'Audit' }, + auditTitle: { + id: 'app.skin-site.admin.users.audit-title', + defaultMessage: 'User management audit', + }, + auditEmpty: { + id: 'app.skin-site.admin.users.audit-empty', + defaultMessage: 'No audit records yet.', + }, + auditMore: { id: 'app.skin-site.admin.users.audit-more', defaultMessage: 'Load earlier' }, + invite: { id: 'app.skin-site.admin.users.invite', defaultMessage: 'Invite user' }, + inviteTitle: { id: 'app.skin-site.admin.users.invite-title', defaultMessage: 'Invite user' }, + inviteEmail: { id: 'app.skin-site.admin.users.invite-email', defaultMessage: 'Email' }, + inviteGenerate: { + id: 'app.skin-site.admin.users.invite-generate', + defaultMessage: 'Generate invite code', + }, + inviteCopy: { id: 'app.skin-site.admin.users.invite-copy', defaultMessage: 'Copy' }, + inviteCopied: { + id: 'app.skin-site.admin.users.invite-copied', + defaultMessage: 'Invite code copied.', + }, + inviteEmailInvalid: { + id: 'app.skin-site.admin.users.invite-email-invalid', + defaultMessage: 'Enter a valid email address.', + }, + boundPlayersTitle: { + id: 'app.skin-site.admin.users.bound-players-title', + defaultMessage: 'Linked players', + }, + legacyAdapted: { + id: 'app.skin-site.admin.users.legacy-adapted', + defaultMessage: 'Legacy internal adapted', + }, + registeredAt: { + id: 'app.skin-site.admin.users.registered-at', + defaultMessage: 'Registered at', + }, + status: { id: 'app.skin-site.admin.users.status', defaultMessage: 'Status' }, + statusNormal: { id: 'app.skin-site.admin.users.status-normal', defaultMessage: 'Normal' }, + statusBanned: { id: 'app.skin-site.admin.users.status-banned', defaultMessage: 'Banned' }, + actions: { id: 'app.skin-site.admin.users.actions', defaultMessage: 'Actions' }, }) const PAGE_SIZE = 20 @@ -83,26 +192,283 @@ const deletingUuid = ref(null) const playersFor = ref(null) const playersLoading = ref(false) -const playersUuid = ref(null) +const playersTarget = ref(null) const page = computed(() => Math.floor(offset.value / PAGE_SIZE) + 1) const pageCount = computed(() => Math.max(1, Math.ceil(total.value / PAGE_SIZE))) const hasPrev = computed(() => offset.value > 0) const hasNext = computed(() => offset.value + PAGE_SIZE < total.value) -const SEARCH_FIELDS: Array<'username' | 'email' | 'uuid'> = ['username', 'email', 'uuid'] +// ---------- bot ---------- +const botTarget = ref(null) +const botInfo = ref(null) +const botLoading = ref(false) +const botBusy = ref(false) +const botUsername = ref('') +const validBotUsername = computed(() => { + const name = botUsername.value.trim() + return name.length >= 2 && name.length <= 32 +}) -function searchFieldLabel(field: string): string { - if (field === 'email') return formatMessage(messages.searchEmail) - if (field === 'uuid') return formatMessage(messages.searchUuid) - return formatMessage(messages.searchName) +function openBot(user: SkinSiteAdminUser) { + botTarget.value = user + botInfo.value = null + botUsername.value = '' + botLoading.value = true + skinSiteAdminBotInfo(user.uuid) + .then((res) => { + if (!res.success) throw new Error(res.errorMessage) + botInfo.value = res.payload + botUsername.value = res.payload.username ?? '' + }) + .catch(handleError) + .finally(() => { + botLoading.value = false + }) } -function formatDate(ts: number): string { +function closeBot() { + botTarget.value = null + botInfo.value = null +} + +async function saveBotUsername() { + if (!botTarget.value || !validBotUsername.value) return + botBusy.value = true try { - return new Date(ts).toLocaleDateString() + const res = await skinSiteAdminRenameBot(botTarget.value.uuid, botUsername.value.trim()) + if (!res.success) throw new Error(res.errorMessage) + botInfo.value = res.payload + botUsername.value = res.payload.username ?? '' + } catch (error) { + handleError(error) + } finally { + botBusy.value = false + } +} + +async function invalidateBot() { + if (!botTarget.value) return + botBusy.value = true + try { + const res = await skinSiteAdminInvalidateBotToken(botTarget.value.uuid) + if (!res.success) throw new Error(res.errorMessage) + botInfo.value = res.payload + } catch (error) { + handleError(error) + } finally { + botBusy.value = false + } +} + +async function toggleBotCreationBan() { + if (!botTarget.value || !botInfo.value) return + botBusy.value = true + try { + const res = await skinSiteAdminSetBotCreationBanned( + botTarget.value.uuid, + !botInfo.value.creationBanned, + ) + if (!res.success) throw new Error(res.errorMessage) + botInfo.value = res.payload + } catch (error) { + handleError(error) + } finally { + botBusy.value = false + } +} + +// ---------- frequency ---------- +const frequencyTarget = ref(null) +const frequencyLoading = ref(false) +const frequencyBusy = ref(false) +const frequencyCurrent = ref(null) +const frequencyPercent = ref(50) + +const validFrequency = computed( + () => + Number.isInteger(frequencyPercent.value) && + frequencyPercent.value >= 10 && + frequencyPercent.value <= 100, +) +const currentFrequencyLimit = computed(() => frequencyCurrent.value?.dropPercent ?? null) + +function openFrequency(user: SkinSiteAdminUser) { + frequencyTarget.value = user + frequencyCurrent.value = null + frequencyPercent.value = 50 + frequencyLoading.value = true + skinSiteAdminUserFrequencyLimit(user.uuid) + .then((res) => { + if (!res.success) throw new Error(res.errorMessage) + frequencyCurrent.value = res.payload + frequencyPercent.value = res.payload.dropPercent ?? 50 + }) + .catch(handleError) + .finally(() => { + frequencyLoading.value = false + }) +} + +function closeFrequency() { + frequencyTarget.value = null + frequencyCurrent.value = null +} + +async function saveFrequency() { + if (!frequencyTarget.value || !validFrequency.value) return + frequencyBusy.value = true + try { + const res = await skinSiteAdminUpdateUserFrequencyLimit( + frequencyTarget.value.uuid, + frequencyPercent.value, + ) + if (!res.success) throw new Error(res.errorMessage) + frequencyCurrent.value = res.payload + } catch (error) { + handleError(error) + } finally { + frequencyBusy.value = false + } +} + +async function clearFrequency() { + if (!frequencyTarget.value) return + frequencyBusy.value = true + try { + const res = await skinSiteAdminUpdateUserFrequencyLimit(frequencyTarget.value.uuid, null) + if (!res.success) throw new Error(res.errorMessage) + frequencyCurrent.value = res.payload + } catch (error) { + handleError(error) + } finally { + frequencyBusy.value = false + } +} + +// ---------- audit ---------- +const auditTarget = ref(null) +const auditItems = ref([]) +const auditLoading = ref(false) +const auditNextCursor = ref(null) + +const AUDIT_ACTION_LABELS: Record = { + USER_UPDATED: 'user-updated', + USER_DELETED: 'user-deleted', + RATE_LIMIT_UPDATED: 'rate-limit-updated', + BOT_USERNAME_UPDATED: 'bot-username-updated', + BOT_TOKEN_INVALIDATED: 'bot-token-invalidated', + BOT_CREATION_BANNED: 'bot-creation-banned', + BOT_CREATION_UNBANNED: 'bot-creation-unbanned', +} + +function auditActionLabel(action: string) { + return formatMessage({ + id: 'app.skin-site.admin.users.audit-action.' + (AUDIT_ACTION_LABELS[action] ?? 'unknown'), + defaultMessage: action, + }) +} + +function auditFieldLabel(field: string) { + return formatMessage({ + id: 'app.skin-site.admin.users.audit-field.' + field, + defaultMessage: field, + }) +} + +function auditValue(value?: string | null) { + if (value === null || value === undefined || value === '') return '—' + return value +} + +async function loadAudit(reset = false) { + auditLoading.value = true + if (reset) { + auditItems.value = [] + auditNextCursor.value = null + } + try { + const res = await skinSiteAdminUserAudit({ + targetUuid: auditTarget.value?.uuid, + before: auditNextCursor.value ?? undefined, + }) + if (!res.success) throw new Error(res.errorMessage) + auditItems.value = reset ? res.payload.items : [...auditItems.value, ...res.payload.items] + auditNextCursor.value = res.payload.nextCursor ?? null + } catch (error) { + handleError(error) + } finally { + auditLoading.value = false + } +} + +function openAudit(user?: SkinSiteAdminUser) { + auditTarget.value = user ?? null + void loadAudit(true) +} + +function closeAudit() { + auditTarget.value = null + auditItems.value = [] + auditNextCursor.value = null +} + +function loadMoreAudit() { + if (auditLoading.value) return + void loadAudit(false) +} + +// ---------- invite ---------- +const inviteOpen = ref(false) +const inviteEmail = ref('') +const inviteCode = ref('') +const inviteBusy = ref(false) +const inviteEmailValid = computed(() => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(inviteEmail.value)) + +function openInvite() { + inviteOpen.value = true + inviteEmail.value = '' + inviteCode.value = '' +} + +function closeInvite() { + inviteOpen.value = false +} + +async function generateInvite() { + if (!inviteEmailValid.value) { + addNotification({ type: 'error', title: formatMessage(messages.inviteEmailInvalid) }) + return + } + inviteBusy.value = true + try { + const res = await skinSiteAdminInviteUser(inviteEmail.value.trim()) + if (!res.success) throw new Error(res.errorMessage) + inviteCode.value = res.payload.code + } catch (error) { + handleError(error) + } finally { + inviteBusy.value = false + } +} + +async function copyInvite() { + if (!inviteCode.value) return + try { + await navigator.clipboard.writeText(inviteCode.value) + addNotification({ type: 'success', title: formatMessage(messages.inviteCopied) }) + } catch (error) { + handleError(error) + } +} + +// ---------- list ---------- +function formatDateTime(ts?: number | null): string { + if (!ts) return '—' + try { + return new Date(ts).toLocaleString() } catch { - return '' + return '—' } } @@ -111,21 +477,19 @@ async function load(reset = false) { loading.value = true failed.value = false try { - const kw = keyword.value.trim() - const listResult = await skinSiteAdminUserList({ + const listRes = await skinSiteAdminUserList({ + keyword: keyword.value.trim() || undefined, + searchField: searchField.value, limit: PAGE_SIZE, offset: offset.value, - searchField: kw ? searchField.value : undefined, - keyword: kw || undefined, }) - if (!listResult.success) throw new Error(listResult.errorMessage) - items.value = listResult.payload ?? [] - const countResult = await skinSiteAdminUserCount( - kw ? { searchField: searchField.value, keyword: kw } : undefined, - ) - if (countResult.success && typeof countResult.payload === 'number') { - total.value = countResult.payload - } + if (!listRes.success) throw new Error(listRes.errorMessage) + items.value = listRes.payload ?? [] + const countRes = await skinSiteAdminUserCount({ + keyword: keyword.value.trim() || undefined, + searchField: searchField.value, + }) + if (countRes.success && typeof countRes.payload === 'number') total.value = countRes.payload } catch (error) { failed.value = true handleError(error) @@ -134,6 +498,10 @@ async function load(reset = false) { } } +function search() { + void load(true) +} + async function prevPage() { if (!hasPrev.value) return offset.value = Math.max(0, offset.value - PAGE_SIZE) @@ -151,46 +519,38 @@ function startEdit(user: SkinSiteAdminUser) { editForm.value = { username: user.username, email: user.email, - qq: user.qq === undefined || user.qq === null ? '' : String(user.qq), + qq: user.qq !== null && user.qq !== undefined ? String(user.qq) : '', password: '', banned: user.banned, - permissions: user.permissions.join(', '), + permissions: (user.permissions ?? []).join(', '), } } function cancelEdit() { editingUuid.value = null - editForm.value = { - username: '', - email: '', - qq: '', - password: '', - banned: false, - permissions: '', - } } async function saveEdit() { if (!editingUuid.value || saving.value) return saving.value = true try { - const qqText = editForm.value.qq.trim() - const permissions = editForm.value.permissions - .split(',') - .map((p) => p.trim()) - .filter(Boolean) - const result = await skinSiteAdminUpdateUser({ - uuid: editingUuid.value, + const target = items.value.find((u) => u.uuid === editingUuid.value) + if (!target) throw new Error('user not found') + const res = await skinSiteAdminUpdateUser({ + uuid: target.uuid, username: editForm.value.username.trim(), email: editForm.value.email.trim(), - qq: qqText ? Number(qqText) : undefined, - password: editForm.value.password ? editForm.value.password : undefined, + password: editForm.value.password.trim() || undefined, + qq: editForm.value.qq.trim() ? Number(editForm.value.qq.trim()) : undefined, banned: editForm.value.banned, - permissions, + permissions: editForm.value.permissions + .split(',') + .map((p) => p.trim()) + .filter(Boolean), }) - if (!result.success) throw new Error(result.errorMessage) + if (!res.success) throw new Error(res.errorMessage) addNotification({ type: 'success', title: formatMessage(messages.updated) }) - cancelEdit() + editingUuid.value = null await load() } catch (error) { handleError(error) @@ -199,13 +559,13 @@ async function saveEdit() { } } -async function remove(user: SkinSiteAdminUser) { - if (deletingUuid.value !== null) return +async function removeUser(user: SkinSiteAdminUser) { + if (deletingUuid.value) return if (!window.confirm(formatMessage(messages.confirmDelete, { name: user.username }))) return deletingUuid.value = user.uuid try { - const result = await skinSiteAdminDeleteUser(user.uuid) - if (!result.success) throw new Error(result.errorMessage) + const res = await skinSiteAdminDeleteUser(user.uuid) + if (!res.success) throw new Error(res.errorMessage) addNotification({ type: 'success', title: formatMessage(messages.deleted) }) await load() } catch (error) { @@ -215,237 +575,194 @@ async function remove(user: SkinSiteAdminUser) { } } -async function togglePlayers(user: SkinSiteAdminUser) { - if (playersUuid.value === user.uuid) { - playersUuid.value = null - playersFor.value = null - return - } - playersUuid.value = user.uuid - playersLoading.value = true +async function openPlayers(user: SkinSiteAdminUser) { + playersTarget.value = user playersFor.value = null + playersLoading.value = true try { - const result = await skinSiteAdminUserPlayers(user.uuid) - if (result.success) { - playersFor.value = result.payload ?? [] - } else { - playersFor.value = [] - } + const res = await skinSiteAdminUserPlayers(user.uuid) + if (!res.success) throw new Error(res.errorMessage) + playersFor.value = res.payload ?? [] } catch (error) { - playersFor.value = [] handleError(error) } finally { playersLoading.value = false } } +function closePlayers() { + playersTarget.value = null + playersFor.value = null +} + onMounted(() => void load()) + + + +
+
+

{{ formatMessage(messages.edit) }}

+ + + + + + +
+ + +
+
+
+ + +
+
+

+ {{ formatMessage(messages.boundPlayersTitle) }} · {{ playersTarget.username }} +

+
+ + {{ formatMessage(messages.loading) }} +
+

+ {{ formatMessage(messages.noPlayers) }} +

+
+
+
+ {{ player.name }} + + {{ player.isMojang ? 'Mojang' : 'StarLight' }} + +
+

{{ player.uuid }}

+

+ {{ formatMessage(messages.legacyAdapted) }} +

+
+
+
+ +
+
+
+ + +
+
+

+ {{ formatMessage(messages.botTitle) }} · {{ botTarget.username }} +

+
+

{{ botTarget.username }}

+

{{ botTarget.uuid }}

+
+
+ + {{ formatMessage(messages.loading) }} +
+ +
+ +
+
+
+ + +
+
+

+ {{ formatMessage(messages.frequencyTitle) }} · {{ frequencyTarget.username }} +

+
+

{{ frequencyTarget.username }}

+

{{ frequencyTarget.uuid }}

+
+

{{ formatMessage(messages.frequencyHint) }}

+
+ + {{ formatMessage(messages.loading) }} +
+ +
+ +
+
+
+ + +
+
+

+ {{ formatMessage(messages.auditTitle) }} + + · {{ auditTarget.username }} + +

+
+ + {{ formatMessage(messages.loading) }} +
+

+ {{ formatMessage(messages.auditEmpty) }} +

+
+
+
+
+

+ {{ auditActionLabel(entry.action) }} +

+

+ {{ entry.actorName }} → {{ entry.targetName }} +

+
+ + {{ formatDateTime(entry.createdAt) }} + +
+
+
+ {{ auditFieldLabel(change.field) }} + + {{ auditValue(change.before) }} + → + {{ auditValue(change.after) }} + +
+
+
+ +
+
+ +
+
+
+ +
+
+

+ {{ formatMessage(messages.inviteTitle) }} +

+ +
+ + +
+
+ + +
+
diff --git a/apps/app-frontend/src/helpers/skin-site-api.ts b/apps/app-frontend/src/helpers/skin-site-api.ts index 313fa7c..eb2e9f5 100644 --- a/apps/app-frontend/src/helpers/skin-site-api.ts +++ b/apps/app-frontend/src/helpers/skin-site-api.ts @@ -1902,3 +1902,116 @@ export function skinSiteUpdateReviewPaperAdmin( { method: 'PUT', body: data }, ) } + +export interface SkinSiteBotInfo { + exists: boolean + username?: string | null + active: boolean + creationBanned: boolean + tokenHint?: string | null + createdAt?: number | null + updatedAt?: number | null +} + +export interface SkinSiteUserFrequencyLimit { + dropPercent?: number | null +} + +export type SkinSiteUserAdminAuditAction = + | 'USER_UPDATED' + | 'USER_DELETED' + | 'RATE_LIMIT_UPDATED' + | 'BOT_USERNAME_UPDATED' + | 'BOT_TOKEN_INVALIDATED' + | 'BOT_CREATION_BANNED' + | 'BOT_CREATION_UNBANNED' + +export interface SkinSiteUserAdminAuditChange { + field: string + before?: string | null + after?: string | null +} + +export interface SkinSiteUserAdminAuditInfo { + id: number + actorUuid: string + actorName: string + targetUuid: string + targetName: string + action: SkinSiteUserAdminAuditAction + changes: SkinSiteUserAdminAuditChange[] + createdAt: number +} + +export interface SkinSiteUserAdminAuditPage { + items: SkinSiteUserAdminAuditInfo[] + nextCursor?: number | null +} + +export interface SkinSiteInviteCode { + email: string + code: string +} + +export function skinSiteAdminBotInfo( + userUuid: string, +): Promise> { + return skinSiteRequest('admin/users/' + encodeURIComponent(userUuid) + '/bot') +} + +export function skinSiteAdminRenameBot( + userUuid: string, + username: string, +): Promise> { + return skinSiteRequest( + 'admin/users/' + encodeURIComponent(userUuid) + '/bot/username', + { method: 'POST', body: { username } }, + ) +} + +export function skinSiteAdminInvalidateBotToken( + userUuid: string, +): Promise> { + return skinSiteRequest( + 'admin/users/' + encodeURIComponent(userUuid) + '/bot/invalidate', + { method: 'POST' }, + ) +} + +export function skinSiteAdminSetBotCreationBanned( + userUuid: string, + banned: boolean, +): Promise> { + return skinSiteRequest( + 'admin/users/' + encodeURIComponent(userUuid) + '/bot/ban', + { method: 'POST', body: { banned } }, + ) +} + +export function skinSiteAdminUpdateUserFrequencyLimit( + userUuid: string, + dropPercent: number | null, +): Promise> { + return skinSiteRequest( + 'admin/users/' + encodeURIComponent(userUuid) + '/frequency-limit', + { method: 'POST', body: { dropPercent } }, + ) +} + +export function skinSiteAdminUserAudit(params?: { + targetUuid?: string + before?: number +}): Promise> { + const query = new URLSearchParams() + if (params?.targetUuid) query.set('targetUuid', params.targetUuid) + if (params?.before) query.set('before', String(params.before)) + const suffix = query.size > 0 ? '?' + query.toString() : '' + return skinSiteRequest('admin/users/audit' + suffix) +} + +export function skinSiteAdminInviteUser(email: string): Promise> { + return skinSiteRequest('admin/inviteCode', { + method: 'POST', + body: { email }, + }) +} diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json index ddfee71..4d34b60 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -12178,5 +12178,44 @@ "app.skin-site.admin.review.move-up": {"message": "Move up"}, "app.skin-site.admin.review.move-down": {"message": "Move down"}, "app.skin-site.admin.review.mail-subject-required": {"message": "Subject is required when mail is enabled."}, - "app.skin-site.admin.review.mail-message-required": {"message": "Body is required when mail is enabled."} + "app.skin-site.admin.review.mail-message-required": {"message": "Body is required when mail is enabled."}, + "app.skin-site.admin.users.actions": {"message": "Actions"}, + "app.skin-site.admin.users.audit": {"message": "Audit"}, + "app.skin-site.admin.users.audit-empty": {"message": "No audit records yet."}, + "app.skin-site.admin.users.audit-more": {"message": "Load earlier"}, + "app.skin-site.admin.users.audit-title": {"message": "User management audit"}, + "app.skin-site.admin.users.bot": {"message": "Bot"}, + "app.skin-site.admin.users.bot-active": {"message": "Token valid"}, + "app.skin-site.admin.users.bot-ban": {"message": "Ban bot creation"}, + "app.skin-site.admin.users.bot-creation-banned": {"message": "Token creation banned"}, + "app.skin-site.admin.users.bot-exists": {"message": "Bot created"}, + "app.skin-site.admin.users.bot-inactive": {"message": "Token invalidated"}, + "app.skin-site.admin.users.bot-invalidate": {"message": "Invalidate token"}, + "app.skin-site.admin.users.bot-not-exists": {"message": "No bot yet"}, + "app.skin-site.admin.users.bot-save-username": {"message": "Save bot username"}, + "app.skin-site.admin.users.bot-title": {"message": "Bot management"}, + "app.skin-site.admin.users.bot-token-hint": {"message": "Token hint"}, + "app.skin-site.admin.users.bot-unban": {"message": "Unban bot creation"}, + "app.skin-site.admin.users.bot-username": {"message": "Bot username"}, + "app.skin-site.admin.users.bound-players-title": {"message": "Linked players"}, + "app.skin-site.admin.users.frequency": {"message": "Rate limit"}, + "app.skin-site.admin.users.frequency-clear": {"message": "Clear limit"}, + "app.skin-site.admin.users.frequency-current": {"message": "Currently {percent}%"}, + "app.skin-site.admin.users.frequency-hint": {"message": "Packets from this user will be dropped at the given rate."}, + "app.skin-site.admin.users.frequency-none": {"message": "Not enabled"}, + "app.skin-site.admin.users.frequency-range": {"message": "Only integer values from 10% to 100% are allowed."}, + "app.skin-site.admin.users.frequency-save": {"message": "Save limit"}, + "app.skin-site.admin.users.frequency-title": {"message": "Rate limit"}, + "app.skin-site.admin.users.invite": {"message": "Invite user"}, + "app.skin-site.admin.users.invite-copied": {"message": "Invite code copied."}, + "app.skin-site.admin.users.invite-copy": {"message": "Copy"}, + "app.skin-site.admin.users.invite-email": {"message": "Email"}, + "app.skin-site.admin.users.invite-email-invalid": {"message": "Enter a valid email address."}, + "app.skin-site.admin.users.invite-generate": {"message": "Generate invite code"}, + "app.skin-site.admin.users.invite-title": {"message": "Invite user"}, + "app.skin-site.admin.users.legacy-adapted": {"message": "Legacy internal adapted"}, + "app.skin-site.admin.users.registered-at": {"message": "Registered at"}, + "app.skin-site.admin.users.status": {"message": "Status"}, + "app.skin-site.admin.users.status-banned": {"message": "Banned"}, + "app.skin-site.admin.users.status-normal": {"message": "Normal"} } diff --git a/apps/app-frontend/src/locales/zh-CN/index.json b/apps/app-frontend/src/locales/zh-CN/index.json index 3cc6a7b..56b811a 100644 --- a/apps/app-frontend/src/locales/zh-CN/index.json +++ b/apps/app-frontend/src/locales/zh-CN/index.json @@ -12009,5 +12009,44 @@ "app.skin-site.admin.review.move-up": {"message": "上移"}, "app.skin-site.admin.review.move-down": {"message": "下移"}, "app.skin-site.admin.review.mail-subject-required": {"message": "开启邮件后必须填写邮件标题。"}, - "app.skin-site.admin.review.mail-message-required": {"message": "开启邮件后必须填写邮件正文。"} + "app.skin-site.admin.review.mail-message-required": {"message": "开启邮件后必须填写邮件正文。"}, + "app.skin-site.admin.users.actions": {"message": "操作"}, + "app.skin-site.admin.users.audit": {"message": "审计"}, + "app.skin-site.admin.users.audit-empty": {"message": "暂无管理审计记录。"}, + "app.skin-site.admin.users.audit-more": {"message": "加载更早记录"}, + "app.skin-site.admin.users.audit-title": {"message": "用户管理审计"}, + "app.skin-site.admin.users.bot": {"message": "机器人"}, + "app.skin-site.admin.users.bot-active": {"message": "Token 有效"}, + "app.skin-site.admin.users.bot-ban": {"message": "封禁 Bot 创建"}, + "app.skin-site.admin.users.bot-creation-banned": {"message": "禁止创建 Token"}, + "app.skin-site.admin.users.bot-exists": {"message": "已创建 Bot"}, + "app.skin-site.admin.users.bot-inactive": {"message": "Token 已失效"}, + "app.skin-site.admin.users.bot-invalidate": {"message": "使 Token 失效"}, + "app.skin-site.admin.users.bot-not-exists": {"message": "尚未创建 Bot"}, + "app.skin-site.admin.users.bot-save-username": {"message": "保存机器人用户名"}, + "app.skin-site.admin.users.bot-title": {"message": "Bot 管理"}, + "app.skin-site.admin.users.bot-token-hint": {"message": "Token 标识"}, + "app.skin-site.admin.users.bot-unban": {"message": "解除 Bot 封禁"}, + "app.skin-site.admin.users.bot-username": {"message": "机器人用户名"}, + "app.skin-site.admin.users.bound-players-title": {"message": "绑定玩家"}, + "app.skin-site.admin.users.frequency": {"message": "频率限制"}, + "app.skin-site.admin.users.frequency-clear": {"message": "取消限制"}, + "app.skin-site.admin.users.frequency-current": {"message": "当前 {percent}%"}, + "app.skin-site.admin.users.frequency-hint": {"message": "以下列频率对指定用户进行丢包处理,限制其进入服务器的频率。"}, + "app.skin-site.admin.users.frequency-none": {"message": "当前未启用"}, + "app.skin-site.admin.users.frequency-range": {"message": "仅允许设置 10% 至 100% 的整数。"}, + "app.skin-site.admin.users.frequency-save": {"message": "保存限制"}, + "app.skin-site.admin.users.frequency-title": {"message": "频率限制"}, + "app.skin-site.admin.users.invite": {"message": "邀请用户"}, + "app.skin-site.admin.users.invite-copied": {"message": "邀请码已复制。"}, + "app.skin-site.admin.users.invite-copy": {"message": "复制"}, + "app.skin-site.admin.users.invite-email": {"message": "邮箱"}, + "app.skin-site.admin.users.invite-email-invalid": {"message": "请输入正确格式的邮箱。"}, + "app.skin-site.admin.users.invite-generate": {"message": "生成邀请码"}, + "app.skin-site.admin.users.invite-title": {"message": "邀请用户"}, + "app.skin-site.admin.users.legacy-adapted": {"message": "已适配旧内服"}, + "app.skin-site.admin.users.registered-at": {"message": "注册时间"}, + "app.skin-site.admin.users.status": {"message": "状态"}, + "app.skin-site.admin.users.status-banned": {"message": "已封禁"}, + "app.skin-site.admin.users.status-normal": {"message": "正常"} }