refactor(license): LicenseAdminController delegates to LicenseService

GET returns {state, invalidReason, envelope, lastValidatedAt}. POST
delegates to licenseService.install(token, userId, "api") so install
goes through audit + persistence + event publish. Removes the inline
LicenseValidator construction from the controller.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-26 15:34:07 +02:00
parent 340d954fed
commit 3f69e546e4

View File

@@ -1,54 +1,71 @@
package com.cameleer.server.app.controller; package com.cameleer.server.app.controller;
import com.cameleer.server.app.license.LicenseRepository;
import com.cameleer.server.app.license.LicenseService;
import com.cameleer.server.core.license.LicenseGate; import com.cameleer.server.core.license.LicenseGate;
import com.cameleer.server.core.license.LicenseInfo; import com.cameleer.server.core.license.LicenseInfo;
import com.cameleer.server.core.license.LicenseValidator;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
/**
* License management for ADMIN users. All mutation goes through {@link LicenseService} so that
* install / replace flows are uniformly audited, persisted, and published to listeners (retention
* policy, license metrics, etc.).
*
* <p>GET returns {@code {state, invalidReason, envelope, lastValidatedAt?}}. The raw JWT-style
* token is deliberately omitted from the response — only the parsed {@link LicenseInfo} is
* exposed.</p>
*/
@RestController @RestController
@RequestMapping("/api/v1/admin/license") @RequestMapping("/api/v1/admin/license")
@PreAuthorize("hasRole('ADMIN')") @PreAuthorize("hasRole('ADMIN')")
@Tag(name = "License Admin", description = "License management") @Tag(name = "License Admin", description = "License management")
public class LicenseAdminController { public class LicenseAdminController {
private final LicenseGate licenseGate; private final LicenseService licenseService;
private final String licensePublicKey; private final LicenseGate gate;
private final String tenantId; private final LicenseRepository repo;
public LicenseAdminController(LicenseGate licenseGate, public LicenseAdminController(LicenseService svc, LicenseGate gate, LicenseRepository repo) {
@Value("${cameleer.server.license.publickey:}") String licensePublicKey, this.licenseService = svc;
@Value("${cameleer.server.tenant.id:default}") String tenantId) { this.gate = gate;
this.licenseGate = licenseGate; this.repo = repo;
this.licensePublicKey = licensePublicKey;
this.tenantId = tenantId;
} }
@GetMapping @GetMapping
@Operation(summary = "Get current license info") @Operation(summary = "Get current license state, invalid reason, and parsed envelope")
public ResponseEntity<LicenseInfo> getCurrent() { public ResponseEntity<Map<String, Object>> getCurrent() {
return ResponseEntity.ok(licenseGate.getCurrent()); Map<String, Object> body = new LinkedHashMap<>();
body.put("state", gate.getState().name());
body.put("invalidReason", gate.getInvalidReason());
body.put("envelope", gate.getCurrent()); // null when ABSENT/INVALID; raw token deliberately omitted
repo.findByTenantId(licenseService.getTenantId()).ifPresent(rec ->
body.put("lastValidatedAt", rec.lastValidatedAt().toString()));
return ResponseEntity.ok(body);
} }
record UpdateLicenseRequest(String token) {} public record UpdateLicenseRequest(String token) {}
@PostMapping @PostMapping
@Operation(summary = "Update license token at runtime") @Operation(summary = "Install or replace the license token at runtime")
public ResponseEntity<?> update(@RequestBody UpdateLicenseRequest request) { public ResponseEntity<?> update(@RequestBody UpdateLicenseRequest request, Authentication auth) {
if (licensePublicKey == null || licensePublicKey.isBlank()) { String userId = auth == null ? "system" : auth.getName().replaceFirst("^user:", "");
return ResponseEntity.badRequest().body(Map.of("error", "No license public key configured"));
}
try { try {
LicenseValidator validator = new LicenseValidator(licensePublicKey, tenantId); LicenseInfo info = licenseService.install(request.token(), userId, "api");
LicenseInfo info = validator.validate(request.token()); return ResponseEntity.ok(Map.of(
licenseGate.load(info); "state", gate.getState().name(),
return ResponseEntity.ok(info); "envelope", info));
} catch (Exception e) { } catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage())); return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
} }