From 5b9d74acb754e54e80b630bcc914afc3f1c45dd3 Mon Sep 17 00:00:00 2001 From: flym Date: Fri, 11 Sep 2026 18:22:48 +0800 Subject: [PATCH] feat: add user profile management and admin user deletion --- components/account-panel.tsx | 17 +++++-- components/admin/users.tsx | 71 +++++++++++++++++++++++--- lib/admin.ts | 15 +++++- services/api/app/main.py | 99 +++++++++++++++++++++++++++++++++++- services/api/app/schemas.py | 79 ++++++++++++++++++++++++++++ 5 files changed, 269 insertions(+), 12 deletions(-) diff --git a/components/account-panel.tsx b/components/account-panel.tsx index f2cd98e..ccb82c0 100644 --- a/components/account-panel.tsx +++ b/components/account-panel.tsx @@ -7,7 +7,7 @@ import { PreviewNotice, StatusCard } from "@/components/ui"; const apiBase = process.env.NEXT_PUBLIC_API_BASE_URL ?? "/api/v1"; -type User = { email: string; username: string; role: "user" | "admin"; plan: "free" | "vip"; status: string; email_verified: boolean; phone_verified: boolean }; +type User = { email: string | null; phone: string | null; username: string; role: "user" | "admin"; plan: "free" | "vip"; status: string; email_verified: boolean; phone_verified: boolean }; type Usage = { period_start: string; period_end: string; limit: number; adjustment: number; used: number; reserved: number; available: number; plan: string }; export function AccountPanel() { @@ -16,6 +16,7 @@ export function AccountPanel() { const [usage, setUsage] = useState(null); const [error, setError] = useState(""); const [passwordMessage, setPasswordMessage] = useState(""); + const [profileMessage, setProfileMessage] = useState(""); async function load() { const csrfResponse = await fetch(`${apiBase}/auth/csrf`, { credentials: "include" }); @@ -40,21 +41,29 @@ export function AccountPanel() { async function changePassword(event: FormEvent) { event.preventDefault(); const form = event.currentTarget; - setPasswordMessage(""); const data = new FormData(form); const response = await fetch(`${apiBase}/auth/password/change`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", "X-CSRF-Token": csrf }, body: JSON.stringify({ current_password: data.get("current_password"), new_password: data.get("new_password") }) }); const body = await response.json().catch(() => ({})); - if (!response.ok) setPasswordMessage(body.error?.message ?? "密码修改失败"); + if (!response.ok) setPasswordMessage(body.error?.message ?? body.detail?.message ?? "密码修改失败"); else { setPasswordMessage("密码已修改,其他会话已撤销"); form.reset(); } } + async function updateProfile(event: FormEvent) { + event.preventDefault(); + const data = new FormData(event.currentTarget); + const response = await fetch(`${apiBase}/auth/profile`, { method: "PATCH", credentials: "include", headers: { "Content-Type": "application/json", "X-CSRF-Token": csrf }, body: JSON.stringify({ username: data.get("username"), email: data.get("email") || null, phone: data.get("phone") || null }) }); + const body = await response.json().catch(() => ({})); + if (!response.ok) setProfileMessage(body.error?.message ?? body.detail?.message ?? "资料保存失败"); + else { setUser(body); setProfileMessage("资料已保存;如修改邮箱或手机号,请重新完成验证"); } + } + if (error) return
{error}。前往登录
; if (!user || !usage) return
; return <>
-

Profile

基本资料

用户名
{user.username}
邮箱
{user.email}
角色
{user.role}
{user.role === "admin" ? <>
业务后台
进入管理后台
内容管理
进入 CMS
: null}
额度周期
{new Date(usage.period_start).toLocaleDateString()} 至 {new Date(usage.period_end).toLocaleDateString()}
+

Profile

基本资料

{profileMessage ?

{profileMessage}

: null}
{user.role === "admin" ?
业务后台
进入管理后台
内容管理
进入 CMS
: null}

Usage

额度明细

打开工作台

Security

修改密码

{passwordMessage ?

{passwordMessage}

: null}
; diff --git a/components/admin/users.tsx b/components/admin/users.tsx index 47e6a52..2512e89 100644 --- a/components/admin/users.tsx +++ b/components/admin/users.tsx @@ -1,15 +1,18 @@ "use client"; -import { useEffect, useState } from "react"; +import { FormEvent, useEffect, useState } from "react"; import { adjustQuota, changeUserStatus, + createUser, + deleteUser, fetchCsrf, grantMembership, getUserDetail, getUserQuota, listUsers, revokeMembership, + updateUserProfile, type AdminUser, type AdminUserDetail, } from "@/lib/admin"; @@ -29,6 +32,7 @@ export function UsersPanel() { const [error, setError] = useState(""); const [selected, setSelected] = useState(null); const [notice, setNotice] = useState(""); + const [showCreate, setShowCreate] = useState(false); async function load(pageNum = page) { setLoading(true); @@ -69,6 +73,9 @@ export function UsersPanel() { + + {showCreate ? { setShowCreate(false); setNotice("用户已创建"); void load(1); }} /> : null} +
setEmail(e.target.value)} placeholder="按邮箱筛选" value={email} /> @@ -109,8 +116,8 @@ export function UsersPanel() { rows.map((row) => ( -

{row.email}

-

{row.phone ?? "无手机号"}

+

{row.username}

+

{row.email ?? "无邮箱"} · {row.phone ?? "无手机号"}

{row.effective_plan.toUpperCase()} {row.role} @@ -156,6 +163,11 @@ function UserDetailPanel({ detail, onRefresh, setNotice, onClose }: { detail: Ad const [amount, setAmount] = useState("100"); const [busy, setBusy] = useState(false); const [message, setMessage] = useState(""); + const [username, setUsername] = useState(detail.user.username); + const [email, setEmail] = useState(detail.user.email ?? ""); + const [phone, setPhone] = useState(detail.user.phone ?? ""); + const [emailVerified, setEmailVerified] = useState(detail.user.email_verified); + const [phoneVerified, setPhoneVerified] = useState(detail.user.phone_verified); useEffect(() => { void fetchCsrf().then(setCsrf); @@ -197,11 +209,24 @@ function UserDetailPanel({ detail, onRefresh, setNotice, onClose }: { detail: Ad return run(() => adjustQuota(detail.user.id, amt, reason, `admin-${Date.now()}-${Math.random()}`, csrf), "额度已调整"); } + function saveProfile() { + if (!username.trim()) { setMessage("用户名不能为空"); return; } + if (!email.trim() && !phone.trim()) { setMessage("邮箱或手机号至少填写一项"); return; } + return run(() => updateUserProfile(detail.user.id, { username: username.trim(), email: email.trim() || null, phone: phone.trim() || null, email_verified: emailVerified, phone_verified: phoneVerified, reason: reason.trim() }, csrf), "用户信息已保存"); + } + + async function removeUser() { + if (!reason.trim()) { setMessage("请填写原因"); return; } + if (!window.confirm("确定删除该用户?删除后不可恢复。")) return; + setBusy(true); + try { await deleteUser(detail.user.id, reason, csrf); setNotice("用户已删除"); onClose(); } catch (e) { setMessage(e instanceof Error ? e.message : "删除失败"); } finally { setBusy(false); } + } + return (
-

{detail.user.email}

+

{detail.user.username}

有效 Plan:{detail.effective_plan.toUpperCase()} · 状态:{detail.user.status === "active" ? "正常" : "已禁用"}

@@ -251,6 +276,20 @@ function UserDetailPanel({ detail, onRefresh, setNotice, onClose }: { detail: Ad
+
+

用户信息

+
+
setUsername(e.target.value)} value={username} />
+
setEmail(e.target.value)} type="email" value={email} />
+
setPhone(e.target.value)} value={phone} />
+
+
+ + +
+ +
+