Skip to content

Conversation

@johnproblems
Copy link
Owner

@johnproblems johnproblems commented Nov 27, 2025

Summary

Complete enterprise license management system with token-based validation, license status checking, and server provisioning controls.

Features

  • EnterpriseLicense Model: Complete license data with expiration, features, and usage tracking
  • LicenseController: REST API for license CRUD operations
  • LicenseStatusController: Real-time license status and validation endpoints
  • LicensingService: Core business logic for license operations
  • LicenseValidation Trait: Shared validation logic across the application
  • Multiple Middleware:
    • ApiLicenseValidation: Token-based API authentication
    • ServerProvisioningLicense: License checks for server operations
    • ValidateLicense: General license validation
    • LicenseValidationMiddleware: Comprehensive validation pipeline
  • LicenseException: Custom exception handling
  • Licensing Facade: Convenient service access
  • Dependency Injection: LicensingServiceInterface contract
  • Configuration: licensing.php with default settings

Database

  • Migration: create_enterprise_licenses_table
  • Factory: EnterpriseLicenseFactory

API Endpoints

  • POST /api/v1/licenses - Create license
  • GET /api/v1/licenses - List licenses
  • GET /api/v1/licenses/{id} - Get license
  • PUT /api/v1/licenses/{id} - Update license
  • DELETE /api/v1/licenses/{id} - Delete license
  • GET /api/v1/license-status - Check license status

🤖 Generated with Claude Code

Co-Authored-By: Claude noreply@anthropic.com


Note

Introduces an enterprise licensing platform (model, service, middleware) with REST endpoints, validation/rate-limiting, and DB schema/supporting config.

  • Backend/API:
    • Add LicenseController (issue, show, validate, suspend/reactivate/revoke, renew, upgrade, usage history, CSV/JSON export).
    • Add LicenseStatusController (status, feature/option checks, limits) with OpenAPI annotations.
  • Middleware:
    • Add ValidateLicense, ApiLicenseValidation, LicenseValidationMiddleware, ServerProvisioningLicense for feature gating, grace-period handling, and tier-based rate limiting/headers.
  • Service/DI:
    • Add LicensingService implementing LicensingServiceInterface (validate with cache, domain/auth checks, usage limits, key generation, suspend/reactivate/revoke, usage stats) and bind via LicensingServiceProvider + Facades\Licensing.
  • Model/DB:
    • Add EnterpriseLicense model with feature/limit checks, status helpers, grace-period logic.
    • Add migration create_enterprise_licenses_table and factory EnterpriseLicenseFactory.
  • Shared/Config:
    • Add Traits\LicenseValidation helpers.
    • Add config/licensing.php (grace period, default features/limits, route feature map, middleware groups).
  • Utilities:
    • Add LicenseValidationResult DTO and LicenseException.

Written by Cursor Bugbot for commit 8c29ba6. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

New Features

  • Enterprise Licensing System: Full license management with validation, issuance, revocation, and reactivation capabilities.
  • License Tier-Based Access Control: Rate limiting and feature gating based on license tier (Basic, Professional, Enterprise).
  • Usage Limits & Tracking: Monitor resource usage against license limits with detailed statistics and violation reporting.
  • Domain Authorization: Authorize specific domains for license usage with wildcard support.
  • Grace Period Support: Extended access for expired licenses with restricted feature availability.
  • License Status & Exports: Real-time license status checks, usage history, and data exports in CSV/JSON formats.
  • Server Provisioning Licensing: License validation for infrastructure provisioning and cloud provider limits.

✏️ Tip: You can customize this high-level summary in your review settings.

- EnterpriseLicense model with complete CRUD operations
- LicenseController for REST API management
- LicenseStatusController for real-time license validation
- LicensingService with core business logic
- LicenseValidation trait for shared validation logic
- ApiLicenseValidation middleware for API token validation
- ServerProvisioningLicense middleware for server operations
- ValidateLicense middleware for general license checks
- LicenseValidationMiddleware for comprehensive validation
- LicensingServiceInterface contract for dependency injection
- LicenseValidationResult data class for structured responses
- LicenseException for custom error handling
- Licensing facade for convenient access
- LicensingServiceProvider for service registration
- License configuration file with default settings
@coderabbitai
Copy link

coderabbitai bot commented Nov 27, 2025

Walkthrough

A comprehensive licensing system was added to the Laravel application, including a service interface, data structures, exception classes, API controllers, middleware for request validation, an Eloquent model, a service provider, implementation service, validation trait, configuration file, database factory, and migration.

Changes

Cohort / File(s) Summary
Licensing Contract & Core Abstractions
app/Contracts/LicensingServiceInterface.php, app/Data/LicenseValidationResult.php, app/Exceptions/LicenseException.php
Interface defining license lifecycle methods (validate, issue, revoke, suspend, reactivate, check usage, domain auth). Immutable data class encapsulating validation outcomes with violations and metadata. Exception class with named constructors for domain-specific license errors.
Facades & Service Provider
app/Facades/Licensing.php, app/Providers/LicensingServiceProvider.php
Facade providing static proxy methods for licensing operations with IDE support. Service provider wiring LicensingServiceInterface binding and 'licensing' singleton registration.
API Controllers
app/Http/Controllers/Api/LicenseController.php, app/Http/Controllers/Api/LicenseStatusController.php
LicenseController implements REST endpoints for issuing, validating, revoking, suspending, reactivating, upgrading, and exporting license data with permission gating. LicenseStatusController exposes license status, feature checks, deployment options, and resource limits.
License Validation Middleware
app/Http/Middleware/ApiLicenseValidation.php, app/Http/Middleware/LicenseValidationMiddleware.php, app/Http/Middleware/ServerProvisioningLicense.php, app/Http/Middleware/ValidateLicense.php
ApiLicenseValidation enforces domain and feature validation with rate limiting per tier. LicenseValidationMiddleware handles grace period and feature-based access control. ServerProvisioningLicense gates server provisioning with capability checks. ValidateLicense enforces per-request license and feature validation with grace period handling.
Licensing Service & Trait
app/Services/LicensingService.php, app/Traits/LicenseValidation.php
LicensingService implements validation with caching, license issuance, lifecycle management, usage checks, and key generation. LicenseValidation trait provides protected methods for feature checks, usage validation, and license metadata exposure to controllers.
Enterprise License Model
app/Models/EnterpriseLicense.php
Eloquent model with feature/domain/limit checks, status transitions (activate, suspend, revoke), expiration tracking, grace period handling, and tier characterization (basic, professional, enterprise).
Configuration & Database
config/licensing.php, database/factories/EnterpriseLicenseFactory.php, database/migrations/2025_08_26_225529_create_enterprise_licenses_table.php
Configuration defining grace period, cache TTL, rate limits per tier, default features/limits, route-feature mapping, and middleware groups. Factory providing default states and fluent modifiers (expired, suspended, revoked, trial, perpetual). Migration creating enterprise_licenses table with organization FK, license metadata, domains, and status enum.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Middleware
    participant LicensingService
    participant Cache as Cache/DB
    participant Controller

    Client->>Middleware: Request + Host
    Middleware->>Middleware: shouldSkipValidation()?
    alt Skip (dev/health)
        Middleware->>Controller: next()
    else Validate
        Middleware->>Middleware: Extract organization & license
        alt No license
            Middleware->>Client: 403 Forbidden
        else Has license
            Middleware->>LicensingService: validateLicense(key, domain)
            LicensingService->>Cache: Check cache
            alt Cache hit
                Cache-->>LicensingService: LicenseValidationResult
            else Cache miss
                LicensingService->>Cache: Query license
                LicensingService->>LicensingService: Check status (revoked, suspended, expired)
                LicensingService->>LicensingService: Check domain authorized
                LicensingService->>LicensingService: Check usage limits
                LicensingService->>Cache: Store result (TTL)
            end
            
            LicensingService-->>Middleware: LicenseValidationResult
            
            alt Invalid/Expired
                Middleware->>Middleware: isWithinGracePeriod()?
                alt Yes, read-only
                    Middleware->>Middleware: validateFeatures(required)
                    Middleware->>Middleware: applyRateLimiting()
                    Middleware->>Middleware: addLicenseHeaders()
                    Middleware->>Controller: next() + warning
                else No
                    Middleware->>Client: 403 + diagnostic data
                end
            else Valid
                Middleware->>Middleware: validateFeatures(required)
                Middleware->>Middleware: applyRateLimiting()
                Middleware->>Middleware: addLicenseHeaders()
                Middleware->>Controller: next()
                Controller->>Client: 200 + response
            end
        end
    end
