feat: switch registration and reset account passwords in the admin page

This commit is contained in:
DiheChen committed 2026-11-09 21:22:40 +08:00
1 parent 246baf5a0e
commit a0956299be
3 files changed
+151 -1

No files matched your search

+81
View File
@@ -0,0 +1,81 @@
import type { AdminAccount } from '@/core/realtime'
import { useState } from 'react'
import { api } from '@/core/api'
import { adminStore } from './admin-store'
import { ConfirmDialog } from './confirm-dialog'
export function Accounts() {
const [username, setUsername] = useState('')
const [found, setFound] = useState<AdminAccount | null>(null)
const [password, setPassword] = useState<string | null>(null)
const [confirming, setConfirming] = useState(false)
const [busy, setBusy] = useState(false)
const find = async () => {
setBusy(true)
try {
setFound(await adminStore.call(() => api.adminGetAccount(username), { NOT_FOUND: '没有这个用户名的账号。' }))
setPassword(null)
}
finally {
setBusy(false)
}
}
const reset = async (account: AdminAccount) => {
const result = await adminStore.call(() => api.adminResetAccountPassword(account.username))
if (result !== null)
setPassword(result.password)
setConfirming(false)
}
return (
<section className="admin-section" aria-labelledby="admin-accounts-title">
<div className="section-heading">
<h2 id="admin-accounts-title" className="text-ui-lg">账号</h2>
</div>
<form
className="admin-list"
onSubmit={(event) => {
event.preventDefault()
void find()
}}
>
<div className="admin-row">
<div className="admin-row-copy">
<label htmlFor="admin-account-username" className="admin-row-title text-ui-base">查找账号</label>
<span id="admin-account-username-description" className="text-ui-sm muted">按用户名查找。用户忘记密码时,可以在这里重置。</span>
</div>
<div className="admin-row-actions">
<input id="admin-account-username" aria-describedby="admin-account-username-description" autoComplete="off" value={username} onChange={event => setUsername(event.target.value.trim().toLowerCase())} />
<button type="submit" className="button-secondary" disabled={busy || username === ''}>查找</button>
</div>
</div>
{found && (
<div className="admin-row">
<div className="admin-row-copy">
<span className="admin-row-title text-ui-base">{found.username}</span>
<span className="text-ui-sm muted">{`昵称:${found.member.name}`}</span>
</div>
<div className="admin-row-actions">
<button type="button" className="button-secondary" onClick={() => setConfirming(true)}>重置密码</button>
</div>
</div>
)}
{found && password && (
<div className="admin-row">
<div className="admin-row-copy">
<span className="admin-row-title text-ui-base">
新密码:
<code>{password}</code>
</span>
<span className="text-ui-sm muted">只显示这一次,请转告用户。这个账号在所有设备上都已退出登录。</span>
</div>
</div>
)}
</form>
{found && confirming && (
<ConfirmDialog title="重置密码" confirmLabel="重置" onConfirm={() => reset(found)} onClose={() => setConfirming(false)}>
{`将为 ${found.username} 生成一个新密码,原来的密码立即失效,这个账号在所有设备上都会退出登录。`}
</ConfirmDialog>
)}
</section>
)
}
+3 -1
View File
@@ -2,6 +2,7 @@ import { LogOut } from 'lucide-react'
import { useState } from 'react'
import { brandStore } from '@/core/brand'
import { useStore } from '@/shared/lib/use-store'
import { Accounts } from './accounts'
import { adminStore } from './admin-store'
import { PersistentRooms } from './persistent-rooms'
import { SiteSettings } from './site-settings'
@@ -38,7 +39,7 @@ export function AdminPage({ onEntered }: { onEntered: (ref: string) => void }) {
<div className="page-title">
<span className="text-ui-sm accent">{brandName}</span>
<h1 className="text-ui-xl">后台</h1>
<p className="text-ui-base muted">管理品牌名、常驻房间、音源账号和已存储的媒体。</p>
<p className="text-ui-base muted">管理品牌名、注册、用户账号、常驻房间、音源账号和已存储的媒体。</p>
</div>
{signedIn && (
<button type="button" className="button-ghost" onClick={() => adminStore.signOut()}>
@@ -51,6 +52,7 @@ export function AdminPage({ onEntered }: { onEntered: (ref: string) => void }) {
? (
<>
<SiteSettings />
<Accounts />
<PersistentRooms onEntered={onEntered} />
<SourceAccounts />
<StoredMedia />
+67
View File
@@ -76,8 +76,75 @@ export function SiteSettings() {
<button type="submit" className="button-secondary" disabled={busy || name.trim() === ''}>{busy ? '正在保存…' : '保存'}</button>
</div>
</div>
<RegistrationSetting />
</form>
)}
</section>
)
}
function RegistrationSetting() {
const [open, setOpen] = useState<boolean | null>(null)
const [failed, setFailed] = useState(false)
const [attempt, setAttempt] = useState(0)
const [busy, setBusy] = useState(false)
useEffect(() => {
let active = true
api.getSite().then((site) => {
if (active)
setOpen(site.registrationOpen)
}, () => {
if (active)
setFailed(true)
})
return () => {
active = false
}
}, [attempt])
const toggle = async () => {
if (open === null)
return
setBusy(true)
try {
const done = await adminStore.call(() => open ? api.adminCloseRegistration() : api.adminOpenRegistration())
if (done === null)
return
setOpen(!open)
notificationStore.success(open ? '已关闭注册' : '已开放注册')
}
finally {
setBusy(false)
}
}
const description = failed
? '暂时无法获取注册状态。'
: open === null
? '正在获取注册状态…'
: open
? '现在任何人都可以注册账号。关闭后不能再注册新账号,已有账号照常登录,游客照常使用。'
: '现在不能注册新账号。已有账号照常登录,游客照常使用。'
return (
<div className="admin-row">
<div className="admin-row-copy">
<span className="admin-row-title text-ui-base">注册</span>
<span className="text-ui-sm muted">{description}</span>
</div>
<div className="admin-row-actions">
{failed
? (
<button
type="button"
className="button-secondary"
onClick={() => {
setFailed(false)
setAttempt(n => n + 1)
}}
>
重试
</button>
)
: <button type="button" className="button-secondary" disabled={open === null || busy} onClick={() => void toggle()}>{open === false ? '开放注册' : '关闭注册'}</button>}
</div>
</div>
)
}