import {Controller, Delete, Get, Middleware, Post} from '@overnightjs/core'; import {DomainSchemas, UtilitySchemas} from '@plunk/shared'; 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 {DomainService} from '../services/DomainService.js'; import {Keys} from '../services/keys.js'; import {MembershipService} from '../services/MembershipService.js'; import {SecurityService} from '../services/SecurityService.js'; import {CatchAsync} from '../utils/asyncHandler.js'; @Controller('domains') export class Domains { /** * Get all domains for a project */ @Get('project/:projectId') @Middleware([isAuthenticated, 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); const domains = await DomainService.getProjectDomains(projectId); return res.status(200).json(domains); } /** * Add a new domain to a project */ @Post('') @Middleware([isAuthenticated, requireEmailVerified]) @CatchAsync public async addDomain(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth; const {projectId, domain} = DomainSchemas.create.parse(req.body); if (!auth.userId) { throw new NotFound('User authentication required'); } // 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) { throw new NotAllowed( 'Cannot add domains to a disabled project. Please contact support to resolve security violations before making changes.', ); } // Check if domain is already linked to another project const ownershipCheck = await DomainService.checkDomainOwnership(domain, auth.userId); if (ownershipCheck.exists) { // If domain exists and user is a member of that project, allow it if (ownershipCheck.isMember) { return res.status(400).json({ error: `This domain is already linked to project "${ownershipCheck.projectName}". You are a member of that project, so you can use the domain from there.`, }); } // Domain exists but user is not a member - deny access return res.status(403).json({ error: 'This domain is already linked to another project. Only members of that project can use this domain.', }); } try { const newDomain = await DomainService.addDomain(projectId, domain); await redis.del(Keys.Domain.project(projectId)); return res.status(201).json(newDomain); } catch (error) { if (error instanceof Error) { return res.status(400).json({error: error.message}); } throw error; } } /** * Check verification status for a domain */ @Get(':id/verify') @Middleware([isAuthenticated, requireEmailVerified]) @CatchAsync public async checkVerification(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth; const {id} = UtilitySchemas.id.parse(req.params); const domain = await DomainService.id(id); if (!domain) { throw new NotFound('Domain not found'); } // Verify user has access to the project this domain belongs to await MembershipService.requireAccess(auth.userId!, domain.projectId); const verificationStatus = await DomainService.checkVerification(id); // Invalidate cache if status changed await redis.del(Keys.Domain.id(id)); await redis.del(Keys.Domain.project(domain.projectId)); return res.status(200).json(verificationStatus); } /** * Remove a domain from a project */ @Delete(':id') @Middleware([isAuthenticated, requireEmailVerified]) @CatchAsync public async removeDomain(req: Request, res: Response, _next: NextFunction) { const auth = res.locals.auth; const {id} = UtilitySchemas.id.parse(req.params); const domain = await DomainService.id(id); if (!domain) { 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); // Block domain changes on disabled projects const isDisabled = await SecurityService.isProjectDisabled(domain.projectId); if (isDisabled) { throw new NotAllowed( 'Cannot remove domains from a disabled project. Please contact support to resolve security violations before making changes.', ); } await DomainService.removeDomain(id); await redis.del(Keys.Domain.id(id)); await redis.del(Keys.Domain.project(domain.projectId)); return res.status(200).json({success: true}); } }