fix: update project disabled messages for clarity and consistency

This commit is contained in:
Dries Augustyns
2026-04-24 19:21:07 +02:00
parent 1c89ed083e
commit d7eb85ffdd
10 changed files with 84 additions and 68 deletions
+10 -2
View File
@@ -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.',
);
}
+3 -3
View File
@@ -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,
);
}
+2 -4
View File
@@ -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
+4 -4
View File
@@ -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,
);
}
+43
View File
@@ -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
+3 -3
View File
@@ -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) {
+5 -5
View File
@@ -63,7 +63,7 @@ export function SecuritySettings({metrics, isLoading}: SecuritySettingsProps) {
<AlertCircle className="h-4 w-4" />
<AlertTitle>Project Disabled</AlertTitle>
<AlertDescription>
This project has been disabled due to security violations. Contact support to resolve.
This project has been disabled. Please contact support for more details.
</AlertDescription>
</Alert>
)}
@@ -73,8 +73,8 @@ export function SecuritySettings({metrics, isLoading}: SecuritySettingsProps) {
<AlertCircle className="h-4 w-4" />
<AlertTitle>Critical</AlertTitle>
<AlertDescription>
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.
</AlertDescription>
</Alert>
)}
@@ -84,8 +84,8 @@ export function SecuritySettings({metrics, isLoading}: SecuritySettingsProps) {
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Warning</AlertTitle>
<AlertDescription>
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.
</AlertDescription>
</Alert>
)}
@@ -29,8 +29,8 @@ export function SecurityWarningBanner({status}: SecurityWarningBannerProps) {
<div className="space-y-2 flex-1">
<p className={`text-sm ${messageColor}`}>
{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.'}
</p>
</div>
<Button asChild size="sm" variant="outline" className="w-full sm:w-auto flex-shrink-0">
+2 -13
View File
@@ -95,22 +95,11 @@ export default function Index() {
<AlertDescription className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
<div className="space-y-2 flex-1">
<p className="text-sm font-medium">
This project has been disabled due to security violations. All scheduled campaigns and workflows
have been cancelled. The project is now in read-only mode - you can view your data but cannot
This project has been disabled and is now in read-only mode. You can view your data but cannot
create, update, or delete anything.
</p>
{securityMetrics && securityMetrics.status.violations.length > 0 && (
<>
<p className="text-sm font-medium mt-3">Security violations that caused suspension:</p>
<ul className="list-disc list-inside space-y-1 text-sm text-red-800">
{securityMetrics.status.violations.map((violation, idx) => (
<li key={idx}>{violation}</li>
))}
</ul>
</>
)}
<p className="text-xs text-red-800 mt-2">
Please contact support to resolve this issue and get your project re-enabled.
Please contact support for more details and to get your project re-enabled.
</p>
</div>
<Button asChild size="sm" variant="outline" className="w-full sm:w-auto flex-shrink-0">