2026-05-25 22:45:23 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
|
|
|
|
|
|
use App\Models\License;
|
|
|
|
|
use App\Support\ApiDataTransformer;
|
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
|
use Inertia\Inertia;
|
|
|
|
|
|
|
|
|
|
class LicenseController extends Controller
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* Display a listing of the resource.
|
|
|
|
|
*
|
|
|
|
|
* Retrieves all licenses from the database, ordered by expiration date.
|
|
|
|
|
*
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
public function index()
|
|
|
|
|
{
|
|
|
|
|
$licenses = License::orderBy('expirationDate', 'asc')->get();
|
|
|
|
|
return ApiDataTransformer::snakeToCamel($licenses->toArray());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Display the specified resource.
|
|
|
|
|
*
|
|
|
|
|
* Retrieves a single license by its ID.
|
|
|
|
|
*
|
|
|
|
|
* @param int $id The ID of the license to retrieve
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
public function single($id)
|
|
|
|
|
{
|
|
|
|
|
$license = License::findOrFail($id);
|
|
|
|
|
return ApiDataTransformer::snakeToCamel($license->toArray());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Display the specified resource.
|
|
|
|
|
*
|
|
|
|
|
* Renders the Inertia view for licenses
|
|
|
|
|
*
|
|
|
|
|
* @return \Inertia\Response
|
|
|
|
|
*/
|
|
|
|
|
public function show()
|
|
|
|
|
{
|
|
|
|
|
return Inertia::render(
|
|
|
|
|
'Licenses',
|
|
|
|
|
['licenseData' => $this->index()]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the directory path for license keys.
|
|
|
|
|
*
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
protected function licenseKeyDirectory(): string
|
|
|
|
|
{
|
|
|
|
|
return 'license-keys';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the path for the private key file.
|
|
|
|
|
*
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
protected function privateKeyPath(): string
|
|
|
|
|
{
|
|
|
|
|
return $this->licenseKeyDirectory() . '/private.pem';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the path for the public key file.
|
|
|
|
|
*
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
protected function publicKeyPath(): string
|
|
|
|
|
{
|
|
|
|
|
return $this->licenseKeyDirectory() . '/public.pem';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the application's key pair.
|
|
|
|
|
*
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
protected function getApplicationKeyPair(): array
|
|
|
|
|
{
|
|
|
|
|
if (!Storage::disk('local')->exists($this->privateKeyPath()) || !Storage::disk('local')->exists($this->publicKeyPath())) {
|
|
|
|
|
abort(500, 'Application license key pair is missing. Generate it with `php artisan license:generate-keys`.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'privateKey' => Storage::disk('local')->get($this->privateKeyPath()),
|
|
|
|
|
'publicKey' => Storage::disk('local')->get($this->publicKeyPath()),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generate a signature for the license data.
|
|
|
|
|
*
|
|
|
|
|
* Uses the private key to sign the data.
|
|
|
|
|
*
|
|
|
|
|
* @param array $licenseData The license data to sign
|
|
|
|
|
* @param string $privateKey The private key to use for signing
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
2026-07-10 16:55:48 +02:00
|
|
|
protected function sign($licenseData, $privateKey)
|
2026-05-25 22:45:23 +02:00
|
|
|
{
|
|
|
|
|
$licenseJson = json_encode($licenseData);
|
|
|
|
|
if ($licenseJson === false) throw new \Exception('Fehler beim JSON-Encode der Lizenzdaten: ' . json_last_error_msg());
|
|
|
|
|
|
|
|
|
|
$success = openssl_sign($licenseJson, $signature, $privateKey, OPENSSL_ALGO_SHA256);
|
|
|
|
|
if ($success === false) throw new \Exception('Fehler beim Signieren der Lizenzdaten: ' . openssl_error_string());
|
|
|
|
|
|
|
|
|
|
return base64_encode($signature);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Store a newly created resource in storage.
|
|
|
|
|
*
|
|
|
|
|
* Validates the request data, generates a signature, and creates a new license.
|
|
|
|
|
*
|
|
|
|
|
* @param \Illuminate\Http\Request $request The HTTP request containing license data
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
public function store(Request $request)
|
|
|
|
|
{
|
|
|
|
|
// Validate the request data
|
|
|
|
|
$validatedData = $request->validate([
|
|
|
|
|
'name' => 'required|string',
|
|
|
|
|
'email' => 'required|email',
|
|
|
|
|
'product' => 'required|string',
|
|
|
|
|
'version' => 'required|string',
|
|
|
|
|
'isPerpetual' => 'required|boolean',
|
|
|
|
|
'expirationDate' => 'nullable|date',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Convert camelCase to snake_case
|
|
|
|
|
$snakeCaseData = ApiDataTransformer::camelToSnake($validatedData);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$keyPair = $this->getApplicationKeyPair();
|
|
|
|
|
|
|
|
|
|
$licenseData = [
|
|
|
|
|
'name' => $snakeCaseData['name'],
|
|
|
|
|
'email' => $snakeCaseData['email'],
|
|
|
|
|
'product' => $snakeCaseData['product'],
|
|
|
|
|
'version' => $snakeCaseData['version'],
|
|
|
|
|
'is_perpetual' => $snakeCaseData['is_perpetual'],
|
|
|
|
|
'expiration_date' => $snakeCaseData['expiration_date'],
|
|
|
|
|
];
|
|
|
|
|
|
2026-07-10 16:55:48 +02:00
|
|
|
$signature = $this->sign($licenseData, $keyPair['privateKey']);
|
2026-05-25 22:45:23 +02:00
|
|
|
|
|
|
|
|
$license = License::create([
|
|
|
|
|
'name' => $snakeCaseData['name'],
|
|
|
|
|
'email' => $snakeCaseData['email'],
|
|
|
|
|
'product' => $snakeCaseData['product'],
|
|
|
|
|
'version' => $snakeCaseData['version'],
|
|
|
|
|
'is_perpetual' => $snakeCaseData['is_perpetual'],
|
|
|
|
|
'expiration_date' => $snakeCaseData['expiration_date'],
|
|
|
|
|
'signature' => $signature,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return ApiDataTransformer::snakeToCamel($license->toArray());
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-10 16:55:48 +02:00
|
|
|
|
2026-05-25 22:45:23 +02:00
|
|
|
/**
|
|
|
|
|
* Validate the license signature and expiration date.
|
|
|
|
|
*
|
2026-07-10 16:55:48 +02:00
|
|
|
* @param \Illuminate\Http\Request $request The HTTP request containing the license data
|
2026-05-25 22:45:23 +02:00
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
public function validate(Request $request)
|
|
|
|
|
{
|
2026-07-10 16:55:48 +02:00
|
|
|
$validatedData = $request->validate([
|
|
|
|
|
'name' => 'required|string',
|
|
|
|
|
'email' => 'required|email',
|
|
|
|
|
'product' => 'required|string',
|
|
|
|
|
'version' => 'required|string',
|
|
|
|
|
'isPerpetual' => 'required|boolean',
|
|
|
|
|
'expirationDate' => 'nullable|date',
|
|
|
|
|
'signature' => 'required|string',
|
|
|
|
|
]);
|
2026-05-25 22:45:23 +02:00
|
|
|
|
2026-07-10 16:55:48 +02:00
|
|
|
return $this->isValid(ApiDataTransformer::camelToSnake($validatedData));
|
|
|
|
|
}
|
2026-05-25 22:45:23 +02:00
|
|
|
|
|
|
|
|
|
2026-07-10 16:55:48 +02:00
|
|
|
/**
|
|
|
|
|
* Validate the license signature and expiration date.
|
|
|
|
|
*
|
|
|
|
|
* The validation logic should be portable to other languages.
|
|
|
|
|
*
|
|
|
|
|
* @param array $licenseData The license data to validate
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
protected function isValid(array $licenseData)
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
$publicKey = Storage::disk('local')->get($this->publicKeyPath());
|
|
|
|
|
|
|
|
|
|
|
2026-05-25 22:45:23 +02:00
|
|
|
// Convert the license data to JSON
|
|
|
|
|
$licenseJson = json_encode($licenseData);
|
2026-07-10 16:55:48 +02:00
|
|
|
|
|
|
|
|
|
2026-05-25 22:45:23 +02:00
|
|
|
// Verify the signature
|
2026-07-10 16:55:48 +02:00
|
|
|
$signature = base64_decode($licenseData['signature']);
|
2026-05-25 22:45:23 +02:00
|
|
|
$isSignatureValid = openssl_verify($licenseJson, $signature, $publicKey, OPENSSL_ALGO_SHA256);
|
|
|
|
|
|
|
|
|
|
// Check the expiration date if the license is not perpetual
|
|
|
|
|
$isExpired = false;
|
2026-07-10 16:55:48 +02:00
|
|
|
if (!$licenseData['is_perpetual'] && $licenseData['expiration_date']) {
|
|
|
|
|
$isExpired = now()->gt($licenseData['expiration_date']);
|
2026-05-25 22:45:23 +02:00
|
|
|
}
|
|
|
|
|
|
2026-07-10 16:55:48 +02:00
|
|
|
// Return the validation info
|
2026-05-25 22:45:23 +02:00
|
|
|
return [
|
2026-07-10 16:55:48 +02:00
|
|
|
'isValid' => $isSignatureValid === 1 && !$isExpired,
|
|
|
|
|
'isSignatureValid' => $isSignatureValid === 1,
|
|
|
|
|
'isExpired' => $isExpired,
|
2026-05-25 22:45:23 +02:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Download the license as a JSON file.
|
|
|
|
|
*
|
|
|
|
|
* @param int $id The ID of the license to download
|
|
|
|
|
* @return \Symfony\Component\HttpFoundation\StreamedResponse
|
|
|
|
|
*/
|
|
|
|
|
public function download(int $id)
|
|
|
|
|
{
|
|
|
|
|
// Find the license
|
|
|
|
|
$license = License::findOrFail($id);
|
|
|
|
|
|
|
|
|
|
// Prepare the license data for download
|
|
|
|
|
$licenseData = [
|
|
|
|
|
'name' => $license->name,
|
|
|
|
|
'email' => $license->email,
|
|
|
|
|
'product' => $license->product,
|
|
|
|
|
'version' => $license->version,
|
|
|
|
|
'is_perpetual' => $license->is_perpetual,
|
|
|
|
|
'expiration_date' => $license->expiration_date,
|
|
|
|
|
'signature' => $license->signature,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Convert the license data to JSON
|
|
|
|
|
$licenseJson = json_encode($licenseData, JSON_PRETTY_PRINT);
|
|
|
|
|
|
|
|
|
|
// Create a response with the JSON file
|
|
|
|
|
$response = response($licenseJson, 200, [
|
|
|
|
|
'Content-Type' => 'application/json',
|
2026-07-10 16:55:48 +02:00
|
|
|
'Content-Disposition' => 'attachment; filename="' . $licenseData['product'] . '.license"',
|
2026-05-25 22:45:23 +02:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return $response;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update the specified resource in storage.
|
|
|
|
|
*
|
|
|
|
|
* Validates the request data, generates a new signature, and updates the license.
|
|
|
|
|
*
|
|
|
|
|
* @param \Illuminate\Http\Request $request The HTTP request containing updated license data
|
|
|
|
|
* @param int $id The ID of the license to update
|
|
|
|
|
* @return array|\Illuminate\Http\JsonResponse
|
|
|
|
|
*/
|
|
|
|
|
public function update(Request $request, int $id)
|
|
|
|
|
{
|
|
|
|
|
// Validate the request data
|
|
|
|
|
$validatedData = $request->validate([
|
|
|
|
|
'name' => 'required|string',
|
|
|
|
|
'email' => 'required|email',
|
|
|
|
|
'product' => 'required|string',
|
|
|
|
|
'version' => 'required|string',
|
|
|
|
|
'isPerpetual' => 'required|boolean',
|
|
|
|
|
'expirationDate' => 'nullable|date',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Convert camelCase to snake_case
|
|
|
|
|
$snakeCaseData = ApiDataTransformer::camelToSnake($validatedData);
|
|
|
|
|
|
|
|
|
|
DB::beginTransaction();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Find license
|
|
|
|
|
$license = License::findOrFail($id);
|
|
|
|
|
|
|
|
|
|
// Retrieve the private key for the license
|
|
|
|
|
$privateKey = $this->getApplicationKeyPair()['privateKey'];
|
|
|
|
|
|
|
|
|
|
// Generate the license data
|
|
|
|
|
$licenseData = [
|
|
|
|
|
'name' => $snakeCaseData['name'],
|
|
|
|
|
'email' => $snakeCaseData['email'],
|
|
|
|
|
'product' => $snakeCaseData['product'],
|
|
|
|
|
'version' => $snakeCaseData['version'],
|
|
|
|
|
'is_perpetual' => $snakeCaseData['is_perpetual'],
|
|
|
|
|
'expiration_date' => $snakeCaseData['expiration_date'],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Generate the signature
|
2026-07-10 16:55:48 +02:00
|
|
|
$signature = $this->sign($licenseData, $privateKey);
|
2026-05-25 22:45:23 +02:00
|
|
|
|
|
|
|
|
// Update the license
|
|
|
|
|
$license->update([
|
|
|
|
|
'name' => $snakeCaseData['name'],
|
|
|
|
|
'email' => $snakeCaseData['email'],
|
|
|
|
|
'product' => $snakeCaseData['product'],
|
|
|
|
|
'version' => $snakeCaseData['version'],
|
|
|
|
|
'is_perpetual' => $snakeCaseData['is_perpetual'],
|
|
|
|
|
'expiration_date' => $snakeCaseData['expiration_date'],
|
|
|
|
|
'signature' => $signature,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
DB::commit();
|
|
|
|
|
|
|
|
|
|
// Return the updated license
|
|
|
|
|
return ApiDataTransformer::snakeToCamel($license->toArray());
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
DB::rollBack();
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => 'Lizenz konnte nicht aktualisiert werden',
|
|
|
|
|
'error' => $e->getMessage()
|
|
|
|
|
], 500);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove the specified resource from storage.
|
|
|
|
|
*
|
|
|
|
|
* Deletes the specified license from the database.
|
|
|
|
|
*
|
|
|
|
|
* @param int $id The ID of the license to delete
|
|
|
|
|
* @return \Illuminate\Http\JsonResponse
|
|
|
|
|
*/
|
|
|
|
|
public function destroy(int $id)
|
|
|
|
|
{
|
|
|
|
|
$license = License::findOrFail($id);
|
|
|
|
|
$license->delete();
|
|
|
|
|
return response()->json([
|
|
|
|
|
'message' => 'Lizenz erfolgreich gelöscht'
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|