Use sync access for the temp JSON file database
All checks were successful
/ build (push) Successful in 1m37s
/ deploy (push) Has been skipped

Replace all async reads and writes to the JSON database with the sync
reads and writes to prevent a data corruption race condition where two
requests are processed at the same time and write to the same file, or
one reads while the other writes causing read of partially written data.
This commit is contained in:
Hornwitser 2025-09-20 23:04:16 +02:00
parent f9d188b2ba
commit 80cec71308
23 changed files with 138 additions and 138 deletions

View file

@ -9,11 +9,11 @@ export default defineEventHandler(async (event) => {
setHeader(event, "Content-Disposition", 'attachment; filename="database-dump.json"'); setHeader(event, "Content-Disposition", 'attachment; filename="database-dump.json"');
setHeader(event, "Content-Type", "application/json; charset=utf-8"); setHeader(event, "Content-Type", "application/json; charset=utf-8");
return { return {
nextUserId: await readNextUserId(), nextUserId: readNextUserId(),
users: await readUsers(), users: readUsers(),
nextSessionId: await readNextSessionId(), nextSessionId: readNextSessionId(),
sessions: await readSessions(), sessions: readSessions(),
subscriptions: await readSubscriptions(), subscriptions: readSubscriptions(),
schedule: await readSchedule(), schedule: readSchedule(),
}; };
}) })

View file

@ -7,14 +7,14 @@ import { generateDemoSchedule, generateDemoAccounts } from "~/server/generate-de
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
await requireServerSessionWithAdmin(event); await requireServerSessionWithAdmin(event);
const accounts = generateDemoAccounts(); const accounts = generateDemoAccounts();
await writeUsers(accounts); writeUsers(accounts);
await writeSchedule(generateDemoSchedule()); writeSchedule(generateDemoSchedule());
await writeAuthenticationMethods(accounts.map((user, index) => ({ writeAuthenticationMethods(accounts.map((user, index) => ({
id: index, id: index,
userId: user.id, userId: user.id,
provider: "demo", provider: "demo",
slug: user.name!, slug: user.name!,
name: user.name!, name: user.name!,
}))); })));
await writeNextAuthenticationMethodId(Math.max(await nextAuthenticationMethodId(), accounts.length)); writeNextAuthenticationMethodId(Math.max(nextAuthenticationMethodId(), accounts.length));
}) })

View file

@ -29,20 +29,20 @@ export default defineEventHandler(async (event) => {
}); });
} }
const currentNextUserId = await readNextUserId(); const currentNextUserId = readNextUserId();
await writeNextUserId(Math.max(currentNextUserId, snapshot.nextUserId)); writeNextUserId(Math.max(currentNextUserId, snapshot.nextUserId));
await writeUsers(snapshot.users); writeUsers(snapshot.users);
const currentNextSessionId = await readNextSessionId(); const currentNextSessionId = readNextSessionId();
await writeNextSessionId(Math.max(currentNextSessionId, snapshot.nextSessionId)); writeNextSessionId(Math.max(currentNextSessionId, snapshot.nextSessionId));
const currentSessions = new Map((await readSessions()).map(session => [session.id, session])); const currentSessions = new Map((readSessions()).map(session => [session.id, session]));
await writeSessions(snapshot.sessions.filter(session => { writeSessions(snapshot.sessions.filter(session => {
const current = currentSessions.get(session.id); const current = currentSessions.get(session.id);
// Only keep sessions that match the account id in both sets to avoid // Only keep sessions that match the account id in both sets to avoid
// resurrecting deleted sessions. This will still cause session cross // resurrecting deleted sessions. This will still cause session cross
// pollution if a snapshot from another instance is loaded here. // pollution if a snapshot from another instance is loaded here.
return current?.accountId !== undefined && current.accountId === session.accountId; return current?.accountId !== undefined && current.accountId === session.accountId;
})); }));
await writeSubscriptions(snapshot.subscriptions); writeSubscriptions(snapshot.subscriptions);
await writeSchedule(snapshot.schedule); writeSchedule(snapshot.schedule);
await sendRedirect(event, "/"); await sendRedirect(event, "/");
}) })

View file

@ -6,5 +6,5 @@ import { deleteDatabase } from "~/server/database";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
await requireServerSessionWithAdmin(event); await requireServerSessionWithAdmin(event);
await deleteDatabase(); deleteDatabase();
}) })

View file

