fix both admin and company login
Build & Push / Pipeline Tests (push) Failing after 1m27s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Successful in 55s
Test / API Unit Tests (push) Successful in 1m9s
Test / Homepage Unit Tests (push) Successful in 50s
Test / Carplace Unit Tests (push) Has been cancelled
Test / Admin Unit Tests (push) Has been cancelled
Test / Dashboard Unit Tests (push) Has been cancelled
Test / API Integration Tests (push) Has been cancelled

This commit is contained in:
root
2026-07-28 23:11:46 -04:00
parent 50c74b9007
commit eeec8f0a82
5 changed files with 167 additions and 117 deletions
+2
View File
@@ -15,6 +15,7 @@ import webhookRouter from './modules/webhooks/webhook.routes'
import companyAuthRouter from './modules/auth/auth.company.routes'
import employeeAuthRouter from './modules/auth/auth.employee.routes'
import accountAuthRouter from './modules/auth/auth.account.routes'
import unifiedAuthRouter from './modules/auth/auth.unified.routes'
import renterAuthRouter from './modules/auth/auth.renter.routes'
import teamRouter from './modules/team/team.routes'
import offersRouter from './modules/offers/offer.routes'
@@ -207,6 +208,7 @@ export function createApp() {
app.use(express.json({ limit: '10mb' }))
// ─── API Routes ─────────────────────────────────────────────
app.use(`${v1}/auth`, authLimiter, unifiedAuthRouter)
app.use(`${v1}/auth/account`, authLimiter, accountAuthRouter)
app.use(`${v1}/auth/renter`, authLimiter, renterAuthRouter)
app.use(`${v1}/auth/company`, authLimiter, companyAuthRouter)
@@ -0,0 +1,57 @@
import { Router } from 'express'
import { z } from 'zod'
import { parseBody } from '../../http/validate'
import { ok } from '../../http/respond'
import { AppError } from '../../http/errors'
import { setSessionCookie, clearSessionCookie } from '../../security/sessionCookies'
import * as employeeService from './auth.employee.service'
import * as adminService from '../admin/admin.service'
const unifiedLoginSchema = z.object({
email: z.string().email().max(255).trim().toLowerCase(),
password: z.string().max(128),
totpCode: z.string().length(6).optional(),
recoveryCode: z.string().min(8).max(32).optional(),
})
const router = Router()
router.post('/login', async (req, res, next) => {
try {
const { email, password, totpCode, recoveryCode } = parseBody(unifiedLoginSchema, req)
if (!totpCode && !recoveryCode) {
try {
const employeeResult = await employeeService.login({ email, password })
if (!('token' in employeeResult)) {
throw new AppError('Invalid email or password', 401, 'invalid_credentials')
}
clearSessionCookie(res, 'admin')
setSessionCookie(res, 'employee', employeeResult.token, 8 * 60 * 60 * 1000)
return ok(res, employeeResult)
} catch (err) {
if (!(err instanceof AppError) || err.error !== 'invalid_credentials') throw err
}
}
const adminResult = await adminService.login(email, password, totpCode, recoveryCode)
if (!adminResult) {
return res.status(401).json({ error: 'invalid_credentials', message: 'Invalid email or password', statusCode: 401 })
}
if ('totpRequired' in adminResult) {
clearSessionCookie(res, 'employee')
return res.status(401).json({ error: 'totp_required', message: '2FA code required', statusCode: 401 })
}
if ('invalidTotp' in adminResult) {
return res.status(401).json({ error: 'invalid_totp', message: 'Invalid 2FA code', statusCode: 401 })
}
clearSessionCookie(res, 'employee')
setSessionCookie(res, 'admin', adminResult.token, 8 * 60 * 60 * 1000)
return ok(res, adminResult)
} catch (err) {
return next(err)
}
})
export default router
@@ -30,12 +30,18 @@ vi.mock('../../modules/admin/admin.service', () => ({
resetPassword: vi.fn(),
}))
vi.mock('../../modules/auth/auth.employee.service', () => ({
login: vi.fn(),
}))
import request from 'supertest'
import jwt from 'jsonwebtoken'
import { createApp } from '../../app'
import { prisma } from '../../lib/prisma'
import * as vehicleService from '../../modules/vehicles/vehicle.service'
import * as adminService from '../../modules/admin/admin.service'
import * as employeeService from '../../modules/auth/auth.employee.service'
import { AppError } from '../../http/errors'
const app = createApp()
@@ -65,6 +71,41 @@ describe('auth middleware API boundaries', () => {
expect(vehicleService.listVehicles).not.toHaveBeenCalled()
})
it('uses the unified login endpoint for employee credentials', async () => {
vi.mocked(employeeService.login).mockResolvedValue({
token: 'employee-jwt',
employee: { id: 'employee_1', email: 'owner@example.test' },
} as never)
const res = await request(app)
.post('/api/v1/auth/login')
.send({ email: 'owner@example.test', password: 'valid-password' })
expect(res.status).toBe(200)
expect(employeeService.login).toHaveBeenCalledWith({ email: 'owner@example.test', password: 'valid-password' })
expect(adminService.login).not.toHaveBeenCalled()
expect(res.headers['set-cookie']).toEqual(expect.arrayContaining([
expect.stringMatching(/^admin_session=;/),
expect.stringMatching(/^employee_session=/),
]))
})
it('falls through to admin 2FA when unified login is not an employee account', async () => {
vi.mocked(employeeService.login).mockRejectedValue(new AppError('Invalid email or password', 401, 'invalid_credentials'))
vi.mocked(adminService.login).mockResolvedValue({ totpRequired: true } as never)
const res = await request(app)
.post('/api/v1/auth/login')
.send({ email: 'admin@example.test', password: 'valid-password' })
expect(res.status).toBe(401)
expect(res.body.error).toBe('totp_required')
expect(adminService.login).toHaveBeenCalledWith('admin@example.test', 'valid-password', undefined, undefined)
expect(res.headers['set-cookie']).toEqual(expect.arrayContaining([
expect.stringMatching(/^employee_session=;/),
]))
})
it('rejects employee-protected routes when a renter token is used', async () => {
vi.mocked(jwt.verify).mockReturnValue({ sub: 'renter_1', type: 'renter' } as never)