Skip to content

Latest commit

 

History

History
913 lines (725 loc) · 22.8 KB

basic-usage.md

File metadata and controls

913 lines (725 loc) · 22.8 KB

Basic usage

Learn the fundamental operations of License Kit with practical examples.

Creating licenses

Method 1: auto-generated license keys (recommended)

use Simtabi\Laranail\Licence\Kit\Models\License;

// Create license with auto-generated key
$license = License::createWithKey([
    'licensable_type' => User::class,
    'licensable_id' => $user->id,
    'status' => LicenseStatus::Pending,
    'max_usages' => 5,
    'expires_at' => now()->addYear(),
    'meta' => [
        'product' => 'Professional Edition',
        'version' => '2.0',
    ],
]);

// Get the generated key immediately
$activationKey = $license->license_key; // e.g., "LIC-A3F2B9K1-C4D8E5H7-9D2EK8F3-L6A9M1B4"

// Give the activation key to your customer
echo "Activation Key: {$activationKey}";

Method 2: custom license keys

// Provide your own license key format
$customKey = 'PRO-2024-ENTERPRISE-001';

$license = License::createWithKey([
    'licensable_type' => Organization::class,
    'licensable_id' => $organization->id,
    'status' => LicenseStatus::Pending,
    'max_usages' => 100,
    'expires_at' => now()->addYear(),
    'meta' => [
        'product' => 'Enterprise Edition',
        'version' => '3.0',
    ],
], $customKey);

echo "Enterprise Key: {$customKey}";

Method 3: hash-only (maximum security)

use Illuminate\Support\Str;

// Traditional hash-only approach for maximum security
$activationKey = strtoupper(bin2hex(random_bytes(16)));
$formattedKey = implode('-', str_split($activationKey, 8)); // XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX

$license = License::create([
    'key_hash' => License::hashKey($activationKey),
    'licensable_type' => User::class,
    'licensable_id' => $user->id,
    'status' => LicenseStatus::Pending,
    'max_usages' => 5,
    'expires_at' => now()->addYear(),
]);

// Store the activation key securely (cannot be retrieved later)
echo "Activation Key: {$formattedKey}";

Using license templates

use Simtabi\Laranail\Licence\Kit\Models\{License, LicenseScope, LicenseTemplate};
use Simtabi\Laranail\Licence\Kit\Services\TemplateService;

// Method 1: Scoped template via TemplateService
$scope = LicenseScope::firstOrCreate([
    'slug' => 'analytics-suite',
], ['name' => 'Analytics Suite']);

$template = LicenseTemplate::updateOrCreate([
    'license_scope_id' => $scope->id,
    'name' => 'Mensile',
], [
    'tier_level' => 1,
    'base_configuration' => [
        'max_usages' => 2,
        'validity_days' => 30,
    ],
]);

$license = app(TemplateService::class)->createLicenseForScope($scope, $template->slug, [
    'licensable_type' => Organization::class,
    'licensable_id' => $organization->id,
    'key_hash' => License::hashKey($activationKey),
]);

// Method 2: Global template (license_scope_id = null)
$trialTemplate = LicenseTemplate::firstOrCreate([
    'license_scope_id' => null,
    'name' => '14-day Trial',
], [
    'tier_level' => 0,
    'base_configuration' => [
        'validity_days' => 14,
        'max_usages' => 1,
    ],
]);

$trialLicense = License::createFromTemplate($trialTemplate, [
    'licensable_type' => Organization::class,
    'licensable_id' => $organization->id,
]);

// Licenses inherit all template settings
echo $license->max_usages;
echo $license->getFeatures();
echo $license->getEntitlements();

Batch license creation

use Illuminate\Support\Facades\DB;

// Create multiple licenses in a transaction
DB::transaction(function () use ($organization, $quantity) {
    $licenses = [];
    
    for ($i = 0; $i < $quantity; $i++) {
        $activationKey = $this->generateUniqueKey();
        
        $licenses[] = License::create([
            'key_hash' => License::hashKey($activationKey),
            'licensable_type' => Organization::class,
            'licensable_id' => $organization->id,
            'status' => LicenseStatus::Pending,
            'max_usages' => 1,
            'expires_at' => now()->addYear(),
            'meta' => [
                'batch_id' => Str::uuid(),
                'index' => $i + 1,
            ],
        ]);
        
        // Store activation keys
        $this->storeActivationKey($organization, $activationKey);
    }
    
    return $licenses;
});

