fix architecture and write new tests
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { Request, Response, NextFunction } from 'express'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { prisma } from '../lib/prisma'
|
||||
import { AdminRole } from '@rentaldrivego/database'
|
||||
import { sendUnauthorized, sendForbidden } from './authHelpers'
|
||||
import { getAuthToken, sendUnauthorized, sendForbidden } from './authHelpers'
|
||||
import { verifyActorToken } from '../security/tokens'
|
||||
import { getSessionCookieName } from '../security/sessionCookies'
|
||||
|
||||
const ROLE_RANK: Record<AdminRole, number> = {
|
||||
SUPER_ADMIN: 5,
|
||||
@@ -12,35 +13,48 @@ const ROLE_RANK: Record<AdminRole, number> = {
|
||||
VIEWER: 1,
|
||||
}
|
||||
|
||||
const ADMIN_2FA_ENROLLMENT_EXEMPT_PATHS = new Set([
|
||||
'/auth/me',
|
||||
'/auth/logout',
|
||||
'/auth/2fa/setup',
|
||||
'/auth/2fa/verify',
|
||||
])
|
||||
|
||||
const FRESH_2FA_WINDOW_MS = Number(process.env.ADMIN_FRESH_2FA_WINDOW_MS ?? 10 * 60 * 1000)
|
||||
|
||||
function is2faEnrollmentExempt(req: Request) {
|
||||
return ADMIN_2FA_ENROLLMENT_EXEMPT_PATHS.has(req.path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires a valid admin Bearer token.
|
||||
* Requires a valid admin session token.
|
||||
*
|
||||
* Guarantees on success:
|
||||
* req.admin — the full AdminUser record
|
||||
*/
|
||||
export async function requireAdminAuth(req: Request, res: Response, next: NextFunction) {
|
||||
const authHeader = req.headers.authorization
|
||||
const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null
|
||||
const token = getAuthToken(req, getSessionCookieName('admin'))
|
||||
|
||||
if (!token) return sendUnauthorized(res, 'unauthenticated', 'Admin authentication required')
|
||||
|
||||
let payload: { sub: string; type: string }
|
||||
let payload: { sub: string; type: string; last2faAt?: number }
|
||||
try {
|
||||
payload = jwt.verify(token, process.env.JWT_SECRET!) as { sub: string; type: string }
|
||||
payload = verifyActorToken(token, 'admin')
|
||||
} catch {
|
||||
return sendUnauthorized(res, 'invalid_token', 'Invalid or expired admin token')
|
||||
}
|
||||
|
||||
if (payload.type !== 'admin') {
|
||||
return sendUnauthorized(res, 'invalid_token', 'Invalid token type')
|
||||
}
|
||||
|
||||
const admin = await prisma.adminUser.findUnique({ where: { id: payload.sub } })
|
||||
if (!admin || !admin.isActive) {
|
||||
return sendUnauthorized(res, 'unauthenticated', 'Admin account not found or deactivated')
|
||||
}
|
||||
|
||||
if (!admin.totpEnabled && !is2faEnrollmentExempt(req)) {
|
||||
return sendForbidden(res, 'admin_2fa_required', 'Admin 2FA enrollment is required before using privileged admin routes')
|
||||
}
|
||||
|
||||
req.admin = admin
|
||||
req.adminAuthLast2faAt = typeof payload.last2faAt === 'number' ? payload.last2faAt : undefined
|
||||
next()
|
||||
}
|
||||
|
||||
@@ -63,3 +77,18 @@ export function requireAdminRole(minimumRole: AdminRole) {
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
export function requireFreshAdmin2FA(req: Request, res: Response, next: NextFunction) {
|
||||
const admin = req.admin
|
||||
if (!admin) return sendUnauthorized(res, 'unauthenticated', 'Admin authentication required')
|
||||
if (!admin.totpEnabled) {
|
||||
return sendForbidden(res, 'admin_2fa_required', 'Admin 2FA enrollment is required for this action')
|
||||
}
|
||||
|
||||
const last2faAt = req.adminAuthLast2faAt
|
||||
if (!last2faAt || Date.now() - last2faAt > FRESH_2FA_WINDOW_MS) {
|
||||
return sendForbidden(res, 'fresh_2fa_required', 'Fresh admin 2FA verification is required for this action')
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user