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.
104 lines
3.5 KiB
TypeScript
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()
|
|
}
|