Implement RBAC with permissions, roles, and route guards
- permission, role, role_permission, user_role_assignment tables - 42 system permissions across 13 domains - 6 default roles: Admin, Manager, Sales Associate, Technician, Instructor, Viewer - Permission inheritance: admin implies edit implies view - requirePermission() Fastify decorator on ALL routes - System permissions and roles seeded per company - Test helpers and API test runner seed RBAC data - All 42 API tests pass with permissions enforced
This commit is contained in:
@@ -12,7 +12,7 @@ export const fileRoutes: FastifyPluginAsync = async (app) => {
|
||||
})
|
||||
|
||||
// List files for an entity
|
||||
app.get('/files', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/files', { preHandler: [app.authenticate, app.requirePermission('files.view')] }, async (request, reply) => {
|
||||
const { entityType, entityId } = request.query as { entityType?: string; entityId?: string }
|
||||
if (!entityType || !entityId) {
|
||||
throw new ValidationError('entityType and entityId query params required')
|
||||
@@ -27,7 +27,7 @@ export const fileRoutes: FastifyPluginAsync = async (app) => {
|
||||
})
|
||||
|
||||
// Upload a file
|
||||
app.post('/files', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.post('/files', { preHandler: [app.authenticate, app.requirePermission('files.upload')] }, async (request, reply) => {
|
||||
const data = await request.file()
|
||||
if (!data) {
|
||||
throw new ValidationError('No file provided')
|
||||
@@ -77,7 +77,7 @@ export const fileRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
// Serve file content (for local provider)
|
||||
// Path traversal protection: validate the path starts with the requesting company's ID
|
||||
app.get('/files/serve/*', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/files/serve/*', { preHandler: [app.authenticate, app.requirePermission('files.view')] }, async (request, reply) => {
|
||||
const filePath = (request.params as { '*': string })['*']
|
||||
if (!filePath) {
|
||||
throw new ValidationError('Path required')
|
||||
@@ -104,7 +104,7 @@ export const fileRoutes: FastifyPluginAsync = async (app) => {
|
||||
})
|
||||
|
||||
// Get file metadata
|
||||
app.get('/files/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/files/:id', { preHandler: [app.authenticate, app.requirePermission('files.view')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const file = await FileService.getById(app.db, request.companyId, id)
|
||||
if (!file) return reply.status(404).send({ error: { message: 'File not found', statusCode: 404 } })
|
||||
@@ -113,7 +113,7 @@ export const fileRoutes: FastifyPluginAsync = async (app) => {
|
||||
})
|
||||
|
||||
// Delete a file
|
||||
app.delete('/files/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.delete('/files/:id', { preHandler: [app.authenticate, app.requirePermission('files.delete')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const file = await FileService.delete(app.db, app.storage, request.companyId, id)
|
||||
if (!file) return reply.status(404).send({ error: { message: 'File not found', statusCode: 404 } })
|
||||
|
||||
Reference in New Issue
Block a user