paddel setup new verifier
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
@@ -17,37 +18,147 @@ final class LicenseActivationController extends Controller
|
||||
public function activate(Request $request, LicenseSigner $signer): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'licenseKey'=>['required','string','max:128'],
|
||||
'installationID'=>['required','uuid'],
|
||||
'appVersion'=>['required','string','max:32'],
|
||||
'platform'=>['required','string','max:32'],
|
||||
'licenseKey' => ['required', 'string', 'max:128'],
|
||||
'installationID' => ['required', 'uuid'],
|
||||
'appVersion' => ['required', 'string', 'max:32'],
|
||||
'platform' => ['required', 'string', 'max:32'],
|
||||
]);
|
||||
|
||||
return DB::transaction(function () use ($data,$signer) {
|
||||
$license = License::where('license_key_hash', LicenseKey::hash($data['licenseKey']))->lockForUpdate()->first();
|
||||
if (!$license) throw ValidationException::withMessages(['licenseKey'=>'The licence key is not valid.']);
|
||||
if (!$license->is_usable) return response()->json(['message'=>'This licence is expired or has been revoked.'], 403);
|
||||
return DB::transaction(function () use ($data, $signer) {
|
||||
$license = License::where('license_key_hash', LicenseKey::hash($data['licenseKey']))
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
$activation = LicenseActivation::where('license_id',$license->id)->where('installation_id',strtolower($data['installationID']))->first();
|
||||
if ($activation && $activation->deactivated_at === null) {
|
||||
$activation->update(['last_seen_at'=>now(),'platform'=>$data['platform'],'app_version'=>$data['appVersion']]);
|
||||
} else {
|
||||
$activeCount = LicenseActivation::where('license_id',$license->id)->whereNull('deactivated_at')->count();
|
||||
if ($activeCount >= $license->activation_limit) return response()->json(['message'=>'This licence has reached its activation limit. Deactivate another Mac first.'], 409);
|
||||
if ($activation) $activation->update(['deactivated_at'=>null,'activated_at'=>now(),'last_seen_at'=>now(),'platform'=>$data['platform'],'app_version'=>$data['appVersion']]);
|
||||
else LicenseActivation::create(['license_id'=>$license->id,'installation_id'=>strtolower($data['installationID']),'platform'=>$data['platform'],'app_version'=>$data['appVersion'],'activated_at'=>now(),'last_seen_at'=>now()]);
|
||||
if (! $license) {
|
||||
throw ValidationException::withMessages(['licenseKey' => 'The licence key is not valid.']);
|
||||
}
|
||||
return response()->json(['licenseToken'=>$signer->sign($license, $data['installationID'])]);
|
||||
|
||||
if (! $license->is_usable) {
|
||||
return response()->json(['message' => 'This licence is expired or has been revoked.'], 403);
|
||||
}
|
||||
|
||||
$installationID = strtolower($data['installationID']);
|
||||
$activation = LicenseActivation::where('license_id', $license->id)
|
||||
->where('installation_id', $installationID)
|
||||
->first();
|
||||
|
||||
if ($activation && $activation->deactivated_at === null) {
|
||||
$activation->update([
|
||||
'last_seen_at' => now(),
|
||||
'platform' => $data['platform'],
|
||||
'app_version' => $data['appVersion'],
|
||||
]);
|
||||
} else {
|
||||
$activeCount = LicenseActivation::where('license_id', $license->id)
|
||||
->whereNull('deactivated_at')
|
||||
->count();
|
||||
|
||||
if ($activeCount >= $license->activation_limit) {
|
||||
return response()->json(['message' => 'This licence has reached its activation limit. Deactivate another Mac first.'], 409);
|
||||
}
|
||||
|
||||
if ($activation) {
|
||||
$activation->update([
|
||||
'deactivated_at' => null,
|
||||
'activated_at' => now(),
|
||||
'last_seen_at' => now(),
|
||||
'platform' => $data['platform'],
|
||||
'app_version' => $data['appVersion'],
|
||||
]);
|
||||
} else {
|
||||
LicenseActivation::create([
|
||||
'license_id' => $license->id,
|
||||
'installation_id' => $installationID,
|
||||
'platform' => $data['platform'],
|
||||
'app_version' => $data['appVersion'],
|
||||
'activated_at' => now(),
|
||||
'last_seen_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'licenseToken' => $signer->sign($license, $data['installationID']),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function validateInstallation(
|
||||
Request $request,
|
||||
LicenseTokenVerifier $verifier,
|
||||
LicenseSigner $signer,
|
||||
): JsonResponse {
|
||||
$data = $request->validate([
|
||||
'licenseToken' => ['required', 'string', 'max:8192'],
|
||||
'installationID' => ['required', 'uuid'],
|
||||
'appVersion' => ['required', 'string', 'max:32'],
|
||||
'platform' => ['required', 'string', 'max:32'],
|
||||
]);
|
||||
|
||||
try {
|
||||
$payload = $verifier->verify($data['licenseToken']);
|
||||
} catch (\Throwable) {
|
||||
return response()->json(['message' => 'The licence token is invalid.'], 400);
|
||||
}
|
||||
|
||||
if (strcasecmp((string) ($payload['installationID'] ?? ''), $data['installationID']) !== 0) {
|
||||
return response()->json(['message' => 'The installation does not match this licence token.'], 400);
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($data, $payload, $signer) {
|
||||
$license = License::whereKey((string) ($payload['licenseID'] ?? ''))
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if (! $license || ! $license->is_usable) {
|
||||
return response()->json(['message' => 'This licence is expired or has been revoked.'], 403);
|
||||
}
|
||||
|
||||
$activation = LicenseActivation::where('license_id', $license->id)
|
||||
->where('installation_id', strtolower($data['installationID']))
|
||||
->first();
|
||||
|
||||
if (! $activation || $activation->deactivated_at !== null) {
|
||||
return response()->json(['message' => 'This Mac has been deactivated from the licence portal.'], 403);
|
||||
}
|
||||
|
||||
$activation->update([
|
||||
'last_seen_at' => now(),
|
||||
'platform' => $data['platform'],
|
||||
'app_version' => $data['appVersion'],
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'active' => true,
|
||||
// Refreshing the signed token also applies subscription edition/expiry changes
|
||||
// without requiring the user to reactivate manually.
|
||||
'licenseToken' => $signer->sign($license, $data['installationID']),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function deactivate(Request $request, LicenseTokenVerifier $verifier): JsonResponse
|
||||
{
|
||||
$data = $request->validate(['licenseToken'=>['required','string','max:8192'],'installationID'=>['required','uuid']]);
|
||||
try { $payload = $verifier->verify($data['licenseToken']); }
|
||||
catch (\Throwable) { return response()->json(['message'=>'The licence token is invalid.'], 400); }
|
||||
if (strcasecmp((string)($payload['installationID'] ?? ''), $data['installationID']) !== 0) return response()->json(['message'=>'The installation does not match this licence token.'], 400);
|
||||
LicenseActivation::where('license_id',(string)($payload['licenseID'] ?? ''))->where('installation_id',strtolower($data['installationID']))->whereNull('deactivated_at')->update(['deactivated_at'=>now(),'last_seen_at'=>now()]);
|
||||
return response()->json((object)[]);
|
||||
$data = $request->validate([
|
||||
'licenseToken' => ['required', 'string', 'max:8192'],
|
||||
'installationID' => ['required', 'uuid'],
|
||||
]);
|
||||
|
||||
try {
|
||||
$payload = $verifier->verify($data['licenseToken']);
|
||||
} catch (\Throwable) {
|
||||
return response()->json(['message' => 'The licence token is invalid.'], 400);
|
||||
}
|
||||
|
||||
if (strcasecmp((string) ($payload['installationID'] ?? ''), $data['installationID']) !== 0) {
|
||||
return response()->json(['message' => 'The installation does not match this licence token.'], 400);
|
||||
}
|
||||
|
||||
LicenseActivation::where('license_id', (string) ($payload['licenseID'] ?? ''))
|
||||
->where('installation_id', strtolower($data['installationID']))
|
||||
->whereNull('deactivated_at')
|
||||
->update(['deactivated_at' => now(), 'last_seen_at' => now()]);
|
||||
|
||||
return response()->json((object) []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::prefix('v1')->group(function () {
|
||||
Route::post('/activate', [LicenseActivationController::class, 'activate'])->middleware('throttle:license-activation');
|
||||
Route::post('/validate', [LicenseActivationController::class, 'validateInstallation'])->middleware('throttle:license-activation');
|
||||
Route::post('/deactivate', [LicenseActivationController::class, 'deactivate'])->middleware('throttle:license-activation');
|
||||
Route::post('/webhooks/{provider}', PaymentWebhookController::class)->whereIn('provider', ['paddle','lemon-squeezy']);
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\LicenseProduct;
|
||||
use App\Models\LicenseActivation;
|
||||
use App\Services\Licensing\LicenseIssuer;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
@@ -18,4 +19,65 @@ class LicenseActivationTest extends TestCase
|
||||
$this->postJson('/api/v1/activate',$one)->assertOk();
|
||||
$this->postJson('/api/v1/activate',array_merge($one,['installationID'=>'22222222-2222-4222-8222-222222222222']))->assertStatus(409);
|
||||
}
|
||||
public function test_validation_refreshes_an_active_installation_without_calling_paddle(): void
|
||||
{
|
||||
$product = LicenseProduct::create([
|
||||
'code' => 'validate-active',
|
||||
'name' => 'Validate Active',
|
||||
'edition' => 'Professional',
|
||||
'activation_limit' => 1,
|
||||
'max_major_version' => 1,
|
||||
'active' => true,
|
||||
]);
|
||||
[, $key] = app(LicenseIssuer::class)->issue($product, 'Test', 'test@example.com');
|
||||
$request = [
|
||||
'licenseKey' => $key,
|
||||
'installationID' => '11111111-1111-4111-8111-111111111111',
|
||||
'appVersion' => '1.0.0',
|
||||
'platform' => 'macOS',
|
||||
];
|
||||
|
||||
$token = $this->postJson('/api/v1/activate', $request)->assertOk()->json('licenseToken');
|
||||
|
||||
$this->postJson('/api/v1/validate', [
|
||||
'licenseToken' => $token,
|
||||
'installationID' => $request['installationID'],
|
||||
'appVersion' => '1.0.1',
|
||||
'platform' => 'macOS',
|
||||
])->assertOk()->assertJson([
|
||||
'active' => true,
|
||||
])->assertJsonStructure(['licenseToken']);
|
||||
}
|
||||
|
||||
public function test_validation_rejects_an_installation_deactivated_from_the_portal(): void
|
||||
{
|
||||
$product = LicenseProduct::create([
|
||||
'code' => 'validate-deactivated',
|
||||
'name' => 'Validate Deactivated',
|
||||
'edition' => 'Personal',
|
||||
'activation_limit' => 1,
|
||||
'max_major_version' => 1,
|
||||
'active' => true,
|
||||
]);
|
||||
[, $key] = app(LicenseIssuer::class)->issue($product, 'Test', 'test@example.com');
|
||||
$request = [
|
||||
'licenseKey' => $key,
|
||||
'installationID' => '22222222-2222-4222-8222-222222222222',
|
||||
'appVersion' => '1.0.0',
|
||||
'platform' => 'macOS',
|
||||
];
|
||||
|
||||
$token = $this->postJson('/api/v1/activate', $request)->assertOk()->json('licenseToken');
|
||||
LicenseActivation::query()->where('installation_id', strtolower($request['installationID']))->update(['deactivated_at' => now()]);
|
||||
|
||||
$this->postJson('/api/v1/validate', [
|
||||
'licenseToken' => $token,
|
||||
'installationID' => $request['installationID'],
|
||||
'appVersion' => '1.0.0',
|
||||
'platform' => 'macOS',
|
||||
])->assertStatus(403)->assertJson([
|
||||
'message' => 'This Mac has been deactivated from the licence portal.',
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user