adventure/src/app.ts
nholtware afbc5e5254
All checks were successful
/ checks (push) Successful in 38s
/ deploy (push) Successful in 11s
feat: passport-local authentication, story ownership
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 13:33:51 -06:00

97 lines
2.8 KiB
TypeScript

import express, { type ErrorRequestHandler } from "express";
import { randomUUID } from "node:crypto";
import { pinoHttp } from "pino-http";
import type { Logger } from "pino";
import type { Config } from "./lib/config.js";
import type { Db } from "./lib/db.js";
import type { Flags } from "./lib/flags.js";
import type { Cache } from "./lib/redis.js";
import {
staticRuntimeConfig,
type RuntimeConfig,
} from "./lib/runtime-config.js";
import { adminRouter } from "./routes/admin.js";
import { healthRouter } from "./routes/health.js";
import { logDemoRouter } from "./routes/log-demo.js";
import { rootRouter } from "./routes/root.js";
import { authRouter } from "./routes/auth.js";
import { createAuth } from "./lib/auth.js";
import {
memoryUserStore,
pgUserStore,
type UserStore,
} from "./lib/user-store.js";
import { storiesRouter } from "./routes/stories.js";
import { memoryStore, pgStore, type StoryStore } from "./lib/story-store.js";
export interface Deps {
config: Config;
logger: Logger;
db?: Db;
cache?: Cache;
flags?: Flags;
runtimeConfig?: RuntimeConfig;
store?: StoryStore;
users?: UserStore;
}
export function createApp({
config,
logger,
db,
cache,
flags,
runtimeConfig = staticRuntimeConfig(),
store = db ? pgStore(db) : memoryStore(),
users = db ? pgUserStore(db) : memoryUserStore(),
}: Deps) {
const app = express();
app.disable("x-powered-by");
app.use(
pinoHttp({
logger,
genReqId: (req, res) => {
const incoming = req.headers["x-request-id"];
const id =
(Array.isArray(incoming) ? incoming[0] : incoming) ?? randomUUID();
res.setHeader("X-Request-Id", id);
return id;
},
// Keep the logging contract flat and small: req.id/method/url,
// res.statusCode, responseTime.
serializers: {
req: (req) => ({ id: req.id, method: req.method, url: req.url }),
res: (res) => ({ statusCode: res.statusCode }),
},
autoLogging: {
ignore: (req) => req.url === "/healthz" || req.url === "/readyz",
},
}),
);
app.use(healthRouter({ db, cache }));
app.use(rootRouter({ config, db, cache, flags, runtimeConfig }));
const auth = createAuth({ config, users, db });
app.use(auth.middleware);
app.use(
authRouter({
auth,
users,
registrationOpen: () => runtimeConfig.get().registrationOpen,
}),
);
app.use(storiesRouter({ store }));
app.use(adminRouter({ runtimeConfig }));
app.use(logDemoRouter());
// Express identifies error handlers by arity; keep all four args.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const onError: ErrorRequestHandler = (err, req, res, _next) => {
req.log.error({ err }, "unhandled error");
res.status(500).json({ error: "internal error" });
};
app.use(onError);
return app;
}