Files
auth-portal/apps/api/src/app.ts
T
DenozordecandCursor 0b6fa65b08
Build and Push Auth Portal Docker Image / build-and-push (push) Successful in 2m30s
Build and Push Auth Portal Docker Image / create-release (push) Skipped
feat(reui): update ReUI components and documentation
- Added new OIDC configuration options in `.env.example`.
- Expanded documentation in `AGENTS.md` to include OIDC endpoints and admin UI.
- Updated ReUI skill version and component count from 17 to 20 across various documentation files.
- Enhanced `README.md` and other related files to reflect the new component structure and usage guidelines.

Co-authored-by: Cursor <[email protected]>
2026-08-11 16:58:47 +07:00

157 lines
4.4 KiB
TypeScript

import type { FastifyInstance } from 'fastify'
import Fastify from 'fastify'
import cors from '@fastify/cors'
import helmet from '@fastify/helmet'
import rateLimit from '@fastify/rate-limit'
import sensible from '@fastify/sensible'
import formbody from '@fastify/formbody'
import fjwt from '@fastify/jwt'
import fastifyStatic from '@fastify/static'
import { existsSync, mkdirSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import {
APP_IDS,
allPermissionKeys,
} from '@authportal/shared'
import {
createDb,
getUserApps,
healthCheck,
migrateSchema,
setUserAccess,
users,
type AppDb,
type Sqlite,
} from '@authportal/db'
import { hash } from '@node-rs/argon2'
import { randomUUID } from 'node:crypto'
import type { AppConfig } from './config.js'
import { authRoutes } from './routes/auth.js'
import { adminRoutes } from './routes/admin.js'
import { auditAdminRoutes } from './routes/audit.js'
import { auditIngestRoutes } from './routes/ingest-audit.js'
import { oidcRoutes } from './routes/oidc.js'
import { startAuditRetentionJob } from './services/audit-retention.js'
import { ensureOidcSigningKey, resetOidcKeyCache } from './lib/oidc/keys.js'
declare module 'fastify' {
interface FastifyInstance {
config: AppConfig
db: AppDb
sqlite: Sqlite
}
}
async function ensureBootstrapAdmin(app: FastifyInstance): Promise<void> {
const existing = app.db.select().from(users).all()
if (existing.length === 0) {
const now = new Date().toISOString()
const passwordHash = await hash(app.config.adminPassword)
const id = randomUUID()
app.db
.insert(users)
.values({
id,
email: app.config.adminEmail.toLowerCase(),
name: app.config.adminName,
passwordHash,
isAdmin: true,
disabled: false,
createdAt: now,
updatedAt: now,
})
.run()
setUserAccess(app.db, id, [...APP_IDS], allPermissionKeys())
app.log.info(`bootstrap admin created: ${app.config.adminEmail}`)
return
}
// Ensure existing admins have app tiles if access was never set
for (const user of existing) {
if (!user.isAdmin) continue
const apps = getUserApps(app.db, user.id)
if (apps.length === 0) {
setUserAccess(app.db, user.id, [...APP_IDS], allPermissionKeys())
app.log.info(`granted full catalog access to admin ${user.email}`)
}
}
}
export async function buildApp(opts: {
config: AppConfig
databaseUrl?: string
}): Promise<FastifyInstance> {
const config = opts.config
const dbUrl = opts.databaseUrl ?? config.databaseUrl
const path = dbUrl.replace(/^sqlite:/, '')
if (path !== ':memory:') {
mkdirSync(dirname(resolve(path)), { recursive: true })
}
const { db, sqlite } = createDb(dbUrl)
migrateSchema(sqlite)
const app = Fastify({
logger: { level: config.logLevel },
trustProxy: true,
})
app.decorate('config', config)
app.decorate('db', db)
app.decorate('sqlite', sqlite)
await app.register(sensible)
await app.register(formbody)
await app.register(cors, { origin: true, credentials: true })
await app.register(helmet, { contentSecurityPolicy: false })
await app.register(rateLimit, { max: 200, timeWindow: '1 minute' })
await app.register(fjwt, { secret: config.jwtSecret })
app.get('/health', async () => {
healthCheck(sqlite)
return { ok: true }
})
app.get('/ready', async () => {
healthCheck(sqlite)
return { ok: true }
})
app.get('/api/v1/health', async () => ({ ok: true, service: 'auth-portal' }))
resetOidcKeyCache()
await ensureOidcSigningKey(app)
await ensureBootstrapAdmin(app)
await app.register(authRoutes)
await app.register(adminRoutes)
await app.register(auditAdminRoutes)
await app.register(auditIngestRoutes)
await app.register(oidcRoutes)
if (process.env.NODE_ENV !== 'test') {
const stopRetention = startAuditRetentionJob(app)
app.addHook('onClose', async () => {
stopRetention()
})
}
if (config.staticDir && existsSync(config.staticDir)) {
await app.register(fastifyStatic, {
root: resolve(config.staticDir),
wildcard: false,
})
app.setNotFoundHandler((req, reply) => {
if (req.method === 'GET' && !req.url.startsWith('/api')) {
return reply.sendFile('index.html')
}
return reply
.status(404)
.send({ error: { code: 'NOT_FOUND', message: 'Not found' } })
})
}
return app
}