Fix auth security issues, add rate limiting, write Phase 2 audit

Security fixes:
- Register route validates company exists before creating user
- Rate limiting on auth routes (10 per 15min per IP)
- Dev auth plugin guards against production use
- Main.ts throws if JWT_SECRET missing in production

Added Phase 2 audit doc (22) covering:
- Built vs planning doc comparison
- Security review with fixes applied
- Duplicate code patterns identified
- Standard POS feature gap analysis
- Music-specific feature gaps

33 tests passing.
This commit is contained in:
Ryan Moon
2026-03-27 19:21:33 -05:00
parent dcc3dd1eed
commit c34ad27b86
6 changed files with 204 additions and 204 deletions

View File

@@ -3,11 +3,22 @@ import { eq } from 'drizzle-orm'
import bcrypt from 'bcrypt'
import { RegisterSchema, LoginSchema } from '@forte/shared/schemas'
import { users } from '../../db/schema/users.js'
import { companies } from '../../db/schema/stores.js'
const SALT_ROUNDS = 10
export const authRoutes: FastifyPluginAsync = async (app) => {
app.post('/auth/register', async (request, reply) => {
// Rate limit auth routes — 10 attempts per 15 minutes per IP
const rateLimitConfig = {
config: {
rateLimit: {
max: 10,
timeWindow: '15 minutes',
},
},
}
app.post('/auth/register', rateLimitConfig, async (request, reply) => {
const parsed = RegisterSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({
@@ -18,6 +29,25 @@ export const authRoutes: FastifyPluginAsync = async (app) => {
const { email, password, firstName, lastName, role } = parsed.data
const companyId = request.companyId
// Validate that the company exists
if (!companyId) {
return reply.status(400).send({
error: { message: 'Company ID is required (x-company-id header)', statusCode: 400 },
})
}
const [company] = await app.db
.select({ id: companies.id })
.from(companies)
.where(eq(companies.id, companyId))
.limit(1)
if (!company) {
return reply.status(400).send({
error: { message: 'Invalid company', statusCode: 400 },
})
}
// Email is globally unique across all companies
const existing = await app.db
.select({ id: users.id })
@@ -61,7 +91,7 @@ export const authRoutes: FastifyPluginAsync = async (app) => {
return reply.status(201).send({ user, token })
})
app.post('/auth/login', async (request, reply) => {
app.post('/auth/login', rateLimitConfig, async (request, reply) => {
const parsed = LoginSchema.safeParse(request.body)
if (!parsed.success) {
return reply.status(400).send({