feat: switch registration and reset account passwords in the admin page
This commit is contained in:
1 parent
246baf5a0e
commit
a0956299be
3 files changed
+151
-1
No files matched your search
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user