@ -18,7 +18,7 @@ export default defineEventHandler(async (event) => {
}); });
} }
const users = await readUsers(); const users = readUsers();
const user = users.find(user => user.id === patch.id); const user = users.find(user => user.id === patch.id);
if (!user || user.deleted) { if (!user || user.deleted) {
throw createError({ throw createError({
@ -52,28 +52,28 @@ export default defineEventHandler(async (event) => {
user.name = patch.name; user.name = patch.name;
} }
user.updatedAt = new Date().toISOString(); user.updatedAt = new Date().toISOString();
await writeUsers(users); writeUsers(users);
broadcastEvent({ broadcastEvent({
id: await nextEventId(), id: nextEventId(),
type: "user-update", type: "user-update",
data: serverUserToApi(user), data: serverUserToApi(user),
}); });
// Rotate sessions with the user in it if the access changed // Rotate sessions with the user in it if the access changed
if (accessChanged) { if (accessChanged) {
const sessions = await readSessions(); const sessions = readSessions();
const nowMs = Date.now(); const nowMs = Date.now();
for (const session of sessions) { for (const session of sessions) {
if (session.accountId === user.id) { if (session.accountId === user.id) {
session.rotatesAtMs = nowMs; session.rotatesAtMs = nowMs;
broadcastEvent({ broadcastEvent({
id: await nextEventId(), id: nextEventId(),
type: "session-expired", type: "session-expired",
sessionId: session.id, sessionId: session.id,
}); });
} }
} }
await writeSessions(sessions); writeSessions(sessions);
} }
// Update Schedule counts. // Update Schedule counts.

View file

@ -11,11 +11,11 @@ import { broadcastEvent, cancelAccountStreams } from "~/server/streams";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const serverSession = await requireServerSessionWithUser(event); const serverSession = await requireServerSessionWithUser(event);
let users = await readUsers(); let users = readUsers();
// Expire sessions for this user // Expire sessions for this user
const expiredSessionIds = new Set<number>(); const expiredSessionIds = new Set<number>();
let sessions = await readSessions(); let sessions = readSessions();
const nowMs = Date.now(); const nowMs = Date.now();
for (const session of sessions) { for (const session of sessions) {
if ( if (
@ -25,7 +25,7 @@ export default defineEventHandler(async (event) => {
) { ) {
session.expiresAtMs = nowMs; session.expiresAtMs = nowMs;
broadcastEvent({ broadcastEvent({
id: await nextEventId(), id: nextEventId(),
type: "session-expired", type: "session-expired",
sessionId: session.id, sessionId: session.id,
}); });
@ -33,24 +33,24 @@ export default defineEventHandler(async (event) => {
} }
} }
cancelAccountStreams(serverSession.accountId); cancelAccountStreams(serverSession.accountId);
await writeSessions(sessions); writeSessions(sessions);
await deleteCookie(event, "session"); await deleteCookie(event, "session");
// Remove subscriptions for this user // Remove subscriptions for this user
let subscriptions = await readSubscriptions(); let subscriptions = readSubscriptions();
subscriptions = subscriptions.filter( subscriptions = subscriptions.filter(
subscription => !expiredSessionIds.has(subscription.sessionId) subscription => !expiredSessionIds.has(subscription.sessionId)
); );
await writeSubscriptions(subscriptions); writeSubscriptions(subscriptions);
// Remove the user // Remove the user
const account = users.find(user => user.id === serverSession.accountId)!; const account = users.find(user => user.id === serverSession.accountId)!;
const now = new Date(nowMs).toISOString(); const now = new Date(nowMs).toISOString();
account.deleted = true; account.deleted = true;
account.updatedAt = now; account.updatedAt = now;
await writeUsers(users); writeUsers(users);
await broadcastEvent({ await broadcastEvent({
id: await nextEventId(), id: nextEventId(),
type: "user-update", type: "user-update",
data: { data: {
id: account.id, id: account.id,

View file

@ -38,7 +38,7 @@ export default defineEventHandler(async (event) => {
} }
} }
const users = await readUsers(); const users = readUsers();
const account = users.find(user => user.id === session.accountId); const account = users.find(user => user.id === session.accountId);
if (!account) { if (!account) {
throw Error("Account does not exist"); throw Error("Account does not exist");
@ -70,7 +70,7 @@ export default defineEventHandler(async (event) => {
else else
delete account.locale; delete account.locale;
} }
await writeUsers(users); writeUsers(users);
// Update Schedule counts. // Update Schedule counts.
await updateScheduleInterestedCounts(users); await updateScheduleInterestedCounts(users);

View file

@ -18,7 +18,7 @@ export default defineEventHandler(async (event): Promise<ApiSession> => {
const body = await readBody(event); const body = await readBody(event);
const name = body?.name; const name = body?.name;
const users = await readUsers(); const users = readUsers();
let user: ServerUser; let user: ServerUser;
if (typeof name === "string") { if (typeof name === "string") {
if (name === "") { if (name === "") {
@ -36,7 +36,7 @@ export default defineEventHandler(async (event): Promise<ApiSession> => {
const firstUser = users.every(user => user.type === "anonymous"); const firstUser = users.every(user => user.type === "anonymous");
user = { user = {
id: await nextUserId(), id: nextUserId(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
type: firstUser ? "admin" : "regular", type: firstUser ? "admin" : "regular",
name, name,
@ -44,7 +44,7 @@ export default defineEventHandler(async (event): Promise<ApiSession> => {
} else if (name === undefined) { } else if (name === undefined) {
user = { user = {
id: await nextUserId(), id: nextUserId(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
type: "anonymous", type: "anonymous",
}; };
@ -76,19 +76,19 @@ export default defineEventHandler(async (event): Promise<ApiSession> => {
}); });
} }
authMethods.push({ authMethods.push({
id: await nextAuthenticationMethodId(), id: nextAuthenticationMethodId(),
userId: user.id, userId: user.id,
provider: session.authenticationProvider, provider: session.authenticationProvider,
slug: session.authenticationSlug!, slug: session.authenticationSlug!,
name: session.authenticationName!, name: session.authenticationName!,
}) })
await writeAuthenticationMethods(authMethods); writeAuthenticationMethods(authMethods);
} }
users.push(user); users.push(user);
await writeUsers(users); writeUsers(users);
await broadcastEvent({ await broadcastEvent({
id: await nextEventId(), id: nextEventId(),
type: "user-update", type: "user-update",
data: user, data: user,
}); });

View file

@ -24,11 +24,11 @@ export default defineEventHandler(async (event) => {
}); });
} }
const authMethods = await readAuthenticationMethods(); const authMethods = readAuthenticationMethods();
const method = authMethods.find(method => method.provider === "demo" && method.slug === slug); const method = authMethods.find(method => method.provider === "demo" && method.slug === slug);
let session; let session;
if (method) { if (method) {
const users = await readUsers(); const users = readUsers();
const account = users.find(user => !user.deleted && user.id === method.userId); const account = users.find(user => !user.deleted && user.id === method.userId);
session = await setServerSession(event, account); session = await setServerSession(event, account);
} else { } else {

View file

@ -84,11 +84,11 @@ export default defineEventHandler(async (event): Promise<ApiSession> => {
} }
const slug = String(data.authData.id); const slug = String(data.authData.id);
const authMethods = await readAuthenticationMethods(); const authMethods = readAuthenticationMethods();
const method = authMethods.find(method => method.provider === "telegram" && method.slug === slug); const method = authMethods.find(method => method.provider === "telegram" && method.slug === slug);
let session; let session;
if (method) { if (method) {
const users = await readUsers(); const users = readUsers();
const account = users.find(user => !user.deleted && user.id === method.userId); const account = users.find(user => !user.deleted && user.id === method.userId);
session = await setServerSession(event, account); session = await setServerSession(event, account);
} else { } else {

View file

@ -8,7 +8,7 @@ import { cancelSessionStreams } from "~/server/streams";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const session = await getServerSession(event, true); const session = await getServerSession(event, true);
if (session) { if (session) {
const users = await readUsers(); const users = readUsers();
const account = users.find(user => user.id === session.accountId); const account = users.find(user => user.id === session.accountId);
if (account?.type === "anonymous") { if (account?.type === "anonymous") {
throw createError({ throw createError({

View file

@ -6,6 +6,6 @@
import { readEvents } from "../database"; import { readEvents } from "../database";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const events = await readEvents(); const events = readEvents();
return events[events.length - 1]?. id ?? 0; return events[events.length - 1]?. id ?? 0;
}); });

View file

@ -35,7 +35,7 @@ export default defineEventHandler(async (event) => {
}); });
} }
const schedule = await readSchedule(); const schedule = readSchedule();
if (schedule.deleted) { if (schedule.deleted) {
throw createError({ throw createError({
@ -85,9 +85,9 @@ export default defineEventHandler(async (event) => {
applyUpdatesToArray(update.shifts, schedule.shifts = schedule.shifts ?? []); applyUpdatesToArray(update.shifts, schedule.shifts = schedule.shifts ?? []);
} }
await writeSchedule(schedule); writeSchedule(schedule);
await broadcastEvent({ await broadcastEvent({
id: await nextEventId(), id: nextEventId(),
type: "schedule-update", type: "schedule-update",
updatedFrom, updatedFrom,
data: update, data: update,

View file

@ -6,6 +6,6 @@ import { readSchedule } from "~/server/database";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const session = await getServerSession(event, false); const session = await getServerSession(event, false);
const schedule = await readSchedule(); const schedule = readSchedule();
return canSeeCrew(session?.access) ? schedule : filterSchedule(schedule); return canSeeCrew(session?.access) ? schedule : filterSchedule(schedule);
}); });

View file

@ -20,7 +20,7 @@ export default defineEventHandler(async (event) => {
message: z.prettifyError(error), message: z.prettifyError(error),
}); });
} }
const subscriptions = await readSubscriptions(); const subscriptions = readSubscriptions();
const existingIndex = subscriptions.findIndex( const existingIndex = subscriptions.findIndex(
sub => sub.type === "push" && sub.sessionId === session.id sub => sub.type === "push" && sub.sessionId === session.id
); );
@ -34,7 +34,7 @@ export default defineEventHandler(async (event) => {
} else { } else {
subscriptions.push(subscription); subscriptions.push(subscription);
} }
await writeSubscriptions(subscriptions); writeSubscriptions(subscriptions);
if (existingIndex !== -1) { if (existingIndex !== -1) {
return { message: "Existing subscription refreshed."}; return { message: "Existing subscription refreshed."};
} }

View file

@ -6,7 +6,7 @@ import { readSubscriptions, writeSubscriptions } from "~/server/database";
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const session = await requireServerSessionWithUser(event); const session = await requireServerSessionWithUser(event);
const subscriptions = await readSubscriptions(); const subscriptions = readSubscriptions();
const existingIndex = subscriptions.findIndex( const existingIndex = subscriptions.findIndex(
sub => sub.type === "push" && sub.sessionId === session.id sub => sub.type === "push" && sub.sessionId === session.id
); );
@ -15,6 +15,6 @@ export default defineEventHandler(async (event) => {
} else { } else {
return { message: "No subscription registered."}; return { message: "No subscription registered."};
} }
await writeSubscriptions(subscriptions); writeSubscriptions(subscriptions);
return { message: "Existing subscription removed."}; return { message: "Existing subscription removed."};
}); });

View file

@ -13,7 +13,7 @@ const detailsSchema = z.object({
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
await requireServerSessionWithAdmin(event); await requireServerSessionWithAdmin(event);
const users = await readUsers(); const users = readUsers();
const { success, error, data: params } = detailsSchema.safeParse(getRouterParams(event)); const { success, error, data: params } = detailsSchema.safeParse(getRouterParams(event));
if (!success) { if (!success) {
throw createError({ throw createError({

View file

@ -6,7 +6,7 @@ import { readUsers } from "~/server/database"
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
const session = await requireServerSessionWithUser(event); const session = await requireServerSessionWithUser(event);
const users = await readUsers(); const users = readUsers();
if (session.access === "admin") { if (session.access === "admin") {
return users.map(serverUserToApi); return users.map(serverUserToApi);

View file

@ -2,7 +2,7 @@
SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no> SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
SPDX-License-Identifier: AGPL-3.0-or-later SPDX-License-Identifier: AGPL-3.0-or-later
*/ */
import { readFile, unlink, writeFile } from "node:fs/promises"; import { readFileSync, writeFileSync, unlinkSync } from "node:fs";
import type { ApiAuthenticationProvider, ApiEvent, ApiSchedule, ApiSubscription, ApiUserType } from "~/shared/types/api"; import type { ApiAuthenticationProvider, ApiEvent, ApiSchedule, ApiSubscription, ApiUserType } from "~/shared/types/api";
import type { Id } from "~/shared/types/common"; import type { Id } from "~/shared/types/common";
@ -53,9 +53,9 @@ const nextAuthenticationMethodIdPath = "data/auth-method-id.json"
const nextEventIdPath = "data/next-event-id.json"; const nextEventIdPath = "data/next-event-id.json";
const eventsPath = "data/events.json"; const eventsPath = "data/events.json";
async function remove(path: string) { function remove(path: string) {
try { try {
await unlink(path); unlinkSync(path);
} catch (err: any) { } catch (err: any) {
if (err.code !== "ENOENT") { if (err.code !== "ENOENT") {
throw err; throw err;
@ -63,17 +63,17 @@ async function remove(path: string) {
} }
} }
export async function deleteDatabase() { export function deleteDatabase() {
await remove(schedulePath); remove(schedulePath);
await remove(subscriptionsPath); remove(subscriptionsPath);
await remove(usersPath); remove(usersPath);
await remove(sessionsPath); remove(sessionsPath);
} }
async function readJson<T>(filePath: string, fallback: T) { function readJson<T>(filePath: string, fallback: T) {
let data: T extends () => infer R ? R : T; let data: T extends () => infer R ? R : T;
try { try {
data = JSON.parse(await readFile(filePath, "utf-8")); data = JSON.parse(readFileSync(filePath, "utf-8"));
} catch (err: any) { } catch (err: any) {
if (err.code !== "ENOENT") if (err.code !== "ENOENT")
throw err; throw err;
@ -82,19 +82,19 @@ async function readJson<T>(filePath: string, fallback: T) {
return data; return data;
} }
export async function readSchedule() { export function readSchedule() {
return readJson(schedulePath, (): ApiSchedule => ({ return readJson(schedulePath, (): ApiSchedule => ({
id: 111, id: 111,
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
})); }));
} }
export async function writeSchedule(schedule: ApiSchedule) { export function writeSchedule(schedule: ApiSchedule) {
await writeFile(schedulePath, JSON.stringify(schedule, undefined, "\t") + "\n", "utf-8"); writeFileSync(schedulePath, JSON.stringify(schedule, undefined, "\t") + "\n", "utf-8");
} }
export async function readSubscriptions() { export function readSubscriptions() {
let subscriptions = await readJson<ApiSubscription[]>(subscriptionsPath, []); let subscriptions = readJson<ApiSubscription[]>(subscriptionsPath, []);
if (subscriptions.length && "keys" in subscriptions[0]) { if (subscriptions.length && "keys" in subscriptions[0]) {
// Discard old format // Discard old format
subscriptions = []; subscriptions = [];
@ -102,89 +102,89 @@ export async function readSubscriptions() {
return subscriptions; return subscriptions;
} }
export async function writeSubscriptions(subscriptions: ApiSubscription[]) { export function writeSubscriptions(subscriptions: ApiSubscription[]) {
await writeFile(subscriptionsPath, JSON.stringify(subscriptions, undefined, "\t") + "\n", "utf-8"); writeFileSync(subscriptionsPath, JSON.stringify(subscriptions, undefined, "\t") + "\n", "utf-8");
} }
export async function readNextUserId() { export function readNextUserId() {
return await readJson(nextUserIdPath, 0); return readJson(nextUserIdPath, 0);
} }
export async function writeNextUserId(nextId: number) { export function writeNextUserId(nextId: number) {
await writeFile(nextUserIdPath, String(nextId), "utf-8"); writeFileSync(nextUserIdPath, String(nextId), "utf-8");
} }
export async function nextUserId() { export function nextUserId() {
let nextId = await readJson(nextUserIdPath, 0); let nextId = readJson(nextUserIdPath, 0);
if (nextId === 0) { if (nextId === 0) {
nextId = Math.max(...(await readUsers()).map(user => user.id), -1) + 1; nextId = Math.max(...(readUsers()).map(user => user.id), -1) + 1;
} }
await writeFile(nextUserIdPath, String(nextId + 1), "utf-8"); writeFileSync(nextUserIdPath, String(nextId + 1), "utf-8");
return nextId; return nextId;
} }
export async function readUsers() { export function readUsers() {
return await readJson(usersPath, (): ServerUser[] => []); return readJson(usersPath, (): ServerUser[] => []);
} }
export async function writeUsers(users: ServerUser[]) { export function writeUsers(users: ServerUser[]) {
await writeFile(usersPath, JSON.stringify(users, undefined, "\t") + "\n", "utf-8"); writeFileSync(usersPath, JSON.stringify(users, undefined, "\t") + "\n", "utf-8");
} }
export async function readNextSessionId() { export function readNextSessionId() {
return await readJson(nextSessionIdPath, 0); return readJson(nextSessionIdPath, 0);
} }
export async function writeNextSessionId(nextId: number) { export function writeNextSessionId(nextId: number) {
await writeFile(nextSessionIdPath, String(nextId), "utf-8"); writeFileSync(nextSessionIdPath, String(nextId), "utf-8");
} }
export async function nextSessionId() { export function nextSessionId() {
const nextId = await readJson(nextSessionIdPath, 0); const nextId = readJson(nextSessionIdPath, 0);
await writeFile(nextSessionIdPath, String(nextId + 1), "utf-8"); writeFileSync(nextSessionIdPath, String(nextId + 1), "utf-8");
return nextId; return nextId;
} }
export async function readSessions() { export function readSessions() {
return readJson<ServerSession[]>(sessionsPath, []) return readJson<ServerSession[]>(sessionsPath, [])
} }
export async function writeSessions(sessions: ServerSession[]) { export function writeSessions(sessions: ServerSession[]) {
await writeFile(sessionsPath, JSON.stringify(sessions, undefined, "\t") + "\n", "utf-8"); writeFileSync(sessionsPath, JSON.stringify(sessions, undefined, "\t") + "\n", "utf-8");
} }
export async function nextAuthenticationMethodId() { export function nextAuthenticationMethodId() {
const nextId = await readJson(nextAuthenticationMethodIdPath, 0); const nextId = readJson(nextAuthenticationMethodIdPath, 0);
await writeFile(nextAuthenticationMethodIdPath, String(nextId + 1), "utf-8"); writeFileSync(nextAuthenticationMethodIdPath, String(nextId + 1), "utf-8");
return nextId; return nextId;
} }
export async function writeNextAuthenticationMethodId(nextId: number) { export function writeNextAuthenticationMethodId(nextId: number) {
await writeFile(nextAuthenticationMethodIdPath, String(nextId), "utf-8"); writeFileSync(nextAuthenticationMethodIdPath, String(nextId), "utf-8");
} }
export async function readAuthenticationMethods() { export function readAuthenticationMethods() {
return readJson<ServerAuthenticationMethod[]>(authMethodPath, []) return readJson<ServerAuthenticationMethod[]>(authMethodPath, [])
} }
export async function writeAuthenticationMethods(authMethods: ServerAuthenticationMethod[]) { export function writeAuthenticationMethods(authMethods: ServerAuthenticationMethod[]) {
await writeFile(authMethodPath, JSON.stringify(authMethods, undefined, "\t") + "\n", "utf-8"); writeFileSync(authMethodPath, JSON.stringify(authMethods, undefined, "\t") + "\n", "utf-8");
} }
export async function nextEventId() { export function nextEventId() {
const nextId = await readJson(nextEventIdPath, 0); const nextId = readJson(nextEventIdPath, 0);
await writeFile(nextEventIdPath, String(nextId + 1), "utf-8"); writeFileSync(nextEventIdPath, String(nextId + 1), "utf-8");
return nextId; return nextId;
} }
export async function writeNextEventId(nextId: number) { export function writeNextEventId(nextId: number) {
await writeFile(nextEventIdPath, String(nextId), "utf-8"); writeFileSync(nextEventIdPath, String(nextId), "utf-8");
} }
export async function readEvents() { export function readEvents() {
return readJson<ApiEvent[]>(eventsPath, []) return readJson<ApiEvent[]>(eventsPath, [])
} }
export async function writeEvents(events: ApiEvent[]) { export function writeEvents(events: ApiEvent[]) {
await writeFile(eventsPath, JSON.stringify(events, undefined, "\t") + "\n", "utf-8"); writeFileSync(eventsPath, JSON.stringify(events, undefined, "\t") + "\n", "utf-8");
} }

View file

@ -33,8 +33,8 @@ export async function createEventStream(
) { ) {
const runtimeConfig = useRuntimeConfig(event); const runtimeConfig = useRuntimeConfig(event);
const now = Date.now(); const now = Date.now();
const events = (await readEvents()).filter(e => e.id > lastEventId); const events = (readEvents()).filter(e => e.id > lastEventId);
const users = await readUsers(); const users = readUsers();
const apiSession = session ? await serverSessionToApi(event, session) : undefined; const apiSession = session ? await serverSessionToApi(event, session) : undefined;
let userType: ApiAccount["type"] | undefined; let userType: ApiAccount["type"] | undefined;
if (session?.accountId !== undefined) { if (session?.accountId !== undefined) {
@ -182,9 +182,9 @@ function encodeEvent(event: ApiEvent, userType: ApiAccount["type"] | undefined)
} }
export async function broadcastEvent(event: ApiEvent) { export async function broadcastEvent(event: ApiEvent) {
const events = await readEvents(); const events = readEvents();
events.push(event); events.push(event);
await writeEvents(events); writeEvents(events);
} }
function sendEventToStream(stream: EventStream, event: ApiEvent) { function sendEventToStream(stream: EventStream, event: ApiEvent) {
@ -222,7 +222,7 @@ async function sendEventUpdates() {
// Send events. // Send events.
const skipEventId = Math.min(...[...streams.values()].map(s => s.lastEventId)); const skipEventId = Math.min(...[...streams.values()].map(s => s.lastEventId));
const events = (await readEvents()).filter(e => e.id > skipEventId); const events = (readEvents()).filter(e => e.id > skipEventId);
if (events.length) if (events.length)
console.log(`broadcasting ${events.length} event(s) to ${streams.size} client(s)`); console.log(`broadcasting ${events.length} event(s) to ${streams.size} client(s)`);
for (const stream of streams.values()) { for (const stream of streams.values()) {

View file

@ -20,7 +20,7 @@ export async function updateScheduleInterestedCounts(users: ServerUser[]) {
eventSlotCounts.set(id, (eventSlotCounts.get(id) ?? 0) + 1); eventSlotCounts.set(id, (eventSlotCounts.get(id) ?? 0) + 1);
} }
const schedule = await readSchedule(); const schedule = readSchedule();
if (schedule.deleted) { if (schedule.deleted) {
throw new Error("Deleted schedule not implemented"); throw new Error("Deleted schedule not implemented");
} }
@ -58,7 +58,7 @@ export async function updateScheduleInterestedCounts(users: ServerUser[]) {
schedule.updatedAt = updatedFrom; schedule.updatedAt = updatedFrom;
await writeSchedule(schedule); await writeSchedule(schedule);
await broadcastEvent({ await broadcastEvent({
id: await nextEventId(), id: nextEventId(),
type: "schedule-update", type: "schedule-update",
updatedFrom, updatedFrom,
data: update, data: update,

View file

@ -19,11 +19,11 @@ import type { ApiAuthenticationProvider, ApiSession } from "~/shared/types/api";
import { serverUserToApiAccount } from "./user"; import { serverUserToApiAccount } from "./user";
async function removeSessionSubscription(sessionId: number) { async function removeSessionSubscription(sessionId: number) {
const subscriptions = await readSubscriptions(); const subscriptions = readSubscriptions();
const index = subscriptions.findIndex(subscription => subscription.sessionId === sessionId); const index = subscriptions.findIndex(subscription => subscription.sessionId === sessionId);
if (index !== -1) { if (index !== -1) {
subscriptions.splice(index, 1); subscriptions.splice(index, 1);
await writeSubscriptions(subscriptions); writeSubscriptions(subscriptions);
} }
} }
@ -35,7 +35,7 @@ async function clearServerSessionInternal(event: H3Event, sessions: ServerSessio
if (session) { if (session) {
session.expiresAtMs = Date.now(); session.expiresAtMs = Date.now();
broadcastEvent({ broadcastEvent({
id: await nextEventId(), id: nextEventId(),
type: "session-expired", type: "session-expired",
sessionId, sessionId,
}); });
@ -47,9 +47,9 @@ async function clearServerSessionInternal(event: H3Event, sessions: ServerSessio
} }
export async function clearServerSession(event: H3Event) { export async function clearServerSession(event: H3Event) {
const sessions = await readSessions(); const sessions = readSessions();
if (await clearServerSessionInternal(event, sessions)) { if (await clearServerSessionInternal(event, sessions)) {
await writeSessions(sessions); writeSessions(sessions);
} }
deleteCookie(event, "session"); deleteCookie(event, "session");
} }
@ -61,7 +61,7 @@ export async function setServerSession(
authenticationSlug?: string, authenticationSlug?: string,
authenticationName?: string, authenticationName?: string,
) { ) {
const sessions = await readSessions(); const sessions = readSessions();
const runtimeConfig = useRuntimeConfig(event); const runtimeConfig = useRuntimeConfig(event);
await clearServerSessionInternal(event, sessions); await clearServerSessionInternal(event, sessions);
@ -78,14 +78,14 @@ export async function setServerSession(
}; };
sessions.push(newSession); sessions.push(newSession);
await writeSessions(sessions); writeSessions(sessions);
await setSignedCookie(event, "session", String(newSession.id), runtimeConfig.sessionDiscardTimeout) await setSignedCookie(event, "session", String(newSession.id), runtimeConfig.sessionDiscardTimeout)
return newSession; return newSession;
} }
async function rotateSession(event: H3Event, sessions: ServerSession[], session: ServerSession) { async function rotateSession(event: H3Event, sessions: ServerSession[], session: ServerSession) {
const runtimeConfig = useRuntimeConfig(event); const runtimeConfig = useRuntimeConfig(event);
const users = await readUsers(); const users = readUsers();
const account = users.find(user => !user.deleted && user.id === session.accountId); const account = users.find(user => !user.deleted && user.id === session.accountId);
const now = Date.now(); const now = Date.now();
const newSession: ServerSession = { const newSession: ServerSession = {
@ -94,12 +94,12 @@ async function rotateSession(event: H3Event, sessions: ServerSession[], session:
// Authentication provider is removed to avoid possibility of an infinite delay before using it. // Authentication provider is removed to avoid possibility of an infinite delay before using it.
rotatesAtMs: now + runtimeConfig.sessionRotatesTimeout * 1000, rotatesAtMs: now + runtimeConfig.sessionRotatesTimeout * 1000,
discardAtMs: now + runtimeConfig.sessionDiscardTimeout * 1000, discardAtMs: now + runtimeConfig.sessionDiscardTimeout * 1000,
id: await nextSessionId(), id: nextSessionId(),
}; };
session.successor = newSession.id; session.successor = newSession.id;
session.expiresAtMs = Date.now() + 10 * 1000; session.expiresAtMs = Date.now() + 10 * 1000;
sessions.push(newSession); sessions.push(newSession);
await writeSessions(sessions); writeSessions(sessions);
await setSignedCookie(event, "session", String(newSession.id), runtimeConfig.sessionDiscardTimeout) await setSignedCookie(event, "session", String(newSession.id), runtimeConfig.sessionDiscardTimeout)
return newSession; return newSession;
} }
@ -108,7 +108,7 @@ export async function getServerSession(event: H3Event, ignoreTaken: boolean) {
const sessionCookie = await getSignedCookie(event, "session"); const sessionCookie = await getSignedCookie(event, "session");
if (sessionCookie) { if (sessionCookie) {
const sessionId = parseInt(sessionCookie, 10); const sessionId = parseInt(sessionCookie, 10);
const sessions = await readSessions(); const sessions = readSessions();
const session = sessions.find(session => session.id === sessionId); const session = sessions.find(session => session.id === sessionId);
if (session) { if (session) {
const nowMs = Date.now(); const nowMs = Date.now();
@ -148,7 +148,7 @@ export async function requireServerSession(event: H3Event, message: string) {
export async function requireServerSessionWithUser(event: H3Event) { export async function requireServerSessionWithUser(event: H3Event) {
const message = "User session required"; const message = "User session required";
const session = await requireServerSession(event, message); const session = await requireServerSession(event, message);
const users = await readUsers(); const users = readUsers();
const account = users.find(user => user.id === session.accountId); const account = users.find(user => user.id === session.accountId);
if (session.accountId === undefined || !account || account.deleted) if (session.accountId === undefined || !account || account.deleted)
throw createError({ throw createError({
@ -163,7 +163,7 @@ export async function requireServerSessionWithUser(event: H3Event) {
export async function requireServerSessionWithAdmin(event: H3Event) { export async function requireServerSessionWithAdmin(event: H3Event) {
const message = "Admin session required"; const message = "Admin session required";
const session = await requireServerSession(event, message); const session = await requireServerSession(event, message);
const users = await readUsers(); const users = readUsers();
const account = users.find(user => user.id === session.accountId); const account = users.find(user => user.id === session.accountId);
if (session.access !== "admin" || account?.type !== "admin") { if (session.access !== "admin" || account?.type !== "admin") {
throw createError({ throw createError({
@ -176,9 +176,9 @@ export async function requireServerSessionWithAdmin(event: H3Event) {
} }
export async function serverSessionToApi(event: H3Event, session: ServerSession): Promise<ApiSession> { export async function serverSessionToApi(event: H3Event, session: ServerSession): Promise<ApiSession> {
const users = await readUsers(); const users = readUsers();
const account = users.find(user => !user.deleted && user.id === session.accountId); const account = users.find(user => !user.deleted && user.id === session.accountId);
const subscriptions = await readSubscriptions(); const subscriptions = readSubscriptions();
const push = Boolean( const push = Boolean(
subscriptions.find(sub => sub.type === "push" && sub.sessionId === session.id) subscriptions.find(sub => sub.type === "push" && sub.sessionId === session.id)
); );

View file

@ -33,7 +33,7 @@ async function useVapidDetails(event: H3Event) {
export async function sendPush(event: H3Event, title: string, body: string) { export async function sendPush(event: H3Event, title: string, body: string) {
const vapidDetails = await useVapidDetails(event); const vapidDetails = await useVapidDetails(event);
const payload = JSON.stringify({ title, body }); const payload = JSON.stringify({ title, body });
const subscriptions = await readSubscriptions(); const subscriptions = readSubscriptions();
console.log(`Sending "${payload}" to ${subscriptions.length} subscribers`); console.log(`Sending "${payload}" to ${subscriptions.length} subscribers`);
const removeIndexes = []; const removeIndexes = [];
for (let index = 0; index < subscriptions.length; index += 1) { for (let index = 0; index < subscriptions.length; index += 1) {
@ -65,7 +65,7 @@ export async function sendPush(event: H3Event, title: string, body: string) {
for (const index of removeIndexes) { for (const index of removeIndexes) {
subscriptions.splice(index, 1); subscriptions.splice(index, 1);
} }
await writeSubscriptions(subscriptions); writeSubscriptions(subscriptions);
} }
console.log("Push notices sent"); console.log("Push notices sent");
} }