Loading
sequenceDiagram
    participant Admin
    participant LicenseController
    participant LicensingService
    participant DB as Database
    participant Cache

    Admin->>LicenseController: POST /api/licenses (issue)
    LicenseController->>LicenseController: Validate permissions
    LicenseController->>LicenseController: Validate input
    LicenseController->>LicensingService: issueLicense(org, config)
    
    LicensingService->>LicensingService: generateLicenseKey(org, config)
    LicensingService->>DB: Create EnterpriseLicense
    LicensingService->>Cache: Clear org cache
    LicensingService-->>LicenseController: EnterpriseLicense
    
    LicenseController->>LicenseController: Load org & stats
    LicenseController-->>Admin: 201 + License JSON
    
    alt Later: Suspend
        Admin->>LicenseController: POST /licenses/{id}/suspend
        LicenseController->>LicenseController: Check permission
        LicenseController->>LicensingService: suspendLicense(license, reason)
        LicensingService->>DB: Update status = suspended
        LicensingService->>Cache: Clear cache
        LicensingService-->>LicenseController: bool
        LicenseController-->>Admin: 200 OK
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

  • High-complexity middleware files with multiple validation paths, grace period logic, rate limiting integration, and request augmentation requiring careful flow analysis
  • Intricate service implementation featuring caching strategies, validation orchestration, license lifecycle state management, and error handling paths
  • Large model with extensive helper methods for feature, domain, limit, and status checks requiring verification of business logic correctness
  • Multiple heterogeneous components (controllers, middleware, traits, factories) with interrelated dependencies demanding cross-file consistency verification
  • Particular attention areas:
    • app/Services/LicensingService.php — validation caching logic and state transition handling
    • app/Http/Middleware/ApiLicenseValidation.php and app/Http/Middleware/ValidateLicense.php — overlapping middleware behaviors and grace period logic
    • app/Models/EnterpriseLicense.php — domain wildcard matching and limit violation reporting
    • app/Http/Controllers/Api/LicenseController.php — permission gates and license upgrade/tier logic

Poem

🐰 A licensing system so grand we've built,
With vaults for features, no licensing guilt,
Domains authorized, limits in check,
Grace periods kindly, no safety wreck,
From validation to upgrades we hop,
Enterprise licensing won't ever stop! 🎖️

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.25% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: implementing a comprehensive enterprise license management system with all core components.
Description check ✅ Passed The description comprehensively covers all changes including models, controllers, services, middleware, configuration, and database components with well-organized sections.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch pr-3-license-management

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

🧹 Nitpick comments (22)
app/Data/LicenseValidationResult.php (1)

9-15: Well-structured immutable value object.

The use of constructor property promotion with readonly is idiomatic PHP 8.1+. Consider adding PHPDoc type hints for the array parameters to improve IDE support and documentation clarity:

+    /**
+     * @param array<int, array{type: string, limit: int, current: int, message: string}> $violations
+     * @param array<string, mixed> $metadata
+     */
     public function __construct(
         public readonly bool $isValid,
         public readonly string $message,
         public readonly ?EnterpriseLicense $license = null,
         public readonly array $violations = [],
         public readonly array $metadata = []
     ) {}
app/Exceptions/LicenseException.php (2)

7-55: Clean exception design with named constructors.

