// 身份认证:账号与密码哈希只保存在服务器(PBKDF2) // 本机仅保存"会话标识"与自动登录凭据(localStorage),用于免重复登录 // 业务数据一律在服务器,本机数据库只是缓存(由同步引擎全量刷新) import { bindDatabase, db, unbindDatabase, WorklogDB } from './schema' import { lockVault } from './vault' import { getSyncBase } from '../lib/server' const LEGACY_DB_NAME = 'intern_worklog' const SESSION_KEY = 'dsh_session_user' export interface Session { userId: string username: string } // ---------- 本地会话与凭据 ---------- export function getSession(): Session | null { try { const raw = localStorage.getItem(SESSION_KEY) if (!raw) return null const s = JSON.parse(raw) if (s && s.userId && s.username) return s as Session } catch { /* 忽略损坏会话 */ } return null } function setSession(s: Session): void { localStorage.setItem(SESSION_KEY, JSON.stringify(s)) } function clearSession(): void { localStorage.removeItem(SESSION_KEY) } // 云端凭据:同用户名同密码,用于应用启动时免输密码的自动登录 const CREDS_KEY = 'dsh_cloud_creds' export function setCloudCreds(username: string, password: string): void { localStorage.setItem(CREDS_KEY, JSON.stringify({ username, password })) } export function getCloudCreds(): { username: string; password: string } | null { try { const raw = localStorage.getItem(CREDS_KEY) if (!raw) return null const c = JSON.parse(raw) if (c && c.username && c.password) return c as { username: string; password: string } } catch { /* 忽略损坏数据 */ } return null } export function clearCloudCreds(): void { localStorage.removeItem(CREDS_KEY) } // ---------- 用户数据库(本地缓存实例) ---------- let openedFor: string | null = null export function openUserDatabase(userId: string): void { if (openedFor === userId && db) return if (db) unbindDatabase() bindDatabase(new WorklogDB(LEGACY_DB_NAME + '_' + userId)) openedFor = userId } // ---------- 登录 / 注册(全部经由服务器) ---------- async function serverAuth(path: 'login' | 'register', username: string, password: string): Promise<{ userId: string; username: string }> { const base = await getSyncBase() let resp: Response try { resp = await fetch(base + '/api/auth/' + path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username.trim(), password }) }) } catch { throw new Error('无法连接同步服务器:请检查网络后重试') } const j = await resp.json().catch(() => ({})) if (!resp.ok) { const msg = (j && j.error) || ('服务返回 ' + resp.status) if (path === 'register' && resp.status === 409) throw new Error('云端已有同名账号且密码一致,直接登录即可') throw new Error(String(msg)) } if (!j.userId || !j.username) throw new Error('登录响应异常') return { userId: String(j.userId), username: String(j.username) } } async function establishSession(username: string, password: string, path: 'login' | 'register'): Promise { const info = await serverAuth(path, username, password) openUserDatabase(info.userId) const session: Session = { userId: info.userId, username: info.username } setSession(session) setCloudCreds(info.username, password) return session } export async function login(username: string, password: string): Promise { const name = (username || '').trim() if (!name || !password) throw new Error('请输入用户名与密码') return establishSession(name, password, 'login') } export interface RegisterOptions { migrateLegacy?: boolean } export async function createUser(username: string, password: string): Promise export async function createUser(username: string, password: string, migrateLegacy: boolean): Promise export async function createUser(username: string, password: string, _migrateLegacy?: boolean): Promise { const name = (username || '').trim() if (name.length < 2 || name.length > 20) throw new Error('用户名需 2 到 20 个字符') if (password.length < 6) throw new Error('登录密码至少 6 位') return establishSession(name, password, 'register') } // 启动引导:已保存过凭据则静默登录,直接进入应用 export async function bootstrapSession(): Promise { const creds = getCloudCreds() if (!creds) return null try { return await establishSession(creds.username, creds.password, 'login') } catch { return null } } export async function logoutUser(): Promise { lockVault() unbindDatabase() openedFor = null // 保留云端凭据:退出登录后再次打开仍可一键重新进入;如需彻底退出请删除用户 clearSession() } export function forgetDevice(): void { lockVault() unbindDatabase() openedFor = null clearSession() clearCloudCreds() } export async function changeLoginPassword(oldPwd: string, newPwd: string): Promise { const s = getSession() if (!s) throw new Error('未登录') if (newPwd.length < 6) throw new Error('新密码至少 6 位') const creds = getCloudCreds() if (!creds || creds.password !== oldPwd) throw new Error('旧密码不正确') const base = await getSyncBase() let resp: Response try { resp = await fetch(base + '/api/auth/password', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + getCachedToken() }, body: JSON.stringify({ oldPassword: oldPwd, newPassword: newPwd }) }) } catch { throw new Error('无法连接同步服务器') } if (!resp.ok) { const j = await resp.json().catch(() => ({})) throw new Error((j && j.error) || ('修改失败 ' + resp.status)) } setCloudCreds(creds.username, newPwd) } export async function deleteCurrentUser(): Promise { const s = getSession() if (!s) throw new Error('未登录') const base = await getSyncBase() let resp: Response try { resp = await fetch(base + '/api/auth/delete', { method: 'POST', headers: { Authorization: 'Bearer ' + getCachedToken() } }) } catch { throw new Error('无法连接同步服务器') } if (!resp.ok) { const j = await resp.json().catch(() => ({})) throw new Error((j && j.error) || ('删除失败 ' + resp.status)) } forgetDevice() } // 令牌由同步引擎负责登录换取/续期;此处读取共享的本地令牌副本 import { getToken } from '../lib/server' function getCachedToken(): string { return getToken() } // 兼容旧导出:纯在线模式下不再存在本机旧数据迁移(账号与数据都在服务器) export async function legacyDataExists(): Promise { void LEGACY_DB_NAME return false }