Fix auth session race
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user