The named constructor pattern provides clear, self-documenting exception creation. Consider adding exception codes for programmatic handling in catch blocks:

 class LicenseException extends Exception
 {
+    public const CODE_NOT_FOUND = 1001;
+    public const CODE_EXPIRED = 1002;
+    public const CODE_REVOKED = 1003;
+    public const CODE_SUSPENDED = 1004;
+    public const CODE_DOMAIN_NOT_AUTHORIZED = 1005;
+    public const CODE_USAGE_LIMIT_EXCEEDED = 1006;
+    public const CODE_VALIDATION_FAILED = 1007;
+    public const CODE_GENERATION_FAILED = 1008;
+
     public static function notFound(): self
     {
-        return new self('License not found');
+        return new self('License not found', self::CODE_NOT_FOUND);
     }
     // ... apply similar pattern to other methods

39-44: Potential silent failure if violations array is malformed.

array_column($violations, 'message') will return an empty array if no elements have a 'message' key, resulting in "Usage limits exceeded: " with no details. Consider adding defensive handling:

 public static function usageLimitExceeded(array $violations): self
 {
     $messages = array_column($violations, 'message');
+    if (empty($messages)) {
+        $messages = ['Unknown limit violations'];
+    }

     return new self('Usage limits exceeded: '.implode(', ', $messages));
 }
app/Contracts/LicensingServiceInterface.php (2)

10-13: Use imported class instead of FQCN for consistency.

The return type uses a fully qualified class name while other return types use imported classes. Add the import for consistency:

 use App\Models\EnterpriseLicense;
 use App\Models\Organization;
+use App\Data\LicenseValidationResult;
 
 interface LicensingServiceInterface
 {
     /**
      * Validate a license key with optional domain checking
      */
-    public function validateLicense(string $licenseKey, ?string $domain = null): \App\Data\LicenseValidationResult;
+    public function validateLicense(string $licenseKey, ?string $domain = null): LicenseValidationResult;

25-28: Consider typed DTOs for array return types.

checkUsageLimits and getUsageStatistics return untyped arrays. For a public contract, consider defining DTOs (similar to LicenseValidationResult) to provide type safety and IDE support. This is optional and can be deferred.

Also applies to: 45-48

app/Facades/Licensing.php (1)

7-18: PHPDoc type hints should match interface signatures.

Two parameters are not marked as nullable in the PHPDoc but are nullable in the interface:

- * @method static \App\Data\LicenseValidationResult validateLicense(string $licenseKey, string $domain = null)
+ * @method static \App\Data\LicenseValidationResult validateLicense(string $licenseKey, ?string $domain = null)
...
- * @method static bool suspendLicense(\App\Models\EnterpriseLicense $license, string $reason = null)
+ * @method static bool suspendLicense(\App\Models\EnterpriseLicense $license, ?string $reason = null)
app/Http/Controllers/Api/LicenseStatusController.php (2)

70-95: Add return type declaration for consistency.

The method returns JsonResponse but lacks a type declaration. This applies to all four public methods in this controller:

-    public function status(Request $request)
+    public function status(Request $request): \Illuminate\Http\JsonResponse

Regarding the unused $request parameter flagged by PHPMD: this is a Laravel convention for controller methods that may need request data in the future. No action needed.


154-159: Clarify upgrade_required logic.

The condition !$available && $license !== null means "upgrade required if feature is unavailable and a license exists." This is correct but might benefit from a comment for maintainability:

+        // upgrade_required is true when user has a license but it doesn't include this feature
         return response()->json([
             'feature' => $feature,
             'available' => $available,
             'license_tier' => $license?->license_tier,
             'upgrade_required' => ! $available && $license !== null,
         ]);
app/Http/Middleware/LicenseValidationMiddleware.php (1)

56-63: Consider masking license key in logs.

The license_key is logged in plain text on validation failures. License keys are sensitive credentials; consider masking them (e.g., showing only the last 4 characters) to prevent exposure in log aggregation systems.

             Log::warning('License validation failed', [
                 'organization_id' => $organization->id,
-                'license_key' => $license->license_key,
+                'license_key' => '***'.substr($license->license_key, -4),
                 'domain' => $domain,
app/Http/Middleware/ValidateLicense.php (2)

99-113: Unused parameters in handleNoOrganization.

The $next and $features parameters are passed but never used. If they're not needed for the current implementation, consider removing them to simplify the method signature.

-    protected function handleNoOrganization(Request $request, Closure $next, array $features): Response
+    protected function handleNoOrganization(Request $request): Response

289-317: Error code derivation via string matching is fragile.

The getErrorCode method derives error codes by parsing the validation message with str_contains. This is brittle—if message wording changes, error codes will break. Consider having LicenseValidationResult include a structured error code.

A more robust approach would be to add an errorCode property to LicenseValidationResult or check $validationResult->getViolations() for structured error data.

app/Http/Middleware/ServerProvisioningLicense.php (2)

92-93: Add type hints to method parameters.

Several methods lack type hints for $license, $organization, and $validationResult. Adding them improves type safety and IDE support.

-    protected function validateProvisioningCapabilities($license, $organization): array
+    protected function validateProvisioningCapabilities(EnterpriseLicense $license, Organization $organization): array

-    protected function validateCloudProviderLimits($license, $organization): array
+    protected function validateCloudProviderLimits(EnterpriseLicense $license, Organization $organization): array

-    protected function handleInvalidLicense(Request $request, $validationResult): Response
+    protected function handleInvalidLicense(Request $request, LicenseValidationResult $validationResult): Response

-    protected function getErrorCode($validationResult): string
+    protected function getErrorCode(LicenseValidationResult $validationResult): string

Add the necessary imports at the top of the file:

use App\Data\LicenseValidationResult;
use App\Models\EnterpriseLicense;
use App\Models\Organization;

Also applies to: 203-204, 248-249, 287-288


287-312: Duplicate getErrorCode implementation across middlewares.

This method is nearly identical to the one in ValidateLicense.php. Consider extracting it to a shared trait or utility class to avoid duplication.

Create a LicenseValidation trait or a LicenseErrorResolver utility class that both middlewares can use.

app/Http/Controllers/Api/LicenseController.php (3)

504-520: Mock usage history data should be replaced before production.

The usage history is generated with random variations (line 511: rand(-2, 2)) rather than from actual historical data. This is noted in the comment, but ensure this is tracked for implementation before release.

Consider adding a TODO or opening an issue to track the implementation of actual usage history tracking:

-            // Mock usage history - in real implementation, this would come from a usage tracking system
+            // TODO: Replace with actual usage tracking system
+            // See: https://github.com/johnproblems/topgun/issues/XXX
             $history = [];

Do you want me to open an issue to track this implementation?


18-21: Consider using interface type hint for better flexibility.

The constructor injects the concrete LicensingService class. Using LicensingServiceInterface would improve testability and follow dependency inversion principle.

+use App\Contracts\LicensingServiceInterface;
-use App\Services\LicensingService;

 class LicenseController extends Controller
 {
-    protected LicensingService $licensingService;
+    protected LicensingServiceInterface $licensingService;

-    public function __construct(LicensingService $licensingService)
+    public function __construct(LicensingServiceInterface $licensingService)
     {
         $this->licensingService = $licensingService;
     }

56-63: Unbounded query without pagination when managing all licenses.

Line 58-60 fetches all licenses with ->get() when user can manage all licenses. For organizations with many licenses, this could cause memory issues. Consider adding pagination.

             if ($canManageAllLicenses) {
-                $licenses = EnterpriseLicense::with('organization')
+                $licenses = EnterpriseLicense::with('organization')
                     ->orderBy('created_at', 'desc')
-                    ->get();
+                    ->paginate(50);
             }

Then adjust the response to include pagination metadata.

app/Models/EnterpriseLicense.php (1)

198-224: Status transitions don't validate current state.

The activate(), suspend(), revoke(), and markAsExpired() methods allow any state transition. Consider adding guards to prevent invalid transitions (e.g., reactivating a revoked license) if your business logic requires it.

app/Traits/LicenseValidation.php (2)

15-16: Unused parameter $action.

The $action parameter is declared but never used. Either remove it or implement the intended logic.

-    protected function validateLicenseForFeature(string $feature, ?string $action = null): ?JsonResponse
+    protected function validateLicenseForFeature(string $feature): ?JsonResponse

202-228: Hardcoded tier options duplicate configuration.

The deployment options per tier are hardcoded here, which may drift from the actual license features or config/licensing.php. Consider deriving these from the license's features array or centralizing in configuration.

app/Services/LicensingService.php (1)

311-327: Caching Eloquent models may cause stale data issues.

The cacheValidationResult method caches the entire EnterpriseLicense model. When retrieved, the model won't reflect database changes and relationships may not work correctly. Consider caching only the license ID and reloading when needed, or cache a DTO/array representation.

app/Http/Middleware/ApiLicenseValidation.php (2)

174-174: Unused parameter $request.

The $request parameter is not used in handleGracePeriodAccess. Remove it or use it for logging/context.

-    protected function handleGracePeriodAccess(Request $request, $license, array $features): Response
+    protected function handleGracePeriodAccess($license, array $features): Response

295-295: Unused parameter $validationResult.

The $validationResult parameter is declared but never used. Either remove it or use it to add validation metadata to headers.

-    protected function addLicenseHeaders(Response $response, $license, $validationResult): void
+    protected function addLicenseHeaders(Response $response, $license): void

Update the call site on line 92 accordingly.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 773b2d7 and 8c29ba6.

📒 Files selected for processing (17)
  • app/Contracts/LicensingServiceInterface.php (1 hunks)
  • app/Data/LicenseValidationResult.php (1 hunks)
  • app/Exceptions/LicenseException.php (1 hunks)
  • app/Facades/Licensing.php (1 hunks)
  • app/Http/Controllers/Api/LicenseController.php (1 hunks)
  • app/Http/Controllers/Api/LicenseStatusController.php (1 hunks)
  • app/Http/Middleware/ApiLicenseValidation.php (1 hunks)
  • app/Http/Middleware/LicenseValidationMiddleware.php (1 hunks)
  • app/Http/Middleware/ServerProvisioningLicense.php (1 hunks)
  • app/Http/Middleware/ValidateLicense.php (1 hunks)
  • app/Models/EnterpriseLicense.php (1 hunks)
  • app/Providers/LicensingServiceProvider.php (1 hunks)
  • app/Services/LicensingService.php (1 hunks)
  • app/Traits/LicenseValidation.php (1 hunks)
  • config/licensing.php (1 hunks)
  • database/factories/EnterpriseLicenseFactory.php (1 hunks)
  • database/migrations/2025_08_26_225529_create_enterprise_licenses_table.php (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (11)
app/Data/LicenseValidationResult.php (1)
app/Models/EnterpriseLicense.php (2)
  • EnterpriseLicense (9-315)
  • isValid (58-62)
app/Providers/LicensingServiceProvider.php (1)
app/Services/LicensingService.php (1)
  • LicensingService (13-347)
app/Contracts/LicensingServiceInterface.php (4)
app/Models/EnterpriseLicense.php (3)
  • EnterpriseLicense (9-315)
  • organization (36-39)
  • isDomainAuthorized (79-101)
app/Http/Controllers/Api/LicenseController.php (1)
  • validateLicense (184-217)
app/Services/LicensingService.php (10)
  • validateLicense (21-134)
  • issueLicense (136-164)
  • revokeLicense (166-181)
  • checkUsageLimits (218-250)
  • generateLicenseKey (252-271)
  • refreshValidation (273-276)
  • isDomainAuthorized (278-281)
  • getUsageStatistics (283-309)
  • suspendLicense (183-199)
  • reactivateLicense (201-216)
app/Data/LicenseValidationResult.php (1)
  • LicenseValidationResult (7-57)
app/Http/Controllers/Api/LicenseStatusController.php (6)
app/Data/LicenseValidationResult.php (1)
  • __construct (9-15)
app/Http/Controllers/Api/LicenseController.php (1)
  • __construct (18-21)
app/Http/Middleware/LicenseValidationMiddleware.php (1)
  • __construct (16-19)
app/Http/Middleware/ValidateLicense.php (1)
  • __construct (15-17)
app/Models/EnterpriseLicense.php (2)
  • organization (36-39)
  • hasFeature (42-45)
app/Traits/LicenseValidation.php (2)
  • getCurrentOrganization (177-185)
  • getLicenseFeatures (152-172)
app/Exceptions/LicenseException.php (1)
database/factories/EnterpriseLicenseFactory.php (3)
  • expired (53-59)
  • revoked (74-79)
  • suspended (64-69)
database/factories/EnterpriseLicenseFactory.php (2)
app/Models/EnterpriseLicense.php (1)
  • EnterpriseLicense (9-315)
app/Exceptions/LicenseException.php (3)
  • expired (14-17)
  • suspended (24-32)
  • revoked (19-22)
app/Services/LicensingService.php (4)
app/Data/LicenseValidationResult.php (6)
  • LicenseValidationResult (7-57)
  • getMessage (22-25)
  • isValid (17-20)
  • getLicense (27-30)
  • getViolations (32-35)
  • getMetadata (37-40)
app/Models/EnterpriseLicense.php (13)
  • EnterpriseLicense (9-315)
  • isRevoked (74-77)
  • isSuspended (69-72)
  • isExpired (64-67)
  • markAsExpired (219-224)
  • isDomainAuthorized (79-101)
  • organization (36-39)
  • revoke (212-217)
  • suspend (205-210)
  • activate (198-203)
  • updateLastValidated (227-232)
  • getDaysUntilExpiration (234-241)
  • isValid (58-62)
app/Contracts/LicensingServiceInterface.php (10)
  • validateLicense (13-13)
  • isDomainAuthorized (43-43)
  • checkUsageLimits (28-28)
  • refreshValidation (38-38)
  • issueLicense (18-18)
  • generateLicenseKey (33-33)
  • revokeLicense (23-23)
  • suspendLicense (53-53)
  • reactivateLicense (58-58)
  • getUsageStatistics (48-48)
app/Http/Controllers/Api/LicenseController.php (3)
  • validateLicense (184-217)
  • revoke (299-332)
  • suspend (222-256)
app/Http/Middleware/ServerProvisioningLicense.php (4)
app/Data/LicenseValidationResult.php (5)
  • __construct (9-15)
  • isValid (17-20)
  • getLicense (27-30)
  • getMessage (22-25)
  • getViolations (32-35)
bootstrap/helpers/shared.php (1)
  • isDev (375-378)
app/Models/EnterpriseLicense.php (4)
  • organization (36-39)
  • isValid (58-62)
  • isExpired (64-67)
  • getDaysUntilExpiration (234-241)
app/Contracts/LicensingServiceInterface.php (1)
  • validateLicense (13-13)
app/Http/Controllers/Api/LicenseController.php (3)
app/Models/EnterpriseLicense.php (6)
  • EnterpriseLicense (9-315)
  • organization (36-39)
  • isValid (58-62)
  • suspend (205-210)
  • revoke (212-217)
  • isWithinLimits (104-121)
app/Data/LicenseValidationResult.php (6)
  • __construct (9-15)
  • getMessage (22-25)
  • isValid (17-20)
  • getViolations (32-35)
  • getMetadata (37-40)
  • toArray (47-56)
app/Contracts/LicensingServiceInterface.php (6)
  • getUsageStatistics (48-48)
  • issueLicense (18-18)
  • validateLicense (13-13)
  • suspendLicense (53-53)
  • reactivateLicense (58-58)
  • revokeLicense (23-23)
database/migrations/2025_08_26_225529_create_enterprise_licenses_table.php (1)
app/Http/Controllers/Api/LicenseController.php (1)
  • index (26-79)
app/Models/EnterpriseLicense.php (5)
app/Data/LicenseValidationResult.php (1)
  • isValid (17-20)
app/Contracts/LicensingServiceInterface.php (1)
  • isDomainAuthorized (43-43)
app/Services/LicensingService.php (1)
  • isDomainAuthorized (278-281)
app/Models/Team.php (1)
  • limits (132-143)
app/Http/Controllers/Api/LicenseController.php (2)
  • suspend (222-256)
  • revoke (299-332)
🪛 PHPMD (2.15.0)
app/Http/Controllers/Api/LicenseStatusController.php

70-70: Avoid unused parameters such as '$request'. (undefined)

(UnusedFormalParameter)


144-144: Avoid unused parameters such as '$request'. (undefined)

(UnusedFormalParameter)


209-209: Avoid unused parameters such as '$request'. (undefined)

(UnusedFormalParameter)


267-267: Avoid unused parameters such as '$request'. (undefined)

(UnusedFormalParameter)

app/Http/Middleware/LicenseValidationMiddleware.php

99-99: Avoid unused parameters such as '$request'. (undefined)

(UnusedFormalParameter)

database/factories/EnterpriseLicenseFactory.php

55-55: Avoid unused parameters such as '$attributes'. (undefined)

(UnusedFormalParameter)


66-66: Avoid unused parameters such as '$attributes'. (undefined)

(UnusedFormalParameter)


76-76: Avoid unused parameters such as '$attributes'. (undefined)

(UnusedFormalParameter)


86-86: Avoid unused parameters such as '$attributes'. (undefined)

(UnusedFormalParameter)


97-97: Avoid unused parameters such as '$attributes'. (undefined)

(UnusedFormalParameter)


108-108: Avoid unused parameters such as '$attributes'. (undefined)

(UnusedFormalParameter)


118-118: Avoid unused parameters such as '$attributes'. (undefined)

(UnusedFormalParameter)


128-128: Avoid unused parameters such as '$attributes'. (undefined)

(UnusedFormalParameter)

app/Http/Middleware/ValidateLicense.php

99-99: Avoid unused parameters such as '$next'. (undefined)

(UnusedFormalParameter)


99-99: Avoid unused parameters such as '$features'. (undefined)

(UnusedFormalParameter)


164-164: Avoid unused local variables such as '$next'. (undefined)

(UnusedLocalVariable)

app/Http/Middleware/ServerProvisioningLicense.php

144-144: Avoid unused local variables such as '$request'. (undefined)

(UnusedLocalVariable)

app/Http/Middleware/ApiLicenseValidation.php

174-174: Avoid unused parameters such as '$request'. (undefined)

(UnusedFormalParameter)


295-295: Avoid unused parameters such as '$validationResult'. (undefined)

(UnusedFormalParameter)

app/Traits/LicenseValidation.php

15-15: Avoid unused parameters such as '$action'. (undefined)

(UnusedFormalParameter)

app/Http/Controllers/Api/LicenseController.php

26-26: Avoid unused parameters such as '$request'. (undefined)

(UnusedFormalParameter)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (22)
app/Data/LicenseValidationResult.php (2)

17-45: Accessor methods are redundant but acceptable.

Given the public readonly properties, the getter methods (isValid(), getMessage(), etc.) are technically redundant since properties can be accessed directly. However, this follows Laravel conventions and provides a consistent API surface. No changes needed.


47-56: LGTM - Clean serialization implementation.

The toArray() method correctly uses the null-safe operator for license_id and provides a well-structured response format suitable for API responses.

app/Contracts/LicensingServiceInterface.php (1)

8-59: Well-designed service contract.

The interface provides a clean separation of concerns with well-documented methods covering the full license lifecycle. The contract is comprehensive and aligns with the implementation in LicensingService.php.

config/licensing.php (2)

67-74: Verify intentional limit on max_concurrent_provisioning for enterprise tier.

Enterprise tier has null (unlimited) for most limits, but max_concurrent_provisioning is capped at 10. If this is intentional (e.g., to prevent resource exhaustion), the asymmetry is fine. Otherwise, consider aligning it with other enterprise limits.


3-169: Well-organized configuration file.

The configuration provides a centralized, comprehensive setup for the licensing system with sensible defaults and clear tier-based organization. The use of environment variables for tunable values (grace period, cache TTL) follows best practices.

database/migrations/2025_08_26_225529_create_enterprise_licenses_table.php (2)

14-32: Well-designed schema for license management.

The migration includes appropriate:

  • UUID keys for distributed ID generation
  • JSON columns with sensible defaults
  • Composite index on (status, expires_at) for efficient license validity queries
  • Cascade delete to clean up licenses when organizations are removed

One consideration: if an organization should have only one active license at a time, consider adding a partial unique index. This can be handled at the application layer instead if the business logic allows multiple licenses.


20-21: JSON default values are database-specific.

The defaults '{}' and '[]' work in MySQL 5.7.8+ and PostgreSQL, but behavior varies. Ensure your target database version supports JSON column defaults, or handle defaults in the model's $attributes property.

app/Facades/Licensing.php (1)

19-25: LGTM - Standard facade implementation.

The facade correctly returns the 'licensing' accessor matching the singleton binding in LicensingServiceProvider.

app/Http/Controllers/Api/LicenseStatusController.php (1)

11-20: LGTM - Clean dependency injection.

The controller properly injects ResourceProvisioningService and uses the LicenseValidation trait for shared organization context logic.

database/factories/EnterpriseLicenseFactory.php (2)

22-48: LGTM! Well-structured factory with comprehensive defaults.

The default state covers all necessary attributes and aligns with the EnterpriseLicense model's fillable fields and casts. Good use of Organization::factory() for the relationship.


53-131: State modifiers are well-designed for testing various license scenarios.

The static analysis warnings about unused $attributes parameters are false positives—this is the standard Laravel factory state callback signature. No changes needed.

app/Http/Middleware/LicenseValidationMiddleware.php (1)

145-186: Action categorization methods are clear and well-structured.

The helper methods isResourceCreationAction(), isReadOnlyAction(), and isGracePeriodAllowedAction() provide clear categorization of actions. Consider extracting these action lists to configuration if they need to be modified without code changes.

app/Http/Middleware/ValidateLicense.php (2)

73-94: Path matching logic is correct.

The shouldSkipValidation method properly handles path comparison by trimming the leading slash from skip paths, aligning with Laravel's Request::path() behavior which returns paths without leading slashes.


196-236: Grace period access handling correctly continues the request.

Unlike LicenseValidationMiddleware, this implementation properly calls $next($request) on line 228 when allowing access during grace period, and adds informative headers to the response.

app/Http/Middleware/ServerProvisioningLicense.php (1)

69-79: Good audit logging for provisioning authorization.

The detailed logging for authorized provisioning attempts includes all relevant context (user, organization, license, endpoint, current usage, limits). This is valuable for compliance and debugging.

app/Traits/LicenseValidation.php (2)

177-185: Fallback to first organization may be unintended.

If currentOrganization is null, falling back to $user->organizations()->first() could silently select an unintended organization for multi-org users, potentially granting access based on the wrong license.

Verify whether this fallback behavior is intentional. If users must always have an explicit organization context, consider returning null instead.


1-11: Well-structured trait for license validation.

The trait provides a cohesive set of helpers for feature checks, usage limits, and response augmentation. The null-safety patterns are consistent throughout.

app/Services/LicensingService.php (2)

56-73: Grace period calculation here is correct.

Unlike the model's isWithinGracePeriod() which has a bug, this implementation correctly calculates days since expiration using now()->diffInDays($license->expires_at, false).


252-271: License key generation approach is reasonable.

The key generation combines organization ID, timestamp, tier, type, and random components with a SHA-256 hash. Note that changing app.key won't affect existing licenses since the key is stored, not re-derived for validation.

app/Http/Middleware/ApiLicenseValidation.php (3)

244-244: Rate limit key combines organization and IP.

The key api_rate_limit:{org_id}:{ip} means users from the same organization but different IPs have separate rate limits. Consider whether the rate limit should be per-organization or per-user instead.


13-17: Well-structured middleware with dependency injection.

The constructor injection of LicensingServiceInterface follows Laravel best practices and enables easy testing.


27-29: The isDev() helper function exists and is properly defined.

The function is located at bootstrap/helpers/shared.php:375 and is a global helper that returns config('app.env') === 'local'. Laravel automatically loads helpers from the bootstrap/helpers/ directory during application bootstrap, making isDev() available throughout the application without explicit imports.

Comment on lines +73 to +78
} catch (\Exception $e) {
return response()->json([
'message' => 'Failed to load license data',
'error' => $e->getMessage(),
], 500);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid exposing internal exception messages to API clients.

Returning $e->getMessage() (line 76) can expose sensitive internal details like database errors, file paths, or stack traces. Return a generic error message instead.

         } catch (\Exception $e) {
+            Log::error('Failed to load license data', ['error' => $e->getMessage()]);
             return response()->json([
                 'message' => 'Failed to load license data',
-                'error' => $e->getMessage(),
             ], 500);
         }

Apply this pattern to all catch blocks in the controller (lines 138-143, 173-178, 211-216, 250-255, 288-293, 326-331, 382-387, 476-481, 526-531).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (\Exception $e) {
return response()->json([
'message' => 'Failed to load license data',
'error' => $e->getMessage(),
], 500);
}
} catch (\Exception $e) {
Log::error('Failed to load license data', ['error' => $e->getMessage()]);
return response()->json([
'message' => 'Failed to load license data',
], 500);
}
🤖 Prompt for AI Agents
In app/Http/Controllers/Api/LicenseController.php around lines 73-78 (and apply
to catch blocks at 138-143, 173-178, 211-216, 250-255, 288-293, 326-331,
382-387, 476-481, 526-531), the controller returns $e->getMessage() to API
clients which may expose sensitive internal details; replace the response
payload to return a generic error message (e.g., "An internal error occurred" or
"Failed to process request") and remove the exception message from the JSON
response, and instead log the full exception server-side (use the application's
logger inside each catch) so clients get a safe, generic message while details
remain in server logs.

Comment on lines +340 to +347
$validator = Validator::make($request->all(), [
'renewal_period' => 'required|in:1_month,3_months,1_year,custom',
'custom_expires_at' => 'required_if:renewal_period,custom|date|after:now',
'auto_renewal' => 'boolean',
'payment_method' => 'required|in:credit_card,bank_transfer,invoice',
'new_expires_at' => 'required|date|after:now',
'cost' => 'required|numeric|min:0',
]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Redundant validation rules for expiration date.

The validation requires both renewal_period (with options like 1_month, 1_year, custom) and new_expires_at as required fields. If renewal_period determines the expiration, new_expires_at should be optional or derived from it. Currently, custom_expires_at is also defined for custom periods.

Simplify the validation—either:

  1. Use renewal_period to compute the new expiration server-side, or
  2. Accept only new_expires_at and remove renewal_period
             $validator = Validator::make($request->all(), [
                 'renewal_period' => 'required|in:1_month,3_months,1_year,custom',
                 'custom_expires_at' => 'required_if:renewal_period,custom|date|after:now',
                 'auto_renewal' => 'boolean',
                 'payment_method' => 'required|in:credit_card,bank_transfer,invoice',
-                'new_expires_at' => 'required|date|after:now',
                 'cost' => 'required|numeric|min:0',
             ]);

Then compute new_expires_at from renewal_period.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/Http/Controllers/Api/LicenseController.php around lines 340-347, the
validator redundantly requires both renewal_period and new_expires_at; change to
compute new_expires_at server-side from renewal_period: keep renewal_period
required|in:1_month,3_months,1_year,custom and keep custom_expires_at
required_if:renewal_period,custom|date|after:now (remove the
required|date|after:now rule from new_expires_at or make it nullable), retain
auto_renewal/payment_method/cost rules, then after validation derive
new_expires_at based on the selected renewal_period (or use custom_expires_at
when renewal_period == custom) and use that computed value for further
processing and persistence.

Comment on lines +436 to +466
// Update license tier and features based on new tier
$license->license_tier = $request->new_tier;

// Set features based on tier
$tierFeatures = [
'basic' => ['application_deployment', 'database_management', 'ssl_certificates'],
'professional' => [
'application_deployment', 'database_management', 'ssl_certificates',
'server_provisioning', 'terraform_integration', 'white_label_branding',
'organization_hierarchy', 'mfa_authentication', 'audit_logging',
],
'enterprise' => [
'application_deployment', 'database_management', 'ssl_certificates',
'server_provisioning', 'terraform_integration', 'white_label_branding',
'organization_hierarchy', 'mfa_authentication', 'audit_logging',
'multi_cloud_support', 'payment_processing', 'domain_management',
'advanced_rbac', 'compliance_reporting',
],
];

$license->features = $tierFeatures[$request->new_tier];

// Update limits based on tier
$tierLimits = [
'basic' => ['max_users' => 5, 'max_servers' => 3, 'max_applications' => 10],
'professional' => ['max_users' => 25, 'max_servers' => 15, 'max_applications' => 50],
'enterprise' => [], // Unlimited
];

$license->limits = $tierLimits[$request->new_tier];
$license->save();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Business logic for tier features/limits should be in the service layer.

The upgrade method contains hardcoded tier features (lines 440-454) and limits (lines 459-463) directly in the controller. This business logic should be in LicensingService to maintain separation of concerns and allow reuse.

Consider refactoring to delegate to the service:

-            // Update license tier and features based on new tier
-            $license->license_tier = $request->new_tier;
-
-            // Set features based on tier
-            $tierFeatures = [
-                'basic' => ['application_deployment', ...],
-                // ... more tiers
-            ];
-
-            $license->features = $tierFeatures[$request->new_tier];
-            // ... limits logic
-            $license->save();
+            $license = $this->licensingService->upgradeLicense($license, $request->new_tier);

The tier features/limits configuration could also move to config/licensing.php.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/Http/Controllers/Api/LicenseController.php around lines 436 to 466 the
controller currently contains hardcoded tier feature and limit arrays; move this
business logic into the service layer by removing the arrays and replacing them
with a call to LicensingService (e.g., $data =
$this->licensingService->getTierConfig($request->new_tier) then assign
$license->features = $data['features'] and $license->limits = $data['limits']
before save). Implement getTierConfig(newTier) in LicensingService to return
features and limits (backed by a new config/licensing.php that defines the tier
maps), include validation for unknown tiers, and add unit tests for the service;
update the controller to only orchestrate request -> service -> persist.

Comment on lines +216 to +218
$deploymentOptions = $this->provisioningService->getAvailableDeploymentOptions($organization);
$available = array_key_exists($option, $deploymentOptions['available_options']);
$description = $deploymentOptions['available_options'][$option] ?? null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against missing 'available_options' key.

If getAvailableDeploymentOptions() returns an array without the 'available_options' key, this will trigger an error. Add defensive access:

     $deploymentOptions = $this->provisioningService->getAvailableDeploymentOptions($organization);
-    $available = array_key_exists($option, $deploymentOptions['available_options']);
-    $description = $deploymentOptions['available_options'][$option] ?? null;
+    $availableOptions = $deploymentOptions['available_options'] ?? [];
+    $available = array_key_exists($option, $availableOptions);
+    $description = $availableOptions[$option] ?? null;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$deploymentOptions = $this->provisioningService->getAvailableDeploymentOptions($organization);
$available = array_key_exists($option, $deploymentOptions['available_options']);
$description = $deploymentOptions['available_options'][$option] ?? null;
$deploymentOptions = $this->provisioningService->getAvailableDeploymentOptions($organization);
$availableOptions = $deploymentOptions['available_options'] ?? [];
$available = array_key_exists($option, $availableOptions);
$description = $availableOptions[$option] ?? null;
🤖 Prompt for AI Agents
In app/Http/Controllers/Api/LicenseStatusController.php around lines 216 to 218,
the code assumes $deploymentOptions contains an 'available_options' key which
may be missing; guard against this by first normalizing
$deploymentOptions['available_options'] to a safe array (e.g. if not an array or
key missing, set to empty array), then compute $available using array_key_exists
or isset against that normalized array and derive $description from it (using
null coalescing or checking existence) so no undefined index error occurs.

Comment on lines +200 to +211
// Allow read-only operations with warnings
return response()->json([
'success' => true,
'message' => 'Request processed with license in grace period',
'warnings' => [
'license_expired' => true,
'days_expired' => abs($license->getDaysUntilExpiration()),
'grace_period_ends' => $license->getGracePeriodEndDate()?->toISOString(),
'restricted_features' => $restrictedFeatures,
],
], 200);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Grace period handler returns early without processing the actual request.

When a non-restricted feature is accessed during the grace period, this method returns a generic 200 response with a success message instead of calling $next($request) to process the actual API request. The client receives a warning payload but not the requested data.

If the intent is to allow read-only access with a warning, the warning should be added to the actual response:

         // Allow read-only operations with warnings
-        return response()->json([
-            'success' => true,
-            'message' => 'Request processed with license in grace period',
-            'warnings' => [
-                'license_expired' => true,
-                'days_expired' => abs($license->getDaysUntilExpiration()),
-                'grace_period_ends' => $license->getGracePeriodEndDate()?->toISOString(),
-                'restricted_features' => $restrictedFeatures,
-            ],
-        ], 200);
+        // Return null to allow the request to proceed
+        // Warnings can be added via response headers in addLicenseHeaders
+        return null;
     }

Then adjust the caller in handleInvalidLicense to proceed if null is returned.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/Http/Middleware/ApiLicenseValidation.php around lines 200–211, the
grace-period branch currently returns a generic 200 JSON payload instead of
letting the actual controller handle the request; change it to allow the request
to proceed and attach the warning metadata to the real response. Specifically,
replace the early return with code that calls $next($request) and then merges
the warnings into the outgoing response (for JSON responses, decode/merge the
warnings into the response body; for other Response types, add a custom header
like X-License-Warnings or equivalent), and finally return that modified
Response; if you prefer the middleware to signal the caller to continue, return
null here and update handleInvalidLicense to treat null as “proceed” and then
attach warnings after $next($request) returns.

Comment on lines +279 to +289
public function isWithinGracePeriod(): bool
{
if (! $this->isExpired()) {
return false;
}

$gracePeriodDays = config('licensing.grace_period_days', 7);
$daysExpired = abs($this->getDaysUntilExpiration());

return $daysExpired <= $gracePeriodDays;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Grace period logic is broken due to clamped day calculation.

getDaysUntilExpiration() returns max(0, ...) so it always returns 0 for expired licenses. This means $daysExpired on line 286 will always be 0, causing any expired license to appear within the grace period.

     public function isWithinGracePeriod(): bool
     {
         if (! $this->isExpired()) {
             return false;
         }

         $gracePeriodDays = config('licensing.grace_period_days', 7);
-        $daysExpired = abs($this->getDaysUntilExpiration());
+        $daysExpired = (int) now()->diffInDays($this->expires_at, false);
+        $daysExpired = abs($daysExpired);

         return $daysExpired <= $gracePeriodDays;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function isWithinGracePeriod(): bool
{
if (! $this->isExpired()) {
return false;
}
$gracePeriodDays = config('licensing.grace_period_days', 7);
$daysExpired = abs($this->getDaysUntilExpiration());
return $daysExpired <= $gracePeriodDays;
}
public function isWithinGracePeriod(): bool
{
if (! $this->isExpired()) {
return false;
}
$gracePeriodDays = config('licensing.grace_period_days', 7);
$daysExpired = (int) now()->diffInDays($this->expires_at, false);
$daysExpired = abs($daysExpired);
return $daysExpired <= $gracePeriodDays;
}
🤖 Prompt for AI Agents
In app/Models/EnterpriseLicense.php around lines 279-289, the grace-period check
uses getDaysUntilExpiration() which is clamped to 0 for expired licenses so
daysExpired is always 0; replace that calculation with the actual
days-since-expiration (only when the license is expired) by using
Carbon::now()->diffInDays($this->expires_at) (or an equivalent method that
returns the real positive days since the expiry) and compare that value to
config('licensing.grace_period_days', 7); ensure Carbon is imported and only
compute this after confirming $this->isExpired().

Comment on lines +14 to +21
public function register(): void
{
$this->app->bind(LicensingServiceInterface::class, LicensingService::class);

$this->app->singleton('licensing', function ($app) {
return $app->make(LicensingServiceInterface::class);
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider using singleton for the interface binding.

The interface is bound with bind() (transient), but the 'licensing' alias is a singleton. This creates an inconsistency: code injecting LicensingServiceInterface directly gets a new instance each time, while code using the facade or 'licensing' alias gets a shared instance.

Since LicensingService manages caching and likely shouldn't have multiple instances, consider making the interface binding a singleton:

 public function register(): void
 {
-    $this->app->bind(LicensingServiceInterface::class, LicensingService::class);
+    $this->app->singleton(LicensingServiceInterface::class, LicensingService::class);

     $this->app->singleton('licensing', function ($app) {
         return $app->make(LicensingServiceInterface::class);
     });
 }

This ensures consistent behavior regardless of how the service is resolved.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function register(): void
{
$this->app->bind(LicensingServiceInterface::class, LicensingService::class);
$this->app->singleton('licensing', function ($app) {
return $app->make(LicensingServiceInterface::class);
});
}
public function register(): void
{
$this->app->singleton(LicensingServiceInterface::class, LicensingService::class);
$this->app->singleton('licensing', function ($app) {
return $app->make(LicensingServiceInterface::class);
});
}
🤖 Prompt for AI Agents
In app/Providers/LicensingServiceProvider.php around lines 14 to 21, the
LicensingServiceInterface is registered with bind() (transient) while a
'licensing' alias is registered as a singleton, causing inconsistent instances;
change the interface binding to singleton so both resolutions return the same
shared LicensingService instance, keeping the existing singleton 'licensing'
alias intact and ensuring any injected LicensingServiceInterface receives the
same singleton used by the facade/alias.


private const LICENSE_KEY_LENGTH = 32;

private const GRACE_PERIOD_DAYS = 7;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Grace period is hardcoded here but configurable in the model.

EnterpriseLicense::isWithinGracePeriod() reads from config('licensing.grace_period_days', 7), but this service uses a hardcoded constant. This inconsistency could cause different grace period behavior depending on which code path is used.

-    private const GRACE_PERIOD_DAYS = 7;
+    // Use config for consistency with EnterpriseLicense model

Then in validateLicense:

-                if ($daysExpired > self::GRACE_PERIOD_DAYS) {
+                if ($daysExpired > config('licensing.grace_period_days', 7)) {

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/Services/LicensingService.php around line 19, the GRACE_PERIOD_DAYS
constant is hardcoded while EnterpriseLicense::isWithinGracePeriod() reads from
config('licensing.grace_period_days', 7); remove the hardcoded constant and read
the grace period from the same configuration (use
config('licensing.grace_period_days', 7)) wherever the constant was used (e.g.,
in validateLicense) so both code paths use the same configurable value, and
adjust any tests or usages to reference the config value instead.

Comment on lines +329 to +346
private function clearLicenseCache(string $licenseKey): void
{
try {
// Clear all cached validation results for this license key
$patterns = [
"license_validation:{$licenseKey}:*",
];

foreach ($patterns as $pattern) {
Cache::forget($pattern);
}
} catch (\Exception $e) {
Log::warning('Failed to clear license cache', [
'license_key' => $licenseKey,
'error' => $e->getMessage(),
]);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Cache::forget does not support wildcard patterns.

The clearLicenseCache method attempts to use a wildcard pattern (license_validation:{$licenseKey}:*), but Laravel's Cache::forget only accepts exact keys. The cache entries won't be cleared.

Consider using tagged caching or storing domain-specific keys to clear, e.g.:

     private function clearLicenseCache(string $licenseKey): void
     {
         try {
-            // Clear all cached validation results for this license key
-            $patterns = [
-                "license_validation:{$licenseKey}:*",
-            ];
-
-            foreach ($patterns as $pattern) {
-                Cache::forget($pattern);
-            }
+            // Clear known domain variations or use cache tags
+            Cache::forget("license_validation:{$licenseKey}:no_domain");
+            // If using Redis, consider Cache::tags(['license', $licenseKey])->flush()
         } catch (\Exception $e) {
             Log::warning('Failed to clear license cache', [
                 'license_key' => $licenseKey,
                 'error' => $e->getMessage(),
             ]);
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private function clearLicenseCache(string $licenseKey): void
{
try {
// Clear all cached validation results for this license key
$patterns = [
"license_validation:{$licenseKey}:*",
];
foreach ($patterns as $pattern) {
Cache::forget($pattern);
}
} catch (\Exception $e) {
Log::warning('Failed to clear license cache', [
'license_key' => $licenseKey,
'error' => $e->getMessage(),
]);
}
}
private function clearLicenseCache(string $licenseKey): void
{
try {
// Clear known domain variations or use cache tags
Cache::forget("license_validation:{$licenseKey}:no_domain");
// If using Redis, consider Cache::tags(['license', $licenseKey])->flush()
} catch (\Exception $e) {
Log::warning('Failed to clear license cache', [
'license_key' => $licenseKey,
'error' => $e->getMessage(),
]);
}
}
🤖 Prompt for AI Agents
In app/Services/LicensingService.php around lines 329 to 346, the method uses
Cache::forget with a wildcard pattern which doesn't work because Cache::forget
only accepts exact keys; update the implementation to either (A) use cache
tags—when storing validation entries save them with
Cache::tags(['license_validation:'.$licenseKey])->put(...), then here call
Cache::tags(['license_validation:'.$licenseKey])->flush() to remove all related
keys—or (B) if using Redis, resolve keys and delete them explicitly via the
Redis facade (e.g., get matching keys with
Redis::keys("license_validation:{$licenseKey}:*") and Redis::del on each or pass
array to del). Replace the current wildcard Cache::forget loop with one of these
approaches and ensure storage side uses the matching tagging or key format so
flush/del removes all related entries.

Comment on lines +160 to +168
'middleware_groups' => [
'basic_license' => ['auth', 'license'],
'api_license' => ['auth:sanctum', 'api.license'],
'server_provisioning' => ['auth', 'license', 'server.provision'],
'infrastructure' => ['auth', 'license:infrastructure_provisioning,terraform_integration'],
'payments' => ['auth', 'license:payment_processing'],
'domains' => ['auth', 'license:domain_management'],
'white_label' => ['auth', 'license:white_label_branding'],
],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for middleware alias registrations in Kernel.php and ServiceProvider files
echo "=== Searching for middleware registrations in Kernel.php ==="
fd -t f "Kernel.php" app/ --exec rg -n "license|server\.provision|api\.license" {} +

echo ""
echo "=== Searching in all ServiceProvider files ==="
fd -t f "ServiceProvider.php" app/ --exec rg -n "license|server\.provision|api\.license" {} +

echo ""
echo "=== Checking for middleware alias configuration patterns ==="
rg -n "aliasMiddleware|protected.*middleware|routeMiddleware" --type=php app/

Repository: johnproblems/topgun

Length of output: 968


🏁 Script executed:

#!/bin/bash
# Check the size first
wc -l app/Http/Kernel.php

# Read the entire file
cat -n app/Http/Kernel.php

Repository: johnproblems/topgun

Length of output: 3954


Middleware aliases referenced in config/licensing.php are not registered in app/Http/Kernel.php.

The middleware aliases 'license', 'api.license', 'server.provision', and parametrized license variants are missing from the $middlewareAliases array in app/Http/Kernel.php. Register these aliases or ensure they are defined in a service provider before these middleware groups can be used.

🤖 Prompt for AI Agents
In config/licensing.php around lines 160-168 the middleware group names
'license', 'api.license', 'server.provision' and parameterized license variants
are referenced but not registered; open app/Http/Kernel.php and add entries to
the $middlewareAliases array for 'license', 'api.license', and
'server.provision' (pointing to the concrete middleware classes implemented in
the app), and ensure parameterized license checks reuse the base 'license' alias
so parameters like license:payment_processing work; alternatively, if different
concrete classes exist for API or server provisioning, map those aliases to
their respective middleware classes and load them before these config groups are
used.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is being reviewed by Cursor Bugbot

Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

}

return max(0, now()->diffInDays($this->expires_at, false));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Grace period check always returns true for expired licenses

The getDaysUntilExpiration() method uses max(0, ...) which clamps the return value to zero for expired licenses. When isWithinGracePeriod() calls abs($this->getDaysUntilExpiration()), it always gets 0 for any expired license, making the comparison 0 <= gracePeriodDays always true. This means a license expired 100 days ago is incorrectly treated as still being within the 7-day grace period. The LicensingService.validateLicense() correctly calculates days expired directly from expires_at, but the model method is broken and used by multiple middleware components (ApiLicenseValidation, LicenseValidationMiddleware, ValidateLicense).

Additional Locations (1)

Fix in Cursor Fix in Web

if (str_ends_with($domain, $pattern)) {
return true;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Wildcard domain matching allows unauthorized domain bypass

The wildcard domain authorization check in isDomainAuthorized() strips *. from patterns like *.example.com to get example.com, then uses str_ends_with() to match. This allows unauthorized domains to pass validation - for instance, attackerexample.com ends with example.com and would be incorrectly authorized for a license restricted to *.example.com. The check needs to verify the domain ends with .example.com (with the leading dot) to properly match only legitimate subdomains and prevent domain authorization bypass.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants