php-tenant/changelog/2026/jan/code-review.md

124 lines
7.9 KiB
Markdown
Raw Permalink Normal View History

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