User CRUD on backend and refactors

This commit is contained in:
2025-10-09 16:25:15 +02:00
parent 486ff6279d
commit 2f96b55205
7 changed files with 246 additions and 174 deletions

View File

@@ -1,6 +1,6 @@
import * as Body from "common/Body";
import { fetch } from "common/Fetch";
import { Cause, Effect, Layer, Option, pipe, Record, Redacted, Stream } from "effect";
import { Cause, Effect, Layer, Match, Option, pipe, Record, Redacted, Stream } from "effect";
import * as path from "node:path";
import { config } from "./config";
import * as Authentication from "./services/Authentication";
@@ -10,6 +10,17 @@ import { handle } from "./the_api";
const FRONTEND_ROOT = "packages/frontend/build";
const FRONTEND_ASSETS_ROOT = path.join(FRONTEND_ROOT, "assets");
const CORS_HEADERS: [string, string][] = Match.value(config.NODE_ENV).pipe(
Match.when("development", (): [string, string][] => [
["Access-Control-Allow-Origin", "http://localhost:5173"],
["Access-Control-Allow-Methods", "POST, OPTIONS"],
["Access-Control-Allow-Credentials", "true"],
["Access-Control-Allow-Headers", "Content-Type"],
]),
Match.when("production", () => []),
Match.exhaustive,
);
const assetRoutes = await pipe(
Stream.fromAsyncIterable(
new Bun.Glob("**/*").scan(FRONTEND_ASSETS_ROOT),
@@ -24,81 +35,28 @@ const assetRoutes = await pipe(
Effect.runPromise,
);
const CORS_HEADERS: [string, string][] = [
["Access-Control-Allow-Origin", "http://localhost:5173"],
["Access-Control-Allow-Methods", "POST, OPTIONS"],
["Access-Control-Allow-Credentials", "true"],
["Access-Control-Allow-Headers", "Content-Type"],
];
const homepage = new Response(Bun.file(path.join(FRONTEND_ROOT, "index.html")));
const databaseLayer = Database.FromPath(config.DB_PATH);
const login = (code: string | null, state: string | null) => Effect.gen(function* () {
const { sessionId } = yield* Authentication.Authentication;
const db = yield* Database.Database;
const session = yield* db
.selectFrom("Session")
.select(["codeVerifier"])
.where("sessionId", "=", sessionId)
.$call(Database.executeTakeFirst);
const codeVerifier = Option.fromNullable(session.codeVerifier);
if (code !== null && state !== null && Option.isSome(codeVerifier)) {
const res = yield* fetch(config.OAUTH_TOKEN_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
"client_id": config.CLIENT_ID,
"code": code,
"redirect_uri": Authentication.REDIRECT_URI,
"grant_type": "authorization_code",
"code_verifier": codeVerifier.value,
"client_secret": Redacted.value(config.CLIENT_SECRET),
}).toString(),
});
const body = yield* Body.json(res);
const { id_token: idToken } = body as { id_token: string };
const idTokenPayload = yield* pipe(
idToken,
Authentication.getJwtTokenPayload(Authentication.IdTokenPayload),
);
const { userId } = yield* Authentication.upsertUser(idTokenPayload);
yield* db
.updateTable("Session")
.set({
codeVerifier: null,
state: null,
userId,
})
.where("sessionId", "=", sessionId)
.$call(Database.execute);
}
});
Bun.serve({
routes: {
...assetRoutes,
"/login": {
GET: (req) => Effect.gen(function* () {
const searchParams = new URL(req.url).searchParams;
// Callback URL with query response type
if (searchParams.has("code") || searchParams.has("state")) {
const code = searchParams.get("code");
const state = searchParams.get("state");
yield* login(code, state);
yield* Authentication.getAndProcessIdToken(code, state);
return Response.redirect(config.NODE_ENV === "production" ? `https://${config.HOSTNAME}/` : "http://localhost:5173/", 303);
}
// Initial login request; redirect to identity provider
const res = yield* pipe(
Authentication.Authentication,
Effect.flatMap(({ sessionId }) => Authentication.makeAuthorizationUrl(sessionId)),
@@ -111,12 +69,14 @@ Bun.serve({
Effect.runPromise,
),
POST: (req) => Effect.gen(function* () {
// Callback URL with form_post response type
const data = yield* Body.formData(req);
const code = data.get("code") as string | null;
const state = data.get("state") as string | null;
yield* login(code, state);
yield* Authentication.getAndProcessIdToken(code, state);
return Response.redirect(config.NODE_ENV === "production" ? `https://${config.HOSTNAME}/` : "http://localhost:5173/", 303);
}).pipe(
@@ -136,6 +96,13 @@ Bun.serve({
});
}
if (req.method !== "POST") {
return new Response(null, {
status: 405,
headers: CORS_HEADERS,
});
}
const authenticationLayer = Authentication.Live(req);
const layers = Layer.provideMerge(authenticationLayer, databaseLayer);