From 3f4db5b7e0bfcfc044af216104eb94227f7be3c2 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Mon, 16 Feb 2026 14:54:14 +0000 Subject: [PATCH] feat: implement project creation restriction for non-admin users --- .../Pages/Settings/Authentication/Index.tsx | 40 +++++++++++++++++++ Common/Models/DatabaseModels/GlobalConfig.ts | 19 +++++++++ Common/Server/DatabaseConfig.ts | 7 ++++ .../1770834237091-MigrationName.ts | 23 +++++++++++ .../Postgres/SchemaMigrations/Index.ts | 2 + Common/Server/Services/ProjectService.ts | 10 +++++ 6 files changed, 101 insertions(+) create mode 100644 Common/Server/Infrastructure/Postgres/SchemaMigrations/1770834237091-MigrationName.ts diff --git a/AdminDashboard/src/Pages/Settings/Authentication/Index.tsx b/AdminDashboard/src/Pages/Settings/Authentication/Index.tsx index 5a0b99f468..a20ab39155 100644 --- a/AdminDashboard/src/Pages/Settings/Authentication/Index.tsx +++ b/AdminDashboard/src/Pages/Settings/Authentication/Index.tsx @@ -73,6 +73,46 @@ const Settings: FunctionComponent = (): ReactElement => { modelId: ObjectID.getZeroObjectID(), }} /> + + ); }; diff --git a/Common/Models/DatabaseModels/GlobalConfig.ts b/Common/Models/DatabaseModels/GlobalConfig.ts index 672c968e42..cfb63a14f4 100644 --- a/Common/Models/DatabaseModels/GlobalConfig.ts +++ b/Common/Models/DatabaseModels/GlobalConfig.ts @@ -58,6 +58,25 @@ export default class GlobalConfig extends GlobalConfigModel { }) public disableSignup?: boolean = undefined; + @ColumnAccessControl({ + create: [], + read: [], + update: [], + }) + @TableColumn({ + type: TableColumnType.Boolean, + title: "Disable User Project Creation", + description: "Only master admins can create projects when enabled.", + defaultValue: false, + }) + @Column({ + type: ColumnType.Boolean, + nullable: true, + default: false, + unique: true, + }) + public disableUserProjectCreation?: boolean = undefined; + // SMTP Settings. @ColumnAccessControl({ diff --git a/Common/Server/DatabaseConfig.ts b/Common/Server/DatabaseConfig.ts index dac451e8e9..bbb6e792ef 100644 --- a/Common/Server/DatabaseConfig.ts +++ b/Common/Server/DatabaseConfig.ts @@ -80,4 +80,11 @@ export default class DatabaseConfig { "disableSignup", )) as boolean; } + + @CaptureSpan() + public static async shouldDisableUserProjectCreation(): Promise { + return (await DatabaseConfig.getFromGlobalConfig( + "disableUserProjectCreation", + )) as boolean; + } } diff --git a/Common/Server/Infrastructure/Postgres/SchemaMigrations/1770834237091-MigrationName.ts b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1770834237091-MigrationName.ts new file mode 100644 index 0000000000..cc3eb4d381 --- /dev/null +++ b/Common/Server/Infrastructure/Postgres/SchemaMigrations/1770834237091-MigrationName.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class MigrationName1770834237091 implements MigrationInterface { + public name = "MigrationName1770834237091"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "GlobalConfig" ADD "disableUserProjectCreation" boolean DEFAULT false`, + ); + await queryRunner.query( + `ALTER TABLE "GlobalConfig" ADD CONSTRAINT "UQ_disableUserProjectCreation" UNIQUE ("disableUserProjectCreation")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "GlobalConfig" DROP CONSTRAINT "UQ_disableUserProjectCreation"`, + ); + await queryRunner.query( + `ALTER TABLE "GlobalConfig" DROP COLUMN "disableUserProjectCreation"`, + ); + } +} diff --git a/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts b/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts index c24bbea036..7c4b602b9e 100644 --- a/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +++ b/Common/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts @@ -258,6 +258,7 @@ import { MigrationName1770728946893 } from "./1770728946893-MigrationName"; import { MigrationName1770732721195 } from "./1770732721195-MigrationName"; import { MigrationName1770833704656 } from "./1770833704656-MigrationName"; import { MigrationName1770834237090 } from "./1770834237090-MigrationName"; +import { MigrationName1770834237091 } from "./1770834237091-MigrationName"; export default [ InitialMigration, @@ -520,4 +521,5 @@ export default [ MigrationName1770732721195, MigrationName1770833704656, MigrationName1770834237090, + MigrationName1770834237091, ]; diff --git a/Common/Server/Services/ProjectService.ts b/Common/Server/Services/ProjectService.ts index 52d4b80486..4838090727 100755 --- a/Common/Server/Services/ProjectService.ts +++ b/Common/Server/Services/ProjectService.ts @@ -124,6 +124,7 @@ export class ProjectService extends DatabaseService { select: { name: true, email: true, + isMasterAdmin: true, companyPhoneNumber: true, companyName: true, utmCampaign: true, @@ -142,6 +143,15 @@ export class ProjectService extends DatabaseService { throw new BadDataException("User not found."); } + // Check if project creation is restricted to admins only + const shouldDisableProjectCreation: boolean = + await DatabaseConfig.shouldDisableUserProjectCreation(); + if (shouldDisableProjectCreation && !user.isMasterAdmin) { + throw new NotAuthorizedException( + "Project creation is restricted to admin users only on this OneUptime Server. Please contact your server admin.", + ); + } + if (IsBillingEnabled) { if (!data.data.paymentProviderPlanId) { throw new BadDataException("Plan required to create the project.");