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.
81 lines
2 KiB
TypeScript
81 lines
2 KiB
TypeScript
/*
|
|
SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
|
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
import { nextEventId, readSessions, readUsers, writeSessions, writeUsers } from "~/server/database";
|
|
import { apiUserPatchSchema } from "~/shared/types/api";
|
|
import { z } from "zod/v4-mini";
|
|
import { broadcastEvent } from "~/server/streams";
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
await requireServerSessionWithAdmin(event);
|
|
const { success, error, data: patch } = apiUserPatchSchema.safeParse(await readBody(event));
|
|
if (!success) {
|
|
throw createError({
|
|
status: 400,
|
|
statusText: "Bad Request",
|
|
message: z.prettifyError(error),
|
|
});
|
|
}
|
|
|
|
const users = readUsers();
|
|
const user = users.find(user => user.id === patch.id);
|
|
if (!user || user.deleted) {
|
|
throw createError({
|
|
status: 409,
|
|
statusText: "Conflict",
|
|
message: "User does not exist",
|
|
});
|
|
|
|
}
|
|
|
|
let accessChanged = false;
|
|
if (patch.type && patch.type !== user.type) {
|
|
if (patch.type === "anonymous" || user.type === "anonymous") {
|
|
throw createError({
|
|
status: 409,
|
|
statusText: "Conflict",
|
|
message: "Anonymous user type cannot be changed.",
|
|
});
|
|
}
|
|
user.type = patch.type;
|
|
accessChanged = true;
|
|
}
|
|
if (patch.name) {
|
|
if (user.type === "anonymous") {
|
|
throw createError({
|
|
status: 409,
|
|
statusText: "Conflict",
|
|
message: "Anonymous user cannot have name set.",
|
|
});
|
|
}
|
|
user.name = patch.name;
|
|
}
|
|
user.updatedAt = new Date().toISOString();
|
|
writeUsers(users);
|
|
broadcastEvent({
|
|
id: nextEventId(),
|
|
type: "user-update",
|
|
data: serverUserToApi(user),
|
|
});
|
|
|
|
// Rotate sessions with the user in it if the access changed
|
|
if (accessChanged) {
|
|
const sessions = readSessions();
|
|
const nowMs = Date.now();
|
|
for (const session of sessions) {
|
|
if (session.accountId === user.id) {
|
|
session.rotatesAtMs = nowMs;
|
|
broadcastEvent({
|
|
id: nextEventId(),
|
|
type: "session-expired",
|
|
sessionId: session.id,
|
|
});
|
|
}
|
|
}
|
|
writeSessions(sessions);
|
|
}
|
|
|
|
// Update Schedule counts.
|
|
await updateScheduleInterestedCounts(users);
|
|
})
|