153 lines
5.1 KiB
TypeScript
153 lines
5.1 KiB
TypeScript
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});
|
|
}
|
|
}
|