Files
music-repo/packages/backend/src/app.ts

113 lines
3.4 KiB
TypeScript

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,
});