Add assistant-ui data agent frontend
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
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<string, { accountId: string; createdAt: string }>;
|
||||
};
|
||||
|
||||
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<AccountSession | null> {
|
||||
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");
|
||||
}
|
||||
|
||||
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<UsersFile> {
|
||||
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<SessionsFile> {
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user