Activating licenses

Basic activation

// Customer provides their activation key
$providedKey = request()->input('activation_key');

// Find the license
$license = License::findByKey($providedKey);

if (!$license) {
    return response()->json(['error' => 'Invalid activation key'], 404);
}

// Verify the key
if (!$license->verifyKey($providedKey)) {
    return response()->json(['error' => 'Invalid activation key'], 401);
}

// Check if already activated
if ($license->status !== LicenseStatus::Pending) {
    return response()->json(['error' => 'License already activated'], 409);
}

// Activate the license
$license->activate();

return response()->json([
    'message' => 'License activated successfully',
    'expires_at' => $license->expires_at,
    'features' => $license->getFeatures(),
]);

Activation with device registration

use Simtabi\Laranail\Licence\Kit\Services\UsageRegistrarService;

public function activateWithDevice(Request $request, UsageRegistrarService $registrar)
{
    $validated = $request->validate([
        'activation_key' => 'required|string',
        'device_name' => 'required|string',
        'device_fingerprint' => 'required|string',
    ]);
    
    $license = License::findByKey($validated['activation_key']);
    
    if (!$license || !$license->verifyKey($validated['activation_key'])) {
        return response()->json(['error' => 'Invalid activation key'], 401);
    }
    
    // Activate if pending
    if ($license->status === LicenseStatus::Pending) {
        $license->activate();
    }
    
    // Register the device
    try {
        $usage = $registrar->register(
            $license,
            $validated['device_fingerprint'],
            [
                'name' => $validated['device_name'],
                'client_type' => 'desktop',
                'ip' => $request->ip(),
            ]
        );
        
        return response()->json([
            'message' => 'License activated and device registered',
            'license_id' => $license->uid,
            'usage_id' => $usage->id,
            'seats_remaining' => $license->getAvailableSeats(),
        ]);
        
    } catch (UsageLimitReachedException $e) {
        return response()->json([
            'error' => 'Device limit reached',
            'max_devices' => $license->max_usages,
        ], 403);
    }
}

Registering devices/usages

Register a new device

use Simtabi\Laranail\Licence\Kit\Services\UsageRegistrarService;

$registrar = app(UsageRegistrarService::class);

// Generate device fingerprint
$fingerprint = hash('sha256', 
    $request->ip() . 
    $request->header('User-Agent') . 
    $request->input('hardware_id')
);

// Register the device
$usage = $registrar->register(
    $license,
    $fingerprint,
    [
        'name' => 'John\'s MacBook Pro',
        'client_type' => 'desktop',
        'meta' => [
            'os' => 'macOS',
            'version' => '14.0',
            'app_version' => '2.1.0',
        ],
    ]
);

// Update heartbeat periodically
$registrar->heartbeat($usage);

Managing multiple devices

// List all devices for a license
$devices = $license->activeUsages()
    ->select('id', 'name', 'last_seen_at', 'registered_at')
    ->get()
    ->map(function ($usage) {
        return [
            'id' => $usage->id,
            'name' => $usage->name,
            'last_active' => $usage->last_seen_at->diffForHumans(),
            'registered' => $usage->registered_at->format('M d, Y'),
        ];
    });

// Remove a device
$usage = $license->usages()->find($deviceId);
if ($usage) {
    $registrar->revoke($usage, 'Removed by user');
}

// Check if can add more devices
if ($license->hasAvailableSeats()) {
    $availableSeats = $license->getAvailableSeats();
    echo "You can add {$availableSeats} more devices";
}

Auto-replace oldest device

// Configure in config/licensing.php
'policies' => [
    'over_limit' => 'auto_replace_oldest',
],

// Or handle manually
if (!$license->hasAvailableSeats()) {
    $oldestUsage = $license->activeUsages()
        ->orderBy('last_seen_at', 'asc')
        ->first();
    
    $registrar->revoke($oldestUsage, 'Auto-replaced by new device');
}

