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); const isDisabled = await SecurityService.isProjectDisabled(projectId);
if (isDisabled) { if (isDisabled) {
throw new NotAllowed( 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); const isDisabled = await SecurityService.isProjectDisabled(domain.projectId);
if (isDisabled) { if (isDisabled) {
throw new NotAllowed( 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) { if (hasDisabledProject) {
throw new HttpException( throw new HttpException(
403, 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, ErrorCode.PROJECT_DISABLED,
); );
} }
@@ -615,7 +615,7 @@ export class Users {
if (isDisabled) { if (isDisabled) {
throw new HttpException( throw new HttpException(
403, 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, ErrorCode.PROJECT_DISABLED,
); );
} }
@@ -703,7 +703,7 @@ export class Users {
if (project.disabled) { if (project.disabled) {
throw new HttpException( throw new HttpException(
403, 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, ErrorCode.PROJECT_DISABLED,
); );
} }
+2 -4
View File
@@ -170,13 +170,11 @@ export async function createEmailWorker() {
where: {id: emailId}, where: {id: emailId},
data: { data: {
status: EmailStatus.FAILED, 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( throw new Error(`Project ${email.projectId} has been disabled due to a policy violation`);
`Phishing content detected with ${phishingCheck.confidence}% confidence - project ${email.projectId} disabled`,
);
} }
// Send via AWS SES // Send via AWS SES
+4 -4
View File
@@ -135,7 +135,7 @@ export const requirePublicKey = async (req: Request, res: Response, next: NextFu
if (isWriteOperation) { if (isWriteOperation) {
throw new HttpException( throw new HttpException(
403, 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, ErrorCode.PROJECT_DISABLED,
); );
} }
@@ -204,7 +204,7 @@ export const requireSecretKey = async (req: Request, res: Response, next: NextFu
if (isWriteOperation) { if (isWriteOperation) {
throw new HttpException( throw new HttpException(
403, 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, ErrorCode.PROJECT_DISABLED,
); );
} }
@@ -271,7 +271,7 @@ export const requireAuth = async (req: Request, res: Response, next: NextFunctio
if (isWriteOperation) { if (isWriteOperation) {
throw new HttpException( throw new HttpException(
403, 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, ErrorCode.PROJECT_DISABLED,
); );
} }
@@ -315,7 +315,7 @@ export const requireAuth = async (req: Request, res: Response, next: NextFunctio
if (isWriteOperation) { if (isWriteOperation) {
throw new HttpException( throw new HttpException(
403, 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, ErrorCode.PROJECT_DISABLED,
); );
} }
+43
View File
@@ -388,6 +388,49 @@ export class DomainService {
return domain; 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 * 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 * 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, landingUrl: LANDING_URI,
}); });
await Promise.all( await Promise.all(
emails.map(email => sendPlatformEmail(email, 'Project Disabled - Security Risk', template)), emails.map(email => sendPlatformEmail(email, 'Project Disabled', template)),
); );
} }
} catch (emailError) { } catch (emailError) {
@@ -974,7 +974,7 @@ ${strippedBody.substring(0, 2000)}`,
data: {disabled: true}, 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 // Log critical security event
signale.error( signale.error(
@@ -1006,7 +1006,7 @@ ${strippedBody.substring(0, 2000)}`,
landingUrl: LANDING_URI, landingUrl: LANDING_URI,
}); });
await Promise.all( await Promise.all(
emails.map(email => sendPlatformEmail(email, 'Project Disabled - Phishing Detected', template)), emails.map(email => sendPlatformEmail(email, 'Project Disabled', template)),
); );
} }
} catch (emailError) { } catch (emailError) {
+5 -5
View File
@@ -63,7 +63,7 @@ export function SecuritySettings({metrics, isLoading}: SecuritySettingsProps) {
<AlertCircle className="h-4 w-4" /> <AlertCircle className="h-4 w-4" />
<AlertTitle>Project Disabled</AlertTitle> <AlertTitle>Project Disabled</AlertTitle>
<AlertDescription> <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> </AlertDescription>
</Alert> </Alert>
)} )}
@@ -73,8 +73,8 @@ export function SecuritySettings({metrics, isLoading}: SecuritySettingsProps) {
<AlertCircle className="h-4 w-4" /> <AlertCircle className="h-4 w-4" />
<AlertTitle>Critical</AlertTitle> <AlertTitle>Critical</AlertTitle>
<AlertDescription> <AlertDescription>
Your bounce or complaint rates have exceeded acceptable levels. Review your contact lists and sending Your account requires immediate attention. Please contact support or review your sending practices to
practices to avoid project suspension. avoid suspension.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
@@ -84,8 +84,8 @@ export function SecuritySettings({metrics, isLoading}: SecuritySettingsProps) {
<AlertTriangle className="h-4 w-4" /> <AlertTriangle className="h-4 w-4" />
<AlertTitle>Warning</AlertTitle> <AlertTitle>Warning</AlertTitle>
<AlertDescription> <AlertDescription>
Your bounce or complaint rates are approaching limits. Review your contact lists and remove invalid Your account health needs attention. Review your contact lists and sending practices to maintain good
addresses to maintain good standing. standing.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
@@ -29,8 +29,8 @@ export function SecurityWarningBanner({status}: SecurityWarningBannerProps) {
<div className="space-y-2 flex-1"> <div className="space-y-2 flex-1">
<p className={`text-sm ${messageColor}`}> <p className={`text-sm ${messageColor}`}>
{hasCriticalViolations {hasCriticalViolations
? '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. Review your sending practices to avoid suspension. Contact support for more details.'
: '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.'}
</p> </p>
</div> </div>
<Button asChild size="sm" variant="outline" className="w-full sm:w-auto flex-shrink-0"> <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"> <AlertDescription className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
<div className="space-y-2 flex-1"> <div className="space-y-2 flex-1">
<p className="text-sm font-medium"> <p className="text-sm font-medium">
This project has been disabled due to security violations. All scheduled campaigns and workflows This project has been disabled and is now in read-only mode. You can view your data but cannot
have been cancelled. The project is now in read-only mode - you can view your data but cannot
create, update, or delete anything. create, update, or delete anything.
</p> </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"> <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> </p>
</div> </div>
<Button asChild size="sm" variant="outline" className="w-full sm:w-auto flex-shrink-0"> <Button asChild size="sm" variant="outline" className="w-full sm:w-auto flex-shrink-0">
+10 -32
View File
@@ -15,7 +15,7 @@ interface ProjectDisabledEmailProps {
export function ProjectDisabledEmail({ export function ProjectDisabledEmail({
projectName = 'My Project', projectName = 'My Project',
projectId = 'proj_example123', projectId = 'proj_example123',
violations = ['Bounce rate exceeded 10% threshold', 'Complaint rate exceeded 0.5% threshold'], violations: _violations = [],
dashboardUrl = 'https://next-app.useplunk.com', dashboardUrl = 'https://next-app.useplunk.com',
landingUrl = 'https://www.useplunk.com', landingUrl = 'https://www.useplunk.com',
}: ProjectDisabledEmailProps) { }: ProjectDisabledEmailProps) {
@@ -27,53 +27,31 @@ export function ProjectDisabledEmail({
<Heading className="mb-2 mt-0 text-2xl font-semibold tracking-tight text-gray-900">Project disabled</Heading> <Heading className="mb-2 mt-0 text-2xl font-semibold tracking-tight text-gray-900">Project disabled</Heading>
<Text className="mb-8 mt-0 text-base leading-relaxed text-gray-600"> <Text className="mb-8 mt-0 text-base leading-relaxed text-gray-600">
Your project <strong className="font-medium text-gray-900">{projectName}</strong> has been automatically Your project <strong className="font-medium text-gray-900">{projectName}</strong> has been disabled. All
disabled to protect your sender reputation. scheduled campaigns and workflows have been cancelled.
</Text> </Text>
<Section className="mb-8 overflow-hidden rounded-lg" style={{border: '1px solid #e5e7eb'}}>
<Section className="bg-gray-50 px-6 py-4">
<Text className="mb-0 mt-0 text-xs font-medium uppercase tracking-wider text-gray-500">
Issues detected
</Text>
</Section>
<Section className="px-6 py-6">
{violations.map((violation, index) => (
<Section key={index} className="mb-3 last:mb-0">
<Text className="mb-0 mt-0 text-sm leading-relaxed text-gray-700">{violation}</Text>
</Section>
))}
</Section>
</Section>
<Section className="mb-8 rounded-lg bg-red-50 px-6 py-4" style={{border: '1px solid #fca5a5'}}> <Section className="mb-8 rounded-lg bg-red-50 px-6 py-4" style={{border: '1px solid #fca5a5'}}>
<Text className="mb-0 mt-0 text-sm leading-relaxed text-red-900"> <Text className="mb-0 mt-0 text-sm leading-relaxed text-red-900">
High bounce or complaint rates can severely damage your sender reputation and email deliverability. We've Your project has been flagged during a routine review and disabled to protect your account and our platform.
disabled your project to prevent further issues. Please contact our support team for more details and to resolve this issue.
</Text> </Text>
</Section> </Section>
<Heading className="mb-4 mt-0 text-lg font-semibold text-gray-900">Steps to restore your project</Heading> <Heading className="mb-4 mt-0 text-lg font-semibold text-gray-900">Next steps</Heading>
<Section className="mb-8"> <Section className="mb-8">
<Section className="mb-3"> <Section className="mb-3">
<Text className="mb-1 mt-0 text-sm font-medium text-gray-900">Review your email lists</Text> <Text className="mb-1 mt-0 text-sm font-medium text-gray-900">Contact support</Text>
<Text className="mb-0 mt-0 text-sm leading-relaxed text-gray-600"> <Text className="mb-0 mt-0 text-sm leading-relaxed text-gray-600">
Remove invalid or unengaged contacts Reach out to our support team to understand the reason for the suspension and how to resolve it
</Text> </Text>
</Section> </Section>
<Section className="mb-3"> <Section className="mb-3">
<Text className="mb-1 mt-0 text-sm font-medium text-gray-900">Verify recipient consent</Text> <Text className="mb-1 mt-0 text-sm font-medium text-gray-900">Review your account</Text>
<Text className="mb-0 mt-0 text-sm leading-relaxed text-gray-600"> <Text className="mb-0 mt-0 text-sm leading-relaxed text-gray-600">
Ensure you have proper consent from all recipients While you wait, review your sending practices and ensure they comply with our terms of service
</Text>
</Section>
<Section>
<Text className="mb-1 mt-0 text-sm font-medium text-gray-900">Check email content</Text>
<Text className="mb-0 mt-0 text-sm leading-relaxed text-gray-600">
Verify your content follows best practices and isn't triggering spam filters
</Text> </Text>
</Section> </Section>
</Section> </Section>