diff --git a/apps/api/src/controllers/Domains.ts b/apps/api/src/controllers/Domains.ts
index 6ace560..8a2f46e 100644
--- a/apps/api/src/controllers/Domains.ts
+++ b/apps/api/src/controllers/Domains.ts
@@ -52,7 +52,15 @@ export class Domains {
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.',
+ 'This project has been disabled. Please contact support for assistance.',
+ );
+ }
+
+ // Block subdomains whose root domain belongs to a disabled project
+ const rootCheck = await DomainService.checkSubdomainOfDisabledRoot(domain);
+ if (rootCheck.blocked) {
+ throw new NotAllowed(
+ 'This domain cannot be added at this time. Please contact support for assistance.',
);
}
@@ -138,7 +146,7 @@ export class Domains {
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.',
+ 'This project has been disabled. Please contact support for assistance.',
);
}
diff --git a/apps/api/src/controllers/Users.ts b/apps/api/src/controllers/Users.ts
index 1a2f2a0..1fe8314 100644
--- a/apps/api/src/controllers/Users.ts
+++ b/apps/api/src/controllers/Users.ts
@@ -68,7 +68,7 @@ export class Users {
if (hasDisabledProject) {
throw new HttpException(
403,
- `You cannot create new projects while you are a member of disabled projects: ${disabledProjectNames.join(', ')}. Please contact support to resolve security violations.`,
+ `You cannot create new projects at this time. Please contact support for assistance.`,
ErrorCode.PROJECT_DISABLED,
);
}
@@ -615,7 +615,7 @@ export class Users {
if (isDisabled) {
throw new HttpException(
403,
- 'Cannot reset a disabled project. Please contact support to resolve security violations before making changes.',
+ 'This project has been disabled. Please contact support for assistance.',
ErrorCode.PROJECT_DISABLED,
);
}
@@ -703,7 +703,7 @@ export class Users {
if (project.disabled) {
throw new HttpException(
403,
- 'Cannot delete a disabled project. Please contact support to resolve security violations.',
+ 'This project has been disabled. Please contact support for assistance.',
ErrorCode.PROJECT_DISABLED,
);
}
diff --git a/apps/api/src/jobs/email-processor.ts b/apps/api/src/jobs/email-processor.ts
index 7485be3..305a7c5 100644
--- a/apps/api/src/jobs/email-processor.ts
+++ b/apps/api/src/jobs/email-processor.ts
@@ -170,13 +170,11 @@ export async function createEmailWorker() {
where: {id: emailId},
data: {
status: EmailStatus.FAILED,
- error: `Phishing content detected with ${phishingCheck.confidence}% confidence - project disabled`,
+ error: 'This email could not be sent. The project has been disabled. Please contact support.',
},
});
- throw new Error(
- `Phishing content detected with ${phishingCheck.confidence}% confidence - project ${email.projectId} disabled`,
- );
+ throw new Error(`Project ${email.projectId} has been disabled due to a policy violation`);
}
// Send via AWS SES
diff --git a/apps/api/src/middleware/auth.ts b/apps/api/src/middleware/auth.ts
index 24e4fbc..83190ac 100644
--- a/apps/api/src/middleware/auth.ts
+++ b/apps/api/src/middleware/auth.ts
@@ -135,7 +135,7 @@ export const requirePublicKey = async (req: Request, res: Response, next: NextFu
if (isWriteOperation) {
throw new HttpException(
403,
- 'Project is disabled due to security violations. All write operations are blocked.',
+ 'This project has been disabled. Please contact support for assistance.',
ErrorCode.PROJECT_DISABLED,
);
}
@@ -204,7 +204,7 @@ export const requireSecretKey = async (req: Request, res: Response, next: NextFu
if (isWriteOperation) {
throw new HttpException(
403,
- 'Project is disabled due to security violations. All write operations are blocked.',
+ 'This project has been disabled. Please contact support for assistance.',
ErrorCode.PROJECT_DISABLED,
);
}
@@ -271,7 +271,7 @@ export const requireAuth = async (req: Request, res: Response, next: NextFunctio
if (isWriteOperation) {
throw new HttpException(
403,
- 'Project is disabled due to security violations. All write operations are blocked.',
+ 'This project has been disabled. Please contact support for assistance.',
ErrorCode.PROJECT_DISABLED,
);
}
@@ -315,7 +315,7 @@ export const requireAuth = async (req: Request, res: Response, next: NextFunctio
if (isWriteOperation) {
throw new HttpException(
403,
- 'Project is disabled due to security violations. All write operations are blocked.',
+ 'This project has been disabled. Please contact support for assistance.',
ErrorCode.PROJECT_DISABLED,
);
}
diff --git a/apps/api/src/services/DomainService.ts b/apps/api/src/services/DomainService.ts
index 86ebcd3..da2562d 100644
--- a/apps/api/src/services/DomainService.ts
+++ b/apps/api/src/services/DomainService.ts
@@ -388,6 +388,49 @@ export class DomainService {
return domain;
}
+ /**
+ * Extract the registrable root domain (last two labels) from a domain name.
+ * e.g. "mail.example.com" → "example.com", "example.com" → "example.com"
+ */
+ private static rootDomain(domain: string): string {
+ const parts = domain.split('.');
+ return parts.length > 2 ? parts.slice(-2).join('.') : domain;
+ }
+
+ /**
+ * Check whether the root domain of `domain` is owned by a disabled project.
+ * Prevents subdomains from being added when the parent domain is flagged.
+ */
+ public static async checkSubdomainOfDisabledRoot(
+ domain: string,
+ ): Promise<{blocked: boolean; projectName?: string; projectId?: string}> {
+ const root = this.rootDomain(domain);
+
+ // Only relevant when the submitted domain is actually a subdomain
+ if (root === domain) {
+ return {blocked: false};
+ }
+
+ const rootDomainRecord = await prisma.domain.findFirst({
+ where: {domain: root},
+ include: {
+ project: {
+ select: {id: true, name: true, disabled: true},
+ },
+ },
+ });
+
+ if (rootDomainRecord?.project.disabled) {
+ return {
+ blocked: true,
+ projectName: rootDomainRecord.project.name,
+ projectId: rootDomainRecord.project.id,
+ };
+ }
+
+ return {blocked: false};
+ }
+
/**
* 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
diff --git a/apps/api/src/services/SecurityService.ts b/apps/api/src/services/SecurityService.ts
index 0ce4a97..1877ba4 100644
--- a/apps/api/src/services/SecurityService.ts
+++ b/apps/api/src/services/SecurityService.ts
@@ -752,7 +752,7 @@ export class SecurityService {
landingUrl: LANDING_URI,
});
await Promise.all(
- emails.map(email => sendPlatformEmail(email, 'Project Disabled - Security Risk', template)),
+ emails.map(email => sendPlatformEmail(email, 'Project Disabled', template)),
);
}
} catch (emailError) {
@@ -974,7 +974,7 @@ ${strippedBody.substring(0, 2000)}`,
data: {disabled: true},
});
- const violation = `Phishing content detected with ${confidence}% confidence in email: "${subject}"${reason ? ` - ${reason}` : ''}`;
+ const violation = `A policy violation was detected. Please contact support for more details.`;
// Log critical security event
signale.error(
@@ -1006,7 +1006,7 @@ ${strippedBody.substring(0, 2000)}`,
landingUrl: LANDING_URI,
});
await Promise.all(
- emails.map(email => sendPlatformEmail(email, 'Project Disabled - Phishing Detected', template)),
+ emails.map(email => sendPlatformEmail(email, 'Project Disabled', template)),
);
}
} catch (emailError) {
diff --git a/apps/web/src/components/SecuritySettings.tsx b/apps/web/src/components/SecuritySettings.tsx
index 8024cc9..02a0dbb 100644
--- a/apps/web/src/components/SecuritySettings.tsx
+++ b/apps/web/src/components/SecuritySettings.tsx
@@ -63,7 +63,7 @@ export function SecuritySettings({metrics, isLoading}: SecuritySettingsProps) {
{hasCriticalViolations - ? 'Your bounce or complaint rates have exceeded acceptable levels. Review your contact lists and sending practices to avoid project suspension.' - : 'Your bounce or complaint rates are approaching limits. Review your contact lists and remove invalid addresses to maintain good standing.'} + ? 'Your account requires immediate attention. Review your sending practices to avoid suspension. Contact support for more details.' + : 'Your account health needs attention. Review your contact lists and sending practices to maintain good standing.'}