// Now register new device
$newUsage = $registrar->register($license, $newFingerprint, $metadata);

License key management

Key operations

// Retrieve the original license key (if enabled in configuration)
if ($license->canRetrieveKey()) {
    $originalKey = $license->retrieveKey();
    echo "Your license key: {$originalKey}";
} else {
    echo "Key retrieval is disabled for security.";
}

// Regenerate a license key (useful for security incidents)
if ($license->canRegenerateKey()) {
    $oldKey = $license->retrieveKey(); // Get old key if possible
    $newKey = $license->regenerateKey();

    echo "New license key: {$newKey}";
    echo "Old key is no longer valid.";

    // Previous key hashes are stored for audit
    $auditTrail = $license->meta['previous_key_hashes'];
}

// Verify a provided key
$userInputKey = 'LIC-A3F2B9K1-C4D8E5H7-9D2EK8F3-L6A9M1B4';
if ($license->verifyKey($userInputKey)) {
    echo "Valid license key!";
} else {
    echo "Invalid license key.";
}

// Find license by key (constant-time lookup)
$license = License::findByKey($userInputKey);

// Find by UID (alternative identifier)
$license = License::findByUid($uid);

Key recovery scenarios

class LicenseKeyRecoveryService
{
    public function recoverKey(User $user, string $email): ?string
    {
        // Find user's active license
        $license = $user->licenses()
            ->whereIn('status', ['active', 'grace'])
            ->first();

        if (!$license) {
            return null;
        }

        // Check if retrieval is enabled
        if (!$license->canRetrieveKey()) {
            // Generate new key instead
            if ($license->canRegenerateKey()) {
                $newKey = $license->regenerateKey();

                // Send via email
                Mail::to($email)->send(
                    new LicenseKeyRegeneratedMail($license, $newKey)
                );

                return $newKey;
            }

            return null;
        }

        // Retrieve original key
        $key = $license->retrieveKey();

        // Send via email
        Mail::to($email)->send(
            new LicenseKeyRecoveryMail($license, $key)
        );

        return $key;
    }
}

Security incident response

class SecurityIncidentService
{
    public function handleKeyCompromise(License $license, string $reason): string
    {
        if (!$license->canRegenerateKey()) {
            throw new \RuntimeException('Key regeneration not available');
        }

        // Log the incident
        Log::warning('License key compromised', [
            'license_id' => $license->id,
            'reason' => $reason,
            'old_key_hash' => $license->key_hash,
        ]);

        // Regenerate key
        $newKey = $license->regenerateKey();

        // Revoke all active usages (force re-registration)
        $license->usages()
            ->where('status', 'active')
            ->update([
                'status' => 'revoked',
                'revoked_at' => now(),
                'meta->revocation_reason' => 'Key compromise: ' . $reason,
            ]);

        // Notify license holder
        event(new LicenseKeyCompromised($license, $newKey, $reason));

        return $newKey;
    }
}

Checking license status

Basic status check

// Check if license is usable (active or in grace period)
if ($license->isUsable()) {
    // Allow access
}

// Check specific status
if ($license->status === LicenseStatus::Active) {
    // Fully active
}

if ($license->isInGracePeriod()) {
    // In grace period - show warning
    $daysRemaining = $license->getGraceDays() - $license->daysUntilExpiration();
    echo "License expired. Grace period: {$daysRemaining} days remaining";
}

// Check expiration
if ($license->isExpired()) {
    $daysSinceExpiration = abs($license->daysUntilExpiration());
    echo "License expired {$daysSinceExpiration} days ago";
} else {
    $daysUntilExpiration = $license->daysUntilExpiration();
    echo "License expires in {$daysUntilExpiration} days";
}

