import express, { type Request as ExpressRequest, type Response as ExpressResponse, } from "express"; import { match } from "path-to-regexp"; import { Session } from "./auth"; import { cli } from "./cli"; import { contentTypes } from "./content-types"; import { runWithContext } from "./context"; import { httpCodes } from "./http-codes"; import { routes } from "./routes"; import { services } from "./services"; // import { URLPattern } from 'node:url'; import { AuthenticationRequired, AuthorizationDenied, type Call, type InternalHandler, isRedirect, type Method, massageMethod, methodParser, type ProcessedRoute, type Result, type Route, } from "./types"; const app = express(); // Parse request bodies app.use(express.json()); app.use(express.urlencoded({ extended: true })); services.logging.log({ source: "logging", text: ["1"] }); const processedRoutes: { [K in Method]: ProcessedRoute[] } = { GET: [], POST: [], PUT: [], PATCH: [], DELETE: [], }; function _isPromise(value: T | Promise): value is Promise { return typeof (value as any)?.then === "function"; } routes.forEach((route: Route, _idx: number, _allRoutes: Route[]) => { // const pattern /*: URLPattern */ = new URLPattern({ pathname: route.path }); const matcher = match>(route.path); const methodList = route.methods; const handler: InternalHandler = async ( request: ExpressRequest, ): Promise => { const method = massageMethod(request.method); console.log("method", method); if (!methodList.includes(method)) { // XXX: Worth asserting this? } console.log("request.originalUrl", request.originalUrl); // Authenticate the request const auth = await services.auth.validateRequest(request); const req: Call = { pattern: route.path, path: request.originalUrl, method, parameters: { one: 1, two: 2 }, request, user: auth.user, session: new Session(auth.session, auth.user), }; try { const retval = await runWithContext( { user: auth.user }, () => route.handler(req), ); return retval; } catch (error) { // Handle authentication errors if (error instanceof AuthenticationRequired) { return { code: httpCodes.clientErrors.Unauthorized, contentType: contentTypes.application.json, result: JSON.stringify({ error: "Authentication required", }), }; } if (error instanceof AuthorizationDenied) { return { code: httpCodes.clientErrors.Forbidden, contentType: contentTypes.application.json, result: JSON.stringify({ error: "Access denied" }), }; } throw error; } }; for (const [_idx, method] of methodList.entries()) { const pr: ProcessedRoute = { matcher, method, handler }; processedRoutes[method].push(pr); } }); async function handler( req: ExpressRequest, _res: ExpressResponse, ): Promise { const method = await methodParser.parseAsync(req.method); const byMethod = processedRoutes[method]; console.log("DEBUG: req.path =", JSON.stringify(req.path), "method =", method); for (const [_idx, pr] of byMethod.entries()) { const match = pr.matcher(req.path); console.log("DEBUG: trying pattern, match result =", match); if (match) { console.log("match", match); const resp = await pr.handler(req); return resp; } } const retval: Result = { code: httpCodes.clientErrors.NotFound, contentType: contentTypes.text.plain, result: "not found!", }; return retval; } app.use(async (req: ExpressRequest, res: ExpressResponse) => { const result0 = await handler(req, res); const code = result0.code.code; const result = result0.result; console.log(result); // Set any cookies from the result if (result0.cookies) { for (const cookie of result0.cookies) { res.cookie(cookie.name, cookie.value, cookie.options ?? {}); } } if (isRedirect(result0)) { res.redirect(code, result0.redirect); } else { res.status(code).send(result); } }); process.title = `diachron:${cli.listen.port}`; app.listen(cli.listen.port, cli.listen.host, () => { console.log(`Listening on ${cli.listen.host}:${cli.listen.port}`); });