Files
plunk/apps/api/src/controllers/Domains.ts
T

179 lines
5.3 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 {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});
}
}