From 780741e37f7fec5b822b91c329ebe98428077ba0 Mon Sep 17 00:00:00 2001 From: Dries Augustyns Date: Mon, 15 Dec 2025 07:41:21 +0100 Subject: [PATCH] fix: Add additional checks for disabled projects --- apps/api/src/controllers/Users.ts | 34 +++++++++++++++++-- apps/api/src/services/SecurityService.ts | 42 ++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/apps/api/src/controllers/Users.ts b/apps/api/src/controllers/Users.ts index b080576..819c1ad 100644 --- a/apps/api/src/controllers/Users.ts +++ b/apps/api/src/controllers/Users.ts @@ -7,7 +7,7 @@ import type {NextFunction, Request, Response} from 'express'; import {DASHBOARD_URI, STRIPE_ENABLED, STRIPE_PRICE_EMAIL_USAGE, STRIPE_PRICE_ONBOARDING} from '../app/constants.js'; import {stripe} from '../app/stripe.js'; import {prisma} from '../database/prisma.js'; -import {NotAuthenticated, NotFound} from '../exceptions/index.js'; +import {ErrorCode, HttpException, NotAuthenticated, NotFound} from '../exceptions/index.js'; import type {AuthResponse} from '../middleware/auth.js'; import {isAuthenticated} from '../middleware/auth.js'; import {BillingLimitService} from '../services/BillingLimitService.js'; @@ -63,6 +63,16 @@ export class Users { throw new NotAuthenticated(); } + // Check if user is a member of any disabled project + const {hasDisabledProject, disabledProjectNames} = await SecurityService.userHasDisabledProject(auth.userId); + 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.`, + ErrorCode.PROJECT_DISABLED, + ); + } + const {name} = ProjectSchemas.create.parse(req.body); // Generate unique API keys @@ -692,6 +702,16 @@ export class Users { throw new NotFound('Project not found or you do not have permission to reset it'); } + // Check if project is disabled - block reset operation + const isDisabled = await SecurityService.isProjectDisabled(id); + if (isDisabled) { + throw new HttpException( + 403, + 'Cannot reset a disabled project. Please contact support to resolve security violations before making changes.', + ErrorCode.PROJECT_DISABLED, + ); + } + // Delete all project data in a transaction await prisma.$transaction(async tx => { // Delete all emails @@ -770,13 +790,14 @@ export class Users { ); } - // Get project to check for active subscription + // Get project to check for active subscription and disabled status const project = await prisma.project.findUnique({ where: {id}, select: { name: true, subscription: true, customer: true, + disabled: true, }, }); @@ -784,6 +805,15 @@ export class Users { throw new NotFound('Project not found'); } + // Check if project is disabled - block delete operation + if (project.disabled) { + throw new HttpException( + 403, + 'Cannot delete a disabled project. Please contact support to resolve security violations.', + ErrorCode.PROJECT_DISABLED, + ); + } + // If project has an active subscription, cancel it first if (STRIPE_ENABLED && stripe && project.subscription) { try { diff --git a/apps/api/src/services/SecurityService.ts b/apps/api/src/services/SecurityService.ts index 6c8b39c..d374182 100644 --- a/apps/api/src/services/SecurityService.ts +++ b/apps/api/src/services/SecurityService.ts @@ -147,6 +147,48 @@ export class SecurityService { } } + /** + * Check if a user is a member of any disabled project + * Users with disabled projects cannot create new projects + */ + public static async userHasDisabledProject(userId: string): Promise<{ + hasDisabledProject: boolean; + disabledProjectNames: string[]; + }> { + const disabledMemberships = await prisma.membership.findMany({ + where: { + userId, + project: { + disabled: true, + }, + }, + include: { + project: { + select: { + name: true, + }, + }, + }, + }); + + return { + hasDisabledProject: disabledMemberships.length > 0, + disabledProjectNames: disabledMemberships.map(m => m.project.name), + }; + } + + /** + * Check if a specific project is disabled + */ + public static async isProjectDisabled(projectId: string): Promise { + const project = await prisma.project.findUnique({ + where: {id: projectId}, + select: {disabled: true}, + }); + + return project?.disabled ?? false; + } + /** * Get a project's security metrics (for admin/dashboard display) */