From 6aee5db588183c468e06fab64ca5c9af5b79568d Mon Sep 17 00:00:00 2001 From: ReylanLugo Date: Sun, 10 May 2026 22:44:28 -0400 Subject: [PATCH] refactor(api): rely on auth middleware for domain endpoint permissions Replace per-route apiKey/jwt branching with auth.projectId from middleware, matching the contacts controller pattern. Preserve JWT admin gating on POST/DELETE; API keys are project-scoped by design and skip the role check. Cross-project domain access by ID now returns 404 instead of 403 to avoid leaking existence. --- apps/api/src/controllers/Domains.ts | 47 +++++++---------------------- 1 file changed, 11 insertions(+), 36 deletions(-) diff --git a/apps/api/src/controllers/Domains.ts b/apps/api/src/controllers/Domains.ts index a87e443..5f101a4 100644 --- a/apps/api/src/controllers/Domains.ts +++ b/apps/api/src/controllers/Domains.ts @@ -19,19 +19,10 @@ export class Domains { @Get('project/:projectId') @Middleware([requireAuth, requireEmailVerified]) @CatchAsync - public async getProjectDomains(req: Request, res: Response, _next: NextFunction) { + public async getProjectDomains(_req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth; - const {projectId} = DomainSchemas.projectId.parse(req.params); - if (auth.type === 'apiKey') { - if (auth.projectId !== projectId) { - throw new NotAllowed('You do not have access to this project'); - } - } else { - await MembershipService.requireAccess(auth.userId!, projectId); - } - - const domains = await DomainService.getProjectDomains(projectId); + const domains = await DomainService.getProjectDomains(auth.projectId!); return res.status(200).json(domains); } @@ -44,17 +35,12 @@ export class Domains { @CatchAsync public async addDomain(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth; - const {projectId: requestedProjectId, domain} = DomainSchemas.create.parse(req.body); - const projectId = auth.type === 'apiKey' ? auth.projectId : requestedProjectId; + const {domain} = DomainSchemas.create.parse(req.body); + const projectId = auth.projectId!; - if (auth.type === 'apiKey') { - if (requestedProjectId !== auth.projectId) { - throw new NotAllowed('You do not have access to this project'); - } - } else if (!auth.userId) { - throw new NotFound('User authentication required'); - } else { - await MembershipService.requireAdminAccess(auth.userId, projectId); + // Require admin role for JWT users (API keys bypass — project-scoped by design) + if (auth.type === 'jwt') { + await MembershipService.requireAdminAccess(auth.userId!, projectId); } // Block domain changes on disabled projects @@ -122,18 +108,10 @@ export class Domains { const domain = await DomainService.id(id); - if (!domain) { + if (!domain || domain.projectId !== auth.projectId) { throw new NotFound('Domain not found'); } - if (auth.type === 'apiKey') { - if (auth.projectId !== domain.projectId) { - throw new NotAllowed('You do not have access to this project'); - } - } else { - await MembershipService.requireAccess(auth.userId!, domain.projectId); - } - const verificationStatus = await DomainService.checkVerification(id); // Invalidate cache if status changed @@ -155,15 +133,12 @@ export class Domains { const domain = await DomainService.id(id); - if (!domain) { + if (!domain || domain.projectId !== auth.projectId) { throw new NotFound('Domain not found'); } - if (auth.type === 'apiKey') { - if (auth.projectId !== domain.projectId) { - throw new NotAllowed('You do not have access to this project'); - } - } else { + // Require admin role for JWT users (API keys bypass — project-scoped by design) + if (auth.type === 'jwt') { await MembershipService.requireAdminAccess(auth.userId!, domain.projectId); }