Middleware for license validation

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class RequireActiveLicense
{
    public function handle(Request $request, Closure $next)
    {
        $user = $request->user();
        
        if (!$user) {
            return redirect()->route('login');
        }
        
        $license = $user->licenses()
            ->whereIn('status', [LicenseStatus::Active, LicenseStatus::Grace])
            ->first();
        
        if (!$license) {
            return redirect()->route('license.expired')
                ->with('error', 'No active license found');
        }
        
        if (!$license->isUsable()) {
            return redirect()->route('license.expired')
                ->with('error', 'Your license has expired');
        }
        
        // Add license to request for easy access
        $request->merge(['license' => $license]);
        
        // Show warning if expiring soon
        if ($license->daysUntilExpiration() <= 7) {
            session()->flash('warning', 
                "Your license expires in {$license->daysUntilExpiration()} days"
            );
        }
        
        return $next($request);
    }
}

Managing features & entitlements

Checking features

// Check if a feature is enabled
if ($license->hasFeature('advanced_analytics')) {
    // Show advanced analytics dashboard
}

// Get all features
$features = $license->getFeatures();
foreach ($features as $feature => $enabled) {
    if ($enabled) {
        echo "[x] {$feature}";
    }
}

// Feature-gated functionality
class AnalyticsController extends Controller
{
    public function advanced(Request $request)
    {
        $license = $request->license;
        
        if (!$license->hasFeature('advanced_analytics')) {
            return redirect()->route('upgrade')
                ->with('error', 'This feature requires an upgrade');
        }
        
        return view('analytics.advanced');
    }
}

Working with entitlements

// Get specific entitlement
$apiCallsLimit = $license->getEntitlement('api_calls_per_day');
$storageLimit = $license->getEntitlement('storage_gb');
$teamMembersLimit = $license->getEntitlement('team_members');

// Check against limits
$currentApiCalls = Cache::get("api_calls:{$license->id}:" . today()->format('Y-m-d'), 0);

if ($apiCallsLimit !== -1 && $currentApiCalls >= $apiCallsLimit) {
    return response()->json([
        'error' => 'API call limit exceeded',
        'limit' => $apiCallsLimit,
        'reset_at' => now()->endOfDay(),
    ], 429);
}

// Track usage
Cache::increment("api_calls:{$license->id}:" . today()->format('Y-m-d'));

Dynamic feature flags

class FeatureService
{
    private $license;
    
    public function __construct(License $license)
    {
        $this->license = $license;
    }
    
    public function enabled(string $feature): bool
    {
        return $this->license->hasFeature($feature);
    }
    
    public function when(string $feature, callable $callback, callable $default = null)
    {
        if ($this->enabled($feature)) {
            return $callback();
        }
        
        return $default ? $default() : null;
    }
    
    public function unless(string $feature, callable $callback)
    {
        if (!$this->enabled($feature)) {
            return $callback();
        }
    }
}

// Usage
$features = new FeatureService($license);

$features->when('export_pdf', function () {
    // Enable PDF export
});

$features->unless('watermark_disabled', function () {
    // Add watermark to exports
});

Renewing licenses

Simple renewal

// Extend license by one year
$license->renew(now()->addYear());

// Extend from current expiration
$newExpiration = $license->expires_at->addYear();
$license->renew($newExpiration);

// Renewal with payment information
$license->renew(now()->addYear(), [
    'amount_cents' => 9900,
    'currency' => 'USD',
    'notes' => 'Annual renewal - Professional Plan',
]);

Renewal workflow

public function processRenewal(Request $request, License $license)
{
    // Check if renewable
    if (!$license->status->canRenew()) {
        return back()->with('error', 'License cannot be renewed');
    }
    
    // Process payment (simplified)
    $payment = $this->processPayment($request, $license);
    
    if (!$payment->successful) {
        return back()->with('error', 'Payment failed');
    }
    
    // Calculate new expiration
    $currentExpiration = $license->expires_at ?? now();
    $newExpiration = $currentExpiration->addYear();
    
    // Renew the license
    $license->renew($newExpiration, [
        'amount_cents' => $payment->amount_cents,
        'currency' => $payment->currency,
        'notes' => "Payment ID: {$payment->id}",
    ]);
    
    // Send confirmation
    Mail::to($license->licensable)->send(
        new LicenseRenewedMail($license)
    );
    
    return redirect()->route('license.show', $license)
        ->with('success', 'License renewed successfully');
}

Auto-renewal

// Schedule in app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
    $schedule->job(new ProcessAutoRenewals)->daily();
}

