diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts new file mode 100644 index 0000000..544e094 --- /dev/null +++ b/src/app/api/admin/users/route.ts @@ -0,0 +1,109 @@ +export const dynamic = 'force-dynamic' + +import { NextRequest, NextResponse } from 'next/server' + +import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' +import { getAccountSession, userHasRole } from '@server/account/session' +import type { AccountUserRole } from '@server/account/session' + +const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() +const REQUIRED_ROLES: AccountUserRole[] = ['admin'] + +const UUID_PATTERN = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i + +type ErrorPayload = { + error: string +} + +type CreateUserBody = { + email?: unknown + uuid?: unknown + groups?: unknown +} + +function normalizeString(value: unknown): string | null { + if (typeof value !== 'string') { + return null + } + const trimmed = value.trim() + return trimmed.length > 0 ? trimmed : null +} + +function normalizeGroups(value: unknown): string[] | null { + if (!Array.isArray(value)) { + return null + } + const result: string[] = [] + for (const item of value) { + const normalized = normalizeString(item) + if (!normalized) { + continue + } + result.push(normalized) + } + if (result.length === 0) { + return null + } + return Array.from(new Set(result)) +} + +function isRootUser(username?: string): boolean { + return username?.trim().toLowerCase() === 'root' +} + +export async function POST(request: NextRequest) { + const session = await getAccountSession(request) + const user = session.user + + if (!user || !session.token) { + return NextResponse.json({ error: 'unauthenticated' }, { status: 401 }) + } + + if (!(await userHasRole(user, REQUIRED_ROLES))) { + return NextResponse.json({ error: 'forbidden' }, { status: 403 }) + } + + if (!isRootUser(user.username)) { + return NextResponse.json({ error: 'root_only' }, { status: 403 }) + } + + const body = (await request.json().catch(() => null)) as CreateUserBody | null + if (!body) { + return NextResponse.json({ error: 'invalid_body' }, { status: 400 }) + } + + const email = normalizeString(body.email) + const uuid = normalizeString(body.uuid) + const groups = normalizeGroups(body.groups) + + if (!email || !uuid || !groups) { + return NextResponse.json({ error: 'invalid_body' }, { status: 400 }) + } + + if (!UUID_PATTERN.test(uuid)) { + return NextResponse.json({ error: 'invalid_uuid' }, { status: 400 }) + } + + const response = await fetch(`${ACCOUNT_API_BASE}/admin/users`, { + method: 'POST', + headers: { + Authorization: `Bearer ${session.token}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email, + uuid, + groups, + }), + cache: 'no-store', + }) + + const payload = await response.json().catch(() => null) + if (payload === null) { + return NextResponse.json({ error: 'invalid_response' }, { status: 502 }) + } + + return NextResponse.json(payload, { status: response.status }) +} diff --git a/src/modules/extensions/builtin/user-center/management/components/UserGroupManagement.tsx b/src/modules/extensions/builtin/user-center/management/components/UserGroupManagement.tsx index 4b1a9ff..2b2826d 100644 --- a/src/modules/extensions/builtin/user-center/management/components/UserGroupManagement.tsx +++ b/src/modules/extensions/builtin/user-center/management/components/UserGroupManagement.tsx @@ -1,6 +1,6 @@ 'use client' -import { useMemo } from 'react' +import { type FormEvent, useMemo, useState } from 'react' import Card from '../../components/Card' export type ManagedUser = { @@ -12,11 +12,18 @@ export type ManagedUser = { created_at?: string } +export type CreateManagedUserInput = { + email: string + uuid: string + groups: string[] +} + type UserGroupManagementProps = { users?: ManagedUser[] isLoading?: boolean pendingUserIds?: Set canEditRoles: boolean + canCreateCustomUser?: boolean onRoleChange?: (userId: string, role: string) => void onInvite?: () => void onImport?: () => void @@ -25,6 +32,7 @@ type UserGroupManagementProps = { onDeleteUser?: (userId: string) => void onRenewUuid?: (userId: string) => void onManageBlacklist?: () => void + onCreateCustomUser?: (input: CreateManagedUserInput) => Promise | void } const ROLE_OPTIONS = [ @@ -33,11 +41,22 @@ const ROLE_OPTIONS = [ { value: 'user', label: '用户' }, ] +function parseGroupList(input: string): string[] { + const values = input + .split(/[\n,,]/) + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0) + + return Array.from(new Set(values)) +} + + export function UserGroupManagement({ users, isLoading = false, pendingUserIds, canEditRoles, + canCreateCustomUser = false, onRoleChange, onInvite, onImport, @@ -46,10 +65,56 @@ export function UserGroupManagement({ onDeleteUser, onRenewUuid, onManageBlacklist, + onCreateCustomUser, }: UserGroupManagementProps) { const data = useMemo(() => users ?? [], [users]) const pendingSet = pendingUserIds ?? new Set() + const [customEmail, setCustomEmail] = useState('') + const [customUuid, setCustomUuid] = useState('') + const [customGroups, setCustomGroups] = useState('') + const [isCreating, setIsCreating] = useState(false) + const [createMessage, setCreateMessage] = useState() + const [createError, setCreateError] = useState() + + const parsedCustomGroups = useMemo(() => parseGroupList(customGroups), [customGroups]) + + const handleCreateCustomUser = async (event: FormEvent) => { + event.preventDefault() + if (!onCreateCustomUser) { + return + } + + const email = customEmail.trim() + const uuid = customUuid.trim() + if (!email || !uuid) { + setCreateError('请填写邮箱与 UUID') + return + } + + const groups = parsedCustomGroups + if (groups.length === 0) { + setCreateError('请至少填写一个用户组') + return + } + + setIsCreating(true) + setCreateError(undefined) + setCreateMessage(undefined) + + try { + await onCreateCustomUser({ email, uuid, groups }) + setCreateMessage('用户创建成功') + setCustomEmail('') + setCustomUuid('') + setCustomGroups('') + } catch (error) { + setCreateError(error instanceof Error ? error.message : '创建失败') + } finally { + setIsCreating(false) + } + } + return (
@@ -83,6 +148,56 @@ export function UserGroupManagement({
+ {canCreateCustomUser ? ( +
+

Root 管理员专用:创建自定义 UUID 用户

+

支持一次配置多个分组(逗号或换行分隔)。

+
+ + +