fix: align dashboard guard with fallback menu access
Build & Deploy / Build & Push Docker Image (push) Successful in 2m56s
Test / Type Check (all packages) (push) Successful in 51s
Build & Deploy / Deploy to VPS (push) Successful in 4s
Test / API Unit Tests (push) Successful in 47s
Test / Homepage Unit Tests (push) Successful in 46s
Test / Storefront Unit Tests (push) Successful in 42s
Test / Admin Unit Tests (push) Successful in 42s
Test / Dashboard Unit Tests (push) Successful in 39s
Test / API Integration Tests (push) Successful in 1m1s
Build & Deploy / Build & Push Docker Image (push) Successful in 2m56s
Test / Type Check (all packages) (push) Successful in 51s
Build & Deploy / Deploy to VPS (push) Successful in 4s
Test / API Unit Tests (push) Successful in 47s
Test / Homepage Unit Tests (push) Successful in 46s
Test / Storefront Unit Tests (push) Successful in 42s
Test / Admin Unit Tests (push) Successful in 42s
Test / Dashboard Unit Tests (push) Successful in 39s
Test / API Integration Tests (push) Successful in 1m1s
This commit is contained in:
@@ -107,4 +107,18 @@ describe('requireSubscription middleware', () => {
|
||||
expect(next).toHaveBeenCalledTimes(1)
|
||||
expect(res.status).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('passes subscription lookup failures to Express error handling', async () => {
|
||||
const error = new Error('database unavailable')
|
||||
vi.mocked(prisma.subscription.findUnique).mockRejectedValue(error as never)
|
||||
|
||||
const req = { company: { id: 'company_1', status: 'ACTIVE' } } as Request
|
||||
const res = responseStub()
|
||||
const next = vi.fn() as NextFunction
|
||||
|
||||
await requireSubscription(req, res, next)
|
||||
|
||||
expect(next).toHaveBeenCalledWith(error)
|
||||
expect(res.status).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,38 +13,42 @@ const BLOCKED_STATUSES = ['SUSPENDED', 'PENDING']
|
||||
* req.company.status is not SUSPENDED or PENDING, and subscription access is not none
|
||||
*/
|
||||
export async function requireSubscription(req: Request, res: Response, next: NextFunction) {
|
||||
const company = req.company
|
||||
if (!company) return sendUnauthorized(res, 'unauthenticated', 'No company context')
|
||||
try {
|
||||
const company = req.company
|
||||
if (!company) return sendUnauthorized(res, 'unauthenticated', 'No company context')
|
||||
|
||||
if (BLOCKED_STATUSES.includes(company.status)) {
|
||||
return sendPaymentRequired(
|
||||
res,
|
||||
`subscription_${company.status.toLowerCase()}`,
|
||||
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.',
|
||||
{ billingUrl: `${process.env.NEXT_PUBLIC_DASHBOARD_URL}/subscription` },
|
||||
)
|
||||
if (BLOCKED_STATUSES.includes(company.status)) {
|
||||
return sendPaymentRequired(
|
||||
res,
|
||||
`subscription_${company.status.toLowerCase()}`,
|
||||
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.',
|
||||
{ billingUrl: `${process.env.NEXT_PUBLIC_DASHBOARD_URL}/subscription` },
|
||||
)
|
||||
}
|
||||
|
||||
const subscription = await prisma.subscription.findUnique({
|
||||
where: { companyId: company.id },
|
||||
select: { status: true },
|
||||
})
|
||||
const subscriptionStatus = subscription?.status ?? 'EXPIRED'
|
||||
|
||||
if (!hasAnyAccess(subscriptionStatus)) {
|
||||
return sendPaymentRequired(
|
||||
res,
|
||||
'subscription_required',
|
||||
'Your subscription has ended. Please reactivate to continue.',
|
||||
{
|
||||
billingUrl: `${process.env.NEXT_PUBLIC_DASHBOARD_URL}/subscription`,
|
||||
subscriptionStatus,
|
||||
accessLevel: getAccessLevel(subscriptionStatus),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
next()
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
|
||||
const subscription = await prisma.subscription.findUnique({
|
||||
where: { companyId: company.id },
|
||||
select: { status: true },
|
||||
})
|
||||
const subscriptionStatus = subscription?.status ?? 'EXPIRED'
|
||||
|
||||
if (!hasAnyAccess(subscriptionStatus)) {
|
||||
return sendPaymentRequired(
|
||||
res,
|
||||
'subscription_required',
|
||||
'Your subscription has ended. Please reactivate to continue.',
|
||||
{
|
||||
billingUrl: `${process.env.NEXT_PUBLIC_DASHBOARD_URL}/subscription`,
|
||||
subscriptionStatus,
|
||||
accessLevel: getAccessLevel(subscriptionStatus),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ vi.mock('../../lib/prisma', () => ({
|
||||
prisma: {
|
||||
employee: { findUnique: vi.fn() },
|
||||
company: { findUnique: vi.fn() },
|
||||
subscription: { findUnique: vi.fn() },
|
||||
adminUser: { findUnique: vi.fn() },
|
||||
renter: { findUnique: vi.fn() },
|
||||
},
|
||||
@@ -53,6 +54,7 @@ describe('auth middleware API boundaries', () => {
|
||||
vi.clearAllMocks()
|
||||
process.env.JWT_SECRET = 'test-secret'
|
||||
process.env.NEXT_PUBLIC_DASHBOARD_URL = 'https://dashboard.example.test'
|
||||
vi.mocked(prisma.subscription.findUnique).mockResolvedValue({ status: 'ACTIVE' } as never)
|
||||
})
|
||||
|
||||
it('rejects protected company routes before service execution when no token is present', async () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ vi.mock('../../lib/prisma', () => ({
|
||||
prisma: {
|
||||
employee: { findUnique: vi.fn() },
|
||||
company: { findUnique: vi.fn() },
|
||||
subscription: { findUnique: vi.fn() },
|
||||
},
|
||||
}))
|
||||
vi.mock('../../modules/vehicles/vehicle.service', () => ({
|
||||
@@ -57,6 +58,7 @@ beforeEach(() => {
|
||||
vi.mocked(jwt.verify).mockReturnValue({ sub: 'employee_1', type: 'employee' } as never)
|
||||
vi.mocked(prisma.employee.findUnique).mockResolvedValue(employee() as never)
|
||||
vi.mocked(prisma.company.findUnique).mockResolvedValue({ id: 'company_1', status: 'ACTIVE' } as never)
|
||||
vi.mocked(prisma.subscription.findUnique).mockResolvedValue({ status: 'ACTIVE' } as never)
|
||||
})
|
||||
|
||||
describe('vehicle pricing API contracts', () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ vi.mock('../../lib/prisma', () => ({
|
||||
prisma: {
|
||||
employee: { findUnique: vi.fn().mockResolvedValue({ id: 'employee_1', role: 'MANAGER', isActive: true, companyId: 'company_1', company: { id: 'company_1', status: 'ACTIVE' } }) },
|
||||
company: { findUnique: vi.fn().mockResolvedValue({ id: 'company_1', status: 'ACTIVE' }) },
|
||||
subscription: { findUnique: vi.fn().mockResolvedValue({ status: 'ACTIVE' }) },
|
||||
},
|
||||
}))
|
||||
vi.mock('../../modules/vehicles/vehicle.service', () => ({
|
||||
|
||||
Reference in New Issue
Block a user