Files
kairos/apps/server/src/routes/setup.tsx
T
imbytecat 830714c94f feat: 单 owner 认证模型 — 替换注册为一次性设置向导 + Recovery Key + CLI 密码重置
自托管 Life OS 不应有公开注册。改为:
- /setup 一次性初始化向导(创建唯一 owner + 生成 Recovery Key)
- /recover 通过 Recovery Key 重置密码
- /login 未初始化时重定向到 /setup,去掉注册链接
- Better Auth databaseHooks 阻止额外用户注册
- citty CLI: bun run cli auth reset-password
- 删除 /signup 路由
- 新增 system_settings 表存储 recovery key hash
- 修复 drizzle.config.ts 非空断言 + sidebar.tsx cookieStore API
- 更新 AGENTS.md shadcn/ui 组件编辑规则
2026-03-31 18:33:16 +08:00

203 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createFileRoute, redirect, useRouter } from '@tanstack/react-router'
import { useState } from 'react'
import { authClient } from '@/server/auth/client'
import { checkInitialized, completeSetup } from '@/server/auth/functions'
export const Route = createFileRoute('/setup' as never)({
beforeLoad: async () => {
const initialized = await checkInitialized()
if (initialized) {
throw redirect({ to: '/login' as never })
}
},
component: SetupPage,
})
function SetupPage() {
const router = useRouter()
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const [recoveryKey, setRecoveryKey] = useState<string | null>(null)
const [copied, setCopied] = useState(false)
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
setError('')
if (password !== confirmPassword) {
setError('两次输入的密码不一致')
return
}
if (password.length < 8) {
setError('密码至少需要 8 个字符')
return
}
setLoading(true)
const { error: signUpError } = await authClient.signUp.email({
name,
email,
password,
})
if (signUpError) {
setError(signUpError.message ?? '创建账号失败,请重试')
setLoading(false)
return
}
const result = await completeSetup()
setRecoveryKey(result.recoveryKey)
setLoading(false)
}
const handleCopy = async () => {
if (!recoveryKey) return
await navigator.clipboard.writeText(recoveryKey)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
const handleContinue = () => {
router.navigate({ to: '/' as never })
}
if (recoveryKey) {
return (
<div className="min-h-screen bg-slate-50 flex items-center justify-center px-4">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-slate-900 tracking-tight">Kairos</h1>
<p className="text-slate-500 mt-2"></p>
</div>
<div className="bg-white rounded-2xl shadow-[0_8px_30px_rgb(0,0,0,0.04)] ring-1 ring-slate-100 p-8">
<div className="space-y-5">
<div className="bg-amber-50 ring-1 ring-amber-200 rounded-xl p-4">
<p className="text-sm font-medium text-amber-800 mb-2"></p>
<p className="text-xs text-amber-600">访</p>
</div>
<div className="relative">
<code className="block w-full px-4 py-3 rounded-xl bg-slate-50 ring-1 ring-slate-200 text-slate-700 text-sm font-mono break-all select-all">
{recoveryKey}
</code>
<button
type="button"
onClick={handleCopy}
className="absolute top-2 right-2 px-2.5 py-1 text-xs bg-white ring-1 ring-slate-200 rounded-lg text-slate-600 hover:bg-slate-50 transition-all"
>
{copied ? '已复制' : '复制'}
</button>
</div>
<button
type="button"
onClick={handleContinue}
className="w-full py-3 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl font-medium transition-all shadow-md shadow-indigo-200 hover:shadow-lg hover:shadow-indigo-300 active:scale-[0.98]"
>
使 Kairos
</button>
</div>
</div>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-slate-50 flex items-center justify-center px-4">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-slate-900 tracking-tight">Kairos</h1>
<p className="text-slate-500 mt-2"></p>
</div>
<div className="bg-white rounded-2xl shadow-[0_8px_30px_rgb(0,0,0,0.04)] ring-1 ring-slate-100 p-8">
<form onSubmit={handleSubmit} className="space-y-5">
<div>
<label htmlFor="name" className="block text-sm font-medium text-slate-700 mb-1.5">
</label>
<input
id="name"
type="text"
required
value={name}
onChange={(e) => setName(e.target.value)}
className="w-full px-4 py-3 rounded-xl bg-slate-50 border-0 ring-1 ring-slate-200 focus:ring-2 focus:ring-indigo-500 outline-none transition-all text-slate-700 placeholder:text-slate-400"
placeholder="你的名字"
disabled={loading}
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-700 mb-1.5">
</label>
<input
id="email"
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-4 py-3 rounded-xl bg-slate-50 border-0 ring-1 ring-slate-200 focus:ring-2 focus:ring-indigo-500 outline-none transition-all text-slate-700 placeholder:text-slate-400"
placeholder="your@email.com"
disabled={loading}
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-slate-700 mb-1.5">
</label>
<input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-3 rounded-xl bg-slate-50 border-0 ring-1 ring-slate-200 focus:ring-2 focus:ring-indigo-500 outline-none transition-all text-slate-700 placeholder:text-slate-400"
placeholder="至少 8 个字符"
disabled={loading}
/>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-slate-700 mb-1.5">
</label>
<input
id="confirmPassword"
type="password"
required
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full px-4 py-3 rounded-xl bg-slate-50 border-0 ring-1 ring-slate-200 focus:ring-2 focus:ring-indigo-500 outline-none transition-all text-slate-700 placeholder:text-slate-400"
placeholder="再次输入密码"
disabled={loading}
/>
</div>
{error && <p className="text-sm text-red-600">{error}</p>}
<button
type="submit"
disabled={loading}
className="w-full py-3 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl font-medium transition-all shadow-md shadow-indigo-200 disabled:opacity-50 disabled:shadow-none hover:shadow-lg hover:shadow-indigo-300 active:scale-[0.98]"
>
{loading ? '初始化中...' : '开始使用 Kairos'}
</button>
</form>
</div>
</div>
</div>
)
}