Store events that are to be broadcasted in the database, and fetch events to serve in the /api/event stream to the client from the database. This ensures that events are not lost if the operation to open the stream takes longer than usual, or the client was not connected at the time the event was broadcast. To ensure no events are lost in the transition from server generating the page to the client hydrating and establishing a connection with the event stream, the /api/last-event-id endpoint is first queried on the server before any other entities is fetched from the database. The client then passes this id when establishing the event stream, and receives all events greater than that id.
97 lines
2.6 KiB
TypeScript
97 lines
2.6 KiB
TypeScript
/*
|
|
SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
|
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
import { readUsers, writeUsers, nextUserId, type ServerUser, readAuthenticationMethods, nextAuthenticationMethodId, writeAuthenticationMethods, nextEventId } from "~/server/database";
|
|
import { broadcastEvent } from "~/server/streams";
|
|
import type { ApiSession } from "~/shared/types/api";
|
|
|
|
export default defineEventHandler(async (event): Promise<ApiSession> => {
|
|
let session = await getServerSession(event, false);
|
|
if (session?.accountId !== undefined) {
|
|
throw createError({
|
|
status: 409,
|
|
message: "Cannot create account while logged in to an account."
|
|
});
|
|
}
|
|
|
|
const body = await readBody(event);
|
|
const name = body?.name;
|
|
|
|
const users = await readUsers();
|
|
let user: ServerUser;
|
|
if (typeof name === "string") {
|
|
if (name === "") {
|
|
throw createError({
|
|
status: 400,
|
|
message: "Name cannot be blank",
|
|
});
|
|
}
|
|
if (users.some(user => user.name && user.name.toLowerCase() === name.toLowerCase())) {
|
|
throw createError({
|
|
status: 409,
|
|
message: "User already exists",
|
|
});
|
|
}
|
|
|
|
const firstUser = users.every(user => user.type === "anonymous");
|
|
user = {
|
|
id: await nextUserId(),
|
|
updatedAt: new Date().toISOString(),
|
|
type: firstUser ? "admin" : "regular",
|
|
name,
|
|
};
|
|
|
|
} else if (name === undefined) {
|
|
user = {
|
|
id: await nextUserId(),
|
|
updatedAt: new Date().toISOString(),
|
|
type: "anonymous",
|
|
};
|
|
} else {
|
|
throw createError({
|
|
status: 400,
|
|
message: "Invalid name",
|
|
});
|
|
}
|
|
|
|
if (user.type !== "anonymous") {
|
|
if (!session?.authenticationProvider) {
|
|
throw createError({
|
|
statusCode: 409,
|
|
statusMessage: "Conflict",
|
|
message: "User account need an authentication method associated with it.",
|
|
});
|
|
}
|
|
const authMethods = await readAuthenticationMethods();
|
|
const method = authMethods.find(method => (
|
|
method.provider === session.authenticationProvider
|
|
&& method.slug === session.authenticationSlug
|
|
));
|
|
if (method) {
|
|
throw createError({
|
|
statusCode: 409,
|
|
statusMessage: "Conflict",
|
|
message: "A user is already associated with the authentication method",
|
|
});
|
|
}
|
|
authMethods.push({
|
|
id: await nextAuthenticationMethodId(),
|
|
userId: user.id,
|
|
provider: session.authenticationProvider,
|
|
slug: session.authenticationSlug!,
|
|
name: session.authenticationName!,
|
|
})
|
|
await writeAuthenticationMethods(authMethods);
|
|
}
|
|
|
|
users.push(user);
|
|
await writeUsers(users);
|
|
await broadcastEvent({
|
|
id: await nextEventId(),
|
|
type: "user-update",
|
|
data: user,
|
|
});
|
|
const newSession = await setServerSession(event, user);
|
|
return await serverSessionToApi(event, newSession);
|
|
})
|