krona-staff/src/app.ts
Ochenstarik a05777c59f style: rebuild formatting without sync behavior changes
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.
2026-09-26 16:09:55 +07:00

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