php-tenant/changelog/2026/jan/code-review.md
Snider d0ad2737cb 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 <noreply@anthropic.com>
2026-01-27 16:30:46 +00:00

123 lines
7.9 KiB
Markdown

# 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.