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.
This commit is contained in:
ReylanLugo
2026-05-10 22:44:28 -04:00
parent 3f30a48c40
commit 6aee5db588
+11 -36
View File
@@ -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);
}