diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/LicenseAdminController.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/LicenseAdminController.java index 039b7e80..3c5152c3 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/LicenseAdminController.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/LicenseAdminController.java @@ -1,54 +1,71 @@ 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.LicenseInfo; -import com.cameleer.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 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; +/** + * 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.). + * + *

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.

+ */ @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; - private final String tenantId; + private final LicenseService licenseService; + private final LicenseGate gate; + private final LicenseRepository repo; - public LicenseAdminController(LicenseGate licenseGate, - @Value("${cameleer.server.license.publickey:}") String licensePublicKey, - @Value("${cameleer.server.tenant.id:default}") String tenantId) { - this.licenseGate = licenseGate; - this.licensePublicKey = licensePublicKey; - this.tenantId = tenantId; + public LicenseAdminController(LicenseService svc, LicenseGate gate, LicenseRepository repo) { + this.licenseService = svc; + this.gate = gate; + this.repo = repo; } @GetMapping - @Operation(summary = "Get current license info") - public ResponseEntity getCurrent() { - return ResponseEntity.ok(licenseGate.getCurrent()); + @Operation(summary = "Get current license state, invalid reason, and parsed envelope") + public ResponseEntity> getCurrent() { + Map 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 - @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")); - } + @Operation(summary = "Install or replace the license token at runtime") + public ResponseEntity update(@RequestBody UpdateLicenseRequest request, Authentication auth) { + String userId = auth == null ? "system" : auth.getName().replaceFirst("^user:", ""); try { - LicenseValidator validator = new LicenseValidator(licensePublicKey, tenantId); - LicenseInfo info = validator.validate(request.token()); - licenseGate.load(info); - return ResponseEntity.ok(info); + LicenseInfo info = licenseService.install(request.token(), userId, "api"); + return ResponseEntity.ok(Map.of( + "state", gate.getState().name(), + "envelope", info)); } catch (Exception e) { return ResponseEntity.badRequest().body(Map.of("error", e.getMessage())); }