add first files

This commit is contained in:
root
2026-04-30 14:59:57 -04:00
parent 2b97c1a04a
commit 695a7f7cc7
92 changed files with 4873 additions and 0 deletions
@@ -0,0 +1,60 @@
import { Request, Response, NextFunction } from 'express'
import jwt from 'jsonwebtoken'
import { prisma } from '../lib/prisma'
import { AdminRole } from '@rentaldrivego/database'
const ROLE_RANK: Record<AdminRole, number> = {
SUPER_ADMIN: 5,
ADMIN: 4,
SUPPORT: 3,
FINANCE: 2,
VIEWER: 1,
}
export async function requireAdminAuth(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization
const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null
if (!token) {
return res.status(401).json({ error: 'unauthenticated', message: 'Admin authentication required', statusCode: 401 })
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as { sub: string; type: string }
if (payload.type !== 'admin') {
return res.status(401).json({ error: 'invalid_token', message: 'Invalid token type', statusCode: 401 })
}
const admin = await prisma.adminUser.findUnique({ where: { id: payload.sub } })
if (!admin || !admin.isActive) {
return res.status(401).json({ error: 'unauthenticated', message: 'Admin account not found or deactivated', statusCode: 401 })
}
req.admin = admin
next()
} catch {
return res.status(401).json({ error: 'invalid_token', message: 'Invalid or expired admin token', statusCode: 401 })
}
}
export function requireAdminRole(minimumRole: AdminRole) {
return (req: Request, res: Response, next: NextFunction) => {
const admin = req.admin
if (!admin) {
return res.status(401).json({ error: 'unauthenticated', message: 'Admin authentication required', statusCode: 401 })
}
const rank = ROLE_RANK[admin.role] ?? 0
const required = ROLE_RANK[minimumRole] ?? 99
if (rank < required) {
return res.status(403).json({
error: 'forbidden',
message: `This action requires the ${minimumRole} role or higher`,
statusCode: 403,
})
}
next()
}
}
+19
View File
@@ -0,0 +1,19 @@
import { Request, Response, NextFunction } from 'express'
import { prisma } from '../lib/prisma'
export async function requireApiKey(req: Request, res: Response, next: NextFunction) {
const apiKey = req.headers['x-api-key'] as string | undefined
if (!apiKey) {
return res.status(401).json({ error: 'missing_api_key', message: 'API key required in x-api-key header', statusCode: 401 })
}
const company = await prisma.company.findUnique({ where: { apiKey } })
if (!company) {
return res.status(401).json({ error: 'invalid_api_key', message: 'Invalid API key', statusCode: 401 })
}
req.company = company
req.companyId = company.id
next()
}
@@ -0,0 +1,45 @@
import { Request, Response, NextFunction } from 'express'
import { clerkClient } from '@clerk/clerk-sdk-node'
import { prisma } from '../lib/prisma'
export async function requireCompanyAuth(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization
const sessionToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null
if (!sessionToken) {
return res.status(401).json({
error: 'unauthenticated',
message: 'Authentication required',
statusCode: 401,
})
}
try {
const session = await clerkClient.sessions.verifySession(sessionToken, sessionToken)
const clerkUserId = session.userId
const employee = await prisma.employee.findUnique({
where: { clerkUserId },
include: { company: true },
})
if (!employee || !employee.isActive) {
return res.status(401).json({
error: 'unauthenticated',
message: 'Employee account not found or inactive',
statusCode: 401,
})
}
req.employee = employee
req.company = employee.company
req.companyId = employee.companyId
next()
} catch {
return res.status(401).json({
error: 'invalid_token',
message: 'Invalid or expired session token',
statusCode: 401,
})
}
}
@@ -0,0 +1,47 @@
import { Request, Response, NextFunction } from 'express'
import jwt from 'jsonwebtoken'
import { prisma } from '../lib/prisma'
export async function requireRenterAuth(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization
const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null
if (!token) {
return res.status(401).json({ error: 'unauthenticated', message: 'Renter authentication required', statusCode: 401 })
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as { sub: string; type: string }
if (payload.type !== 'renter') {
return res.status(401).json({ error: 'invalid_token', message: 'Invalid token type', statusCode: 401 })
}
const renter = await prisma.renter.findUnique({ where: { id: payload.sub } })
if (!renter || !renter.isActive) {
return res.status(401).json({ error: 'unauthenticated', message: 'Renter account not found or inactive', statusCode: 401 })
}
req.renterId = renter.id
next()
} catch {
return res.status(401).json({ error: 'invalid_token', message: 'Invalid or expired token', statusCode: 401 })
}
}
export async function optionalRenterAuth(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization
const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null
if (!token) return next()
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as { sub: string; type: string }
if (payload.type === 'renter') {
req.renterId = payload.sub
}
} catch {
// Optional — ignore invalid tokens
}
next()
}
+32
View File
@@ -0,0 +1,32 @@
import { Request, Response, NextFunction } from 'express'
import { EmployeeRole } from '@rentaldrivego/database'
const ROLE_RANK: Record<EmployeeRole, number> = {
OWNER: 3,
MANAGER: 2,
AGENT: 1,
}
export function requireRole(minimumRole: EmployeeRole) {
return (req: Request, res: Response, next: NextFunction) => {
const employee = req.employee
if (!employee) {
return res.status(401).json({ error: 'unauthenticated', message: 'Authentication required', statusCode: 401 })
}
const employeeRank = ROLE_RANK[employee.role] ?? 0
const requiredRank = ROLE_RANK[minimumRole] ?? 99
if (employeeRank < requiredRank) {
return res.status(403).json({
error: 'forbidden',
message: `This action requires the ${minimumRole} role or higher`,
statusCode: 403,
requiredRole: minimumRole,
yourRole: employee.role,
})
}
next()
}
}
@@ -0,0 +1,24 @@
import { Request, Response, NextFunction } from 'express'
const BLOCKED_STATUSES = ['SUSPENDED', 'PENDING']
export function requireSubscription(req: Request, res: Response, next: NextFunction) {
const company = req.company
if (!company) {
return res.status(401).json({ error: 'unauthenticated', message: 'No company context', statusCode: 401 })
}
if (BLOCKED_STATUSES.includes(company.status)) {
return res.status(402).json({
error: 'subscription_' + company.status.toLowerCase(),
message:
company.status === 'SUSPENDED'
? 'Your account has been suspended. Please contact support or renew your subscription.'
: 'Your account is pending activation. Please complete your subscription setup.',
statusCode: 402,
billingUrl: `${process.env.NEXT_PUBLIC_DASHBOARD_URL}/billing`,
})
}
next()
}
+24
View File
@@ -0,0 +1,24 @@
import { Request, Response, NextFunction } from 'express'
import { prisma } from '../lib/prisma'
export async function requireTenant(req: Request, res: Response, next: NextFunction) {
if (!req.companyId) {
return res.status(401).json({
error: 'unauthenticated',
message: 'Tenant context missing — requireCompanyAuth must run first',
statusCode: 401,
})
}
const company = await prisma.company.findUnique({ where: { id: req.companyId } })
if (!company) {
return res.status(401).json({
error: 'company_not_found',
message: 'Company not found',
statusCode: 401,
})
}
req.company = company
next()
}