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 {NotFound} from '../exceptions/index.js'; import type {AuthResponse} from '../middleware/auth.js'; import {isAuthenticated, requireEmailVerified} from '../middleware/auth.js'; import {DomainService} from '../services/DomainService.js'; import {Keys} from '../services/keys.js'; import {prisma} from '../database/prisma.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 as AuthResponse; const {projectId} = DomainSchemas.projectId.parse(req.params); // Verify user has access to this project const membership = await prisma.membership.findFirst({ where: { userId: auth.userId, projectId, }, }); if (!membership) { throw new NotFound('Project not found or you do not have access'); } 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 as AuthResponse; 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 const membership = await prisma.membership.findFirst({ where: { userId: auth.userId, projectId, role: { in: ['ADMIN', 'OWNER'], }, }, }); if (!membership) { throw new NotFound('Project not found or you do not have permission'); } // 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 as AuthResponse; 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 const membership = await prisma.membership.findFirst({ where: { userId: auth.userId, projectId: domain.projectId, }, }); if (!membership) { throw new NotFound('Domain not found or you do not have access'); } 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 as AuthResponse; 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 const membership = await prisma.membership.findFirst({ where: { userId: auth.userId, projectId: domain.projectId, role: { in: ['ADMIN', 'OWNER'], }, }, }); if (!membership) { throw new NotFound('Domain not found or you do not have permission'); } 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}); } }