diff --git a/apps/server/src/modules/bookmarks/index.ts b/apps/server/src/modules/bookmarks/index.ts index 7c7c226..b219cca 100644 --- a/apps/server/src/modules/bookmarks/index.ts +++ b/apps/server/src/modules/bookmarks/index.ts @@ -1,4 +1,4 @@ -import type { ModuleMetadata } from '../registry' +import type { ModuleMetadata } from '@/modules/registry' export const bookmarksModule: ModuleMetadata = { id: 'bookmarks', diff --git a/apps/server/src/routes/login.tsx b/apps/server/src/routes/login.tsx index 9d557bd..b7880d5 100644 --- a/apps/server/src/routes/login.tsx +++ b/apps/server/src/routes/login.tsx @@ -1,4 +1,4 @@ -import { createFileRoute, Link, redirect, useRouter } from '@tanstack/react-router' +import { createFileRoute, redirect, useRouter } from '@tanstack/react-router' import { useState } from 'react' import { authClient } from '@/server/auth/client' import { checkInitialized, getSession } from '@/server/auth/functions' @@ -95,12 +95,6 @@ function LoginPage() { {loading ? '登录中...' : '登录'} - -

- - 无法登录?使用恢复密钥 - -

diff --git a/apps/server/src/routes/recover.tsx b/apps/server/src/routes/recover.tsx deleted file mode 100644 index 95074eb..0000000 --- a/apps/server/src/routes/recover.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { createFileRoute, Link, redirect, useRouter } from '@tanstack/react-router' -import { useState } from 'react' -import { checkInitialized, recoverAccount } from '@/server/auth/functions' - -export const Route = createFileRoute('/recover' as never)({ - beforeLoad: async () => { - const initialized = await checkInitialized() - if (!initialized) { - throw redirect({ to: '/setup' as never }) - } - }, - component: RecoverPage, -}) - -function RecoverPage() { - const router = useRouter() - const [recoveryKey, setRecoveryKey] = useState('') - const [newPassword, setNewPassword] = useState('') - const [confirmPassword, setConfirmPassword] = useState('') - const [error, setError] = useState('') - const [loading, setLoading] = useState(false) - const [success, setSuccess] = useState(false) - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setError('') - - if (newPassword !== confirmPassword) { - setError('两次输入的密码不一致') - return - } - - if (newPassword.length < 8) { - setError('密码至少需要 8 个字符') - return - } - - setLoading(true) - - try { - await recoverAccount({ data: { recoveryKey: recoveryKey.trim(), newPassword } }) - setSuccess(true) - } catch (err) { - setError(err instanceof Error ? err.message : '恢复失败,请检查恢复密钥是否正确') - } finally { - setLoading(false) - } - } - - if (success) { - return ( -
-
-
-

Kairos

-

密码已重置

-
- -
-
-
-

密码已成功重置,所有会话已失效。请使用新密码重新登录。

-
- - -
-
-
-
- ) - } - - return ( -
-
-
-

Kairos

-

账户恢复

-
- -
-
-
- - setRecoveryKey(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 font-mono text-sm" - placeholder="kairos-recovery-..." - disabled={loading} - /> -
- -
- - setNewPassword(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} - /> -
- -
- - 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} - /> -
- - {error &&

{error}

} - - -
- -

- - 返回登录 - -

-
-
-
- ) -} diff --git a/apps/server/src/routes/setup.tsx b/apps/server/src/routes/setup.tsx index ba1fc81..62a59d9 100644 --- a/apps/server/src/routes/setup.tsx +++ b/apps/server/src/routes/setup.tsx @@ -1,7 +1,7 @@ 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' +import { checkInitialized } from '@/server/auth/functions' export const Route = createFileRoute('/setup' as never)({ beforeLoad: async () => { @@ -21,8 +21,6 @@ function SetupPage() { const [confirmPassword, setConfirmPassword] = useState('') const [error, setError] = useState('') const [loading, setLoading] = useState(false) - const [recoveryKey, setRecoveryKey] = useState(null) - const [copied, setCopied] = useState(false) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() @@ -52,151 +50,92 @@ function SetupPage() { 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 ( -
-
-
-

Kairos

-

初始化完成

-
- -
-
-
-

请妥善保存你的恢复密钥(仅显示一次)

-

如果无法登录且无法访问服务器终端,可通过此密钥重置账户。

-
- -
- - {recoveryKey} - - -
- - -
-
-
+ ;
+
+
+

Kairos

+

初始化你的人生操作系统

- ) - } - return ( -
-
-
-

Kairos

-

初始化你的人生操作系统

-
- -
-
-
- - 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} - /> -
- -
- - 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} - /> -
- -
- - 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} - /> -
- -
- - 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} - /> -
- - {error &&

{error}

} - - -
-
+ /> +
+ +
+ + 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} + /> +
+ +
+ + 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} + /> +
+ +
+ + 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} + /> +
+ + {error &&

{error}

} + + +
- ) +
} diff --git a/apps/server/src/server/api/routers/index.ts b/apps/server/src/server/api/routers/index.ts index ede4e3e..d2d5191 100644 --- a/apps/server/src/server/api/routers/index.ts +++ b/apps/server/src/server/api/routers/index.ts @@ -1,5 +1,5 @@ import * as bookmarks from '@/modules/bookmarks/router' -import { os } from '../server' +import { os } from '@/server/api/server' export const router = os.router({ bookmarks, diff --git a/apps/server/src/server/auth/functions.ts b/apps/server/src/server/auth/functions.ts index 596b248..40b6075 100644 --- a/apps/server/src/server/auth/functions.ts +++ b/apps/server/src/server/auth/functions.ts @@ -1,23 +1,8 @@ import { createServerFn } from '@tanstack/react-start' import { getRequestHeaders } from '@tanstack/react-start/server' -import { hashPassword, verifyPassword } from 'better-auth/crypto' -import { eq } from 'drizzle-orm' -import { z } from 'zod' import { auth } from '@/server/auth' import * as authSchema from '@/server/auth/schema' import { db } from '@/server/db' -import { systemSettings } from '@/server/db/schema/system' - -const RECOVERY_KEY_PREFIX = 'kairos-recovery-' -const RECOVERY_KEY_SETTING = 'recovery_key_hash' - -function generateRecoveryKey(): string { - const bytes = crypto.getRandomValues(new Uint8Array(32)) - const hex = Array.from(bytes) - .map((b) => b.toString(16).padStart(2, '0')) - .join('') - return `${RECOVERY_KEY_PREFIX}${hex}` -} export const getSession = createServerFn({ method: 'GET' }).handler(async () => { const headers = getRequestHeaders() @@ -28,60 +13,3 @@ export const checkInitialized = createServerFn({ method: 'GET' }).handler(async const users = await db.select({ id: authSchema.user.id }).from(authSchema.user).limit(1) return users.length > 0 }) - -export const completeSetup = createServerFn({ method: 'POST' }).handler(async () => { - const headers = getRequestHeaders() - const sessionData = await auth.api.getSession({ headers }) - if (!sessionData?.user) { - throw new Error('Not authenticated') - } - - const users = await db.select({ id: authSchema.user.id }).from(authSchema.user).limit(2) - if (users.length !== 1) { - throw new Error('Invalid system state') - } - - const existing = await db.select().from(systemSettings).where(eq(systemSettings.key, RECOVERY_KEY_SETTING)) - if (existing.length > 0) { - throw new Error('Setup already completed') - } - - const recoveryKey = generateRecoveryKey() - const hash = await hashPassword(recoveryKey) - - await db.insert(systemSettings).values({ key: RECOVERY_KEY_SETTING, value: hash }) - - return { recoveryKey } -}) - -const recoverAccountSchema = z.object({ - recoveryKey: z.string().min(1), - newPassword: z.string().min(8), -}) - -export const recoverAccount = createServerFn({ method: 'POST' }) - .inputValidator((data: z.input) => recoverAccountSchema.parse(data)) - .handler(async ({ data }) => { - const stored = await db.select().from(systemSettings).where(eq(systemSettings.key, RECOVERY_KEY_SETTING)) - if (stored.length === 0 || !stored[0]) { - throw new Error('Recovery not available') - } - - const isValid = await verifyPassword({ hash: stored[0].value, password: data.recoveryKey }) - if (!isValid) { - throw new Error('Invalid recovery key') - } - - const owner = await db.select().from(authSchema.user).limit(1) - if (owner.length === 0 || !owner[0]) { - throw new Error('No owner found') - } - - const newHash = await hashPassword(data.newPassword) - - await db.update(authSchema.account).set({ password: newHash }).where(eq(authSchema.account.userId, owner[0].id)) - - await db.delete(authSchema.session).where(eq(authSchema.session.userId, owner[0].id)) - - return { success: true } - }) diff --git a/apps/server/src/server/db/schema/index.ts b/apps/server/src/server/db/schema/index.ts index 6d357ab..b56a0c0 100644 --- a/apps/server/src/server/db/schema/index.ts +++ b/apps/server/src/server/db/schema/index.ts @@ -1,3 +1,2 @@ export * from '../../../modules/bookmarks/schema' export * from '../../auth/schema' -export * from './system' diff --git a/apps/server/src/server/db/schema/system.ts b/apps/server/src/server/db/schema/system.ts deleted file mode 100644 index 65efb34..0000000 --- a/apps/server/src/server/db/schema/system.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { pgTable, text, timestamp } from 'drizzle-orm/pg-core' - -export const systemSettings = pgTable('system_settings', { - key: text('key').primaryKey(), - value: text('value').notNull(), - createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), - updatedAt: timestamp('updated_at', { withTimezone: true }) - .notNull() - .defaultNow() - .$onUpdateFn(() => new Date()), -})