import { serve } from "@hono/node-server"; import { getConnInfo } from "@hono/node-server/conninfo"; import { serveStatic } from "@hono/node-server/serve-static"; import * as Body from "common/Body"; import { Effect, Layer, Match, pipe } from "effect"; import { Hono } from "hono"; import * as path from "node:path"; import { config } from "./config.ts"; import * as Authentication from "./services/Authentication.ts"; import * as Database from "./services/Database.ts"; import { handle } from "./the_api.ts"; const FRONTEND_ROOT = "packages/frontend/build"; 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 databaseLayer = Database.FromPath(config.DB_PATH); const app = new Hono() .get("/login", (ctx) => Effect.gen(function* () { const code = ctx.req.query("code"); const state = ctx.req.query("state"); // Callback URL with query response type if (code !== undefined || state !== undefined) { yield* Authentication.getAndProcessIdToken(code ?? null, state ?? null); return ctx.redirect(config.NODE_ENV === "production" ? `https://${config.HOSTNAME}/` : "http://localhost:5173/", 303); } // Initial login request; redirect to identity provider const url = yield* pipe( Authentication.Authentication, Effect.flatMap(({ sessionId }) => Authentication.makeAuthorizationUrl(sessionId)), ); return ctx.redirect(url); }).pipe( Effect.provide(Layer.provideMerge(Authentication.Live(ctx), databaseLayer)), Effect.runPromise, )) .post("/login", (ctx) => Effect.gen(function* () { // Callback URL with form_post response type const data = yield* Body.formData(ctx.req); const code = data.get("code") as string | null; const state = data.get("state") as string | null; yield* Authentication.getAndProcessIdToken(code, state); return ctx.redirect(config.NODE_ENV === "production" ? `https://${config.HOSTNAME}/` : "http://localhost:5173/", 303); }).pipe( Effect.provide(Layer.provideMerge(Authentication.Live(ctx), databaseLayer)), Effect.runPromise, )) .all("/api/:key", async (ctx) => { const req = ctx.req; const timestamp = new Date().toISOString(); const connInfo = getConnInfo(ctx); console.log(`${timestamp} ${req.method} ${req.url} ${connInfo.remote.address!}`); if (req.method === "OPTIONS") { return new Response(null, { status: 204, headers: CORS_HEADERS, }); } if (req.method !== "POST") { return new Response(null, { status: 405, headers: CORS_HEADERS, }); } const authenticationLayer = Authentication.Live(ctx); const layers = Layer.provideMerge(authenticationLayer, databaseLayer); const { body, status } = await pipe( handle(req), Effect.provide(layers), Effect.runPromise, ); for (const [name, value] of CORS_HEADERS) { ctx.header(name, value); } if (body !== null) { ctx.header("Content-Type", "application/cbor"); return ctx.body(body, status); } else { return ctx.body(body, status); } }) .use("/assets/*", serveStatic({ root: FRONTEND_ROOT })) .use("*", serveStatic({ path: path.join(FRONTEND_ROOT, "index.html") })); serve({ fetch: app.fetch, port: config.PORT, });