diff --git a/apps/api/src/controllers/Domains.ts b/apps/api/src/controllers/Domains.ts index 8a2f46e..a87e443 100644 --- a/apps/api/src/controllers/Domains.ts +++ b/apps/api/src/controllers/Domains.ts @@ -4,7 +4,7 @@ import type {NextFunction, Request, Response} from 'express'; import {redis} from '../database/redis.js'; import {NotAllowed, NotFound} from '../exceptions/index.js'; -import {isAuthenticated, requireEmailVerified} from '../middleware/auth.js'; +import {requireAuth, requireEmailVerified} from '../middleware/auth.js'; import {DomainService} from '../services/DomainService.js'; import {Keys} from '../services/keys.js'; import {MembershipService} from '../services/MembershipService.js'; @@ -17,14 +17,19 @@ export class Domains { * Get all domains for a project */ @Get('project/:projectId') - @Middleware([isAuthenticated, requireEmailVerified]) + @Middleware([requireAuth, requireEmailVerified]) @CatchAsync public async getProjectDomains(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth; const {projectId} = DomainSchemas.projectId.parse(req.params); - // Verify user has access to this project - await MembershipService.requireAccess(auth.userId!, projectId); + 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); @@ -35,19 +40,23 @@ export class Domains { * Add a new domain to a project */ @Post('') - @Middleware([isAuthenticated, requireEmailVerified]) + @Middleware([requireAuth, requireEmailVerified]) @CatchAsync public async addDomain(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth; - const {projectId, domain} = DomainSchemas.create.parse(req.body); + const {projectId: requestedProjectId, domain} = DomainSchemas.create.parse(req.body); + const projectId = auth.type === 'apiKey' ? auth.projectId : requestedProjectId; - if (!auth.userId) { + 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); } - // Verify user has admin access to this project - await MembershipService.requireAdminAccess(auth.userId!, projectId); - // Block domain changes on disabled projects const isDisabled = await SecurityService.isProjectDisabled(projectId); if (isDisabled) { @@ -68,6 +77,12 @@ export class Domains { const ownershipCheck = await DomainService.checkDomainOwnership(domain, auth.userId); if (ownershipCheck.exists) { + if (ownershipCheck.projectId === projectId) { + return res.status(400).json({ + error: 'This domain is already linked to this project.', + }); + } + // If domain exists and user is a member of that project, allow it if (ownershipCheck.isMember) { return res.status(400).json({ @@ -99,7 +114,7 @@ export class Domains { * Check verification status for a domain */ @Get(':id/verify') - @Middleware([isAuthenticated, requireEmailVerified]) + @Middleware([requireAuth, requireEmailVerified]) @CatchAsync public async checkVerification(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth; @@ -111,8 +126,13 @@ export class Domains { throw new NotFound('Domain not found'); } - // Verify user has access to the project this domain belongs to - await MembershipService.requireAccess(auth.userId!, domain.projectId); + 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); @@ -127,7 +147,7 @@ export class Domains { * Remove a domain from a project */ @Delete(':id') - @Middleware([isAuthenticated, requireEmailVerified]) + @Middleware([requireAuth, requireEmailVerified]) @CatchAsync public async removeDomain(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth; @@ -139,8 +159,13 @@ export class Domains { throw new NotFound('Domain not found'); } - // Verify user has admin access to the project this domain belongs to - await MembershipService.requireAdminAccess(auth.userId!, domain.projectId); + if (auth.type === 'apiKey') { + if (auth.projectId !== domain.projectId) { + throw new NotAllowed('You do not have access to this project'); + } + } else { + await MembershipService.requireAdminAccess(auth.userId!, domain.projectId); + } // Block domain changes on disabled projects const isDisabled = await SecurityService.isProjectDisabled(domain.projectId); diff --git a/apps/api/src/services/DomainService.ts b/apps/api/src/services/DomainService.ts index da2562d..17189ab 100644 --- a/apps/api/src/services/DomainService.ts +++ b/apps/api/src/services/DomainService.ts @@ -438,15 +438,14 @@ export class DomainService { * @param userId User ID to check membership * @returns Object with exists flag and membership info */ - public static async checkDomainOwnership(domain: string, userId: string) { + public static async checkDomainOwnership(domain: string, userId?: string) { const existingDomain = await prisma.domain.findFirst({ where: {domain}, include: { project: { - include: { - members: { - where: {userId}, - }, + select: { + id: true, + name: true, }, }, }, @@ -456,8 +455,20 @@ export class DomainService { return {exists: false}; } - // Check if user is a member of the project that owns this domain - const isMember = existingDomain.project.members.length > 0; + let isMember = false; + + if (userId) { + const membership = await prisma.membership.findUnique({ + where: { + userId_projectId: { + userId, + projectId: existingDomain.project.id, + }, + }, + }); + + isMember = membership !== null; + } return { exists: true,