57 lines
3.4 KiB
TypeScript
57 lines
3.4 KiB
TypeScript
import { createDiscordRoutes, type CommandConfig } from "./discord/routes";
|
|
import { createLiveDiscord } from "./sharing/live-discord";
|
|
import { createReminderRoutes } from "./reminders/routes";
|
|
import { errorDetails } from "./ops/monitor";
|
|
import { createAccountRoutes } from "./account/routes";
|
|
import { createHabitRoutes } from "./habits/routes";
|
|
import { ApiError } from "./habits/service";
|
|
import { Hono } from "hono";
|
|
import { sql } from "drizzle-orm";
|
|
import { createAuth, type AppDatabase, type AuthEnv } from "./auth";
|
|
import type { AuthConfig } from "./auth/config";
|
|
import type { FetchDiscord } from "./auth/discord";
|
|
import { createSharingRoutes, type DiscordFetch } from "./sharing/routes";
|
|
import type { DiscordSharingConfig } from "./sharing/config";
|
|
|
|
export function createApi(db: AppDatabase, config: AuthConfig, request?: FetchDiscord, now?: () => number, discordRequest?: DiscordFetch, sharingConfig: DiscordSharingConfig = { token: "", channelId: "" }, operations: { healthy?: () => boolean; log?: (event: Record<string, unknown>) => void; discord?: CommandConfig } = {}) {
|
|
const app = new Hono<AuthEnv>();
|
|
app.use("*", async (c, next) => {
|
|
const requestId = crypto.randomUUID();
|
|
c.set("requestId", requestId); c.header("X-Request-ID", requestId);
|
|
await next();
|
|
});
|
|
const auth = createAuth(db, config, request, now);
|
|
const liveSharing = createLiveDiscord(db, auth, sharingConfig, now ?? Date.now, discordRequest);
|
|
const sharing = createSharingRoutes(db, auth, sharingConfig, now ?? Date.now, discordRequest);
|
|
app.use("*", async (c, next) => {
|
|
await next();
|
|
if (!["GET", "HEAD", "OPTIONS"].includes(c.req.method) && /^\/api\/(habits|me|account)(\/|$)/.test(c.req.path) && c.res.status < 400 && c.get("user"))
|
|
liveSharing.changed(c.get("user").id);
|
|
});
|
|
app.get("/api/health", c => {
|
|
c.header("Cache-Control", "no-store");
|
|
db.get(sql`SELECT 1`);
|
|
if (operations.healthy && !operations.healthy()) return c.json({ status: "degraded" }, 503);
|
|
return c.json({ status: "ok" });
|
|
});
|
|
app.route("/api/discord/interactions", createDiscordRoutes(db, { ...(operations.discord ?? { publicKey: "", applicationId: config.clientId, origin: config.origin }), remindersAvailable: Boolean(sharingConfig.token) }, now ?? Date.now, liveSharing.changed, { progress: { sharing, live: liveSharing } }));
|
|
app.route("/api/sharing/live", liveSharing.routes);
|
|
app.route("/api/auth", auth.routes);
|
|
app.route("/api/reminders", createReminderRoutes(db, auth, Boolean(sharingConfig.token), now ?? Date.now));
|
|
app.route("/api/account", createAccountRoutes(db, auth, now ?? Date.now));
|
|
app.get("/api/me", auth.requireAuth, c => c.json(c.get("user")));
|
|
app.route("/api", createHabitRoutes(db, auth, now ?? Date.now));
|
|
app.route("/api/sharing", sharing);
|
|
app.notFound(c => c.json({ error: "Not found" }, 404));
|
|
app.onError((_error, c) => {
|
|
c.header("Cache-Control", "no-store");
|
|
if (_error instanceof ApiError) return c.json({ error: _error.message }, _error.status);
|
|
const requestId = c.get("requestId");
|
|
const event = { event: "request_failed", timestamp: new Date().toISOString(), requestId, method: c.req.method,
|
|
route: c.req.routePath, ...errorDetails(_error) };
|
|
(operations.log ?? (event => console.error(JSON.stringify(event))))(event);
|
|
return c.json({ error: "Internal server error", requestId }, 500);
|
|
});
|
|
return Object.assign(app, { stopLiveSharing: liveSharing.stop, flushLiveSharing: liveSharing.flush });
|
|
}
|