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:
@@ -26,7 +26,7 @@ import {
|
||||
export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
// --- Accounts ---
|
||||
|
||||
app.post('/accounts', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.post('/accounts', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => {
|
||||
const parsed = AccountCreateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
|
||||
@@ -35,20 +35,20 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.status(201).send(account)
|
||||
})
|
||||
|
||||
app.get('/accounts', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/accounts', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
|
||||
const params = PaginationSchema.parse(request.query)
|
||||
const result = await AccountService.list(app.db, request.companyId, params)
|
||||
return reply.send(result)
|
||||
})
|
||||
|
||||
app.get('/accounts/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/accounts/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const account = await AccountService.getById(app.db, request.companyId, id)
|
||||
if (!account) return reply.status(404).send({ error: { message: 'Account not found', statusCode: 404 } })
|
||||
return reply.send(account)
|
||||
})
|
||||
|
||||
app.patch('/accounts/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.patch('/accounts/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const parsed = AccountUpdateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
@@ -59,7 +59,7 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.send(account)
|
||||
})
|
||||
|
||||
app.delete('/accounts/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.delete('/accounts/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const account = await AccountService.softDelete(app.db, request.companyId, id)
|
||||
if (!account) return reply.status(404).send({ error: { message: 'Account not found', statusCode: 404 } })
|
||||
@@ -69,7 +69,7 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
// --- Members (top-level) ---
|
||||
|
||||
app.get('/members', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/members', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
|
||||
const params = PaginationSchema.parse(request.query)
|
||||
const result = await MemberService.list(app.db, request.companyId, params)
|
||||
return reply.send(result)
|
||||
@@ -77,7 +77,7 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
// --- Members (scoped to account) ---
|
||||
|
||||
app.post('/accounts/:accountId/members', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.post('/accounts/:accountId/members', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => {
|
||||
const { accountId } = request.params as { accountId: string }
|
||||
const parsed = MemberCreateSchema.safeParse({ ...(request.body as object), accountId })
|
||||
if (!parsed.success) {
|
||||
@@ -87,21 +87,21 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.status(201).send(member)
|
||||
})
|
||||
|
||||
app.get('/accounts/:accountId/members', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/accounts/:accountId/members', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
|
||||
const { accountId } = request.params as { accountId: string }
|
||||
const params = PaginationSchema.parse(request.query)
|
||||
const result = await MemberService.listByAccount(app.db, request.companyId, accountId, params)
|
||||
return reply.send(result)
|
||||
})
|
||||
|
||||
app.get('/members/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/members/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const member = await MemberService.getById(app.db, request.companyId, id)
|
||||
if (!member) return reply.status(404).send({ error: { message: 'Member not found', statusCode: 404 } })
|
||||
return reply.send(member)
|
||||
})
|
||||
|
||||
app.patch('/members/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.patch('/members/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const parsed = MemberUpdateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
@@ -112,7 +112,7 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.send(member)
|
||||
})
|
||||
|
||||
app.post('/members/:id/move', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.post('/members/:id/move', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const { accountId } = (request.body as { accountId?: string }) ?? {}
|
||||
|
||||
@@ -136,7 +136,7 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
// --- Member Identifiers ---
|
||||
|
||||
app.post('/members/:memberId/identifiers', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.post('/members/:memberId/identifiers', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => {
|
||||
const { memberId } = request.params as { memberId: string }
|
||||
const parsed = MemberIdentifierCreateSchema.safeParse({ ...(request.body as object), memberId })
|
||||
if (!parsed.success) {
|
||||
@@ -146,13 +146,13 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.status(201).send(identifier)
|
||||
})
|
||||
|
||||
app.get('/members/:memberId/identifiers', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/members/:memberId/identifiers', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
|
||||
const { memberId } = request.params as { memberId: string }
|
||||
const identifiers = await MemberIdentifierService.listByMember(app.db, request.companyId, memberId)
|
||||
return reply.send({ data: identifiers })
|
||||
})
|
||||
|
||||
app.patch('/identifiers/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.patch('/identifiers/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const parsed = MemberIdentifierUpdateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
@@ -163,14 +163,14 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.send(identifier)
|
||||
})
|
||||
|
||||
app.delete('/identifiers/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.delete('/identifiers/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const identifier = await MemberIdentifierService.delete(app.db, request.companyId, id)
|
||||
if (!identifier) return reply.status(404).send({ error: { message: 'Identifier not found', statusCode: 404 } })
|
||||
return reply.send(identifier)
|
||||
})
|
||||
|
||||
app.delete('/members/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.delete('/members/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const member = await MemberService.delete(app.db, request.companyId, id)
|
||||
if (!member) return reply.status(404).send({ error: { message: 'Member not found', statusCode: 404 } })
|
||||
@@ -179,7 +179,7 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
// --- Processor Links ---
|
||||
|
||||
app.post('/accounts/:accountId/processor-links', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.post('/accounts/:accountId/processor-links', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => {
|
||||
const { accountId } = request.params as { accountId: string }
|
||||
const parsed = ProcessorLinkCreateSchema.safeParse({ ...(request.body as object), accountId })
|
||||
if (!parsed.success) {
|
||||
@@ -189,13 +189,13 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.status(201).send(link)
|
||||
})
|
||||
|
||||
app.get('/accounts/:accountId/processor-links', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/accounts/:accountId/processor-links', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
|
||||
const { accountId } = request.params as { accountId: string }
|
||||
const links = await ProcessorLinkService.listByAccount(app.db, request.companyId, accountId)
|
||||
return reply.send({ data: links })
|
||||
})
|
||||
|
||||
app.patch('/processor-links/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.patch('/processor-links/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const parsed = ProcessorLinkUpdateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
@@ -206,7 +206,7 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.send(link)
|
||||
})
|
||||
|
||||
app.delete('/processor-links/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.delete('/processor-links/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const link = await ProcessorLinkService.delete(app.db, request.companyId, id)
|
||||
if (!link) return reply.status(404).send({ error: { message: 'Processor link not found', statusCode: 404 } })
|
||||
@@ -215,7 +215,7 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
// --- Payment Methods ---
|
||||
|
||||
app.post('/accounts/:accountId/payment-methods', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.post('/accounts/:accountId/payment-methods', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => {
|
||||
const { accountId } = request.params as { accountId: string }
|
||||
const parsed = PaymentMethodCreateSchema.safeParse({ ...(request.body as object), accountId })
|
||||
if (!parsed.success) {
|
||||
@@ -225,20 +225,20 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.status(201).send(method)
|
||||
})
|
||||
|
||||
app.get('/accounts/:accountId/payment-methods', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/accounts/:accountId/payment-methods', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
|
||||
const { accountId } = request.params as { accountId: string }
|
||||
const methods = await PaymentMethodService.listByAccount(app.db, request.companyId, accountId)
|
||||
return reply.send({ data: methods })
|
||||
})
|
||||
|
||||
app.get('/payment-methods/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/payment-methods/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const method = await PaymentMethodService.getById(app.db, request.companyId, id)
|
||||
if (!method) return reply.status(404).send({ error: { message: 'Payment method not found', statusCode: 404 } })
|
||||
return reply.send(method)
|
||||
})
|
||||
|
||||
app.patch('/payment-methods/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.patch('/payment-methods/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const parsed = PaymentMethodUpdateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
@@ -249,7 +249,7 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.send(method)
|
||||
})
|
||||
|
||||
app.delete('/payment-methods/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.delete('/payment-methods/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const method = await PaymentMethodService.delete(app.db, request.companyId, id)
|
||||
if (!method) return reply.status(404).send({ error: { message: 'Payment method not found', statusCode: 404 } })
|
||||
@@ -258,7 +258,7 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
// --- Tax Exemptions ---
|
||||
|
||||
app.post('/accounts/:accountId/tax-exemptions', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.post('/accounts/:accountId/tax-exemptions', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => {
|
||||
const { accountId } = request.params as { accountId: string }
|
||||
const parsed = TaxExemptionCreateSchema.safeParse({ ...(request.body as object), accountId })
|
||||
if (!parsed.success) {
|
||||
@@ -268,20 +268,20 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.status(201).send(exemption)
|
||||
})
|
||||
|
||||
app.get('/accounts/:accountId/tax-exemptions', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/accounts/:accountId/tax-exemptions', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
|
||||
const { accountId } = request.params as { accountId: string }
|
||||
const exemptions = await TaxExemptionService.listByAccount(app.db, request.companyId, accountId)
|
||||
return reply.send({ data: exemptions })
|
||||
})
|
||||
|
||||
app.get('/tax-exemptions/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/tax-exemptions/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.view')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const exemption = await TaxExemptionService.getById(app.db, request.companyId, id)
|
||||
if (!exemption) return reply.status(404).send({ error: { message: 'Tax exemption not found', statusCode: 404 } })
|
||||
return reply.send(exemption)
|
||||
})
|
||||
|
||||
app.patch('/tax-exemptions/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.patch('/tax-exemptions/:id', { preHandler: [app.authenticate, app.requirePermission('accounts.edit')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const parsed = TaxExemptionUpdateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
@@ -292,7 +292,7 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.send(exemption)
|
||||
})
|
||||
|
||||
app.post('/tax-exemptions/:id/approve', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.post('/tax-exemptions/:id/approve', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const exemption = await TaxExemptionService.approve(app.db, request.companyId, id, request.user.id)
|
||||
if (!exemption) return reply.status(404).send({ error: { message: 'Tax exemption not found', statusCode: 404 } })
|
||||
@@ -300,7 +300,7 @@ export const accountRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.send(exemption)
|
||||
})
|
||||
|
||||
app.post('/tax-exemptions/:id/revoke', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.post('/tax-exemptions/:id/revoke', { preHandler: [app.authenticate, app.requirePermission('accounts.admin')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const { reason } = (request.body as { reason?: string }) ?? {}
|
||||
if (!reason) {
|
||||
|
||||
@@ -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 } })
|
||||
|
||||
@@ -11,7 +11,7 @@ import { CategoryService, SupplierService } from '../../services/inventory.servi
|
||||
export const inventoryRoutes: FastifyPluginAsync = async (app) => {
|
||||
// --- Categories ---
|
||||
|
||||
app.post('/categories', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.post('/categories', { preHandler: [app.authenticate, app.requirePermission('inventory.edit')] }, async (request, reply) => {
|
||||
const parsed = CategoryCreateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
|
||||
@@ -20,20 +20,20 @@ export const inventoryRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.status(201).send(category)
|
||||
})
|
||||
|
||||
app.get('/categories', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/categories', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
|
||||
const params = PaginationSchema.parse(request.query)
|
||||
const result = await CategoryService.list(app.db, request.companyId, params)
|
||||
return reply.send(result)
|
||||
})
|
||||
|
||||
app.get('/categories/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/categories/:id', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const category = await CategoryService.getById(app.db, request.companyId, id)
|
||||
if (!category) return reply.status(404).send({ error: { message: 'Category not found', statusCode: 404 } })
|
||||
return reply.send(category)
|
||||
})
|
||||
|
||||
app.patch('/categories/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.patch('/categories/:id', { preHandler: [app.authenticate, app.requirePermission('inventory.edit')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const parsed = CategoryUpdateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
@@ -44,7 +44,7 @@ export const inventoryRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.send(category)
|
||||
})
|
||||
|
||||
app.delete('/categories/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.delete('/categories/:id', { preHandler: [app.authenticate, app.requirePermission('inventory.admin')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const category = await CategoryService.softDelete(app.db, request.companyId, id)
|
||||
if (!category) return reply.status(404).send({ error: { message: 'Category not found', statusCode: 404 } })
|
||||
@@ -53,7 +53,7 @@ export const inventoryRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
// --- Suppliers ---
|
||||
|
||||
app.post('/suppliers', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.post('/suppliers', { preHandler: [app.authenticate, app.requirePermission('inventory.edit')] }, async (request, reply) => {
|
||||
const parsed = SupplierCreateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
|
||||
@@ -62,20 +62,20 @@ export const inventoryRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.status(201).send(supplier)
|
||||
})
|
||||
|
||||
app.get('/suppliers', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/suppliers', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
|
||||
const params = PaginationSchema.parse(request.query)
|
||||
const result = await SupplierService.list(app.db, request.companyId, params)
|
||||
return reply.send(result)
|
||||
})
|
||||
|
||||
app.get('/suppliers/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/suppliers/:id', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const supplier = await SupplierService.getById(app.db, request.companyId, id)
|
||||
if (!supplier) return reply.status(404).send({ error: { message: 'Supplier not found', statusCode: 404 } })
|
||||
return reply.send(supplier)
|
||||
})
|
||||
|
||||
app.patch('/suppliers/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.patch('/suppliers/:id', { preHandler: [app.authenticate, app.requirePermission('inventory.edit')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const parsed = SupplierUpdateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
@@ -86,7 +86,7 @@ export const inventoryRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.send(supplier)
|
||||
})
|
||||
|
||||
app.delete('/suppliers/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.delete('/suppliers/:id', { preHandler: [app.authenticate, app.requirePermission('inventory.admin')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const supplier = await SupplierService.softDelete(app.db, request.companyId, id)
|
||||
if (!supplier) return reply.status(404).send({ error: { message: 'Supplier not found', statusCode: 404 } })
|
||||
|
||||
@@ -5,12 +5,12 @@ import { ConflictError, ValidationError } from '../../lib/errors.js'
|
||||
|
||||
function createLookupRoutes(prefix: string, service: typeof UnitStatusService) {
|
||||
const routes: FastifyPluginAsync = async (app) => {
|
||||
app.get(`/${prefix}`, { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get(`/${prefix}`, { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
|
||||
const data = await service.list(app.db, request.companyId)
|
||||
return reply.send({ data })
|
||||
})
|
||||
|
||||
app.post(`/${prefix}`, { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.post(`/${prefix}`, { preHandler: [app.authenticate, app.requirePermission('inventory.admin')] }, async (request, reply) => {
|
||||
const parsed = LookupCreateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
throw new ValidationError('Validation failed', parsed.error.flatten())
|
||||
@@ -25,7 +25,7 @@ function createLookupRoutes(prefix: string, service: typeof UnitStatusService) {
|
||||
return reply.status(201).send(row)
|
||||
})
|
||||
|
||||
app.patch(`/${prefix}/:id`, { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.patch(`/${prefix}/:id`, { preHandler: [app.authenticate, app.requirePermission('inventory.admin')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const parsed = LookupUpdateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
@@ -36,7 +36,7 @@ function createLookupRoutes(prefix: string, service: typeof UnitStatusService) {
|
||||
return reply.send(row)
|
||||
})
|
||||
|
||||
app.delete(`/${prefix}/:id`, { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.delete(`/${prefix}/:id`, { preHandler: [app.authenticate, app.requirePermission('inventory.admin')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const row = await service.delete(app.db, request.companyId, id)
|
||||
if (!row) return reply.status(404).send({ error: { message: 'Not found', statusCode: 404 } })
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ProductService, InventoryUnitService } from '../../services/product.ser
|
||||
export const productRoutes: FastifyPluginAsync = async (app) => {
|
||||
// --- Products ---
|
||||
|
||||
app.post('/products', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.post('/products', { preHandler: [app.authenticate, app.requirePermission('inventory.edit')] }, async (request, reply) => {
|
||||
const parsed = ProductCreateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
|
||||
@@ -20,20 +20,20 @@ export const productRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.status(201).send(product)
|
||||
})
|
||||
|
||||
app.get('/products', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/products', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
|
||||
const params = PaginationSchema.parse(request.query)
|
||||
const result = await ProductService.list(app.db, request.companyId, params)
|
||||
return reply.send(result)
|
||||
})
|
||||
|
||||
app.get('/products/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/products/:id', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const product = await ProductService.getById(app.db, request.companyId, id)
|
||||
if (!product) return reply.status(404).send({ error: { message: 'Product not found', statusCode: 404 } })
|
||||
return reply.send(product)
|
||||
})
|
||||
|
||||
app.patch('/products/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.patch('/products/:id', { preHandler: [app.authenticate, app.requirePermission('inventory.edit')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const parsed = ProductUpdateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
@@ -44,7 +44,7 @@ export const productRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.send(product)
|
||||
})
|
||||
|
||||
app.delete('/products/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.delete('/products/:id', { preHandler: [app.authenticate, app.requirePermission('inventory.admin')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const product = await ProductService.softDelete(app.db, request.companyId, id)
|
||||
if (!product) return reply.status(404).send({ error: { message: 'Product not found', statusCode: 404 } })
|
||||
@@ -53,7 +53,7 @@ export const productRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
// --- Inventory Units ---
|
||||
|
||||
app.post('/products/:productId/units', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.post('/products/:productId/units', { preHandler: [app.authenticate, app.requirePermission('inventory.edit')] }, async (request, reply) => {
|
||||
const { productId } = request.params as { productId: string }
|
||||
const parsed = InventoryUnitCreateSchema.safeParse({ ...(request.body as object), productId })
|
||||
if (!parsed.success) {
|
||||
@@ -63,21 +63,21 @@ export const productRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.status(201).send(unit)
|
||||
})
|
||||
|
||||
app.get('/products/:productId/units', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/products/:productId/units', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
|
||||
const { productId } = request.params as { productId: string }
|
||||
const params = PaginationSchema.parse(request.query)
|
||||
const result = await InventoryUnitService.listByProduct(app.db, request.companyId, productId, params)
|
||||
return reply.send(result)
|
||||
})
|
||||
|
||||
app.get('/units/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.get('/units/:id', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const unit = await InventoryUnitService.getById(app.db, request.companyId, id)
|
||||
if (!unit) return reply.status(404).send({ error: { message: 'Unit not found', statusCode: 404 } })
|
||||
return reply.send(unit)
|
||||
})
|
||||
|
||||
app.patch('/units/:id', { preHandler: [app.authenticate] }, async (request, reply) => {
|
||||
app.patch('/units/:id', { preHandler: [app.authenticate, app.requirePermission('inventory.edit')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const parsed = InventoryUnitUpdateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
|
||||
Reference in New Issue
Block a user