First Commit
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
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 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'
|
||||
|
||||
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 },
|
||||
})
|
||||
|
||||
app.decorate('config', config)
|
||||
app.decorate('db', db)
|
||||
app.decorate('sqlite', sqlite)
|
||||
|
||||
await app.register(sensible)
|
||||
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' }))
|
||||
|
||||
await ensureBootstrapAdmin(app)
|
||||
await app.register(authRoutes)
|
||||
await app.register(adminRoutes)
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user