import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { cookies } from "next/headers"; const AUTH_ROOT = path.join( /* turbopackIgnore: true */ process.cwd(), "../../.port_sessions/accounts", ); const USERS_PATH = path.join(AUTH_ROOT, "users.json"); const SESSIONS_PATH = path.join(AUTH_ROOT, "auth_sessions.json"); const COOKIE_NAME = "claw_account_session"; type UserRecord = { id: string; username: string; passwordHash: string; createdAt: string; }; type UsersFile = { users: UserRecord[]; }; type SessionsFile = { sessions: Record; }; export type AccountSession = { id: string; username: string; }; export async function registerAccount(username: string, password: string) { const cleanUsername = normalizeUsername(username); validatePassword(password); const usersFile = await readUsers(); if (usersFile.users.some((user) => user.username === cleanUsername)) { throw new Error("账号已存在"); } const account: UserRecord = { id: cleanUsername, username: cleanUsername, passwordHash: hashPassword(password), createdAt: new Date().toISOString(), }; usersFile.users.push(account); await writeUsers(usersFile); await createAccountDirectories(account.id); return createSession(account); } export async function loginAccount(username: string, password: string) { const cleanUsername = normalizeUsername(username); const usersFile = await readUsers(); const account = usersFile.users.find( (user) => user.username === cleanUsername, ); if (!account || !verifyPassword(password, account.passwordHash)) { throw new Error("账号或密码错误"); } await createAccountDirectories(account.id); return createSession(account); } export async function logoutAccount() { const cookieStore = await cookies(); const token = cookieStore.get(COOKIE_NAME)?.value; if (token) { const sessionsFile = await readSessions(); delete sessionsFile.sessions[token]; await writeSessions(sessionsFile); } cookieStore.delete(COOKIE_NAME); } export async function getCurrentAccount(): Promise { const cookieStore = await cookies(); const token = cookieStore.get(COOKIE_NAME)?.value; if (!token) return null; const sessionsFile = await readSessions(); const session = sessionsFile.sessions[token]; if (!session) return null; const usersFile = await readUsers(); const account = usersFile.users.find((user) => user.id === session.accountId); if (!account) return null; return { id: account.id, username: account.username }; } export function accountUploadRoot(accountId: string) { return path.join(AUTH_ROOT, accountId, "sessions"); } export function accountOutputRoot(accountId: string) { return path.join(AUTH_ROOT, accountId, "sessions"); } export function accountBaseRoot(accountId: string) { return path.join(AUTH_ROOT, accountId); } export function accountSessionInputRoot(accountId: string, sessionId: string) { return path.join(accountBaseRoot(accountId), "sessions", sessionId, "input"); } export function accountSessionOutputRoot(accountId: string, sessionId: string) { return path.join(accountBaseRoot(accountId), "sessions", sessionId, "output"); } export function accountSessionScratchpadRoot( accountId: string, sessionId: string, ) { return path.join( accountBaseRoot(accountId), "sessions", sessionId, "scratchpad", ); } export function accountSessionRoot(accountId: string, sessionId: string) { return path.join(accountBaseRoot(accountId), "sessions", sessionId); } async function createSession(account: UserRecord) { const token = randomBytes(32).toString("hex"); const sessionsFile = await readSessions(); sessionsFile.sessions[token] = { accountId: account.id, createdAt: new Date().toISOString(), }; await writeSessions(sessionsFile); const cookieStore = await cookies(); cookieStore.set(COOKIE_NAME, token, { httpOnly: true, sameSite: "lax", path: "/", }); return { id: account.id, username: account.username }; } async function createAccountDirectories(accountId: string) { await Promise.all( ["sessions"].map((name) => mkdir(path.join(AUTH_ROOT, accountId, name), { recursive: true }), ), ); } async function readUsers(): Promise { await mkdir(AUTH_ROOT, { recursive: true }); try { return JSON.parse(await readFile(USERS_PATH, "utf8")) as UsersFile; } catch { return { users: [] }; } } async function writeUsers(payload: UsersFile) { await mkdir(AUTH_ROOT, { recursive: true }); await writeFile(USERS_PATH, JSON.stringify(payload, null, 2), "utf8"); } async function readSessions(): Promise { await mkdir(AUTH_ROOT, { recursive: true }); try { return JSON.parse(await readFile(SESSIONS_PATH, "utf8")) as SessionsFile; } catch { return { sessions: {} }; } } async function writeSessions(payload: SessionsFile) { await mkdir(AUTH_ROOT, { recursive: true }); await writeFile(SESSIONS_PATH, JSON.stringify(payload, null, 2), "utf8"); } function normalizeUsername(username: string) { const cleanUsername = username.trim().toLowerCase(); if (!/^[a-z0-9._-]{2,40}$/.test(cleanUsername)) { throw new Error( "账号只能包含小写字母、数字、点、下划线或中划线,长度 2-40", ); } return cleanUsername; } function validatePassword(password: string) { if (password.length < 6) throw new Error("密码至少 6 位"); } function hashPassword(password: string) { const salt = randomBytes(16).toString("hex"); const hash = scryptSync(password, salt, 64).toString("hex"); return `${salt}:${hash}`; } function verifyPassword(password: string, passwordHash: string) { const [salt, expectedHash] = passwordHash.split(":"); if (!salt || !expectedHash) return false; const actual = Buffer.from(scryptSync(password, salt, 64).toString("hex")); const expected = Buffer.from(expectedHash); return actual.length === expected.length && timingSafeEqual(actual, expected); }