234 lines
6.2 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
}
|