From 8b05b8a76dd5b951d9559a161a85bd7c0c381cbe Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 13:11:29 +0000 Subject: [PATCH 1/5] fix: cascade delete namespaces when workspace is removed Change namespaces.workspace_id FK from nullOnDelete to cascadeOnDelete so that namespaces are properly cleaned up when their parent workspace is deleted, instead of being orphaned with a null workspace_id. Fixes #10 Co-Authored-By: Claude Opus 4.6 (1M context) --- ..._delete_namespaces_on_workspace_delete.php | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 Migrations/2026_03_24_000000_cascade_delete_namespaces_on_workspace_delete.php diff --git a/Migrations/2026_03_24_000000_cascade_delete_namespaces_on_workspace_delete.php b/Migrations/2026_03_24_000000_cascade_delete_namespaces_on_workspace_delete.php new file mode 100644 index 0000000..bb5f6a5 --- /dev/null +++ b/Migrations/2026_03_24_000000_cascade_delete_namespaces_on_workspace_delete.php @@ -0,0 +1,40 @@ +dropForeign(['workspace_id']); + + $table->foreign('workspace_id') + ->references('id') + ->on('workspaces') + ->cascadeOnDelete(); + }); + } + + public function down(): void + { + Schema::table('namespaces', function (Blueprint $table) { + $table->dropForeign(['workspace_id']); + + $table->foreign('workspace_id') + ->references('id') + ->on('workspaces') + ->nullOnDelete(); + }); + } +}; -- 2.45.3 From 70ad94d66d2f2ec76974e70026007bb0f04a77a3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 13:11:34 +0000 Subject: [PATCH 2/5] fix: add FK constraints on feature_code columns to entitlement_features Add foreign key constraints from usage_alert_history.feature_code, entitlement_boosts.feature_code, and entitlement_usage_records.feature_code to entitlement_features.code to prevent orphaned records. Uses cascadeOnUpdate (code renames propagate) and restrictOnDelete (cannot delete a feature that has usage/alert/boost records). Fixes #12 Co-Authored-By: Claude Opus 4.6 (1M context) --- ...4_000000_add_feature_code_foreign_keys.php | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 Migrations/2026_03_24_000000_add_feature_code_foreign_keys.php diff --git a/Migrations/2026_03_24_000000_add_feature_code_foreign_keys.php b/Migrations/2026_03_24_000000_add_feature_code_foreign_keys.php new file mode 100644 index 0000000..9680b1b --- /dev/null +++ b/Migrations/2026_03_24_000000_add_feature_code_foreign_keys.php @@ -0,0 +1,57 @@ +foreign('feature_code', 'usage_alert_feature_code_fk') + ->references('code') + ->on('entitlement_features') + ->cascadeOnUpdate() + ->restrictOnDelete(); + }); + + Schema::table('entitlement_boosts', function (Blueprint $table) { + $table->foreign('feature_code', 'boosts_feature_code_fk') + ->references('code') + ->on('entitlement_features') + ->cascadeOnUpdate() + ->restrictOnDelete(); + }); + + Schema::table('entitlement_usage_records', function (Blueprint $table) { + $table->foreign('feature_code', 'usage_records_feature_code_fk') + ->references('code') + ->on('entitlement_features') + ->cascadeOnUpdate() + ->restrictOnDelete(); + }); + } + + public function down(): void + { + Schema::table('entitlement_usage_alert_history', function (Blueprint $table) { + $table->dropForeign('usage_alert_feature_code_fk'); + }); + + Schema::table('entitlement_boosts', function (Blueprint $table) { + $table->dropForeign('boosts_feature_code_fk'); + }); + + Schema::table('entitlement_usage_records', function (Blueprint $table) { + $table->dropForeign('usage_records_feature_code_fk'); + }); + } +}; -- 2.45.3 From d2548f7a627d89f4c017c83e175660bdc978b29d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 13:11:34 +0000 Subject: [PATCH 3/5] fix: cascade delete child features when parent is removed The self-referential FK on entitlement_features.parent_feature_id used nullOnDelete(), which orphaned child features when a parent was deleted. Children that belong to a pool have no meaning without their parent, so cascade deletion is the correct behaviour. Adds a migration that drops and re-creates the FK with cascadeOnDelete(). Fixes #40 Co-Authored-By: Claude Opus 4.6 (1M context) --- ...0000_fix_parent_feature_cascade_delete.php | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 Migrations/2026_03_24_000000_fix_parent_feature_cascade_delete.php diff --git a/Migrations/2026_03_24_000000_fix_parent_feature_cascade_delete.php b/Migrations/2026_03_24_000000_fix_parent_feature_cascade_delete.php new file mode 100644 index 0000000..81698fa --- /dev/null +++ b/Migrations/2026_03_24_000000_fix_parent_feature_cascade_delete.php @@ -0,0 +1,47 @@ +dropForeign(['parent_feature_id']); + + $table->foreign('parent_feature_id') + ->references('id') + ->on('entitlement_features') + ->cascadeOnDelete(); + }); + } + + /** + * Revert to the original nullOnDelete behaviour. + */ + public function down(): void + { + Schema::table('entitlement_features', function (Blueprint $table) { + $table->dropForeign(['parent_feature_id']); + + $table->foreign('parent_feature_id') + ->references('id') + ->on('entitlement_features') + ->nullOnDelete(); + }); + } +}; -- 2.45.3 From 74b81589c11e3b00688ba813526b848e5312a812 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 13:11:55 +0000 Subject: [PATCH 4/5] fix: remove hardcoded hub.host.uk.com domain from controllers Replace hardcoded 'hub.host.uk.com' with config('app.base_domain') to match the existing pattern used in middleware and Blade views. Fixes #7 Fixes #8 Co-Authored-By: Claude Opus 4.6 (1M context) --- Controllers/EntitlementApiController.php | 2 +- Controllers/WorkspaceController.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Controllers/EntitlementApiController.php b/Controllers/EntitlementApiController.php index 8773eab..2b4fdbc 100644 --- a/Controllers/EntitlementApiController.php +++ b/Controllers/EntitlementApiController.php @@ -102,7 +102,7 @@ class EntitlementApiController extends Controller $workspace = Workspace::create([ 'name' => $user->name."'s Workspace", 'slug' => Str::slug($user->name).'-'.Str::random(6), - 'domain' => 'hub.host.uk.com', + 'domain' => 'hub.'.config('app.base_domain', 'host.uk.com'), 'type' => 'custom', ]); diff --git a/Controllers/WorkspaceController.php b/Controllers/WorkspaceController.php index bc63131..809b90d 100644 --- a/Controllers/WorkspaceController.php +++ b/Controllers/WorkspaceController.php @@ -139,7 +139,7 @@ class WorkspaceController extends Controller } // Set default domain - $validated['domain'] = 'hub.host.uk.com'; + $validated['domain'] = 'hub.'.config('app.base_domain', 'host.uk.com'); $validated['type'] = $validated['type'] ?? 'custom'; $workspace = Workspace::create($validated); @@ -261,7 +261,7 @@ class WorkspaceController extends Controller ->whereIn('workspace_id', function ($query) { $query->select('id') ->from('workspaces') - ->where('domain', 'hub.host.uk.com'); + ->where('domain', 'hub.'.config('app.base_domain', 'host.uk.com')); }) ->update(['is_default' => false]); -- 2.45.3 From 1434c7e9d84ac15e70e958335669c6241079c560 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 13:12:16 +0000 Subject: [PATCH 5/5] fix: validate invitation token format before database lookup Add route-level regex constraints to all token route parameters, requiring exactly 64 alphanumeric characters. Malformed tokens (path traversal attempts, overly long strings, special characters) now receive a 404 at the routing layer before reaching controllers or triggering database lookups. Fixes #43 Co-Authored-By: Claude Opus 4.6 (1M context) --- Routes/web.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Routes/web.php b/Routes/web.php index 6a41eb5..c9fdd3a 100644 --- a/Routes/web.php +++ b/Routes/web.php @@ -26,10 +26,12 @@ use Illuminate\Support\Facades\Route; Route::prefix('account')->name('account.')->group(function () { Route::get('/delete/{token}', ConfirmDeletion::class) - ->name('delete.confirm'); + ->name('delete.confirm') + ->where('token', '[a-zA-Z0-9]{64}'); Route::get('/delete/{token}/cancel', CancelDeletion::class) - ->name('delete.cancel'); + ->name('delete.cancel') + ->where('token', '[a-zA-Z0-9]{64}'); }); /* @@ -43,7 +45,8 @@ Route::prefix('account')->name('account.')->group(function () { */ Route::get('/workspace/invitation/{token}', WorkspaceInvitationController::class) - ->name('workspace.invitation.accept'); + ->name('workspace.invitation.accept') + ->where('token', '[a-zA-Z0-9]{64}'); /* |-------------------------------------------------------------------------- -- 2.45.3