Add user auth with JWT, switch to bun test
- User table with company_id FK, unique email, role enum - Register/login routes with bcrypt + JWT token generation - Auth plugin with authenticate decorator and role guards - Login uses globally unique email (no company header needed) - Dev-auth plugin kept as fallback when JWT_SECRET not set - Switched from vitest to bun:test (vitest had ESM resolution issues with zod in Bun's module structure) - Upgraded to zod 4 - Added Dockerfile.dev and API service to docker-compose - 8 tests passing (health + auth)
This commit is contained in:
62
packages/backend/src/plugins/auth.ts
Normal file
62
packages/backend/src/plugins/auth.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import fp from 'fastify-plugin'
|
||||
import fjwt from '@fastify/jwt'
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyRequest {
|
||||
companyId: string
|
||||
locationId: string
|
||||
user: { id: string; companyId: string; role: string }
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@fastify/jwt' {
|
||||
interface FastifyJWT {
|
||||
payload: { id: string; companyId: string; role: string }
|
||||
user: { id: string; companyId: string; role: string }
|
||||
}
|
||||
}
|
||||
|
||||
export const authPlugin = fp(async (app) => {
|
||||
const secret = process.env.JWT_SECRET
|
||||
if (!secret) {
|
||||
throw new Error('JWT_SECRET environment variable is required')
|
||||
}
|
||||
|
||||
await app.register(fjwt, {
|
||||
secret,
|
||||
sign: { expiresIn: '24h' },
|
||||
})
|
||||
|
||||
// Set companyId from header on all requests (for unauthenticated routes like register/login).
|
||||
// Authenticated routes override this with the JWT payload via the authenticate decorator.
|
||||
app.addHook('onRequest', async (request) => {
|
||||
request.companyId = (request.headers['x-company-id'] as string) ?? ''
|
||||
request.locationId = (request.headers['x-location-id'] as string) ?? ''
|
||||
})
|
||||
|
||||
app.decorate('authenticate', async function (request: any, reply: any) {
|
||||
try {
|
||||
await request.jwtVerify()
|
||||
request.companyId = request.user.companyId
|
||||
} catch (_err) {
|
||||
reply.status(401).send({ error: { message: 'Unauthorized', statusCode: 401 } })
|
||||
}
|
||||
})
|
||||
|
||||
app.decorate('requireRole', function (...roles: string[]) {
|
||||
return async function (request: any, reply: any) {
|
||||
if (!roles.includes(request.user.role)) {
|
||||
reply
|
||||
.status(403)
|
||||
.send({ error: { message: 'Insufficient permissions', statusCode: 403 } })
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyInstance {
|
||||
authenticate: (request: any, reply: any) => Promise<void>
|
||||
requireRole: (...roles: string[]) => (request: any, reply: any) => Promise<void>
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user