From d7eb85ffdd99177c5e27104779281f5a08fe36a7 Mon Sep 17 00:00:00 2001 From: Dries Augustyns Date: Fri, 24 Apr 2026 19:21:07 +0200 Subject: [PATCH] fix: update project disabled messages for clarity and consistency --- apps/api/src/controllers/Domains.ts | 12 +++++- apps/api/src/controllers/Users.ts | 6 +-- apps/api/src/jobs/email-processor.ts | 6 +-- apps/api/src/middleware/auth.ts | 8 ++-- apps/api/src/services/DomainService.ts | 43 +++++++++++++++++++ apps/api/src/services/SecurityService.ts | 6 +-- apps/web/src/components/SecuritySettings.tsx | 10 ++--- .../src/components/SecurityWarningBanner.tsx | 4 +- apps/web/src/pages/index.tsx | 15 +------ packages/email/src/emails/ProjectDisabled.tsx | 42 +++++------------- 10 files changed, 84 insertions(+), 68 deletions(-) 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) { Project Disabled - This project has been disabled due to security violations. Contact support to resolve. + This project has been disabled. Please contact support for more details. )} @@ -73,8 +73,8 @@ export function SecuritySettings({metrics, isLoading}: SecuritySettingsProps) { Critical - Your bounce or complaint rates have exceeded acceptable levels. Review your contact lists and sending - practices to avoid project suspension. + Your account requires immediate attention. Please contact support or review your sending practices to + avoid suspension. )} @@ -84,8 +84,8 @@ export function SecuritySettings({metrics, isLoading}: SecuritySettingsProps) { Warning - Your bounce or complaint rates are approaching limits. Review your contact lists and remove invalid - addresses to maintain good standing. + Your account health needs attention. Review your contact lists and sending practices to maintain good + standing. )} diff --git a/apps/web/src/components/SecurityWarningBanner.tsx b/apps/web/src/components/SecurityWarningBanner.tsx index 2b0852c..836a711 100644 --- a/apps/web/src/components/SecurityWarningBanner.tsx +++ b/apps/web/src/components/SecurityWarningBanner.tsx @@ -29,8 +29,8 @@ export function SecurityWarningBanner({status}: SecurityWarningBannerProps) {

{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.'}