Fix auth session race

This commit is contained in:
wuyang6
2026-07-13 20:01:21 +08:00
parent f5f98688dd
commit 359b84ba55
+37 -14
View File
@@ -1,6 +1,6 @@
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto"; import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises"; import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
@@ -14,6 +14,7 @@ const COOKIE_NAME = authCookieName();
const SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 30; const SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;
const SESSION_REFRESH_INTERVAL_MS = 60 * 60 * 12 * 1000; const SESSION_REFRESH_INTERVAL_MS = 60 * 60 * 12 * 1000;
const LINUX_ACCOUNT_WORKSPACE_NAME = "zk-agent"; const LINUX_ACCOUNT_WORKSPACE_NAME = "zk-agent";
let sessionsWriteQueue: Promise<unknown> = Promise.resolve();
type UserRecord = { type UserRecord = {
id: string; id: string;
@@ -131,9 +132,10 @@ export async function logoutAccount() {
const cookieStore = await cookies(); const cookieStore = await cookies();
const token = cookieStore.get(COOKIE_NAME)?.value; const token = cookieStore.get(COOKIE_NAME)?.value;
if (token) { if (token) {
const sessionsFile = await readSessions(); await mutateSessions((sessionsFile) => {
delete sessionsFile.sessions[token]; delete sessionsFile.sessions[token];
await writeSessions(sessionsFile); return sessionsFile;
});
} }
cookieStore.delete(COOKIE_NAME); cookieStore.delete(COOKIE_NAME);
} }
@@ -151,8 +153,12 @@ export async function getCurrentAccount(): Promise<AccountSession | null> {
const account = usersFile.users.find((user) => user.id === session.accountId); const account = usersFile.users.find((user) => user.id === session.accountId);
if (!account) return null; if (!account) return null;
if (shouldRefreshSession(session.updatedAt ?? session.createdAt)) { if (shouldRefreshSession(session.updatedAt ?? session.createdAt)) {
session.updatedAt = new Date().toISOString(); await mutateSessions((latestSessionsFile) => {
await writeSessions(sessionsFile); const latestSession = latestSessionsFile.sessions[token];
if (!latestSession) return latestSessionsFile;
latestSession.updatedAt = new Date().toISOString();
return latestSessionsFile;
});
await setSessionCookie(token); await setSessionCookie(token);
} }
return { id: account.id, username: account.username }; return { id: account.id, username: account.username };
@@ -199,13 +205,15 @@ export function accountSessionRoot(accountId: string, sessionId: string) {
async function createSession(account: UserRecord) { async function createSession(account: UserRecord) {
const token = randomBytes(32).toString("hex"); const token = randomBytes(32).toString("hex");
const sessionsFile = await readSessions(); const now = new Date().toISOString();
sessionsFile.sessions[token] = { await mutateSessions((sessionsFile) => {
accountId: account.id, sessionsFile.sessions[token] = {
createdAt: new Date().toISOString(), accountId: account.id,
updatedAt: new Date().toISOString(), createdAt: now,
}; updatedAt: now,
await writeSessions(sessionsFile); };
return sessionsFile;
});
await setSessionCookie(token); await setSessionCookie(token);
return { id: account.id, username: account.username }; return { id: account.id, username: account.username };
@@ -257,7 +265,22 @@ async function readSessions(): Promise<SessionsFile> {
async function writeSessions(payload: SessionsFile) { async function writeSessions(payload: SessionsFile) {
await mkdir(AUTH_STATE_ROOT, { recursive: true }); await mkdir(AUTH_STATE_ROOT, { recursive: true });
await writeFile(SESSIONS_PATH, JSON.stringify(payload, null, 2), "utf8"); const tempPath = `${SESSIONS_PATH}.${process.pid}.${Date.now()}.${randomBytes(4).toString("hex")}.tmp`;
await writeFile(tempPath, JSON.stringify(payload, null, 2), "utf8");
await rename(tempPath, SESSIONS_PATH);
}
async function mutateSessions(
mutator: (payload: SessionsFile) => SessionsFile | Promise<SessionsFile>,
) {
const run = sessionsWriteQueue.then(async () => {
const current = await readSessions();
const next = await mutator(current);
await writeSessions(next);
return next;
});
sessionsWriteQueue = run.catch(() => undefined);
return run;
} }
function normalizeUsername(username: string) { function normalizeUsername(username: string) {