Initial push of Plunk Next
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
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., "[email protected]")
|
||||
* @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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user