Files
plunk/apps/api/src/services/DomainService.ts
T
2025-12-01 09:56:56 +01:00

234 lines
6.2 KiB
TypeScript

import {prisma} from '../database/prisma.js';
import {wrapRedis} from '../database/redis.js';
import {HttpException} from '../exceptions/index.js';
import {Keys} from './keys.js';
import {getDomainVerificationAttributes, verifyDomain} from './SESService.js';
export class DomainService {
/**
* Get a domain by ID
*/
public static async id(id: string) {
return wrapRedis(Keys.Domain.id(id), async () => {
return prisma.domain.findUnique({where: {id}});
});
}
/**
* Get all domains for a project
*/
public static async getProjectDomains(projectId: string) {
return wrapRedis(Keys.Domain.project(projectId), async () => {
return prisma.domain.findMany({
where: {projectId},
orderBy: {createdAt: 'desc'},
});
});
}
/**
* Add a new domain to a project and start verification
*/
public static async addDomain(projectId: string, domain: string) {
// Start verification process with AWS SES
const dkimTokens = await verifyDomain(domain);
// Create domain record
const newDomain = await prisma.domain.create({
data: {
projectId,
domain,
verified: false,
dkimTokens,
},
});
return newDomain;
}
/**
* Check verification status for a domain
*/
public static async checkVerification(domainId: string) {
const domain = await prisma.domain.findUnique({where: {id: domainId}});
if (!domain) {
throw new Error('Domain not found');
}
const attributes = await getDomainVerificationAttributes(domain.domain);
// Update domain if verification status changed
if (attributes.status === 'Success' && !domain.verified) {
await prisma.domain.update({
where: {id: domainId},
data: {verified: true},
});
} else if (attributes.status !== 'Success' && domain.verified) {
await prisma.domain.update({
where: {id: domainId},
data: {verified: false},
});
}
return {
domain: domain.domain,
tokens: attributes.tokens,
status: attributes.status,
verified: attributes.status === 'Success',
};
}
/**
* Remove a domain from a project
*/
public static async removeDomain(domainId: string) {
const domain = await prisma.domain.findUnique({where: {id: domainId}});
if (!domain) {
throw new Error('Domain not found');
}
// Extract domain name for checking usage
const domainName = domain.domain;
// Check if domain is used in any templates
const templatesUsingDomain = await prisma.template.count({
where: {
projectId: domain.projectId,
from: {
contains: `@${domainName}`,
},
},
});
if (templatesUsingDomain > 0) {
throw new HttpException(
409,
`Cannot delete domain: it is currently used in ${templatesUsingDomain} template(s). Update the templates first.`,
);
}
// Check if domain is used in any campaigns
const campaignsUsingDomain = await prisma.campaign.count({
where: {
projectId: domain.projectId,
from: {
contains: `@${domainName}`,
},
status: {
not: 'SENT', // Allow deletion if all campaigns using it are completed
},
},
});
if (campaignsUsingDomain > 0) {
throw new HttpException(
409,
`Cannot delete domain: it is currently used in ${campaignsUsingDomain} active campaign(s). Update or complete the campaigns first.`,
);
}
await prisma.domain.delete({where: {id: domainId}});
return true;
}
/**
* Get verified domains for a project
*/
public static async getVerifiedDomains(projectId: string) {
return prisma.domain.findMany({
where: {
projectId,
verified: true,
},
});
}
/**
* Verify that an email domain belongs to the specified project and is verified
* @param email Full email address (e.g., "hello@example.com")
* @param projectId Project ID to verify ownership
* @returns The verified domain object
* @throws HttpException if domain not found, not owned by project, or not verified
*/
public static async verifyEmailDomain(email: string, projectId: string) {
// Extract domain from email
const emailParts = email.split('@');
if (emailParts.length !== 2) {
throw new HttpException(400, 'Invalid email format');
}
const domainName = emailParts[1];
// Find domain in database
const domain = await prisma.domain.findFirst({
where: {
domain: domainName,
},
});
if (!domain) {
throw new HttpException(
403,
`Domain "${domainName}" is not registered. Please add and verify this domain in your project settings.`,
);
}
// Verify domain belongs to the project
if (domain.projectId !== projectId) {
throw new HttpException(
403,
`Domain "${domainName}" belongs to a different project. You cannot use this domain.`,
);
}
// Verify domain is verified
if (!domain.verified) {
throw new HttpException(
403,
`Domain "${domainName}" is not verified. Please complete the DNS verification process in your domain settings.`,
);
}
return domain;
}
/**
* Check if a domain is already linked to another project
* Used when adding a new domain to verify if the user has access to the existing project
* @param domain Domain name to check
* @param userId User ID to check membership
* @returns Object with exists flag and membership info
*/
public static async checkDomainOwnership(domain: string, userId: string) {
const existingDomain = await prisma.domain.findFirst({
where: {domain},
include: {
project: {
include: {
members: {
where: {userId},
},
},
},
},
});
if (!existingDomain) {
return {exists: false};
}
// Check if user is a member of the project that owns this domain
const isMember = existingDomain.project.members.length > 0;
return {
exists: true,
projectId: existingDomain.project.id,
projectName: existingDomain.project.name,
isMember,
};
}
}