Fix auth session race

This commit is contained in:
wuyang6
2026-07-13 20:01:21 +08:00
parent f5f98688dd
commit 359b84ba55
+33 -10
View File
@@ -1,6 +1,6 @@
import { spawn } from "node:child_process";
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 { cookies } from "next/headers";
@@ -14,6 +14,7 @@ const COOKIE_NAME = authCookieName();
const SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;
const SESSION_REFRESH_INTERVAL_MS = 60 * 60 * 12 * 1000;
const LINUX_ACCOUNT_WORKSPACE_NAME = "zk-agent";
let sessionsWriteQueue: Promise<unknown> = Promise.resolve();
type UserRecord = {
id: string;
@@ -131,9 +132,10 @@ export async function logoutAccount() {
const cookieStore = await cookies();
const token = cookieStore.get(COOKIE_NAME)?.value;
if (token) {
const sessionsFile = await readSessions();
await mutateSessions((sessionsFile) => {
delete sessionsFile.sessions[token];
await writeSessions(sessionsFile);
return sessionsFile;
});
}
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);
if (!account) return null;
if (shouldRefreshSession(session.updatedAt ?? session.createdAt)) {
session.updatedAt = new Date().toISOString();
await writeSessions(sessionsFile);
await mutateSessions((latestSessionsFile) => {
const latestSession = latestSessionsFile.sessions[token];
if (!latestSession) return latestSessionsFile;
latestSession.updatedAt = new Date().toISOString();
return latestSessionsFile;
});
await setSessionCookie(token);
}
return { id: account.id, username: account.username };
@@ -199,13 +205,15 @@ export function accountSessionRoot(accountId: string, sessionId: string) {
async function createSession(account: UserRecord) {
const token = randomBytes(32).toString("hex");
const sessionsFile = await readSessions();
const now = new Date().toISOString();
await mutateSessions((sessionsFile) => {
sessionsFile.sessions[token] = {
accountId: account.id,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
createdAt: now,
updatedAt: now,
};
await writeSessions(sessionsFile);
return sessionsFile;
});
await setSessionCookie(token);
return { id: account.id, username: account.username };
@@ -257,7 +265,22 @@ async function readSessions(): Promise<SessionsFile> {
async function writeSessions(payload: SessionsFile) {
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) {