Recreate the formatting baseline from f5ac179 using only Prettier output. Leave sync and tenant-isolation files byte-identical to the base so Antigravity and Grok retain ownership. Normalized comparison confirms all 19 changed files are formatting-only; npm run check passes. Full format check retains five upstream warnings in excluded files.
169 lines
5.1 KiB
TypeScript
169 lines
5.1 KiB
TypeScript
import Fastify from 'fastify';
|
|
import { readFileSync } from 'node:fs';
|
|
import helmet from '@fastify/helmet';
|
|
import sensible from '@fastify/sensible';
|
|
import swagger from '@fastify/swagger';
|
|
import type { AppConfig } from './config.js';
|
|
import { Database } from './infrastructure/database.js';
|
|
import { newUlid } from './core/ids.js';
|
|
import { SERVICE_NAME, SERVICE_VERSION } from './version.js';
|
|
import { registerPortalRoutes } from './portal.js';
|
|
import { registerSyncRoutes } from './sync.js';
|
|
import {
|
|
portalCssEnhanced as portalCss,
|
|
portalHtml,
|
|
portalIcon192,
|
|
portalIcon512,
|
|
portalJsEnhanced as portalJs,
|
|
portalManifest,
|
|
portalServiceWorkerEnhanced as portalServiceWorker,
|
|
} from './portal-ui.js';
|
|
|
|
export interface BuildAppOptions {
|
|
config: AppConfig;
|
|
database?: Database;
|
|
}
|
|
|
|
const apiPrefix = '/api/v1/staff';
|
|
const ulidPattern = /^[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
|
|
export async function buildApp({
|
|
config,
|
|
database = new Database(config.DATABASE_URL),
|
|
}: BuildAppOptions) {
|
|
const https = config.HTTPS_PFX_PATH
|
|
? {
|
|
pfx: readFileSync(config.HTTPS_PFX_PATH),
|
|
passphrase: config.HTTPS_PFX_PASSPHRASE,
|
|
minVersion: 'TLSv1.2' as const,
|
|
}
|
|
: undefined;
|
|
const app = Fastify({
|
|
...(https ? { https } : {}),
|
|
logger: { level: config.LOG_LEVEL },
|
|
genReqId: (request) => {
|
|
const supplied = request.headers['x-correlation-id']?.toString();
|
|
return supplied && ulidPattern.test(supplied) ? supplied : newUlid();
|
|
},
|
|
});
|
|
await app.register(helmet);
|
|
await app.register(sensible);
|
|
app.addHook('onRequest', async (request, reply) => {
|
|
reply.header('x-correlation-id', request.id);
|
|
});
|
|
await app.register(swagger, {
|
|
openapi: {
|
|
info: { title: 'Krona Staff API', version: SERVICE_VERSION },
|
|
servers: [{ url: apiPrefix }],
|
|
},
|
|
});
|
|
|
|
await app.register(
|
|
async (api) => {
|
|
api.get(
|
|
'/live',
|
|
{
|
|
schema: {
|
|
tags: ['Operations'],
|
|
response: {
|
|
200: {
|
|
type: 'object',
|
|
required: ['status'],
|
|
properties: { status: { type: 'string', const: 'ok' } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async () => ({ status: 'ok' }),
|
|
);
|
|
api.get(
|
|
'/ready',
|
|
{
|
|
schema: {
|
|
tags: ['Operations'],
|
|
response: {
|
|
200: {
|
|
type: 'object',
|
|
required: ['status'],
|
|
properties: { status: { type: 'string', const: 'ready' } },
|
|
},
|
|
503: {
|
|
type: 'object',
|
|
required: ['status'],
|
|
properties: { status: { type: 'string', const: 'unavailable' } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async (_request, reply) => {
|
|
try {
|
|
await database.ping();
|
|
return { status: 'ready' };
|
|
} catch (error) {
|
|
api.log.error({ err: error }, 'readiness dependency check failed');
|
|
return reply.code(503).send({ status: 'unavailable' });
|
|
}
|
|
},
|
|
);
|
|
api.get(
|
|
'/version',
|
|
{
|
|
schema: {
|
|
tags: ['Operations'],
|
|
response: {
|
|
200: {
|
|
type: 'object',
|
|
required: ['name', 'version', 'mode'],
|
|
properties: {
|
|
name: { type: 'string' },
|
|
version: { type: 'string' },
|
|
mode: { type: 'string' },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
async () => ({
|
|
name: SERVICE_NAME,
|
|
version: SERVICE_VERSION,
|
|
mode: config.STAFF_RUNTIME_MODE,
|
|
}),
|
|
);
|
|
api.get('/openapi.json', async () => app.swagger());
|
|
await registerPortalRoutes(api, config, database);
|
|
await registerSyncRoutes(api, config, database);
|
|
},
|
|
{ prefix: apiPrefix },
|
|
);
|
|
app.get('/', async (_request, reply) => reply.redirect('/staff'));
|
|
app.get('/staff', async (_request, reply) =>
|
|
reply.type('text/html; charset=utf-8').send(portalHtml),
|
|
);
|
|
app.get('/staff/styles.css', async (_request, reply) =>
|
|
reply.type('text/css; charset=utf-8').send(portalCss),
|
|
);
|
|
app.get('/staff/app.js', async (_request, reply) =>
|
|
reply.type('application/javascript; charset=utf-8').send(portalJs),
|
|
);
|
|
app.get('/staff/sw.js', async (_request, reply) =>
|
|
reply
|
|
.header('service-worker-allowed', '/staff')
|
|
.type('application/javascript; charset=utf-8')
|
|
.send(portalServiceWorker),
|
|
);
|
|
app.get('/staff/manifest.webmanifest', async (_request, reply) =>
|
|
reply.type('application/manifest+json').send(portalManifest),
|
|
);
|
|
app.get('/staff/icon-192.svg', async (_request, reply) =>
|
|
reply.type('image/svg+xml').send(portalIcon192),
|
|
);
|
|
app.get('/staff/icon-512.svg', async (_request, reply) =>
|
|
reply.type('image/svg+xml').send(portalIcon512),
|
|
);
|
|
app.get('/staff/*', async (_request, reply) =>
|
|
reply.type('text/html; charset=utf-8').send(portalHtml),
|
|
);
|
|
|
|
app.addHook('onClose', async () => database.close());
|
|
return app;
|
|
}
|