Files
lunarfront-app/packages/backend/src/main.ts
Ryan Moon 0f6cc104d2 Add shared file storage with folder tree, permissions, and file manager UI
New document hub for centralized file storage — replaces scattered
drives and USB sticks for non-technical SMBs. Three new tables:
storage_folder (nested hierarchy), storage_folder_permission (role
and user-level access control), storage_file.

Backend: folder CRUD with nested paths, file upload/download via
signed URLs, permission checks (view/edit/admin with inheritance
from parent folders), public/private toggle, breadcrumb navigation,
file search.

Frontend: two-panel file manager — collapsible folder tree on left,
icon grid view on right. Folder icons by type, file size display,
upload button, context menu for download/delete. Breadcrumb nav.
Files sidebar link added.
2026-03-29 15:31:20 -05:00

104 lines
3.5 KiB
TypeScript

import Fastify from 'fastify'
import rateLimit from '@fastify/rate-limit'
import { databasePlugin } from './plugins/database.js'
import { redisPlugin } from './plugins/redis.js'
import { corsPlugin } from './plugins/cors.js'
import { errorHandlerPlugin } from './plugins/error-handler.js'
import { authPlugin } from './plugins/auth.js'
import { devAuthPlugin } from './plugins/dev-auth.js'
import { storagePlugin } from './plugins/storage.js'
import { healthRoutes } from './routes/v1/health.js'
import { authRoutes } from './routes/v1/auth.js'
import { accountRoutes } from './routes/v1/accounts.js'
import { inventoryRoutes } from './routes/v1/inventory.js'
import { productRoutes } from './routes/v1/products.js'
import { lookupRoutes } from './routes/v1/lookups.js'
import { fileRoutes } from './routes/v1/files.js'
import { rbacRoutes } from './routes/v1/rbac.js'
import { repairRoutes } from './routes/v1/repairs.js'
import { storageRoutes } from './routes/v1/storage.js'
import { RbacService } from './services/rbac.service.js'
export async function buildApp() {
const app = Fastify({
logger: {
level: process.env.LOG_LEVEL ?? 'info',
...(process.env.NODE_ENV === 'development'
? { transport: { target: 'pino-pretty' } }
: process.env.LOG_FILE
? {
transport: {
targets: [
{ target: 'pino/file', options: { destination: 1 } }, // stdout
{ target: 'pino/file', options: { destination: process.env.LOG_FILE, mkdir: true } },
],
},
}
: {}),
},
genReqId: () => crypto.randomUUID(),
requestTimeout: 30000, // 30 seconds
})
// Plugins
await app.register(corsPlugin)
await app.register(errorHandlerPlugin)
await app.register(databasePlugin)
await app.register(redisPlugin)
await app.register(rateLimit, { global: false })
await app.register(storagePlugin)
// Auth — JWT in production/test, dev bypass only in development without JWT_SECRET
if (process.env.JWT_SECRET) {
await app.register(authPlugin)
} else if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
app.log.warn('JWT_SECRET not set — using dev auth bypass. DO NOT USE IN PRODUCTION.')
await app.register(devAuthPlugin)
} else {
throw new Error('JWT_SECRET is required in production')
}
// Routes
await app.register(healthRoutes, { prefix: '/v1' })
await app.register(authRoutes, { prefix: '/v1' })
await app.register(accountRoutes, { prefix: '/v1' })
await app.register(inventoryRoutes, { prefix: '/v1' })
await app.register(productRoutes, { prefix: '/v1' })
await app.register(lookupRoutes, { prefix: '/v1' })
await app.register(fileRoutes, { prefix: '/v1' })
await app.register(rbacRoutes, { prefix: '/v1' })
await app.register(repairRoutes, { prefix: '/v1' })
await app.register(storageRoutes, { prefix: '/v1' })
// Auto-seed system permissions on startup
app.addHook('onReady', async () => {
try {
await RbacService.seedPermissions(app.db)
app.log.info('System permissions seeded')
} catch (err) {
app.log.error({ err }, 'Failed to seed permissions')
}
})
return app
}
async function start() {
const app = await buildApp()
const port = parseInt(process.env.PORT ?? '8000', 10)
const host = process.env.HOST ?? '0.0.0.0'
try {
await app.listen({ port, host })
} catch (err) {
app.log.error(err)
process.exit(1)
}
}
// Only auto-start when not imported by tests
if (process.env.NODE_ENV !== 'test') {
start()
}