Download apps/web/src/components/features/NativeAccountAccess.tsx from hunian003/message: direct link, hf CLI and curl.
- Browser
- Download file 5.54 kB
-
https://huggingface.co/spaces/hunian003/message/resolve/main/apps/web/src/components/features/NativeAccountAccess.tsx
- Command line
-
hf download hf://spaces/hunian003/message/apps/web/src/components/features/NativeAccountAccess.tsx
-
curl -L -o NativeAccountAccess.tsx https://huggingface.co/spaces/hunian003/message/resolve/main/apps/web/src/components/features/NativeAccountAccess.tsx
5.54 kB
| import { useEffect, useRef, useState, type FormEvent } from 'react'; | |
| import { ShieldCheck } from 'lucide-react'; | |
| import { NativeApi } from '@api/native'; | |
| import { Button } from '@components/ui/button'; | |
| import { Input } from '@components/ui/input'; | |
| import { Label } from '@components/ui/label'; | |
| import type { NativeAuthStatus } from '@typings/native'; | |
| interface Props { | |
| status: NativeAuthStatus | null; | |
| loading: boolean; | |
| onRetry: () => Promise<void>; | |
| onLogin: (email: string, password: string, signal: AbortSignal) => Promise<void>; | |
| onError: (message: string) => void; | |
| } | |
| export function NativeAccountAccess({ status, loading, onRetry, onLogin, onError }: Props) { | |
| const [email, setEmail] = useState(''); | |
| const [password, setPassword] = useState(''); | |
| const [recovering, setRecovering] = useState(false); | |
| const [pending, setPending] = useState(false); | |
| const [notice, setNotice] = useState(''); | |
| const request = useRef<AbortController>(); | |
| const embedded = window.self !== window.top; | |
| useEffect(() => () => request.current?.abort(), []); | |
| const submit = async (event: FormEvent) => { | |
| event.preventDefault(); | |
| if (embedded || request.current || !email.trim() || (!recovering && !password)) return; | |
| const controller = new AbortController(); request.current = controller; | |
| setPending(true); setNotice(''); onError(''); | |
| try { | |
| if (recovering) { | |
| // 匿名恢复同样先取预登录 CSRF,邮件提交不会绕过会话保护。 | |
| const next = await new NativeApi().authStatus(controller.signal); | |
| const api = new NativeApi(next.csrf_token); | |
| await api.ensureCsrf(controller.signal); | |
| await api.recover(email.trim(), controller.signal); | |
| if (!controller.signal.aborted) setNotice('如果此邮箱可以恢复登录,你会收到重置邮件。请在当前浏览器中打开邮件链接。'); | |
| } else await onLogin(email.trim(), password, controller.signal); | |
| } catch (error) { | |
| if (!controller.signal.aborted) onError(error instanceof Error ? error.message : '请求失败,请重试'); | |
| } finally { | |
| // 密码只在提交期间留在组件内存,任何结果都清空输入,不保存到浏览器存储。 | |
| setPassword(''); request.current = undefined; | |
| if (!controller.signal.aborted) setPending(false); | |
| } | |
| }; | |
| return <section className="max-w-xl rounded-lg border border-console-border bg-console-surface-base p-5" aria-labelledby="native-login-heading"> | |
| <ShieldCheck className="mb-3 h-8 w-8 text-console-status-info" /> | |
| <h2 id="native-login-heading" className="text-lg font-semibold">{loading ? '正在检查登录状态' : !status || !status.initialized || status.bootstrap.mode === 'locked' ? '安全初始化' : recovering ? '找回密码' : '管理员登录'}</h2> | |
| {loading ? <p role="status" className="mt-3 text-sm text-console-text-secondary">正在连接认证服务…</p> : !status ? <div className="mt-3 space-y-4"><p className="text-sm text-console-text-secondary">暂时无法确认认证配置,请重试连接。</p><Button variant="outline" onClick={() => void onRetry()}>重新检查认证</Button></div> : !status.initialized || status.bootstrap.mode === 'locked' ? <div className="mt-3 space-y-4"> | |
| <p className="text-sm text-console-text-secondary">{status.bootstrap.reason || '认证服务尚未配置,管理功能已锁定。'}</p> | |
| <p className="text-sm text-console-text-secondary">请由运营者配置 Supabase Auth,并在服务端明确授权管理员账号。此页面不提供公开注册或首次访问者创建管理员。</p> | |
| <Button variant="outline" onClick={() => void onRetry()}>重新检查认证</Button> | |
| </div> : <> | |
| <p className="mb-5 mt-2 text-sm text-console-text-secondary">{recovering ? '输入管理员邮箱,我们会发送密码重置邮件。请使用此浏览器打开邮件中的链接。' : '使用已授权的 Supabase 管理员账号登录。会话通过 HttpOnly cookie 保存;前端不保存密码或管理员 token。'}</p> | |
| {embedded && <p role="alert" className="mb-4 text-sm text-amber-700">嵌入页面无法可靠保存安全登录cookie,请先点击上方“在独立页面登录”,再登录或发送重置邮件。</p>} | |
| <form onSubmit={submit} className="space-y-4" autoComplete="off"> | |
| <div><Label htmlFor="admin-email">管理员邮箱</Label><Input id="admin-email" type="email" maxLength={254} autoComplete="off" spellCheck={false} value={email} onChange={event => setEmail(event.target.value)} required disabled={pending || embedded} className="mt-2" /></div> | |
| {!recovering && <div><Label htmlFor="admin-password">密码</Label><Input id="admin-password" type="password" maxLength={256} autoComplete="off" value={password} onChange={event => setPassword(event.target.value)} required disabled={pending || embedded} className="mt-2" /></div>} | |
| <div className="flex flex-wrap gap-2"><Button type="submit" disabled={pending || embedded || !email.trim() || (!recovering && !password)}>{pending ? '正在提交…' : recovering ? '发送重置邮件' : '登录原生市场'}</Button><Button type="button" variant="ghost" disabled={pending || embedded} onClick={() => { setRecovering(value => !value); setPassword(''); setNotice(''); onError(''); }}>{recovering ? '返回登录' : '忘记密码'}</Button></div> | |
| </form> | |
| {notice && <p role="status" className="mt-4 rounded-md bg-blue-50 p-3 text-sm text-blue-900">{notice}</p>} | |
| </>} | |
| </section>; | |
| } | |