From d0ad2737cbc5f2e18a368ca2d6ae90655a202c40 Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 27 Jan 2026 16:30:46 +0000 Subject: [PATCH] refactor: rename namespace from Core\Mod\Tenant to Core\Tenant Simplifies the namespace hierarchy by removing the intermediate Mod segment. Updates all 118 files including models, services, controllers, middleware, tests, and composer.json autoload configuration. Co-Authored-By: Claude Opus 4.5 --- Boot.php | 46 +- Concerns/BelongsToNamespace.php | 6 +- Concerns/BelongsToWorkspace.php | 10 +- Concerns/HasWorkspaceCache.php | 6 +- Concerns/TwoFactorAuthenticatable.php | 8 +- Console/Commands/CheckUsageAlerts.php | 6 +- Console/Commands/ProcessAccountDeletions.php | 4 +- Console/Commands/RefreshUserStats.php | 6 +- Console/Commands/ResetBillingCycles.php | 14 +- Contracts/EntitlementWebhookEvent.php | 2 +- Contracts/TwoFactorAuthenticationProvider.php | 2 +- .../Api/EntitlementWebhookController.php | 10 +- Controllers/EntitlementApiController.php | 2 +- Controllers/ReferralController.php | 2 +- Controllers/WorkspaceController.php | 2 +- Controllers/WorkspaceInvitationController.php | 6 +- Database/Factories/UserFactory.php | 6 +- Database/Factories/UserTokenFactory.php | 6 +- Database/Factories/WaitlistEntryFactory.php | 6 +- Database/Factories/WorkspaceFactory.php | 6 +- .../Factories/WorkspaceInvitationFactory.php | 6 +- Database/Seeders/DemoTestUserSeeder.php | 8 +- Database/Seeders/DemoWorkspaceSeeder.php | 12 +- Database/Seeders/FeatureSeeder.php | 4 +- Database/Seeders/SystemWorkspaceSeeder.php | 8 +- Database/Seeders/WorkspaceSeeder.php | 12 +- Enums/UserTier.php | 2 +- Enums/WebhookDeliveryStatus.php | 2 +- Events/Webhook/BoostActivatedEvent.php | 10 +- Events/Webhook/BoostExpiredEvent.php | 10 +- Events/Webhook/LimitReachedEvent.php | 8 +- Events/Webhook/LimitWarningEvent.php | 8 +- Events/Webhook/PackageChangedEvent.php | 8 +- Exceptions/EntitlementException.php | 2 +- .../MissingWorkspaceContextException.php | 2 +- Features/ApolloTier.php | 10 +- Features/BetaFeatures.php | 2 +- Features/HadesTier.php | 10 +- Features/UnlimitedWorkspaces.php | 10 +- Jobs/ComputeUserStats.php | 6 +- Jobs/DispatchEntitlementWebhook.php | 6 +- Jobs/ProcessAccountDeletion.php | 4 +- Listeners/SendWelcomeEmail.php | 4 +- Mail/AccountDeletionRequested.php | 4 +- Middleware/CheckWorkspacePermission.php | 6 +- Middleware/RequireAdminDomain.php | 2 +- Middleware/RequireWorkspaceContext.php | 8 +- Middleware/ResolveNamespace.php | 4 +- Middleware/ResolveWorkspaceFromSubdomain.php | 6 +- Models/AccountDeletionRequest.php | 2 +- Models/AgentReferralBonus.php | 2 +- Models/Boost.php | 2 +- Models/EntitlementLog.php | 2 +- Models/EntitlementWebhook.php | 6 +- Models/EntitlementWebhookDelivery.php | 4 +- Models/Feature.php | 2 +- Models/NamespacePackage.php | 2 +- Models/Namespace_.php | 2 +- Models/Package.php | 2 +- Models/UsageAlertHistory.php | 2 +- Models/UsageRecord.php | 2 +- Models/User.php | 8 +- Models/UserToken.php | 6 +- Models/UserTwoFactorAuth.php | 2 +- Models/WaitlistEntry.php | 6 +- Models/Workspace.php | 14 +- Models/WorkspaceInvitation.php | 6 +- Models/WorkspaceMember.php | 2 +- Models/WorkspacePackage.php | 2 +- Models/WorkspaceTeam.php | 4 +- Notifications/BoostExpiredNotification.php | 8 +- Notifications/UsageAlertNotification.php | 8 +- Notifications/WaitlistInviteNotification.php | 4 +- Notifications/WelcomeNotification.php | 2 +- .../WorkspaceInvitationNotification.php | 4 +- Routes/admin.php | 4 +- Routes/api.php | 2 +- Routes/web.php | 8 +- Rules/CheckUserPasswordRule.php | 4 +- Rules/ResourceStatusRule.php | 2 +- Scopes/WorkspaceScope.php | 6 +- Services/EntitlementResult.php | 2 +- Services/EntitlementService.php | 22 +- Services/EntitlementWebhookService.php | 24 +- Services/NamespaceManager.php | 8 +- Services/NamespaceService.php | 8 +- Services/TotpService.php | 4 +- Services/UsageAlertService.php | 16 +- Services/UserStatsService.php | 8 +- Services/WorkspaceCacheManager.php | 4 +- Services/WorkspaceManager.php | 6 +- Services/WorkspaceService.php | 4 +- Services/WorkspaceTeamService.php | 10 +- View/Blade/emails/usage-alert.blade.php | 4 +- .../Modal/Admin/EntitlementWebhookManager.php | 10 +- View/Modal/Admin/MemberManager.php | 8 +- View/Modal/Admin/TeamManager.php | 10 +- View/Modal/Admin/WorkspaceDetails.php | 46 +- View/Modal/Admin/WorkspaceManager.php | 6 +- View/Modal/Web/CancelDeletion.php | 4 +- View/Modal/Web/ConfirmDeletion.php | 4 +- View/Modal/Web/WorkspaceHome.php | 6 +- .../TASK-003-workspace-as-universal-tenant.md | 556 ++++++++++++++++++ changelog/2026/jan/code-review.md | 123 ++++ changelog/2026/jan/features.md | 50 ++ composer.json | 92 +-- tests/Feature/AccountDeletionTest.php | 8 +- tests/Feature/AuthenticationTest.php | 4 +- tests/Feature/EntitlementApiTest.php | 10 +- tests/Feature/EntitlementServiceTest.php | 20 +- tests/Feature/Guards/AccessTokenGuardTest.php | 4 +- tests/Feature/ProfileTest.php | 4 +- tests/Feature/ResetBillingCyclesTest.php | 18 +- tests/Feature/SettingsTest.php | 4 +- .../Feature/TwoFactorAuthenticatableTest.php | 4 +- tests/Feature/UsageAlertServiceTest.php | 18 +- tests/Feature/WaitlistTest.php | 4 +- tests/Feature/WorkspaceCacheTest.php | 14 +- tests/Feature/WorkspaceInvitationTest.php | 10 +- tests/Feature/WorkspaceSecurityTest.php | 14 +- tests/Feature/WorkspaceTenancyTest.php | 6 +- 121 files changed, 1189 insertions(+), 460 deletions(-) create mode 100644 changelog/2026/jan/TASK-003-workspace-as-universal-tenant.md create mode 100644 changelog/2026/jan/code-review.md create mode 100644 changelog/2026/jan/features.md diff --git a/Boot.php b/Boot.php index 2b5e4d6..b8023e2 100644 --- a/Boot.php +++ b/Boot.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Mod\Tenant; +namespace Core\Tenant; use Core\Events\AdminPanelBooting; use Core\Events\ApiRoutesRegistering; @@ -40,48 +40,48 @@ class Boot extends ServiceProvider public function register(): void { $this->app->singleton( - \Core\Mod\Tenant\Contracts\TwoFactorAuthenticationProvider::class, - \Core\Mod\Tenant\Services\TotpService::class + \Core\Tenant\Contracts\TwoFactorAuthenticationProvider::class, + \Core\Tenant\Services\TotpService::class ); $this->app->singleton( - \Core\Mod\Tenant\Services\EntitlementService::class, - \Core\Mod\Tenant\Services\EntitlementService::class + \Core\Tenant\Services\EntitlementService::class, + \Core\Tenant\Services\EntitlementService::class ); $this->app->singleton( - \Core\Mod\Tenant\Services\WorkspaceManager::class, - \Core\Mod\Tenant\Services\WorkspaceManager::class + \Core\Tenant\Services\WorkspaceManager::class, + \Core\Tenant\Services\WorkspaceManager::class ); $this->app->singleton( - \Core\Mod\Tenant\Services\UserStatsService::class, - \Core\Mod\Tenant\Services\UserStatsService::class + \Core\Tenant\Services\UserStatsService::class, + \Core\Tenant\Services\UserStatsService::class ); $this->app->singleton( - \Core\Mod\Tenant\Services\WorkspaceService::class, - \Core\Mod\Tenant\Services\WorkspaceService::class + \Core\Tenant\Services\WorkspaceService::class, + \Core\Tenant\Services\WorkspaceService::class ); $this->app->singleton( - \Core\Mod\Tenant\Services\WorkspaceCacheManager::class, - \Core\Mod\Tenant\Services\WorkspaceCacheManager::class + \Core\Tenant\Services\WorkspaceCacheManager::class, + \Core\Tenant\Services\WorkspaceCacheManager::class ); $this->app->singleton( - \Core\Mod\Tenant\Services\UsageAlertService::class, - \Core\Mod\Tenant\Services\UsageAlertService::class + \Core\Tenant\Services\UsageAlertService::class, + \Core\Tenant\Services\UsageAlertService::class ); $this->app->singleton( - \Core\Mod\Tenant\Services\EntitlementWebhookService::class, - \Core\Mod\Tenant\Services\EntitlementWebhookService::class + \Core\Tenant\Services\EntitlementWebhookService::class, + \Core\Tenant\Services\EntitlementWebhookService::class ); $this->app->singleton( - \Core\Mod\Tenant\Services\WorkspaceTeamService::class, - \Core\Mod\Tenant\Services\WorkspaceTeamService::class + \Core\Tenant\Services\WorkspaceTeamService::class, + \Core\Tenant\Services\WorkspaceTeamService::class ); $this->registerBackwardCompatAliases(); @@ -91,28 +91,28 @@ class Boot extends ServiceProvider { if (! class_exists(\App\Services\WorkspaceManager::class)) { class_alias( - \Core\Mod\Tenant\Services\WorkspaceManager::class, + \Core\Tenant\Services\WorkspaceManager::class, \App\Services\WorkspaceManager::class ); } if (! class_exists(\App\Services\UserStatsService::class)) { class_alias( - \Core\Mod\Tenant\Services\UserStatsService::class, + \Core\Tenant\Services\UserStatsService::class, \App\Services\UserStatsService::class ); } if (! class_exists(\App\Services\WorkspaceService::class)) { class_alias( - \Core\Mod\Tenant\Services\WorkspaceService::class, + \Core\Tenant\Services\WorkspaceService::class, \App\Services\WorkspaceService::class ); } if (! class_exists(\App\Services\WorkspaceCacheManager::class)) { class_alias( - \Core\Mod\Tenant\Services\WorkspaceCacheManager::class, + \Core\Tenant\Services\WorkspaceCacheManager::class, \App\Services\WorkspaceCacheManager::class ); } diff --git a/Concerns/BelongsToNamespace.php b/Concerns/BelongsToNamespace.php index ab25e5b..01a7403 100644 --- a/Concerns/BelongsToNamespace.php +++ b/Concerns/BelongsToNamespace.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Concerns; +namespace Core\Tenant\Concerns; -use Core\Mod\Tenant\Models\Namespace_; -use Core\Mod\Tenant\Models\User; +use Core\Tenant\Models\Namespace_; +use Core\Tenant\Models\User; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Support\Collection; diff --git a/Concerns/BelongsToWorkspace.php b/Concerns/BelongsToWorkspace.php index 61d45ee..43035c8 100644 --- a/Concerns/BelongsToWorkspace.php +++ b/Concerns/BelongsToWorkspace.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Concerns; +namespace Core\Tenant\Concerns; -use Core\Mod\Tenant\Exceptions\MissingWorkspaceContextException; -use Core\Mod\Tenant\Models\Workspace; -use Core\Mod\Tenant\Scopes\WorkspaceScope; -use Core\Mod\Tenant\Services\WorkspaceCacheManager; +use Core\Tenant\Exceptions\MissingWorkspaceContextException; +use Core\Tenant\Models\Workspace; +use Core\Tenant\Scopes\WorkspaceScope; +use Core\Tenant\Services\WorkspaceCacheManager; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Support\Collection; diff --git a/Concerns/HasWorkspaceCache.php b/Concerns/HasWorkspaceCache.php index 5ba50ba..942770d 100644 --- a/Concerns/HasWorkspaceCache.php +++ b/Concerns/HasWorkspaceCache.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Concerns; +namespace Core\Tenant\Concerns; use Closure; -use Core\Mod\Tenant\Models\Workspace; -use Core\Mod\Tenant\Services\WorkspaceCacheManager; +use Core\Tenant\Models\Workspace; +use Core\Tenant\Services\WorkspaceCacheManager; use Illuminate\Support\Collection; /** diff --git a/Concerns/TwoFactorAuthenticatable.php b/Concerns/TwoFactorAuthenticatable.php index f838870..ca0d59c 100644 --- a/Concerns/TwoFactorAuthenticatable.php +++ b/Concerns/TwoFactorAuthenticatable.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Concerns; +namespace Core\Tenant\Concerns; -use Core\Mod\Tenant\Contracts\TwoFactorAuthenticationProvider; -use Core\Mod\Tenant\Models\UserTwoFactorAuth; -use Core\Mod\Tenant\Services\TotpService; +use Core\Tenant\Contracts\TwoFactorAuthenticationProvider; +use Core\Tenant\Models\UserTwoFactorAuth; +use Core\Tenant\Services\TotpService; use Illuminate\Database\Eloquent\Relations\HasOne; /** diff --git a/Console/Commands/CheckUsageAlerts.php b/Console/Commands/CheckUsageAlerts.php index 35cf2ca..17e0444 100644 --- a/Console/Commands/CheckUsageAlerts.php +++ b/Console/Commands/CheckUsageAlerts.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Console\Commands; +namespace Core\Tenant\Console\Commands; -use Core\Mod\Tenant\Models\Workspace; -use Core\Mod\Tenant\Services\UsageAlertService; +use Core\Tenant\Models\Workspace; +use Core\Tenant\Services\UsageAlertService; use Illuminate\Console\Command; /** diff --git a/Console/Commands/ProcessAccountDeletions.php b/Console/Commands/ProcessAccountDeletions.php index 09c57a5..7ef72b9 100644 --- a/Console/Commands/ProcessAccountDeletions.php +++ b/Console/Commands/ProcessAccountDeletions.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Console\Commands; +namespace Core\Tenant\Console\Commands; -use Core\Mod\Tenant\Models\AccountDeletionRequest; +use Core\Tenant\Models\AccountDeletionRequest; use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; diff --git a/Console/Commands/RefreshUserStats.php b/Console/Commands/RefreshUserStats.php index 2e69729..6743a62 100644 --- a/Console/Commands/RefreshUserStats.php +++ b/Console/Commands/RefreshUserStats.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Console\Commands; +namespace Core\Tenant\Console\Commands; -use Core\Mod\Tenant\Jobs\ComputeUserStats; -use Core\Mod\Tenant\Models\User; +use Core\Tenant\Jobs\ComputeUserStats; +use Core\Tenant\Models\User; use Illuminate\Console\Command; class RefreshUserStats extends Command diff --git a/Console/Commands/ResetBillingCycles.php b/Console/Commands/ResetBillingCycles.php index 4c64106..9b19cd7 100644 --- a/Console/Commands/ResetBillingCycles.php +++ b/Console/Commands/ResetBillingCycles.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Console\Commands; +namespace Core\Tenant\Console\Commands; -use Core\Mod\Tenant\Models\Boost; -use Core\Mod\Tenant\Models\EntitlementLog; -use Core\Mod\Tenant\Models\UsageRecord; -use Core\Mod\Tenant\Models\Workspace; -use Core\Mod\Tenant\Notifications\BoostExpiredNotification; -use Core\Mod\Tenant\Services\EntitlementService; +use Core\Tenant\Models\Boost; +use Core\Tenant\Models\EntitlementLog; +use Core\Tenant\Models\UsageRecord; +use Core\Tenant\Models\Workspace; +use Core\Tenant\Notifications\BoostExpiredNotification; +use Core\Tenant\Services\EntitlementService; use Illuminate\Console\Command; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; diff --git a/Contracts/EntitlementWebhookEvent.php b/Contracts/EntitlementWebhookEvent.php index 569a070..f46b668 100644 --- a/Contracts/EntitlementWebhookEvent.php +++ b/Contracts/EntitlementWebhookEvent.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Contracts; +namespace Core\Tenant\Contracts; /** * Contract for entitlement webhook events. diff --git a/Contracts/TwoFactorAuthenticationProvider.php b/Contracts/TwoFactorAuthenticationProvider.php index eb5230b..f1d0b4b 100644 --- a/Contracts/TwoFactorAuthenticationProvider.php +++ b/Contracts/TwoFactorAuthenticationProvider.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Contracts; +namespace Core\Tenant\Contracts; /** * Contract for two-factor authentication providers. diff --git a/Controllers/Api/EntitlementWebhookController.php b/Controllers/Api/EntitlementWebhookController.php index fe3863a..ad4204f 100644 --- a/Controllers/Api/EntitlementWebhookController.php +++ b/Controllers/Api/EntitlementWebhookController.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Controllers\Api; +namespace Core\Tenant\Controllers\Api; -use Core\Mod\Tenant\Models\EntitlementWebhook; -use Core\Mod\Tenant\Models\EntitlementWebhookDelivery; -use Core\Mod\Tenant\Models\Workspace; -use Core\Mod\Tenant\Services\EntitlementWebhookService; +use Core\Tenant\Models\EntitlementWebhook; +use Core\Tenant\Models\EntitlementWebhookDelivery; +use Core\Tenant\Models\Workspace; +use Core\Tenant\Services\EntitlementWebhookService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Routing\Controller; diff --git a/Controllers/EntitlementApiController.php b/Controllers/EntitlementApiController.php index 41f2e48..35a1c2f 100644 --- a/Controllers/EntitlementApiController.php +++ b/Controllers/EntitlementApiController.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Controllers; +namespace Core\Tenant\Controllers; use Core\Front\Controller; use Illuminate\Auth\Events\Registered; diff --git a/Controllers/ReferralController.php b/Controllers/ReferralController.php index 2382ac1..fa1272a 100644 --- a/Controllers/ReferralController.php +++ b/Controllers/ReferralController.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Controllers; +namespace Core\Tenant\Controllers; use Core\Helpers\PrivacyHelper; use Core\Mod\Trees\Models\TreePlanting; diff --git a/Controllers/WorkspaceController.php b/Controllers/WorkspaceController.php index 86c57ad..ab74220 100644 --- a/Controllers/WorkspaceController.php +++ b/Controllers/WorkspaceController.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Controllers; +namespace Core\Tenant\Controllers; use Core\Front\Controller; use Illuminate\Http\JsonResponse; diff --git a/Controllers/WorkspaceInvitationController.php b/Controllers/WorkspaceInvitationController.php index 999d1ff..238820b 100644 --- a/Controllers/WorkspaceInvitationController.php +++ b/Controllers/WorkspaceInvitationController.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Controllers; +namespace Core\Tenant\Controllers; -use Core\Mod\Tenant\Models\Workspace; -use Core\Mod\Tenant\Models\WorkspaceInvitation; +use Core\Tenant\Models\Workspace; +use Core\Tenant\Models\WorkspaceInvitation; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Routing\Controller; diff --git a/Database/Factories/UserFactory.php b/Database/Factories/UserFactory.php index 3a56e26..69affd1 100644 --- a/Database/Factories/UserFactory.php +++ b/Database/Factories/UserFactory.php @@ -1,13 +1,13 @@ + * @extends \Illuminate\Database\Eloquent\Factories\Factory<\Core\Tenant\Models\User> */ class UserFactory extends Factory { @@ -17,7 +17,7 @@ class UserFactory extends Factory * Uses the backward-compatible alias class to ensure type compatibility * with existing code that expects Mod\Tenant\Models\User. */ - protected $model = \Core\Mod\Tenant\Models\User::class; + protected $model = \Core\Tenant\Models\User::class; /** * The current password being used by the factory. diff --git a/Database/Factories/UserTokenFactory.php b/Database/Factories/UserTokenFactory.php index dab5b03..cf8b126 100644 --- a/Database/Factories/UserTokenFactory.php +++ b/Database/Factories/UserTokenFactory.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Database\Factories; +namespace Core\Tenant\Database\Factories; -use Core\Mod\Tenant\Models\User; -use Core\Mod\Tenant\Models\UserToken; +use Core\Tenant\Models\User; +use Core\Tenant\Models\UserToken; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Str; diff --git a/Database/Factories/WaitlistEntryFactory.php b/Database/Factories/WaitlistEntryFactory.php index 01ca0dd..23fd696 100644 --- a/Database/Factories/WaitlistEntryFactory.php +++ b/Database/Factories/WaitlistEntryFactory.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Database\Factories; +namespace Core\Tenant\Database\Factories; -use Core\Mod\Tenant\Models\WaitlistEntry; +use Core\Tenant\Models\WaitlistEntry; use Illuminate\Database\Eloquent\Factories\Factory; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\Core\Mod\Tenant\Models\WaitlistEntry> + * @extends \Illuminate\Database\Eloquent\Factories\Factory<\Core\Tenant\Models\WaitlistEntry> */ class WaitlistEntryFactory extends Factory { diff --git a/Database/Factories/WorkspaceFactory.php b/Database/Factories/WorkspaceFactory.php index 55f4cc2..460fef6 100644 --- a/Database/Factories/WorkspaceFactory.php +++ b/Database/Factories/WorkspaceFactory.php @@ -1,12 +1,12 @@ + * @extends \Illuminate\Database\Eloquent\Factories\Factory<\Core\Tenant\Models\Workspace> */ class WorkspaceFactory extends Factory { diff --git a/Database/Factories/WorkspaceInvitationFactory.php b/Database/Factories/WorkspaceInvitationFactory.php index c1771b2..cfdf9a3 100644 --- a/Database/Factories/WorkspaceInvitationFactory.php +++ b/Database/Factories/WorkspaceInvitationFactory.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Database\Factories; +namespace Core\Tenant\Database\Factories; -use Core\Mod\Tenant\Models\WorkspaceInvitation; +use Core\Tenant\Models\WorkspaceInvitation; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Str; /** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\Core\Mod\Tenant\Models\WorkspaceInvitation> + * @extends \Illuminate\Database\Eloquent\Factories\Factory<\Core\Tenant\Models\WorkspaceInvitation> */ class WorkspaceInvitationFactory extends Factory { diff --git a/Database/Seeders/DemoTestUserSeeder.php b/Database/Seeders/DemoTestUserSeeder.php index d1da763..10d703b 100644 --- a/Database/Seeders/DemoTestUserSeeder.php +++ b/Database/Seeders/DemoTestUserSeeder.php @@ -1,10 +1,10 @@ \Core\Mod\Tenant\Middleware\RequireWorkspaceContext::class, + * 'workspace.required' => \Core\Tenant\Middleware\RequireWorkspaceContext::class, */ class RequireWorkspaceContext { diff --git a/Middleware/ResolveNamespace.php b/Middleware/ResolveNamespace.php index 9a8eed9..d8cab27 100644 --- a/Middleware/ResolveNamespace.php +++ b/Middleware/ResolveNamespace.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Middleware; +namespace Core\Tenant\Middleware; use Closure; -use Core\Mod\Tenant\Services\NamespaceService; +use Core\Tenant\Services\NamespaceService; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; diff --git a/Middleware/ResolveWorkspaceFromSubdomain.php b/Middleware/ResolveWorkspaceFromSubdomain.php index 9f195ff..4bf64a0 100644 --- a/Middleware/ResolveWorkspaceFromSubdomain.php +++ b/Middleware/ResolveWorkspaceFromSubdomain.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Middleware; +namespace Core\Tenant\Middleware; use Closure; -use Core\Mod\Tenant\Models\Workspace; -use Core\Mod\Tenant\Services\WorkspaceService; +use Core\Tenant\Models\Workspace; +use Core\Tenant\Services\WorkspaceService; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; diff --git a/Models/AccountDeletionRequest.php b/Models/AccountDeletionRequest.php index 5716742..430b902 100644 --- a/Models/AccountDeletionRequest.php +++ b/Models/AccountDeletionRequest.php @@ -1,6 +1,6 @@ check() && auth()->user() instanceof \Core\Mod\Tenant\Models\User) { + if (auth()->check() && auth()->user() instanceof \Core\Tenant\Models\User) { return auth()->user()->defaultHostWorkspace(); } @@ -680,7 +680,7 @@ class Workspace extends Model ]); // Send notification - $invitation->notify(new \Core\Mod\Tenant\Notifications\WorkspaceInvitationNotification($invitation)); + $invitation->notify(new \Core\Tenant\Notifications\WorkspaceInvitationNotification($invitation)); return $invitation; } diff --git a/Models/WorkspaceInvitation.php b/Models/WorkspaceInvitation.php index a863a82..cfef336 100644 --- a/Models/WorkspaceInvitation.php +++ b/Models/WorkspaceInvitation.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Models; +namespace Core\Tenant\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -15,9 +15,9 @@ class WorkspaceInvitation extends Model use HasFactory; use Notifiable; - protected static function newFactory(): \Core\Mod\Tenant\Database\Factories\WorkspaceInvitationFactory + protected static function newFactory(): \Core\Tenant\Database\Factories\WorkspaceInvitationFactory { - return \Core\Mod\Tenant\Database\Factories\WorkspaceInvitationFactory::new(); + return \Core\Tenant\Database\Factories\WorkspaceInvitationFactory::new(); } protected $fillable = [ diff --git a/Models/WorkspaceMember.php b/Models/WorkspaceMember.php index 6d49df7..b813580 100644 --- a/Models/WorkspaceMember.php +++ b/Models/WorkspaceMember.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Models; +namespace Core\Tenant\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; diff --git a/Models/WorkspacePackage.php b/Models/WorkspacePackage.php index 629073b..ccd4c6f 100644 --- a/Models/WorkspacePackage.php +++ b/Models/WorkspacePackage.php @@ -1,6 +1,6 @@ prefix('admin/tenant')->name('hub.admin.tenant.')->group(function () { // Team Manager - Route::get('/teams', \Core\Mod\Tenant\View\Modal\Admin\TeamManager::class) + Route::get('/teams', \Core\Tenant\View\Modal\Admin\TeamManager::class) ->name('teams'); // Member Manager - Route::get('/members', \Core\Mod\Tenant\View\Modal\Admin\MemberManager::class) + Route::get('/members', \Core\Tenant\View\Modal\Admin\MemberManager::class) ->name('members'); }); diff --git a/Routes/api.php b/Routes/api.php index fd148cb..e8696e7 100644 --- a/Routes/api.php +++ b/Routes/api.php @@ -10,7 +10,7 @@ declare(strict_types=1); */ use Core\Mod\Api\Controllers\WorkspaceController; -use Core\Mod\Tenant\Controllers\Api\EntitlementWebhookController; +use Core\Tenant\Controllers\Api\EntitlementWebhookController; use Illuminate\Support\Facades\Route; /* diff --git a/Routes/web.php b/Routes/web.php index e3bf445..c673767 100644 --- a/Routes/web.php +++ b/Routes/web.php @@ -8,9 +8,9 @@ declare(strict_types=1); * Account management and workspace routes. */ -use Core\Mod\Tenant\View\Modal\Web\CancelDeletion; -use Core\Mod\Tenant\View\Modal\Web\ConfirmDeletion; -use Core\Mod\Tenant\View\Modal\Web\WorkspaceHome; +use Core\Tenant\View\Modal\Web\CancelDeletion; +use Core\Tenant\View\Modal\Web\ConfirmDeletion; +use Core\Tenant\View\Modal\Web\WorkspaceHome; use Illuminate\Support\Facades\Route; /* @@ -41,7 +41,7 @@ Route::prefix('account')->name('account.')->group(function () { | */ -Route::get('/workspace/invitation/{token}', \Core\Mod\Tenant\Controllers\WorkspaceInvitationController::class) +Route::get('/workspace/invitation/{token}', \Core\Tenant\Controllers\WorkspaceInvitationController::class) ->name('workspace.invitation.accept'); /* diff --git a/Rules/CheckUserPasswordRule.php b/Rules/CheckUserPasswordRule.php index 94d5814..1cc46ce 100644 --- a/Rules/CheckUserPasswordRule.php +++ b/Rules/CheckUserPasswordRule.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Rules; +namespace Core\Tenant\Rules; use Closure; -use Core\Mod\Tenant\Models\User; +use Core\Tenant\Models\User; use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Support\Facades\Hash; diff --git a/Rules/ResourceStatusRule.php b/Rules/ResourceStatusRule.php index 8d338c0..b7c80bf 100644 --- a/Rules/ResourceStatusRule.php +++ b/Rules/ResourceStatusRule.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Rules; +namespace Core\Tenant\Rules; use Closure; use Core\Mod\Social\Enums\ResourceStatus; diff --git a/Scopes/WorkspaceScope.php b/Scopes/WorkspaceScope.php index 3af629f..cf0810b 100644 --- a/Scopes/WorkspaceScope.php +++ b/Scopes/WorkspaceScope.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Scopes; +namespace Core\Tenant\Scopes; -use Core\Mod\Tenant\Exceptions\MissingWorkspaceContextException; -use Core\Mod\Tenant\Models\Workspace; +use Core\Tenant\Exceptions\MissingWorkspaceContextException; +use Core\Tenant\Models\Workspace; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Scope; diff --git a/Services/EntitlementResult.php b/Services/EntitlementResult.php index 078ba09..187d9bc 100644 --- a/Services/EntitlementResult.php +++ b/Services/EntitlementResult.php @@ -1,6 +1,6 @@ cached_stats) { // Queue background refresh - dispatch(new \Core\Mod\Tenant\Jobs\ComputeUserStats($user->id))->onQueue('stats'); + dispatch(new \Core\Tenant\Jobs\ComputeUserStats($user->id))->onQueue('stats'); return $user->cached_stats; } diff --git a/Services/WorkspaceCacheManager.php b/Services/WorkspaceCacheManager.php index ef046f8..4bb769f 100644 --- a/Services/WorkspaceCacheManager.php +++ b/Services/WorkspaceCacheManager.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Services; +namespace Core\Tenant\Services; use Closure; -use Core\Mod\Tenant\Models\Workspace; +use Core\Tenant\Models\Workspace; use Illuminate\Cache\TaggableStore; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; diff --git a/Services/WorkspaceManager.php b/Services/WorkspaceManager.php index f3b9a48..0b0207c 100644 --- a/Services/WorkspaceManager.php +++ b/Services/WorkspaceManager.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Services; +namespace Core\Tenant\Services; -use Core\Mod\Tenant\Models\User; -use Core\Mod\Tenant\Models\Workspace; +use Core\Tenant\Models\User; +use Core\Tenant\Models\Workspace; use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Facades\Cache; use Illuminate\Validation\Rule; diff --git a/Services/WorkspaceService.php b/Services/WorkspaceService.php index 30d08fc..131a695 100644 --- a/Services/WorkspaceService.php +++ b/Services/WorkspaceService.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Services; +namespace Core\Tenant\Services; -use Core\Mod\Tenant\Models\Workspace; +use Core\Tenant\Models\Workspace; use Illuminate\Support\Facades\Session; /** diff --git a/Services/WorkspaceTeamService.php b/Services/WorkspaceTeamService.php index 34dcf0e..d59d0e2 100644 --- a/Services/WorkspaceTeamService.php +++ b/Services/WorkspaceTeamService.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\Services; +namespace Core\Tenant\Services; -use Core\Mod\Tenant\Models\User; -use Core\Mod\Tenant\Models\Workspace; -use Core\Mod\Tenant\Models\WorkspaceMember; -use Core\Mod\Tenant\Models\WorkspaceTeam; +use Core\Tenant\Models\User; +use Core\Tenant\Models\Workspace; +use Core\Tenant\Models\WorkspaceMember; +use Core\Tenant\Models\WorkspaceTeam; use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; diff --git a/View/Blade/emails/usage-alert.blade.php b/View/Blade/emails/usage-alert.blade.php index ef8290e..c64266c 100644 --- a/View/Blade/emails/usage-alert.blade.php +++ b/View/Blade/emails/usage-alert.blade.php @@ -1,7 +1,7 @@ @php $appName = config('core.app.name', __('core::core.brand.name')); - $isLimit = $threshold === \Core\Mod\Tenant\Models\UsageAlertHistory::THRESHOLD_LIMIT; - $isCritical = $threshold === \Core\Mod\Tenant\Models\UsageAlertHistory::THRESHOLD_CRITICAL; + $isLimit = $threshold === \Core\Tenant\Models\UsageAlertHistory::THRESHOLD_LIMIT; + $isCritical = $threshold === \Core\Tenant\Models\UsageAlertHistory::THRESHOLD_CRITICAL; @endphp diff --git a/View/Modal/Admin/EntitlementWebhookManager.php b/View/Modal/Admin/EntitlementWebhookManager.php index 7e3ea60..d7d365a 100644 --- a/View/Modal/Admin/EntitlementWebhookManager.php +++ b/View/Modal/Admin/EntitlementWebhookManager.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\View\Modal\Admin; +namespace Core\Tenant\View\Modal\Admin; -use Core\Mod\Tenant\Models\EntitlementWebhook; -use Core\Mod\Tenant\Models\EntitlementWebhookDelivery; -use Core\Mod\Tenant\Models\Workspace; -use Core\Mod\Tenant\Services\EntitlementWebhookService; +use Core\Tenant\Models\EntitlementWebhook; +use Core\Tenant\Models\EntitlementWebhookDelivery; +use Core\Tenant\Models\Workspace; +use Core\Tenant\Services\EntitlementWebhookService; use Illuminate\Contracts\View\View; use Livewire\Attributes\Computed; use Livewire\Attributes\Title; diff --git a/View/Modal/Admin/MemberManager.php b/View/Modal/Admin/MemberManager.php index 1cc3f26..4c08a29 100644 --- a/View/Modal/Admin/MemberManager.php +++ b/View/Modal/Admin/MemberManager.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\View\Modal\Admin; +namespace Core\Tenant\View\Modal\Admin; -use Core\Mod\Tenant\Models\Workspace; -use Core\Mod\Tenant\Models\WorkspaceMember; -use Core\Mod\Tenant\Models\WorkspaceTeam; +use Core\Tenant\Models\Workspace; +use Core\Tenant\Models\WorkspaceMember; +use Core\Tenant\Models\WorkspaceTeam; use Illuminate\Contracts\View\View; use Livewire\Attributes\Computed; use Livewire\Component; diff --git a/View/Modal/Admin/TeamManager.php b/View/Modal/Admin/TeamManager.php index 68b34c1..213f060 100644 --- a/View/Modal/Admin/TeamManager.php +++ b/View/Modal/Admin/TeamManager.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Core\Mod\Tenant\View\Modal\Admin; +namespace Core\Tenant\View\Modal\Admin; -use Core\Mod\Tenant\Models\Workspace; -use Core\Mod\Tenant\Models\WorkspaceMember; -use Core\Mod\Tenant\Models\WorkspaceTeam; -use Core\Mod\Tenant\Services\WorkspaceTeamService; +use Core\Tenant\Models\Workspace; +use Core\Tenant\Models\WorkspaceMember; +use Core\Tenant\Models\WorkspaceTeam; +use Core\Tenant\Services\WorkspaceTeamService; use Illuminate\Contracts\View\View; use Livewire\Attributes\Computed; use Livewire\Component; diff --git a/View/Modal/Admin/WorkspaceDetails.php b/View/Modal/Admin/WorkspaceDetails.php index 8f0c0c0..a01d2e4 100644 --- a/View/Modal/Admin/WorkspaceDetails.php +++ b/View/Modal/Admin/WorkspaceDetails.php @@ -1,9 +1,9 @@ workspace->entitlementLogs() ->with('user', 'feature') @@ -147,7 +147,7 @@ class WorkspaceDetails extends Component } // Usage records - if (class_exists(\Core\Mod\Tenant\Models\UsageRecord::class)) { + if (class_exists(\Core\Tenant\Models\UsageRecord::class)) { try { $usage = $this->workspace->usageRecords() ->with('user', 'feature') @@ -325,7 +325,7 @@ class WorkspaceDetails extends Component #[Computed] public function allPackages() { - return \Core\Mod\Tenant\Models\Package::active() + return \Core\Tenant\Models\Package::active() ->ordered() ->get(); } @@ -333,7 +333,7 @@ class WorkspaceDetails extends Component #[Computed] public function allFeatures() { - return \Core\Mod\Tenant\Models\Feature::active() + return \Core\Tenant\Models\Feature::active() ->orderBy('category') ->orderBy('sort_order') ->get(); @@ -403,7 +403,7 @@ class WorkspaceDetails extends Component public function resolvedEntitlements() { try { - return app(\Core\Mod\Tenant\Services\EntitlementService::class) + return app(\Core\Tenant\Services\EntitlementService::class) ->getUsageSummary($this->workspace); } catch (\Exception $e) { return collect(); @@ -431,7 +431,7 @@ class WorkspaceDetails extends Component return; } - $package = \Core\Mod\Tenant\Models\Package::findOrFail($this->selectedPackageId); + $package = \Core\Tenant\Models\Package::findOrFail($this->selectedPackageId); // Check if already assigned $existing = $this->workspace->workspacePackages() @@ -446,7 +446,7 @@ class WorkspaceDetails extends Component return; } - \Core\Mod\Tenant\Models\WorkspacePackage::create([ + \Core\Tenant\Models\WorkspacePackage::create([ 'workspace_id' => $this->workspace->id, 'package_id' => $package->id, 'status' => 'active', @@ -461,7 +461,7 @@ class WorkspaceDetails extends Component public function removePackage(int $workspacePackageId): void { - $wp = \Core\Mod\Tenant\Models\WorkspacePackage::where('workspace_id', $this->workspace->id) + $wp = \Core\Tenant\Models\WorkspacePackage::where('workspace_id', $this->workspace->id) ->findOrFail($workspacePackageId); $packageName = $wp->package?->name ?? 'Package'; @@ -474,7 +474,7 @@ class WorkspaceDetails extends Component public function suspendPackage(int $workspacePackageId): void { - $wp = \Core\Mod\Tenant\Models\WorkspacePackage::where('workspace_id', $this->workspace->id) + $wp = \Core\Tenant\Models\WorkspacePackage::where('workspace_id', $this->workspace->id) ->findOrFail($workspacePackageId); $wp->suspend(); @@ -486,7 +486,7 @@ class WorkspaceDetails extends Component public function reactivatePackage(int $workspacePackageId): void { - $wp = \Core\Mod\Tenant\Models\WorkspacePackage::where('workspace_id', $this->workspace->id) + $wp = \Core\Tenant\Models\WorkspacePackage::where('workspace_id', $this->workspace->id) ->findOrFail($workspacePackageId); $wp->reactivate(); @@ -523,7 +523,7 @@ class WorkspaceDetails extends Component return; } - $feature = \Core\Mod\Tenant\Models\Feature::where('code', $this->selectedFeatureCode)->first(); + $feature = \Core\Tenant\Models\Feature::where('code', $this->selectedFeatureCode)->first(); if (! $feature) { $this->actionMessage = 'Feature not found.'; @@ -534,24 +534,24 @@ class WorkspaceDetails extends Component // Map type to boost type constant $boostType = match ($this->entitlementType) { - 'enable' => \Core\Mod\Tenant\Models\Boost::BOOST_TYPE_ENABLE, - 'add_limit' => \Core\Mod\Tenant\Models\Boost::BOOST_TYPE_ADD_LIMIT, - 'unlimited' => \Core\Mod\Tenant\Models\Boost::BOOST_TYPE_UNLIMITED, - default => \Core\Mod\Tenant\Models\Boost::BOOST_TYPE_ENABLE, + 'enable' => \Core\Tenant\Models\Boost::BOOST_TYPE_ENABLE, + 'add_limit' => \Core\Tenant\Models\Boost::BOOST_TYPE_ADD_LIMIT, + 'unlimited' => \Core\Tenant\Models\Boost::BOOST_TYPE_UNLIMITED, + default => \Core\Tenant\Models\Boost::BOOST_TYPE_ENABLE, }; $durationType = $this->entitlementDuration === 'permanent' - ? \Core\Mod\Tenant\Models\Boost::DURATION_PERMANENT - : \Core\Mod\Tenant\Models\Boost::DURATION_DURATION; + ? \Core\Tenant\Models\Boost::DURATION_PERMANENT + : \Core\Tenant\Models\Boost::DURATION_DURATION; - \Core\Mod\Tenant\Models\Boost::create([ + \Core\Tenant\Models\Boost::create([ 'workspace_id' => $this->workspace->id, 'feature_code' => $this->selectedFeatureCode, 'boost_type' => $boostType, 'duration_type' => $durationType, 'limit_value' => $this->entitlementType === 'add_limit' ? $this->entitlementLimit : null, 'consumed_quantity' => 0, - 'status' => \Core\Mod\Tenant\Models\Boost::STATUS_ACTIVE, + 'status' => \Core\Tenant\Models\Boost::STATUS_ACTIVE, 'starts_at' => now(), 'expires_at' => $this->entitlementExpiresAt ? \Carbon\Carbon::parse($this->entitlementExpiresAt) : null, 'metadata' => ['granted_by' => auth()->id(), 'granted_at' => now()->toDateTimeString()], @@ -565,7 +565,7 @@ class WorkspaceDetails extends Component public function removeBoost(int $boostId): void { - $boost = \Core\Mod\Tenant\Models\Boost::where('workspace_id', $this->workspace->id) + $boost = \Core\Tenant\Models\Boost::where('workspace_id', $this->workspace->id) ->findOrFail($boostId); $featureCode = $boost->feature_code; diff --git a/View/Modal/Admin/WorkspaceManager.php b/View/Modal/Admin/WorkspaceManager.php index f01fa04..52a2484 100644 --- a/View/Modal/Admin/WorkspaceManager.php +++ b/View/Modal/Admin/WorkspaceManager.php @@ -1,9 +1,9 @@ socialAccounts()` returns SocialHost accounts +- [x] AC7: `$workspace->bioPages()` returns BioHost pages +- [x] AC8: `$workspace->analyticsSites()` returns AnalyticsHost sites +- [x] AC9: `$workspace->trustWidgets()` returns TrustHost widgets +- [x] AC10: `$workspace->notifications()` returns NotifyHost configs (notificationSites/pushCampaigns) + +### Access Control +- [x] AC11: Middleware can resolve workspace from subdomain/domain +- [x] AC12: All queries automatically scope to current workspace (via BelongsToWorkspace trait) +- [x] AC13: Cross-workspace access is explicitly prevented (test verified) + +### Migration from MixPost Workspace +- [x] AC14: `mixpost_workspace_id` bridging deprecated (methods marked @deprecated) +- [x] AC15: MixPost workspace table can be deprecated (bridge kept for transition) +- [x] AC16: Data migration preserves all relationships (uses user's default workspace) + +--- + +## Implementation Checklist + +### Phase 1: Audit Current State +- [x] List all models that currently have workspace relationships +- [x] List all models that should have workspace relationships but don't +- [x] Identify MixPost-specific workspace references +- [x] Document current access control patterns + +### Phase 2: Workspace Model Enhancement +- [x] File: `app/Models/Workspace.php` — Add all relationship methods +- [x] File: `app/Models/Domain.php` — Not needed (BioLinkDomain exists, domains stored as string) +- [x] Migration: Add `workspace_id` to tables missing it (created 3 migrations) +- [x] Migration: Not needed (using BioLinkDomain, no general domains table) + +### Phase 3: Service Model Updates +- [x] File: `app/Models/Social/Account.php` — Already has `workspace_id` FK +- [x] File: `app/Models/Social/Post.php` — Already has `workspace_id` FK +- [x] File: `app/Models/BioLink/BioLink.php` — Added `workspace_id` FK and relationship +- [x] File: `app/Models/Analytics/AnalyticsWebsite.php` — Added `workspace_id` FK and relationship +- [x] File: `app/Models/SocialProof/SocialProofCampaign.php` — Added `workspace_id` FK and relationship +- [x] File: `app/Models/Push/PushWebsite.php` — Already has `workspace_id` FK + +### Phase 4: Access Control +- [x] File: `app/Http/Middleware/ResolveWorkspaceFromSubdomain.php` — Enhanced to set workspace model +- [x] File: `app/Scopes/WorkspaceScope.php` — Created global scope for automatic filtering +- [x] File: `app/Traits/BelongsToWorkspace.php` — Already exists with caching functionality +- [ ] Apply WorkspaceScope to all tenant models (optional - trait provides scopes) +- [ ] File: `app/Policies/` — Update policies to check workspace membership + +### Phase 5: Remove MixPost Bridge +- [x] Deprecated MixPost methods in Workspace model (marked @deprecated) +- [ ] Remove `mixpost_workspace_id` from Workspace model (deferred - needs data migration) +- [ ] Remove `app/MixPost/WorkspaceAdapter.php` (deferred) +- [ ] Update any code referencing MixPost workspaces (deferred) +- [ ] Migration: Drop bridge columns after data migration (deferred) + +**Note:** Phase 5 intentionally deferred. MixPost bridge kept for backward compatibility during transition. +Native Social models already use workspace_id. Bridge can be removed in separate task after full SocialHost rewrite. + +### Phase 6: Testing +- [x] Test: `tests/Feature/WorkspaceTenancyTest.php` — 7 tests passing +- [x] Test: Cross-workspace isolation (user A can't see user B's data) +- [ ] Test: Domain-based workspace resolution (not yet tested) +- [x] Test: All relationship methods return correct data + +--- + +## Technical Notes + +### Workspace Resolution Strategy + +```php +// Option 1: Subdomain +// social.host.uk.com → resolve from subdomain 'social' + +// Option 2: Custom domain +// myagency.com → lookup in workspace_domains table + +// Option 3: Explicit (API) +// X-Workspace-Id header or workspace_id parameter + +// Option 4: User default +// auth()->user()->defaultWorkspace() +``` + +### Global Scope Pattern + +```php +// app/Scopes/WorkspaceScope.php +class WorkspaceScope implements Scope +{ + public function apply(Builder $builder, Model $model): void + { + if ($workspace = Workspace::current()) { + $builder->where('workspace_id', $workspace->id); + } + } +} + +// On models: +protected static function booted(): void +{ + static::addGlobalScope(new WorkspaceScope); +} +``` + +### Relationship Definitions + +```php +// app/Models/Workspace.php +public function socialAccounts(): HasMany +{ + return $this->hasMany(SocialAccount::class); +} + +public function bioPages(): HasMany +{ + return $this->hasMany(BioPage::class); +} + +public function domains(): HasMany +{ + return $this->hasMany(Domain::class); +} + +// Accessor for primary domain +public function getPrimaryDomainAttribute(): ?Domain +{ + return $this->domains()->where('is_primary', true)->first(); +} +``` + +--- + +## Clarifications Needed + +Before implementation, verify with lead developer: + +1. Should domains be a separate model or JSON column on Workspace? +2. What's the subdomain vs custom domain priority for resolution? +3. Are there any services that should NOT be workspace-scoped? +4. Should we support workspace hierarchies (parent/child)? + +--- + +## Implementation Summary for Verifier + +### Core Achievement +Workspace is now the universal tenant for all Host Hub services. Every service model belongs to a Workspace, and the Workspace model has clean relationship methods to access all owned resources. + +### Evidence to Check + +1. **Workspace Model Relationships** (`app/Models/Workspace.php` lines 210-445) + - Check methods exist: `socialAccounts()`, `bioPages()`, `analyticsSites()`, `trustWidgets()`, `notificationSites()`, etc. + - All return `HasMany` relationship type + - MixPost methods marked `@deprecated` + +2. **Service Models Updated** (check workspace relationship exists) + - `app/Models/BioLink/BioLink.php` - has `workspace()` method + - `app/Models/Analytics/AnalyticsWebsite.php` - has `workspace()` method + - `app/Models/SocialProof/SocialProofCampaign.php` - has `workspace()` method + - `app/Models/Social/Account.php` - already had `workspace()` method + +3. **Migrations Created** (check files exist) + - `database/migrations/2026_01_01_080000_add_workspace_id_to_biolink_tables.php` + - `database/migrations/2026_01_01_080001_add_workspace_id_to_analytics_tables.php` + - `database/migrations/2026_01_01_080002_add_workspace_id_to_socialproof_tables.php` + +4. **Access Control Infrastructure** + - `app/Scopes/WorkspaceScope.php` - exists, implements Scope interface + - `app/Traits/BelongsToWorkspace.php` - already existed, provides scoping + - `app/Http/Middleware/ResolveWorkspaceFromSubdomain.php` - sets `workspace_model` on request + - `app/Models/Workspace.php` - has `current()` static method + +5. **Tests Created** + - `tests/Feature/WorkspaceTenancyTest.php` - 7 test methods + - Run: `./vendor/bin/pest tests/Feature/WorkspaceTenancyTest.php` + +### What Was NOT Done (Intentionally) +- Policies not updated (marked as optional in Phase 4) +- MixPost bridge not removed (Phase 5 deferred) +- WorkspaceScope not manually applied to models (BelongsToWorkspace trait provides this) + +## Verification Results + +### Check 1: 2026-01-01 by Claude Opus 4.5 (Verification Agent) + +| Criterion | Status | Evidence | +|-----------|--------|----------| +| AC1: Workspace has relationship methods | ✅ PASS | `app/Models/Workspace.php` lines 210-445 contain `socialAccounts()`, `bioPages()`, `analyticsSites()`, `trustWidgets()`, `notificationSites()`, etc. All return `HasMany` | +| AC2: Workspace has domains() | ✅ PASS | `bioDomains()` method exists at line 328, returns HasMany to BioLinkDomain | +| AC3: Workspace has users() with roles | ✅ PASS | Pre-existing `users()` relationship verified | +| AC4: Service models have workspace_id FK | ✅ PASS | Migrations created for biolink, analytics, socialproof tables | +| AC5: Workspace::current() helper | ✅ PASS | Static method at line 454 returns `?self`, checks request attributes then auth user | +| AC6: socialAccounts() | ✅ PASS | Line 215, returns HasMany to `\Mod\Social\Models\Account::class` | +| AC7: bioPages() | ✅ PASS | Line 312, returns HasMany to `\App\Models\BioLink\BioLink::class` | +| AC8: analyticsSites() | ✅ PASS | Line 346, returns HasMany to `\App\Models\Analytics\AnalyticsWebsite::class` | +| AC9: trustWidgets() | ✅ PASS | Line 364, returns HasMany to `\App\Models\SocialProof\SocialProofCampaign::class` | +| AC10: notifications() | ✅ PASS | Line 382 `notificationSites()`, line 390 `pushCampaigns()` | +| AC11: Middleware resolves workspace | ✅ PASS | `ResolveWorkspaceFromSubdomain.php` sets `workspace_model` on request | +| AC12: Queries auto-scope | ✅ PASS | `WorkspaceScope.php` created with `apply()` method using `Workspace::current()` | +| AC13: Cross-workspace prevented | ⚠️ PARTIAL | Scope exists but not applied to models by default (relies on manual use or trait) | +| AC14: mixpost_workspace_id bridging deprecated | ✅ PASS | Methods marked `@deprecated` at lines 273, 299 | +| AC15: MixPost table deprecated | ⚠️ DEFERRED | Intentionally kept for backward compat (documented) | +| AC16: Data migration preserves relationships | ✅ PASS | Migrations use user's default workspace, safe 3-step process | + +**Additional Checks:** + +| Item | Status | Evidence | +|------|--------|----------| +| Migrations exist | ✅ PASS | 3 files in database/migrations/2026_01_01_08000* | +| WorkspaceScope.php | ✅ PASS | File exists at app/Scopes/, implements Scope interface correctly | +| BioLink.php has workspace() | ✅ PASS | Line 56, returns BelongsTo Workspace | +| AnalyticsWebsite.php has workspace() | ✅ PASS | Line 45, returns BelongsTo Workspace | +| Test file exists | ✅ PASS | tests/Feature/WorkspaceTenancyTest.php exists with 7 test methods | +| Tests pass | ❌ FAIL | PHPUnit 12 doesn't recognize `@test` annotation. Methods need `test_` prefix. | + +**Verdict:** ⚠️ PARTIAL PASS — Implementation is correct but tests don't run + +**Required Fix:** +Test methods use deprecated `@test` docblock annotation which PHPUnit 12 ignores. Methods must be renamed with `test_` prefix: +- `workspace_has_relationship_methods_for_all_services` → `test_workspace_has_relationship_methods_for_all_services` +- (or convert to Pest closure syntax) + +**Recommendation:** Fix test naming, re-run verification. Core implementation is solid. + +--- + +### Check 2 (FINAL): 2026-01-01 14:15 by Claude Opus 4.5 (Manager) + +All issues from Check 1 have been resolved by subsequent agent runs. + +| Item | Status | Evidence | +|------|--------|----------| +| Tests pass | ✅ PASS | 7/7 tests pass: `./vendor/bin/pest tests/Feature/WorkspaceTenancyTest.php` | +| Test methods renamed | ✅ PASS | All use `test_` prefix (PHPUnit 12 compatible) | +| Migrations portable | ✅ PASS | Converted to Query Builder (SQLite + MariaDB) | +| BelongsToWorkspace auto-assigns | ✅ PASS | `static::creating()` hook in trait | +| Models use trait | ✅ PASS | Account, BioLink, AnalyticsWebsite all have trait | +| Factories exist | ✅ PASS | BioLinkFactory + AnalyticsWebsiteFactory created | + +**Test Output:** +``` +PASS Tests\Feature\WorkspaceTenancyTest + ✓ workspace has relationship methods for all services + ✓ workspace current resolves from authenticated user + ✓ workspace scoping isolates data between workspaces + ✓ workspace relationships return correct models + ✓ models with workspace trait auto assign workspace on create + ✓ workspace scope prevents cross workspace access + ✓ belongs to workspace method checks ownership + + Tests: 7 passed (26 assertions) +``` + +**Final Verdict:** ✅ VERIFIED + +All acceptance criteria are met. The Workspace model is now the universal tenant for all Host Hub services. Implementation is solid, tests pass, and the architecture is correctly documented. + +**Follow-up Work:** TASK-005 created for updating 159 failing tests that need workspace setup. + +--- + +## Notes + +### Phase 1 Audit Findings (2026-01-01 08:15) + +**Models WITH workspace_id:** +- Social: `Account`, `Post`, `Template`, `HashtagGroup`, `Webhook`, `Analytics`, `QueueTime` (all in app/Models/Social) +- Push: `PushWebsite`, `PushCampaign`, `PushFlow`, `PushSegment` +- Commerce: `Order`, `Invoice`, `Payment`, `PaymentMethod`, `Subscription`, `Coupon` +- Entitlement: `WorkspacePackage`, `UsageRecord`, `Boost`, `EntitlementLog` +- Content: `ContentItem`, `ContentMedia`, `ContentTask`, `ContentAuthor`, `ContentTaxonomy`, `ContentWebhookLog` +- Agent: `AgentSession`, `AgentPlan` +- API: `ApiKey`, `WebhookEndpoint`, `WebhookDelivery` + +**Models with user_id INSTEAD (should migrate to workspace_id):** +- BioLink: `BioLink`, `BioLinkProject`, `BioLinkDomain`, `BioLinkPixel`, `BioLinkBlock` +- Analytics: `AnalyticsWebsite`, `AnalyticsGoal` +- SocialProof: `SocialProofCampaign`, `SocialProofNotification` +- Support: `SupportCustomer`, `CannedResponse`, `Thread` + +**MixPost Bridge Pattern (TO BE REMOVED):** +- Workspace model has `mixpost_workspace_id` column (line 57) +- Relationships using `Inovector\Mixpost\Models\*` (lines 215-275): + - `mixpostWorkspace()` - BelongsTo MixPost workspace + - `socialAccounts()` - via `host_workspace_id` on MixPost Account + - `socialPosts()` - via `host_workspace_id` on MixPost Post + - `socialTemplates()` - via `host_workspace_id` + - `socialMedia()` - via `host_workspace_id` +- Method `getOrCreateMixpostWorkspace()` uses `WorkspaceAdapter` (line 271) + +**Current Access Control:** +- `ResolveWorkspaceFromSubdomain` middleware resolves workspace slug from subdomain +- **CRITICAL:** WorkspaceService returns ARRAY, not Model (this is the "two workspace" bug!) +- No global scopes yet - manual filtering required +- Social models already use workspace_id FK with cascade delete +- Push models use workspace_id FK with cascade delete + +**Domain Handling:** +- NO Domain model exists currently +- BioLinkDomain is specific to BioHost (has user_id, not workspace_id) +- WorkspaceService has hardcoded subdomain mappings +- Workspace model has `domain` column (string, not relationship) + +### Phase 2-4 Implementation Notes (2026-01-01 08:45) + +**Files Created:** +- `database/migrations/2026_01_01_080000_add_workspace_id_to_biolink_tables.php` +- `database/migrations/2026_01_01_080001_add_workspace_id_to_analytics_tables.php` +- `database/migrations/2026_01_01_080002_add_workspace_id_to_socialproof_tables.php` +- `app/Scopes/WorkspaceScope.php` (global scope for auto-filtering) +- `tests/Feature/WorkspaceTenancyTest.php` (7 test cases) + +**Files Modified:** +- `app/Models/Workspace.php` - Added 20+ relationship methods for all services +- `app/Models/BioLink/BioLink.php` - Added workspace_id and relationship +- `app/Models/Analytics/AnalyticsWebsite.php` - Added workspace_id and relationship +- `app/Models/SocialProof/SocialProofCampaign.php` - Added workspace_id and relationship +- `app/Http/Middleware/ResolveWorkspaceFromSubdomain.php` - Sets workspace_model on request + +**Key Decisions:** +1. **No Domain model needed** - BioLinkDomain serves BioHost. General domains stored as string on Workspace. +2. **BelongsToWorkspace trait exists** - Already provides scoping, caching, auto-assignment. No need for manual WorkspaceScope application. +3. **Workspace::current()** - Returns Workspace MODEL (not array) from request or auth user. +4. **MixPost bridge deprecated** - Methods marked @deprecated but kept for backward compat during SocialHost rewrite. +5. **Migration strategy** - Adds workspace_id, migrates from user's default workspace, makes required. + +**Relationships Added to Workspace:** +- SocialHost: accounts, posts, templates, media, hashtagGroups, webhooks, analytics +- BioHost: bioPages, bioProjects, bioDomains, bioPixels +- AnalyticsHost: analyticsSites, analyticsGoals +- TrustHost: trustWidgets, trustNotifications +- NotifyHost: notificationSites, pushCampaigns, pushFlows, pushSegments +- API: apiKeys, webhookEndpoints +- Content: contentItems, contentAuthors + +**What's Left for Later:** +- Policies update (Phase 4) - Current policies may need workspace membership checks +- Complete MixPost bridge removal (Phase 5) - Deferred until full SocialHost rewrite +- Run migrations on production (needs coordination) +- Additional test coverage for edge cases + +**Migration Safety:** +The migrations use a two-step process: +1. Add workspace_id as nullable +2. Migrate data from user's default workspace +3. Make workspace_id required + +This allows rollback at any stage without data loss. + +### Why This Matters + +This is foundational architecture. Getting workspace tenancy right means: +- Simpler code everywhere (no scattered tenant checks) +- Cleaner data model (relationships, not linking tables) +- Easier feature development (new service? just add workspace_id) +- Better security (global scope prevents data leaks) + +### Historical Context + +The "two workspace concepts" documented in CLAUDE.md was a misunderstanding. There's ONE workspace concept — it's just that MixPost brought its own workspace table that needed bridging. This task eliminates that bridge entirely. + +### Test Method Naming Fix (2026-01-01) + +Fixed PHPUnit 12 compatibility issue in `tests/Feature/WorkspaceTenancyTest.php`: +- PHPUnit 12 deprecated the `@test` docblock annotation +- Renamed all test methods to use `test_` prefix (e.g., `workspace_has_relationship_methods_for_all_services` → `test_workspace_has_relationship_methods_for_all_services`) +- Removed the `/** @test */` docblocks + +**Note:** Tests are now recognised by PHPUnit (7 tests run), but fail due to a separate issue: the migrations at `database/migrations/2026_01_01_08000*.php` use MySQL-specific `UPDATE ... JOIN` syntax which is incompatible with SQLite (used by Pest tests). This migration issue needs to be fixed separately. + +### Migration SQLite Compatibility Fix (2026-01-01) + +Fixed MySQL-specific `UPDATE ... JOIN` syntax in three migration files: +- `database/migrations/2026_01_01_080000_add_workspace_id_to_biolink_tables.php` +- `database/migrations/2026_01_01_080001_add_workspace_id_to_analytics_tables.php` +- `database/migrations/2026_01_01_080002_add_workspace_id_to_socialproof_tables.php` + +**Problem:** Raw SQL `UPDATE table JOIN ... SET` is MySQL-specific and fails on SQLite (used by test suite). + +**Solution:** Replaced with Laravel Query Builder using a reusable `migrateTableToWorkspace()` helper method: +```php +private function migrateTableToWorkspace(string $table): void +{ + $records = DB::table("{$table} as t") + ->join('user_workspace as uw', function ($join) { + $join->on('t.user_id', '=', 'uw.user_id') + ->where('uw.is_default', '=', true); + }) + ->whereNull('t.workspace_id') + ->select('t.id', 'uw.workspace_id') + ->get(); + + foreach ($records as $record) { + DB::table($table) + ->where('id', $record->id) + ->update(['workspace_id' => $record->workspace_id]); + } +} +``` + +This approach: +- Uses Laravel Query Builder for database portability +- Works with SQLite (test suite) and MySQL/MariaDB (production) +- Preserves the same migration logic (assigns user's default workspace) +- For biolink_blocks, uses a similar pattern joining to parent biolinks table + +**Remaining test failures** are unrelated to migrations — they're about missing trait methods (`ownedByCurrentWorkspace()`, `belongsToWorkspace()`) and factories on models. These are test infrastructure issues, not migration issues. + +### Model Infrastructure Fix (2026-01-01) by Claude Opus 4.5 (Implementation Agent) + +**Problem Discovered:** Previous agent claimed "BelongsToWorkspace trait already exists" but: +1. The trait existed but models were NOT using it +2. The trait lacked auto-assignment of `workspace_id` on model creation +3. Missing factories for `BioLink` and `AnalyticsWebsite` models + +**Audit Findings:** + +| Model | Had BelongsToWorkspace? | Had workspace()? | Had Factory? | +|-------|------------------------|------------------|--------------| +| `Mod\Social\Models\Account` | No | Yes (duplicate) | Yes | +| `App\Models\BioLink\BioLink` | No | Yes (duplicate) | No | +| `App\Models\Analytics\AnalyticsWebsite` | No | Yes (duplicate) | No | + +**Fixes Applied:** + +1. **Enhanced `BelongsToWorkspace` trait** (`app/Traits/BelongsToWorkspace.php`): + - Added `static::creating()` hook to auto-assign `workspace_id` from current user's default workspace + - The trait already had `scopeOwnedByCurrentWorkspace()` and `belongsToWorkspace()` methods + +2. **Added trait to models:** + - `Mod\Social\Models\Account` — added `use BelongsToWorkspace`, removed duplicate `workspace()` method + - `App\Models\BioLink\BioLink` — added `use BelongsToWorkspace` and `use HasFactory`, removed duplicate `workspace()` method + - `App\Models\Analytics\AnalyticsWebsite` — added `use BelongsToWorkspace` and `use HasFactory`, removed duplicate `workspace()` method + +3. **Created missing factories:** + - `database/factories/BioLink/BioLinkFactory.php` + - `database/factories/Analytics/AnalyticsWebsiteFactory.php` + +4. **Fixed test:** + - `tests/Feature/WorkspaceTenancyTest.php` line 125 — added required `credentials` field to `Account::create()` call + +**Test Results:** +``` +PASS Tests\Feature\WorkspaceTenancyTest + ✓ workspace has relationship methods for all services + ✓ workspace current resolves from authenticated user + ✓ workspace scoping isolates data between workspaces + ✓ workspace relationships return correct models + ✓ models with workspace trait auto assign workspace on create + ✓ workspace scope prevents cross workspace access + ✓ belongs to workspace method checks ownership + + Tests: 7 passed (26 assertions) +``` + +### Remaining Work (2026-01-01) by Claude Opus 4.5 + +The core TASK-004 tests pass (7/7), but 159 other tests fail across the codebase. These failures are NOT bugs in the implementation - they're tests that were written before workspace tenancy and don't set up workspaces. + +**Pattern of failures:** +- Tests create models (AnalyticsWebsite, SocialProofCampaign, etc.) using `Model::create()` without workspace_id +- The BelongsToWorkspace trait auto-assigns workspace only if authenticated user has a workspace +- Tests that don't call `actingAs()` before creating models fail with NOT NULL constraint violations + +**Additional models fixed with trait:** +- `App\Models\Analytics\AnalyticsGoal` — added `use BelongsToWorkspace` +- `App\Models\SocialProof\SocialProofCampaign` — added `use BelongsToWorkspace` + +**Tests fixed:** +- `tests/Feature/Api/AnalyticsApiTest.php` — added workspace setup in beforeEach, added workspace_id to all create() calls + +**Tests that still need fixing (not in TASK-004 scope):** +- `tests/Feature/SocialProof/SocialProofWidgetApiTest.php` +- And approximately 158 other test files + +**Recommendation:** Create a follow-up task TASK-XXX to systematically update all test files to: +1. Create workspaces in `beforeEach()` +2. Attach workspaces to users as default +3. Include workspace_id in all model create() calls, OR call actingAs() before creating models + +### For Verification Agent + +This is a refactoring task. Verify by: +1. Checking relationship methods exist and return correct types +2. Running queries and confirming workspace scoping works +3. Testing cross-workspace isolation +4. Confirming MixPost bridge code is removed +5. Running full test suite (NOTE: 159 tests fail due to missing workspace setup in tests, not implementation bugs) diff --git a/changelog/2026/jan/code-review.md b/changelog/2026/jan/code-review.md new file mode 100644 index 0000000..2412f25 --- /dev/null +++ b/changelog/2026/jan/code-review.md @@ -0,0 +1,123 @@ +# Tenant Module Review + +**Updated:** 2026-01-21 - All implementations verified complete. Rate limiting, deletion logging, query optimisation, soft deletes for WorkspacePackage, and cache invalidation optimisation implemented + +## Overview + +The Tenant module is the core multi-tenancy system for Host Hub, handling: +- **Users and Authentication**: User model, 2FA, API tokens, email verification +- **Workspaces**: The tenant boundary - users belong to workspaces, resources are scoped to workspaces +- **Entitlements**: Feature access control via packages, boosts, and usage tracking +- **Account Management**: Account deletion with grace period, settings +- **Referrals**: Agent referral tracking for the Trees programme + +This is a foundational module that other modules depend on heavily. + +## Production Readiness Score: 92/100 (was 90/100 - cache optimisation and soft deletes added 2026-01-21) + +The module has solid architecture, good test coverage for core functionality, proper security patterns, and configuration externalisation. Critical namespace issues fixed in Wave 2. Cache invalidation now uses efficient version-based approach. WorkspacePackage now has soft deletes for audit history. + +## Critical Issues (Must Fix) + +- [x] **Workspace.packages() uses wrong namespace**: FIXED - Now uses `Mod\Tenant\Models\Package::class` +- [x] **Workspace.bioPages/bioProjects/bioDomains/bioPixels use wrong namespace**: FIXED - 12 namespace replacements from `\App\Models\BioLink\*` to `Mod\Web\Models\*` across DemoTestUserSeeder.php, WorkspaceDetails.php, and WorkspaceManager.php +- [ ] **User boosts relationship may conflict**: User has a `boosts()` relationship but Boost has `workspace_id` as the primary foreign key, not `user_id`. The relationship exists but the intended use case is unclear +- [x] **WorkspaceService.get() bypasses user authorisation**: VERIFIED SECURE - The `get()` method calls `getModel()` which correctly queries through `$user->workspaces()` relationship, ensuring only accessible workspaces are returned. The review item was outdated. + +## Recommended Improvements + +- [x] **Add rate limiting to referral tracking**: DONE - `ReferralController::track()` now has rate limiting applied. +- [x] **Add logging to account deletion confirmation**: DONE - `ConfirmDeletion` Livewire component now logs deletion actions. +- [x] **Cache invalidation in EntitlementService is expensive**: DONE - Refactored to use version-based cache keys. `buildCacheKey()` now includes version in key (e.g., `entitlement:{id}:v{version}:limit:{code}`). Invalidation simply increments the version, making all old keys stale without iteration. +- [x] **WorkspaceManager.setDefault() could be optimised**: DONE - Refactored to use a single optimised query. +- [ ] **Add index hints for UsageRecord queries**: `getTotalUsage()` and `getRollingUsage()` query by workspace_id + feature_code + recorded_at - ensure composite index exists +- [x] **Consider soft deletes for WorkspacePackage**: DONE - Added `SoftDeletes` trait to WorkspacePackage model and migration `2026_01_21_200000_add_soft_deletes_to_workspace_packages.php` for the `deleted_at` column. +- [x] **EntitlementService.getTotalLimit() cache key does not include version**: DONE - Cache keys now include version via `buildCacheKey()` method. All entitlement cache keys use format `entitlement:{id}:v{version}:{type}:{code}`. + +## Missing Features (Future) + +- [ ] **UserStatsService has multiple TODO comments**: Lines 83-93 for social accounts, scheduled posts, and storage usage tracking +- [ ] **Workspace teams/roles beyond owner**: Current pivot only has 'role' but no team management UI or additional role types implemented +- [ ] **Entitlement webhook notifications**: No webhook dispatch when limits are reached or packages change +- [ ] **Usage alerts/notifications**: No mechanism to notify users when approaching limits +- [ ] **Billing cycle reset automation**: `expireCycleBoundBoosts()` exists but no scheduler entry visible +- [ ] **Web routes file missing**: No `Routes/web.php` file exists despite the Boot.php checking for it +- [ ] **Workspace invitation system**: No invite flow for adding users to workspaces + +## Test Coverage Assessment + +**Well Tested (Good Coverage):** +- `EntitlementServiceTest.php` - Comprehensive coverage of can(), recordUsage(), provisionPackage/Boost, suspend/reactivate, revokePackage (600+ lines) +- `AccountDeletionTest.php` - Full lifecycle including model methods, job, and command (300+ lines) +- `WorkspaceTenancyTest.php` - Workspace isolation, scoping, relationships +- `AccessTokenGuardTest.php` - Token authentication, expiry, creation, revocation + +**Tested but Limited:** +- `EntitlementApiTest.php` - Tests API endpoints but uses non-existent package code 'social-creator' (tests may fail) +- `AuthenticationTest.php`, `ProfileTest.php`, `SettingsTest.php` - Exist but not reviewed in detail + +**Missing Test Coverage:** +- [ ] `WorkspaceService` - No dedicated tests +- [ ] `WorkspaceManager` - No dedicated tests +- [ ] `UserStatsService` - No tests +- [ ] `ResolveWorkspaceFromSubdomain` middleware - No tests +- [ ] `ReferralController` - No tests +- [ ] `BelongsToWorkspace` trait - Only integration tests via WorkspaceTenancyTest +- [ ] `TwoFactorAuthenticatable` concern - Test file exists but not reviewed +- [ ] Livewire components (ConfirmDeletion, CancelDeletion, WorkspaceHome) - No dedicated tests + +## Security Concerns + +**Positive Security Patterns:** +- Tokens stored as SHA-256 hashes (UserToken) +- Password re-verification required for account deletion (ConfirmDeletion) +- LIKE pattern escaping in WorkspaceService.findBySubdomain() (SQL injection prevention) +- Sensitive fields ($hidden) on User and Workspace models +- WP connector secret guarded against mass assignment + +**Concerns:** +- [x] **WorkspaceService.get() has no authorisation**: VERIFIED SECURE - Method correctly uses `$user->workspaces()` relationship, not a raw query +- [ ] **ReferralController stores IP in cookie**: IP address stored in JSON cookie - GDPR consideration, should be session-only +- [ ] **No CSRF protection visible for Livewire deletion**: executeDelete() is called via Livewire dispatch - verify CSRF is enforced +- [ ] **Workspace.validateWebhookSignature() timing attack**: Uses hash_equals which is good, but hash_hmac result should also be compared in constant time +- [ ] **User tier bypass for email verification**: Hades users bypass email verification - ensure this is intentional business logic + +## Notes + +### Architecture Observations +- Clean separation between WorkspaceManager (request-scoped operations) and WorkspaceService (session/persistence) +- EntitlementService is well-designed with proper caching, logging, and transaction handling +- Good use of value objects (EntitlementResult) for type safety +- Backward compatibility aliases in Boot.php for migration from old namespace + +### Code Quality +- Consistent use of strict types +- Good docblocks on most public methods +- Uses Laravel conventions (factories, seeders, Pest tests) +- Models have proper casts, fillable/guarded definitions + +### Configuration +- Proper config externalisation in `config/tenant.php` +- Environment variables for cache TTL and grace period +- No hardcoded secrets or credentials found + +### Dual Entitlement Systems +The module has two entitlement approaches that may cause confusion: +1. **UserTier enum** (FREE/APOLLO/HADES) - Used on User model, defines features as simple array +2. **Package/Feature/Boost system** - Used on Workspace model, full database-driven entitlements + +These should be reconciled - currently User.getTier() returns enum-based limits while Workspace.can() uses the database-driven system. + +### Dependencies +The module has relationships to models in other modules: +- `Mod\Analytics\Models\*` +- `Mod\Social\Models\*` +- `Mod\Web\Models\*` +- `Mod\Trust\Models\*` +- `Mod\Notify\Models\*` +- `Mod\Commerce\Models\*` +- `Mod\Trees\Models\*` +- `Mod\Api\Models\*` +- `Mod\Content\Models\*` + +These cross-module dependencies are appropriate for a tenant module but ensure circular dependencies are avoided. diff --git a/changelog/2026/jan/features.md b/changelog/2026/jan/features.md new file mode 100644 index 0000000..6a77180 --- /dev/null +++ b/changelog/2026/jan/features.md @@ -0,0 +1,50 @@ +# Core-Tenant - January 2026 + +## Features Implemented + +### Workspace as Universal Tenant (TASK-003) + +Consolidated tenancy model with workspace as the primary organisational unit. + +**Changes:** +- All workspace_id columns now nullable for system-level entities +- Workspace invitations system +- User tier system (free, pro, hades) +- Namespace system for sub-workspace scoping + +**Models:** +- `Workspace` - enhanced with invitation support +- `WorkspaceInvitation` - new model with notification +- `Namespace` - sub-workspace resource grouping +- `User` - tier management + +**Files:** +- `Models/Workspace.php` +- `Models/WorkspaceInvitation.php` +- `Models/Namespace.php` +- `Notifications/WorkspaceInvitationNotification.php` + +--- + +### Web Routes + +Created `Routes/web.php` with: +- Account deletion flow +- Workspace management routes +- Invitation acceptance + +--- + +### Two-Factor Authentication + +User 2FA support with TOTP. + +**Files:** +- `Models/UserTwoFactorAuth.php` +- Migration for 2FA table + +--- + +### Soft Deletes + +Added soft delete support to User model for GDPR compliance. diff --git a/composer.json b/composer.json index 17a19f1..37fecd0 100644 --- a/composer.json +++ b/composer.json @@ -1,48 +1,48 @@ { - "name": "host-uk/core-tenant", - "description": "Multi-tenancy and workspaces for Laravel", - "keywords": [ - "multi-tenant", - "workspaces", - "teams" - ], - "license": "EUPL-1.2", - "require": { - "php": "^8.2", - "host-uk/core": "dev-main" - }, - "require-dev": { - "laravel/pint": "^1.18", - "orchestra/testbench": "^9.0|^10.0", - "pestphp/pest": "^3.0" - }, - "autoload": { - "psr-4": { - "Core\\Mod\\Tenant\\": "" - } - }, - "autoload-dev": { - "psr-4": { - "Core\\Mod\\Tenant\\Tests\\": "tests/" - } - }, - "extra": { - "laravel": { - "providers": [ - "Core\\Mod\\Tenant\\Boot" - ] - } - }, - "scripts": { - "lint": "pint", - "test": "pest" - }, - "config": { - "sort-packages": true, - "allow-plugins": { - "pestphp/pest-plugin": true - } - }, - "minimum-stability": "dev", - "prefer-stable": true + "name": "host-uk/core-tenant", + "description": "Multi-tenancy and workspaces for Laravel", + "keywords": [ + "multi-tenant", + "workspaces", + "teams" + ], + "license": "EUPL-1.2", + "require": { + "php": "^8.2", + "host-uk/core": "dev-main" + }, + "require-dev": { + "laravel/pint": "^1.18", + "orchestra/testbench": "^9.0|^10.0", + "pestphp/pest": "^3.0" + }, + "autoload": { + "psr-4": { + "Core\\Tenant\\": "" + } + }, + "autoload-dev": { + "psr-4": { + "Core\\Tenant\\Tests\\": "Tests/" + } + }, + "extra": { + "laravel": { + "providers": [ + "Core\\Tenant\\Boot" + ] + } + }, + "scripts": { + "lint": "pint", + "test": "pest" + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true + } + }, + "minimum-stability": "dev", + "prefer-stable": true } diff --git a/tests/Feature/AccountDeletionTest.php b/tests/Feature/AccountDeletionTest.php index 7d9455b..fcb3737 100644 --- a/tests/Feature/AccountDeletionTest.php +++ b/tests/Feature/AccountDeletionTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -use Core\Mod\Tenant\Jobs\ProcessAccountDeletion; -use Core\Mod\Tenant\Models\AccountDeletionRequest; -use Core\Mod\Tenant\Models\User; -use Core\Mod\Tenant\Models\Workspace; +use Core\Tenant\Jobs\ProcessAccountDeletion; +use Core\Tenant\Models\AccountDeletionRequest; +use Core\Tenant\Models\User; +use Core\Tenant\Models\Workspace; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Queue; diff --git a/tests/Feature/AuthenticationTest.php b/tests/Feature/AuthenticationTest.php index f165040..d69a762 100644 --- a/tests/Feature/AuthenticationTest.php +++ b/tests/Feature/AuthenticationTest.php @@ -1,8 +1,8 @@ create(); diff --git a/tests/Feature/ProfileTest.php b/tests/Feature/ProfileTest.php index 0c75a4b..53d8111 100644 --- a/tests/Feature/ProfileTest.php +++ b/tests/Feature/ProfileTest.php @@ -1,9 +1,9 @@