From 67ca1e726faecb27dbcb83ded8adadc637627613 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 7 Apr 2026 23:12:03 +0200 Subject: [PATCH] feat: add license admin API for runtime license updates - GET /api/v1/admin/license returns current license info - POST /api/v1/admin/license validates and loads new license token - Requires ADMIN role, validates Ed25519 signature before applying - OpenAPI annotations for Swagger documentation --- .../controller/LicenseAdminController.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/LicenseAdminController.java diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/LicenseAdminController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/LicenseAdminController.java new file mode 100644 index 00000000..84c5b810 --- /dev/null +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/LicenseAdminController.java @@ -0,0 +1,53 @@ +package com.cameleer3.server.app.controller; + +import com.cameleer3.server.core.license.LicenseGate; +import com.cameleer3.server.core.license.LicenseInfo; +import com.cameleer3.server.core.license.LicenseValidator; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/admin/license") +@PreAuthorize("hasRole('ADMIN')") +@Tag(name = "License Admin", description = "License management") +public class LicenseAdminController { + + private final LicenseGate licenseGate; + private final String licensePublicKey; + + public LicenseAdminController(LicenseGate licenseGate, + @Value("${license.public-key:}") String licensePublicKey) { + this.licenseGate = licenseGate; + this.licensePublicKey = licensePublicKey; + } + + @GetMapping + @Operation(summary = "Get current license info") + public ResponseEntity getCurrent() { + return ResponseEntity.ok(licenseGate.getCurrent()); + } + + record UpdateLicenseRequest(String token) {} + + @PostMapping + @Operation(summary = "Update license token at runtime") + public ResponseEntity update(@RequestBody UpdateLicenseRequest request) { + if (licensePublicKey == null || licensePublicKey.isBlank()) { + return ResponseEntity.badRequest().body(Map.of("error", "No license public key configured")); + } + try { + LicenseValidator validator = new LicenseValidator(licensePublicKey); + LicenseInfo info = validator.validate(request.token()); + licenseGate.load(info); + return ResponseEntity.ok(info); + } catch (Exception e) { + return ResponseEntity.badRequest().body(Map.of("error", e.getMessage())); + } + } +}