-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Implement enterprise license management system #208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- 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
WalkthroughA 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this 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
readonlyis 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.
checkUsageLimitsandgetUsageStatisticsreturn untyped arrays. For a public contract, consider defining DTOs (similar toLicenseValidationResult) 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
JsonResponsebut 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\JsonResponseRegarding the unused
$requestparameter flagged by PHPMD: this is a Laravel convention for controller methods that may need request data in the future. No action needed.
154-159: Clarifyupgrade_requiredlogic.The condition
!$available && $license !== nullmeans "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_keyis 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 inhandleNoOrganization.The
$nextand$featuresparameters 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
getErrorCodemethod derives error codes by parsing the validation message withstr_contains. This is brittle—if message wording changes, error codes will break. Consider havingLicenseValidationResultinclude a structured error code.A more robust approach would be to add an
errorCodeproperty toLicenseValidationResultor 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): stringAdd 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: DuplicategetErrorCodeimplementation 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
LicenseValidationtrait or aLicenseErrorResolverutility 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
LicensingServiceclass. UsingLicensingServiceInterfacewould 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(), andmarkAsExpired()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
$actionparameter 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'sfeaturesarray or centralizing in configuration.app/Services/LicensingService.php (1)
311-327: Caching Eloquent models may cause stale data issues.The
cacheValidationResultmethod caches the entireEnterpriseLicensemodel. 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
$requestparameter is not used inhandleGracePeriodAccess. 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
$validationResultparameter 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): voidUpdate the call site on line 92 accordingly.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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 readonlyproperties, 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 forlicense_idand 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 onmax_concurrent_provisioningfor enterprise tier.Enterprise tier has
null(unlimited) for most limits, butmax_concurrent_provisioningis 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$attributesproperty.app/Facades/Licensing.php (1)
19-25: LGTM - Standard facade implementation.The facade correctly returns the
'licensing'accessor matching the singleton binding inLicensingServiceProvider.app/Http/Controllers/Api/LicenseStatusController.php (1)
11-20: LGTM - Clean dependency injection.The controller properly injects
ResourceProvisioningServiceand uses theLicenseValidationtrait 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
EnterpriseLicensemodel's fillable fields and casts. Good use ofOrganization::factory()for the relationship.
53-131: State modifiers are well-designed for testing various license scenarios.The static analysis warnings about unused
$attributesparameters 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(), andisGracePeriodAllowedAction()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
shouldSkipValidationmethod properly handles path comparison by trimming the leading slash from skip paths, aligning with Laravel'sRequest::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
currentOrganizationis 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
nullinstead.
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 usingnow()->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.keywon'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
LicensingServiceInterfacefollows Laravel best practices and enables easy testing.
27-29: TheisDev()helper function exists and is properly defined.The function is located at
bootstrap/helpers/shared.php:375and is a global helper that returnsconfig('app.env') === 'local'. Laravel automatically loads helpers from thebootstrap/helpers/directory during application bootstrap, makingisDev()available throughout the application without explicit imports.
| } catch (\Exception $e) { | ||
| return response()->json([ | ||
| 'message' => 'Failed to load license data', | ||
| 'error' => $e->getMessage(), | ||
| ], 500); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| } 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.
| $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', | ||
| ]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
- Use
renewal_periodto compute the new expiration server-side, or - Accept only
new_expires_atand removerenewal_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.
| // 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(); |
There was a problem hiding this comment.
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.
| $deploymentOptions = $this->provisioningService->getAvailableDeploymentOptions($organization); | ||
| $available = array_key_exists($option, $deploymentOptions['available_options']); | ||
| $description = $deploymentOptions['available_options'][$option] ?? null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| $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.
| // 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| public function isWithinGracePeriod(): bool | ||
| { | ||
| if (! $this->isExpired()) { | ||
| return false; | ||
| } | ||
|
|
||
| $gracePeriodDays = config('licensing.grace_period_days', 7); | ||
| $daysExpired = abs($this->getDaysUntilExpiration()); | ||
|
|
||
| return $daysExpired <= $gracePeriodDays; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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().
| public function register(): void | ||
| { | ||
| $this->app->bind(LicensingServiceInterface::class, LicensingService::class); | ||
|
|
||
| $this->app->singleton('licensing', function ($app) { | ||
| return $app->make(LicensingServiceInterface::class); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 modelThen 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.
| 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(), | ||
| ]); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| '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'], | ||
| ], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.phpRepository: 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.
There was a problem hiding this 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)); | ||
| } |
There was a problem hiding this comment.
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)
| if (str_ends_with($domain, $pattern)) { | ||
| return true; | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
Summary
Complete enterprise license management system with token-based validation, license status checking, and server provisioning controls.
Features
Database
create_enterprise_licenses_tableEnterpriseLicenseFactoryAPI Endpoints
POST /api/v1/licenses- Create licenseGET /api/v1/licenses- List licensesGET /api/v1/licenses/{id}- Get licensePUT /api/v1/licenses/{id}- Update licenseDELETE /api/v1/licenses/{id}- Delete licenseGET /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.
LicenseController(issue, show, validate, suspend/reactivate/revoke, renew, upgrade, usage history, CSV/JSON export).LicenseStatusController(status, feature/option checks, limits) with OpenAPI annotations.ValidateLicense,ApiLicenseValidation,LicenseValidationMiddleware,ServerProvisioningLicensefor feature gating, grace-period handling, and tier-based rate limiting/headers.LicensingServiceimplementingLicensingServiceInterface(validate with cache, domain/auth checks, usage limits, key generation, suspend/reactivate/revoke, usage stats) and bind viaLicensingServiceProvider+Facades\Licensing.EnterpriseLicensemodel with feature/limit checks, status helpers, grace-period logic.create_enterprise_licenses_tableand factoryEnterpriseLicenseFactory.Traits\LicenseValidationhelpers.config/licensing.php(grace period, default features/limits, route feature map, middleware groups).LicenseValidationResultDTO andLicenseException.Written by Cursor Bugbot for commit 8c29ba6. This will update automatically on new commits. Configure here.
Summary by CodeRabbit
New Features
✏️ Tip: You can customize this high-level summary in your review settings.