// Job implementation
class ProcessAutoRenewals implements ShouldQueue
{
    public function handle()
    {
        $licenses = License::where('status', LicenseStatus::Active)
            ->where('expires_at', '<=', now()->addDays(7))
            ->whereJsonContains('meta->auto_renew', true)
            ->get();
        
        foreach ($licenses as $license) {
            try {
                // Process payment
                $payment = $this->chargeStoredPaymentMethod($license);
                
                // Renew license
                $license->renew(
                    $license->expires_at->addYear(),
                    ['payment_id' => $payment->id]
                );
                
                // Notify customer
                event(new LicenseAutoRenewed($license));
                
            } catch (\Exception $e) {
                // Handle failure
                event(new AutoRenewalFailed($license, $e->getMessage()));
            }
        }
    }
}

Handling expiration

Grace period management

// Check and transition to grace period
if ($license->status === LicenseStatus::Active && $license->isExpired()) {
    $license->transitionToGrace();
    
    // Notify user
    Mail::to($license->licensable)->send(
        new LicenseInGracePeriodMail($license)
    );
}

// Check if grace period expired
if ($license->gracePeriodExpired()) {
    $license->transitionToExpired();
    
    // Disable access
    $license->usages()->update(['status' => UsageStatus::Revoked]);
}

Expiration warnings

// Schedule expiration warnings
class SendExpirationWarnings implements ShouldQueue
{
    public function handle()
    {
        $warningDays = [30, 14, 7, 1];
        
        foreach ($warningDays as $days) {
            $licenses = License::where('status', LicenseStatus::Active)
                ->whereDate('expires_at', now()->addDays($days))
                ->get();
            
            foreach ($licenses as $license) {
                event(new LicenseExpiringSoon($license, $days));
            }
        }
    }
}

Common patterns

License key generator

class LicenseKeyGenerator
{
    public static function generate(): string
    {
        // Format: XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX (128-bit entropy)
        $segments = [];
        
        for ($i = 0; $i < 4; $i++) {
            $segments[] = strtoupper(bin2hex(random_bytes(4)));
        }
        
        return implode('-', $segments);
    }
    
    public static function generateBatch(int $count): array
    {
        $keys = [];
        
        while (count($keys) < $count) {
            $key = self::generate();
            
            // Ensure uniqueness
            if (!License::findByKey($key)) {
                $keys[] = $key;
            }
        }
        
        return $keys;
    }
}

License repository pattern

class LicenseRepository
{
    public function findActive(Model $licensable): ?License
    {
        return License::where('licensable_type', get_class($licensable))
            ->where('licensable_id', $licensable->id)
            ->whereIn('status', [LicenseStatus::Active, LicenseStatus::Grace])
            ->first();
    }
    
    public function findByKeyForUser(string $key, User $user): ?License
    {
        $license = License::findByKey($key);
        
        if (!$license) {
            return null;
        }
        
        // Verify ownership
        if ($license->licensable_type === User::class 
            && $license->licensable_id === $user->id) {
            return $license;
        }
        
        return null;
    }
    
    public function getExpiringLicenses(int $days = 30): Collection
    {
        return License::where('status', LicenseStatus::Active)
            ->whereBetween('expires_at', [now(), now()->addDays($days)])
            ->orderBy('expires_at')
            ->get();
    }
}

License validation service

class LicenseValidationService
{
    public function validate(License $license): ValidationResult
    {
        $result = new ValidationResult();
        
        // Check status
        if (!$license->isUsable()) {
            $result->addError('License is not active');
        }
        
        // Check expiration
        if ($license->isExpired()) {
            if ($license->isInGracePeriod()) {
                $result->addWarning('License is in grace period');
            } else {
                $result->addError('License has expired');
            }
        }
        
        // Check usage limits
        if (!$license->hasAvailableSeats()) {
            $result->addWarning('No available seats');
        }
        
        // Check device registration
        $currentDevice = $this->getCurrentDeviceFingerprint();
        $usage = $license->usages()
            ->where('usage_fingerprint', $currentDevice)
            ->first();
        
        if (!$usage || !$usage->isActive()) {
            $result->addError('Device not registered');
        }
        
        return $result;
    }
}

Next steps


← Docs index