fix: update project disabled messages for clarity and consistency
This commit is contained in:
@@ -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.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user