where('purchase_id', $purchaseId)->exists()) { return; } $key = 'TNX-'.Str::upper(bin2hex(random_bytes(20))); DB::table('licenses')->insert(['user_id' => $userId, 'purchase_id' => $purchaseId, 'key_hash' => hash('sha256', $key), 'key_encrypted' => Crypt::encryptString($key), 'status' => 'active', 'device_limit' => 2, 'created_at' => now(), 'updated_at' => now()]); } public function activate(string $key, string $device): array { return DB::transaction(function () use ($key, $device) { $license = DB::table('licenses')->where('key_hash', hash('sha256', $key))->lockForUpdate()->first(); abort_unless($license && $license->status === 'active', 403, 'Invalid or revoked license.'); $deviceHash = hash('sha256', $device); $existing = DB::table('activations')->where('license_id', $license->id)->where('device_hash', $deviceHash)->first(); abort_if(! $existing && DB::table('activations')->where('license_id', $license->id)->count() >= $license->device_limit, 409, 'Device limit reached.'); $token = bin2hex(random_bytes(32)); DB::table('activations')->updateOrInsert(['license_id' => $license->id, 'device_hash' => $deviceHash], ['token_hash' => hash('sha256', $token), 'last_seen_at' => now(), 'created_at' => now(), 'updated_at' => now()]); return ['activation_token' => $token, 'valid' => true, 'check_again_in' => 86400]; }, 3); } public function check(string $token, bool $deactivate = false): array { return DB::transaction(function () use ($token, $deactivate) { $activation = DB::table('activations')->where('token_hash', hash('sha256', $token))->first(); abort_unless($activation, 403, 'Invalid activation.'); $license = DB::table('licenses')->where('id', $activation->license_id)->lockForUpdate()->first(); if ($deactivate) { DB::table('activations')->where('id', $activation->id)->delete(); return ['deactivated' => true]; } abort_unless($license->status === 'active', 403, 'License revoked.'); DB::table('activations')->where('id', $activation->id)->update(['last_seen_at' => now()]); return ['valid' => true, 'check_again_in' => 86400]; }, 3); } }