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 */ protected function generateSignature($licenseData, $privateKey) { $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'], ]; $signature = $this->generateSignature($licenseData, $keyPair['privateKey']); $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()); } /** * Validate the license signature and expiration date. * * The validation logic should be portable to other languages. * * @param \Illuminate\Http\Request $request The HTTP request containing the license ID * @return array */ public function validate(Request $request) { // Get the license ID from the request $licenseId = $request->input('license_id'); // Find the license $license = License::findOrFail($licenseId); $publicKey = Storage::disk('local')->get($this->publicKeyPath()); $licenseData = [ 'name' => $license->name, 'email' => $license->email, 'product' => $license->product, 'version' => $license->version, 'is_perpetual' => $license->is_perpetual, 'expiration_date' => $license->expiration_date, ]; // Convert the license data to JSON $licenseJson = json_encode($licenseData); // Verify the signature $signature = base64_decode($license->signature); $isSignatureValid = openssl_verify($licenseJson, $signature, $publicKey, OPENSSL_ALGO_SHA256); // Check the expiration date if the license is not perpetual $isExpired = false; if (!$license->is_perpetual && $license->expiration_date) { $isExpired = now()->gt($license->expiration_date); } // Return the validation result return [ 'is_valid' => $isSignatureValid === 1 && !$isExpired, 'is_signature_valid' => $isSignatureValid === 1, 'is_expired' => $isExpired, ]; } /** * 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', 'Content-Disposition' => 'attachment; filename="' . $licenseData['product'] . '.json"', ]); 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 $signature = $this->generateSignature($licenseData, $privateKey); // 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' ]); } }