Use sync access for the temp JSON file database
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:
parent
f9d188b2ba
commit
80cec71308
23 changed files with 138 additions and 138 deletions
|
@ -9,11 +9,11 @@ export default defineEventHandler(async (event) => {
|
|||
setHeader(event, "Content-Disposition", 'attachment; filename="database-dump.json"');
|
||||
setHeader(event, "Content-Type", "application/json; charset=utf-8");
|
||||
return {
|
||||
nextUserId: await readNextUserId(),
|
||||
users: await readUsers(),
|
||||
nextSessionId: await readNextSessionId(),
|
||||
sessions: await readSessions(),
|
||||
subscriptions: await readSubscriptions(),
|
||||
schedule: await readSchedule(),
|
||||
nextUserId: readNextUserId(),
|
||||
users: readUsers(),
|
||||
nextSessionId: readNextSessionId(),
|
||||
sessions: readSessions(),
|
||||
subscriptions: readSubscriptions(),
|
||||
schedule: readSchedule(),
|
||||
};
|
||||
})
|
||||
|
|
|
@ -7,14 +7,14 @@ import { generateDemoSchedule, generateDemoAccounts } from "~/server/generate-de
|
|||
export default defineEventHandler(async (event) => {
|
||||
await requireServerSessionWithAdmin(event);
|
||||
const accounts = generateDemoAccounts();
|
||||
await writeUsers(accounts);
|
||||
await writeSchedule(generateDemoSchedule());
|
||||
await writeAuthenticationMethods(accounts.map((user, index) => ({
|
||||
writeUsers(accounts);
|
||||
writeSchedule(generateDemoSchedule());
|
||||
writeAuthenticationMethods(accounts.map((user, index) => ({
|
||||
id: index,
|
||||
userId: user.id,
|
||||
provider: "demo",
|
||||
slug: user.name!,
|
||||
name: user.name!,
|
||||
})));
|
||||
await writeNextAuthenticationMethodId(Math.max(await nextAuthenticationMethodId(), accounts.length));
|
||||
writeNextAuthenticationMethodId(Math.max(nextAuthenticationMethodId(), accounts.length));
|
||||
})
|
||||
|
|
|
@ -29,20 +29,20 @@ export default defineEventHandler(async (event) => {
|
|||
});
|
||||
}
|
||||
|
||||
const currentNextUserId = await readNextUserId();
|
||||
await writeNextUserId(Math.max(currentNextUserId, snapshot.nextUserId));
|
||||
await writeUsers(snapshot.users);
|
||||
const currentNextSessionId = await readNextSessionId();
|
||||
await writeNextSessionId(Math.max(currentNextSessionId, snapshot.nextSessionId));
|
||||
const currentSessions = new Map((await readSessions()).map(session => [session.id, session]));
|
||||
await writeSessions(snapshot.sessions.filter(session => {
|
||||
const currentNextUserId = readNextUserId();
|
||||
writeNextUserId(Math.max(currentNextUserId, snapshot.nextUserId));
|
||||
writeUsers(snapshot.users);
|
||||
const currentNextSessionId = readNextSessionId();
|
||||
writeNextSessionId(Math.max(currentNextSessionId, snapshot.nextSessionId));
|
||||
const currentSessions = new Map((readSessions()).map(session => [session.id, session]));
|
||||
writeSessions(snapshot.sessions.filter(session => {
|
||||
const current = currentSessions.get(session.id);
|
||||
// Only keep sessions that match the account id in both sets to avoid
|
||||
// resurrecting deleted sessions. This will still cause session cross
|
||||
// pollution if a snapshot from another instance is loaded here.
|
||||
return current?.accountId !== undefined && current.accountId === session.accountId;
|
||||
}));
|
||||
await writeSubscriptions(snapshot.subscriptions);
|
||||
await writeSchedule(snapshot.schedule);
|
||||
writeSubscriptions(snapshot.subscriptions);
|
||||
writeSchedule(snapshot.schedule);
|
||||
await sendRedirect(event, "/");
|
||||
})
|
||||
|
|
|
@ -6,5 +6,5 @@ import { deleteDatabase } from "~/server/database";
|
|||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireServerSessionWithAdmin(event);
|
||||
await deleteDatabase();
|
||||
deleteDatabase();
|
||||
})
|
||||
|
|
|
@ -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);
|
||||
if (!user || user.deleted) {
|
||||
throw createError({
|
||||
|
@ -52,28 +52,28 @@ export default defineEventHandler(async (event) => {
|
|||
user.name = patch.name;
|
||||
}
|
||||
user.updatedAt = new Date().toISOString();
|
||||
await writeUsers(users);
|
||||
writeUsers(users);
|
||||
broadcastEvent({
|
||||
id: await nextEventId(),
|
||||
id: nextEventId(),
|
||||
type: "user-update",
|
||||
data: serverUserToApi(user),
|
||||
});
|
||||
|
||||
// Rotate sessions with the user in it if the access changed
|
||||
if (accessChanged) {
|
||||
const sessions = await readSessions();
|
||||
const sessions = readSessions();
|
||||
const nowMs = Date.now();
|
||||
for (const session of sessions) {
|
||||
if (session.accountId === user.id) {
|
||||
session.rotatesAtMs = nowMs;
|
||||
broadcastEvent({
|
||||
id: await nextEventId(),
|
||||
id: nextEventId(),
|
||||
type: "session-expired",
|
||||
sessionId: session.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
await writeSessions(sessions);
|
||||
writeSessions(sessions);
|
||||
}
|
||||
|
||||
// Update Schedule counts.
|
||||
|
|
|
@ -11,11 +11,11 @@ import { broadcastEvent, cancelAccountStreams } from "~/server/streams";
|
|||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const serverSession = await requireServerSessionWithUser(event);
|
||||
let users = await readUsers();
|
||||
let users = readUsers();
|
||||
|
||||
// Expire sessions for this user
|
||||
const expiredSessionIds = new Set<number>();
|
||||
let sessions = await readSessions();
|
||||
let sessions = readSessions();
|
||||
const nowMs = Date.now();
|
||||
for (const session of sessions) {
|
||||
if (
|
||||
|
@ -25,7 +25,7 @@ export default defineEventHandler(async (event) => {
|
|||
) {
|
||||
session.expiresAtMs = nowMs;
|
||||
broadcastEvent({
|
||||
id: await nextEventId(),
|
||||
id: nextEventId(),
|
||||
type: "session-expired",
|
||||
sessionId: session.id,
|
||||
});
|
||||
|
@ -33,24 +33,24 @@ export default defineEventHandler(async (event) => {
|
|||
}
|
||||
}
|
||||
cancelAccountStreams(serverSession.accountId);
|
||||
await writeSessions(sessions);
|
||||
writeSessions(sessions);
|
||||
await deleteCookie(event, "session");
|
||||
|
||||
// Remove subscriptions for this user
|
||||
let subscriptions = await readSubscriptions();
|
||||
let subscriptions = readSubscriptions();
|
||||
subscriptions = subscriptions.filter(
|
||||
subscription => !expiredSessionIds.has(subscription.sessionId)
|
||||
);
|
||||
await writeSubscriptions(subscriptions);
|
||||
writeSubscriptions(subscriptions);
|
||||
|
||||
// Remove the user
|
||||
const account = users.find(user => user.id === serverSession.accountId)!;
|
||||
const now = new Date(nowMs).toISOString();
|
||||
account.deleted = true;
|
||||
account.updatedAt = now;
|
||||
await writeUsers(users);
|
||||
writeUsers(users);
|
||||
await broadcastEvent({
|
||||
id: await nextEventId(),
|
||||
id: nextEventId(),
|
||||
type: "user-update",
|
||||
data: {
|
||||
id: account.id,
|
||||
|
|
|
@ -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);
|
||||
if (!account) {
|
||||
throw Error("Account does not exist");
|
||||
|
@ -70,7 +70,7 @@ export default defineEventHandler(async (event) => {
|
|||
else
|
||||
delete account.locale;
|
||||
}
|
||||
await writeUsers(users);
|
||||
writeUsers(users);
|
||||
|
||||
// Update Schedule counts.
|
||||
await updateScheduleInterestedCounts(users);
|
||||
|
|
|
@ -18,7 +18,7 @@ export default defineEventHandler(async (event): Promise<ApiSession> => {
|
|||
const body = await readBody(event);
|
||||
const name = body?.name;
|
||||
|
||||
const users = await readUsers();
|
||||
const users = readUsers();
|
||||
let user: ServerUser;
|
||||
if (typeof name === "string") {
|
||||
if (name === "") {
|
||||
|
@ -36,7 +36,7 @@ export default defineEventHandler(async (event): Promise<ApiSession> => {
|
|||
|
||||
const firstUser = users.every(user => user.type === "anonymous");
|
||||
user = {
|
||||
id: await nextUserId(),
|
||||
id: nextUserId(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
type: firstUser ? "admin" : "regular",
|
||||
name,
|
||||
|
@ -44,7 +44,7 @@ export default defineEventHandler(async (event): Promise<ApiSession> => {
|
|||
|
||||
} else if (name === undefined) {
|
||||
user = {
|
||||
id: await nextUserId(),
|
||||
id: nextUserId(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
type: "anonymous",
|
||||
};
|
||||
|
@ -76,19 +76,19 @@ export default defineEventHandler(async (event): Promise<ApiSession> => {
|
|||
});
|
||||
}
|
||||
authMethods.push({
|
||||
id: await nextAuthenticationMethodId(),
|
||||
id: nextAuthenticationMethodId(),
|
||||
userId: user.id,
|
||||
provider: session.authenticationProvider,
|
||||
slug: session.authenticationSlug!,
|
||||
name: session.authenticationName!,
|
||||
})
|
||||
await writeAuthenticationMethods(authMethods);
|
||||
writeAuthenticationMethods(authMethods);
|
||||
}
|
||||
|
||||
users.push(user);
|
||||
await writeUsers(users);
|
||||
writeUsers(users);
|
||||
await broadcastEvent({
|
||||
id: await nextEventId(),
|
||||
id: nextEventId(),
|
||||
type: "user-update",
|
||||
data: user,
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
let session;
|
||||
if (method) {
|
||||
const users = await readUsers();
|
||||
const users = readUsers();
|
||||
const account = users.find(user => !user.deleted && user.id === method.userId);
|
||||
session = await setServerSession(event, account);
|
||||
} else {
|
||||
|
|
|
@ -84,11 +84,11 @@ export default defineEventHandler(async (event): Promise<ApiSession> => {
|
|||
}
|
||||
|
||||
const slug = String(data.authData.id);
|
||||
const authMethods = await readAuthenticationMethods();
|
||||
const authMethods = readAuthenticationMethods();
|
||||
const method = authMethods.find(method => method.provider === "telegram" && method.slug === slug);
|
||||
let session;
|
||||
if (method) {
|
||||
const users = await readUsers();
|
||||
const users = readUsers();
|
||||
const account = users.find(user => !user.deleted && user.id === method.userId);
|
||||
session = await setServerSession(event, account);
|
||||
} else {
|
||||
|
|
|
@ -8,7 +8,7 @@ import { cancelSessionStreams } from "~/server/streams";
|
|||
export default defineEventHandler(async (event) => {
|
||||
const session = await getServerSession(event, true);
|
||||
if (session) {
|
||||
const users = await readUsers();
|
||||
const users = readUsers();
|
||||
const account = users.find(user => user.id === session.accountId);
|
||||
if (account?.type === "anonymous") {
|
||||
throw createError({
|
||||
|
|
|
@ -6,6 +6,6 @@
|
|||
import { readEvents } from "../database";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const events = await readEvents();
|
||||
const events = readEvents();
|
||||
return events[events.length - 1]?. id ?? 0;
|
||||
});
|
||||
|
|
|
@ -35,7 +35,7 @@ export default defineEventHandler(async (event) => {
|
|||
});
|
||||
}
|
||||
|
||||
const schedule = await readSchedule();
|
||||
const schedule = readSchedule();
|
||||
|
||||
if (schedule.deleted) {
|
||||
throw createError({
|
||||
|
@ -85,9 +85,9 @@ export default defineEventHandler(async (event) => {
|
|||
applyUpdatesToArray(update.shifts, schedule.shifts = schedule.shifts ?? []);
|
||||
}
|
||||
|
||||
await writeSchedule(schedule);
|
||||
writeSchedule(schedule);
|
||||
await broadcastEvent({
|
||||
id: await nextEventId(),
|
||||
id: nextEventId(),
|
||||
type: "schedule-update",
|
||||
updatedFrom,
|
||||
data: update,
|
||||
|
|
|
@ -6,6 +6,6 @@ import { readSchedule } from "~/server/database";
|
|||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await getServerSession(event, false);
|
||||
const schedule = await readSchedule();
|
||||
const schedule = readSchedule();
|
||||
return canSeeCrew(session?.access) ? schedule : filterSchedule(schedule);
|
||||
});
|
||||
|
|
|
@ -20,7 +20,7 @@ export default defineEventHandler(async (event) => {
|
|||
message: z.prettifyError(error),
|
||||
});
|
||||
}
|
||||
const subscriptions = await readSubscriptions();
|
||||
const subscriptions = readSubscriptions();
|
||||
const existingIndex = subscriptions.findIndex(
|
||||
sub => sub.type === "push" && sub.sessionId === session.id
|
||||
);
|
||||
|
@ -34,7 +34,7 @@ export default defineEventHandler(async (event) => {
|
|||
} else {
|
||||
subscriptions.push(subscription);
|
||||
}
|
||||
await writeSubscriptions(subscriptions);
|
||||
writeSubscriptions(subscriptions);
|
||||
if (existingIndex !== -1) {
|
||||
return { message: "Existing subscription refreshed."};
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { readSubscriptions, writeSubscriptions } from "~/server/database";
|
|||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await requireServerSessionWithUser(event);
|
||||
const subscriptions = await readSubscriptions();
|
||||
const subscriptions = readSubscriptions();
|
||||
const existingIndex = subscriptions.findIndex(
|
||||
sub => sub.type === "push" && sub.sessionId === session.id
|
||||
);
|
||||
|
@ -15,6 +15,6 @@ export default defineEventHandler(async (event) => {
|
|||
} else {
|
||||
return { message: "No subscription registered."};
|
||||
}
|
||||
await writeSubscriptions(subscriptions);
|
||||
writeSubscriptions(subscriptions);
|
||||
return { message: "Existing subscription removed."};
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ const detailsSchema = z.object({
|
|||
|
||||
export default defineEventHandler(async (event) => {
|
||||
await requireServerSessionWithAdmin(event);
|
||||
const users = await readUsers();
|
||||
const users = readUsers();
|
||||
const { success, error, data: params } = detailsSchema.safeParse(getRouterParams(event));
|
||||
if (!success) {
|
||||
throw createError({
|
||||
|
|
|
@ -6,7 +6,7 @@ import { readUsers } from "~/server/database"
|
|||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const session = await requireServerSessionWithUser(event);
|
||||
const users = await readUsers();
|
||||
const users = readUsers();
|
||||
|
||||
if (session.access === "admin") {
|
||||
return users.map(serverUserToApi);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
|
||||
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 { Id } from "~/shared/types/common";
|
||||
|
||||
|
@ -53,9 +53,9 @@ const nextAuthenticationMethodIdPath = "data/auth-method-id.json"
|
|||
const nextEventIdPath = "data/next-event-id.json";
|
||||
const eventsPath = "data/events.json";
|
||||
|
||||
async function remove(path: string) {
|
||||
function remove(path: string) {
|
||||
try {
|
||||
await unlink(path);
|
||||
unlinkSync(path);
|
||||
} catch (err: any) {
|
||||
if (err.code !== "ENOENT") {
|
||||
throw err;
|
||||
|
@ -63,17 +63,17 @@ async function remove(path: string) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function deleteDatabase() {
|
||||
await remove(schedulePath);
|
||||
await remove(subscriptionsPath);
|
||||
await remove(usersPath);
|
||||
await remove(sessionsPath);
|
||||
export function deleteDatabase() {
|
||||
remove(schedulePath);
|
||||
remove(subscriptionsPath);
|
||||
remove(usersPath);
|
||||
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;
|
||||
try {
|
||||
data = JSON.parse(await readFile(filePath, "utf-8"));
|
||||
data = JSON.parse(readFileSync(filePath, "utf-8"));
|
||||
} catch (err: any) {
|
||||
if (err.code !== "ENOENT")
|
||||
throw err;
|
||||
|
@ -82,19 +82,19 @@ async function readJson<T>(filePath: string, fallback: T) {
|
|||
return data;
|
||||
}
|
||||
|
||||
export async function readSchedule() {
|
||||
export function readSchedule() {
|
||||
return readJson(schedulePath, (): ApiSchedule => ({
|
||||
id: 111,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function writeSchedule(schedule: ApiSchedule) {
|
||||
await writeFile(schedulePath, JSON.stringify(schedule, undefined, "\t") + "\n", "utf-8");
|
||||
export function writeSchedule(schedule: ApiSchedule) {
|
||||
writeFileSync(schedulePath, JSON.stringify(schedule, undefined, "\t") + "\n", "utf-8");
|
||||
}
|
||||
|
||||
export async function readSubscriptions() {
|
||||
let subscriptions = await readJson<ApiSubscription[]>(subscriptionsPath, []);
|
||||
export function readSubscriptions() {
|
||||
let subscriptions = readJson<ApiSubscription[]>(subscriptionsPath, []);
|
||||
if (subscriptions.length && "keys" in subscriptions[0]) {
|
||||
// Discard old format
|
||||
subscriptions = [];
|
||||
|
@ -102,89 +102,89 @@ export async function readSubscriptions() {
|
|||
return subscriptions;
|
||||
}
|
||||
|
||||
export async function writeSubscriptions(subscriptions: ApiSubscription[]) {
|
||||
await writeFile(subscriptionsPath, JSON.stringify(subscriptions, undefined, "\t") + "\n", "utf-8");
|
||||
export function writeSubscriptions(subscriptions: ApiSubscription[]) {
|
||||
writeFileSync(subscriptionsPath, JSON.stringify(subscriptions, undefined, "\t") + "\n", "utf-8");
|
||||
}
|
||||
|
||||
export async function readNextUserId() {
|
||||
return await readJson(nextUserIdPath, 0);
|
||||
export function readNextUserId() {
|
||||
return readJson(nextUserIdPath, 0);
|
||||
}
|
||||
|
||||
export async function writeNextUserId(nextId: number) {
|
||||
await writeFile(nextUserIdPath, String(nextId), "utf-8");
|
||||
export function writeNextUserId(nextId: number) {
|
||||
writeFileSync(nextUserIdPath, String(nextId), "utf-8");
|
||||
}
|
||||
|
||||
export async function nextUserId() {
|
||||
let nextId = await readJson(nextUserIdPath, 0);
|
||||
export function nextUserId() {
|
||||
let nextId = readJson(nextUserIdPath, 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;
|
||||
}
|
||||
|
||||
export async function readUsers() {
|
||||
return await readJson(usersPath, (): ServerUser[] => []);
|
||||
export function readUsers() {
|
||||
return readJson(usersPath, (): ServerUser[] => []);
|
||||
}
|
||||
|
||||
export async function writeUsers(users: ServerUser[]) {
|
||||
await writeFile(usersPath, JSON.stringify(users, undefined, "\t") + "\n", "utf-8");
|
||||
export function writeUsers(users: ServerUser[]) {
|
||||
writeFileSync(usersPath, JSON.stringify(users, undefined, "\t") + "\n", "utf-8");
|
||||
}
|
||||
|
||||
export async function readNextSessionId() {
|
||||
return await readJson(nextSessionIdPath, 0);
|
||||
export function readNextSessionId() {
|
||||
return readJson(nextSessionIdPath, 0);
|
||||
}
|
||||
|
||||
export async function writeNextSessionId(nextId: number) {
|
||||
await writeFile(nextSessionIdPath, String(nextId), "utf-8");
|
||||
export function writeNextSessionId(nextId: number) {
|
||||
writeFileSync(nextSessionIdPath, String(nextId), "utf-8");
|
||||
}
|
||||
|
||||
export async function nextSessionId() {
|
||||
const nextId = await readJson(nextSessionIdPath, 0);
|
||||
await writeFile(nextSessionIdPath, String(nextId + 1), "utf-8");
|
||||
export function nextSessionId() {
|
||||
const nextId = readJson(nextSessionIdPath, 0);
|
||||
writeFileSync(nextSessionIdPath, String(nextId + 1), "utf-8");
|
||||
return nextId;
|
||||
}
|
||||
|
||||
export async function readSessions() {
|
||||
export function readSessions() {
|
||||
return readJson<ServerSession[]>(sessionsPath, [])
|
||||
}
|
||||
|
||||
export async function writeSessions(sessions: ServerSession[]) {
|
||||
await writeFile(sessionsPath, JSON.stringify(sessions, undefined, "\t") + "\n", "utf-8");
|
||||
export function writeSessions(sessions: ServerSession[]) {
|
||||
writeFileSync(sessionsPath, JSON.stringify(sessions, undefined, "\t") + "\n", "utf-8");
|
||||
}
|
||||
|
||||
export async function nextAuthenticationMethodId() {
|
||||
const nextId = await readJson(nextAuthenticationMethodIdPath, 0);
|
||||
await writeFile(nextAuthenticationMethodIdPath, String(nextId + 1), "utf-8");
|
||||
export function nextAuthenticationMethodId() {
|
||||
const nextId = readJson(nextAuthenticationMethodIdPath, 0);
|
||||
writeFileSync(nextAuthenticationMethodIdPath, String(nextId + 1), "utf-8");
|
||||
return nextId;
|
||||
}
|
||||
|
||||
export async function writeNextAuthenticationMethodId(nextId: number) {
|
||||
await writeFile(nextAuthenticationMethodIdPath, String(nextId), "utf-8");
|
||||
export function writeNextAuthenticationMethodId(nextId: number) {
|
||||
writeFileSync(nextAuthenticationMethodIdPath, String(nextId), "utf-8");
|
||||
}
|
||||
|
||||
export async function readAuthenticationMethods() {
|
||||
export function readAuthenticationMethods() {
|
||||
return readJson<ServerAuthenticationMethod[]>(authMethodPath, [])
|
||||
}
|
||||
|
||||
export async function writeAuthenticationMethods(authMethods: ServerAuthenticationMethod[]) {
|
||||
await writeFile(authMethodPath, JSON.stringify(authMethods, undefined, "\t") + "\n", "utf-8");
|
||||
export function writeAuthenticationMethods(authMethods: ServerAuthenticationMethod[]) {
|
||||
writeFileSync(authMethodPath, JSON.stringify(authMethods, undefined, "\t") + "\n", "utf-8");
|
||||
}
|
||||
|
||||
export async function nextEventId() {
|
||||
const nextId = await readJson(nextEventIdPath, 0);
|
||||
await writeFile(nextEventIdPath, String(nextId + 1), "utf-8");
|
||||
export function nextEventId() {
|
||||
const nextId = readJson(nextEventIdPath, 0);
|
||||
writeFileSync(nextEventIdPath, String(nextId + 1), "utf-8");
|
||||
return nextId;
|
||||
}
|
||||
|
||||
export async function writeNextEventId(nextId: number) {
|
||||
await writeFile(nextEventIdPath, String(nextId), "utf-8");
|
||||
export function writeNextEventId(nextId: number) {
|
||||
writeFileSync(nextEventIdPath, String(nextId), "utf-8");
|
||||
}
|
||||
|
||||
export async function readEvents() {
|
||||
export function readEvents() {
|
||||
return readJson<ApiEvent[]>(eventsPath, [])
|
||||
}
|
||||
|
||||
export async function writeEvents(events: ApiEvent[]) {
|
||||
await writeFile(eventsPath, JSON.stringify(events, undefined, "\t") + "\n", "utf-8");
|
||||
export function writeEvents(events: ApiEvent[]) {
|
||||
writeFileSync(eventsPath, JSON.stringify(events, undefined, "\t") + "\n", "utf-8");
|
||||
}
|
||||
|
|
|
@ -33,8 +33,8 @@ export async function createEventStream(
|
|||
) {
|
||||
const runtimeConfig = useRuntimeConfig(event);
|
||||
const now = Date.now();
|
||||
const events = (await readEvents()).filter(e => e.id > lastEventId);
|
||||
const users = await readUsers();
|
||||
const events = (readEvents()).filter(e => e.id > lastEventId);
|
||||
const users = readUsers();
|
||||
const apiSession = session ? await serverSessionToApi(event, session) : undefined;
|
||||
let userType: ApiAccount["type"] | undefined;
|
||||
if (session?.accountId !== undefined) {
|
||||
|
@ -182,9 +182,9 @@ function encodeEvent(event: ApiEvent, userType: ApiAccount["type"] | undefined)
|
|||
}
|
||||
|
||||
export async function broadcastEvent(event: ApiEvent) {
|
||||
const events = await readEvents();
|
||||
const events = readEvents();
|
||||
events.push(event);
|
||||
await writeEvents(events);
|
||||
writeEvents(events);
|
||||
}
|
||||
|
||||
function sendEventToStream(stream: EventStream, event: ApiEvent) {
|
||||
|
@ -222,7 +222,7 @@ async function sendEventUpdates() {
|
|||
|
||||
// Send events.
|
||||
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)
|
||||
console.log(`broadcasting ${events.length} event(s) to ${streams.size} client(s)`);
|
||||
for (const stream of streams.values()) {
|
||||
|
|
|
@ -20,7 +20,7 @@ export async function updateScheduleInterestedCounts(users: ServerUser[]) {
|
|||
eventSlotCounts.set(id, (eventSlotCounts.get(id) ?? 0) + 1);
|
||||
}
|
||||
|
||||
const schedule = await readSchedule();
|
||||
const schedule = readSchedule();
|
||||
if (schedule.deleted) {
|
||||
throw new Error("Deleted schedule not implemented");
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ export async function updateScheduleInterestedCounts(users: ServerUser[]) {
|
|||
schedule.updatedAt = updatedFrom;
|
||||
await writeSchedule(schedule);
|
||||
await broadcastEvent({
|
||||
id: await nextEventId(),
|
||||
id: nextEventId(),
|
||||
type: "schedule-update",
|
||||
updatedFrom,
|
||||
data: update,
|
||||
|
|
|
@ -19,11 +19,11 @@ import type { ApiAuthenticationProvider, ApiSession } from "~/shared/types/api";
|
|||
import { serverUserToApiAccount } from "./user";
|
||||
|
||||
async function removeSessionSubscription(sessionId: number) {
|
||||
const subscriptions = await readSubscriptions();
|
||||
const subscriptions = readSubscriptions();
|
||||
const index = subscriptions.findIndex(subscription => subscription.sessionId === sessionId);
|
||||
if (index !== -1) {
|
||||
subscriptions.splice(index, 1);
|
||||
await writeSubscriptions(subscriptions);
|
||||
writeSubscriptions(subscriptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,7 @@ async function clearServerSessionInternal(event: H3Event, sessions: ServerSessio
|
|||
if (session) {
|
||||
session.expiresAtMs = Date.now();
|
||||
broadcastEvent({
|
||||
id: await nextEventId(),
|
||||
id: nextEventId(),
|
||||
type: "session-expired",
|
||||
sessionId,
|
||||
});
|
||||
|
@ -47,9 +47,9 @@ async function clearServerSessionInternal(event: H3Event, sessions: ServerSessio
|
|||
}
|
||||
|
||||
export async function clearServerSession(event: H3Event) {
|
||||
const sessions = await readSessions();
|
||||
const sessions = readSessions();
|
||||
if (await clearServerSessionInternal(event, sessions)) {
|
||||
await writeSessions(sessions);
|
||||
writeSessions(sessions);
|
||||
}
|
||||
deleteCookie(event, "session");
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ export async function setServerSession(
|
|||
authenticationSlug?: string,
|
||||
authenticationName?: string,
|
||||
) {
|
||||
const sessions = await readSessions();
|
||||
const sessions = readSessions();
|
||||
const runtimeConfig = useRuntimeConfig(event);
|
||||
await clearServerSessionInternal(event, sessions);
|
||||
|
||||
|
@ -78,14 +78,14 @@ export async function setServerSession(
|
|||
};
|
||||
|
||||
sessions.push(newSession);
|
||||
await writeSessions(sessions);
|
||||
writeSessions(sessions);
|
||||
await setSignedCookie(event, "session", String(newSession.id), runtimeConfig.sessionDiscardTimeout)
|
||||
return newSession;
|
||||
}
|
||||
|
||||
async function rotateSession(event: H3Event, sessions: ServerSession[], session: ServerSession) {
|
||||
const runtimeConfig = useRuntimeConfig(event);
|
||||
const users = await readUsers();
|
||||
const users = readUsers();
|
||||
const account = users.find(user => !user.deleted && user.id === session.accountId);
|
||||
const now = Date.now();
|
||||
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.
|
||||
rotatesAtMs: now + runtimeConfig.sessionRotatesTimeout * 1000,
|
||||
discardAtMs: now + runtimeConfig.sessionDiscardTimeout * 1000,
|
||||
id: await nextSessionId(),
|
||||
id: nextSessionId(),
|
||||
};
|
||||
session.successor = newSession.id;
|
||||
session.expiresAtMs = Date.now() + 10 * 1000;
|
||||
sessions.push(newSession);
|
||||
await writeSessions(sessions);
|
||||
writeSessions(sessions);
|
||||
await setSignedCookie(event, "session", String(newSession.id), runtimeConfig.sessionDiscardTimeout)
|
||||
return newSession;
|
||||
}
|
||||
|
@ -108,7 +108,7 @@ export async function getServerSession(event: H3Event, ignoreTaken: boolean) {
|
|||
const sessionCookie = await getSignedCookie(event, "session");
|
||||
if (sessionCookie) {
|
||||
const sessionId = parseInt(sessionCookie, 10);
|
||||
const sessions = await readSessions();
|
||||
const sessions = readSessions();
|
||||
const session = sessions.find(session => session.id === sessionId);
|
||||
if (session) {
|
||||
const nowMs = Date.now();
|
||||
|
@ -148,7 +148,7 @@ export async function requireServerSession(event: H3Event, message: string) {
|
|||
export async function requireServerSessionWithUser(event: H3Event) {
|
||||
const message = "User session required";
|
||||
const session = await requireServerSession(event, message);
|
||||
const users = await readUsers();
|
||||
const users = readUsers();
|
||||
const account = users.find(user => user.id === session.accountId);
|
||||
if (session.accountId === undefined || !account || account.deleted)
|
||||
throw createError({
|
||||
|
@ -163,7 +163,7 @@ export async function requireServerSessionWithUser(event: H3Event) {
|
|||
export async function requireServerSessionWithAdmin(event: H3Event) {
|
||||
const message = "Admin session required";
|
||||
const session = await requireServerSession(event, message);
|
||||
const users = await readUsers();
|
||||
const users = readUsers();
|
||||
const account = users.find(user => user.id === session.accountId);
|
||||
if (session.access !== "admin" || account?.type !== "admin") {
|
||||
throw createError({
|
||||
|
@ -176,9 +176,9 @@ export async function requireServerSessionWithAdmin(event: H3Event) {
|
|||
}
|
||||
|
||||
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 subscriptions = await readSubscriptions();
|
||||
const subscriptions = readSubscriptions();
|
||||
const push = Boolean(
|
||||
subscriptions.find(sub => sub.type === "push" && sub.sessionId === session.id)
|
||||
);
|
||||
|
|
|
@ -33,7 +33,7 @@ async function useVapidDetails(event: H3Event) {
|
|||
export async function sendPush(event: H3Event, title: string, body: string) {
|
||||
const vapidDetails = await useVapidDetails(event);
|
||||
const payload = JSON.stringify({ title, body });
|
||||
const subscriptions = await readSubscriptions();
|
||||
const subscriptions = readSubscriptions();
|
||||
console.log(`Sending "${payload}" to ${subscriptions.length} subscribers`);
|
||||
const removeIndexes = [];
|
||||
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) {
|
||||
subscriptions.splice(index, 1);
|
||||
}
|
||||
await writeSubscriptions(subscriptions);
|
||||
writeSubscriptions(subscriptions);
|
||||
}
|
||||
console.log("Push notices sent");
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue