feat: tenant portal API (dashboard, license, OIDC, team, settings)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
|||||||
|
package net.siegeln.cameleer.saas.portal;
|
||||||
|
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PatchMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/tenant")
|
||||||
|
public class TenantPortalController {
|
||||||
|
|
||||||
|
private final TenantPortalService portalService;
|
||||||
|
|
||||||
|
public TenantPortalController(TenantPortalService portalService) {
|
||||||
|
this.portalService = portalService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Request bodies ---
|
||||||
|
|
||||||
|
public record InviteRequest(String email, String roleId) {}
|
||||||
|
|
||||||
|
public record RoleChangeRequest(String roleId) {}
|
||||||
|
|
||||||
|
// --- Endpoints ---
|
||||||
|
|
||||||
|
@GetMapping("/dashboard")
|
||||||
|
public ResponseEntity<TenantPortalService.DashboardData> getDashboard() {
|
||||||
|
return ResponseEntity.ok(portalService.getDashboard());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/license")
|
||||||
|
public ResponseEntity<TenantPortalService.LicenseData> getLicense() {
|
||||||
|
var license = portalService.getLicense();
|
||||||
|
if (license == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(license);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/oidc")
|
||||||
|
public ResponseEntity<Map<String, Object>> getOidcConfig() {
|
||||||
|
return ResponseEntity.ok(portalService.getOidcConfig());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/oidc")
|
||||||
|
public ResponseEntity<Void> updateOidcConfig(@RequestBody Map<String, Object> body) {
|
||||||
|
portalService.updateOidcConfig(body);
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/team")
|
||||||
|
public ResponseEntity<List<Map<String, Object>>> listTeamMembers() {
|
||||||
|
return ResponseEntity.ok(portalService.listTeamMembers());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/team/invite")
|
||||||
|
public ResponseEntity<Map<String, String>> inviteTeamMember(@RequestBody InviteRequest body) {
|
||||||
|
String userId = portalService.inviteTeamMember(body.email(), body.roleId());
|
||||||
|
return ResponseEntity.ok(Map.of("userId", userId != null ? userId : ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/team/{userId}")
|
||||||
|
public ResponseEntity<Void> removeTeamMember(@PathVariable String userId) {
|
||||||
|
portalService.removeTeamMember(userId);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PatchMapping("/team/{userId}/role")
|
||||||
|
public ResponseEntity<Void> changeTeamMemberRole(@PathVariable String userId,
|
||||||
|
@RequestBody RoleChangeRequest body) {
|
||||||
|
portalService.changeTeamMemberRole(userId, body.roleId());
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/settings")
|
||||||
|
public ResponseEntity<TenantPortalService.TenantSettingsData> getSettings() {
|
||||||
|
return ResponseEntity.ok(portalService.getSettings());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
package net.siegeln.cameleer.saas.portal;
|
||||||
|
|
||||||
|
import net.siegeln.cameleer.saas.config.TenantContext;
|
||||||
|
import net.siegeln.cameleer.saas.identity.LogtoManagementClient;
|
||||||
|
import net.siegeln.cameleer.saas.identity.ServerApiClient;
|
||||||
|
import net.siegeln.cameleer.saas.license.LicenseEntity;
|
||||||
|
import net.siegeln.cameleer.saas.license.LicenseService;
|
||||||
|
import net.siegeln.cameleer.saas.tenant.TenantEntity;
|
||||||
|
import net.siegeln.cameleer.saas.tenant.TenantService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class TenantPortalService {
|
||||||
|
|
||||||
|
private final TenantService tenantService;
|
||||||
|
private final LicenseService licenseService;
|
||||||
|
private final ServerApiClient serverApiClient;
|
||||||
|
private final LogtoManagementClient logtoClient;
|
||||||
|
|
||||||
|
public TenantPortalService(TenantService tenantService,
|
||||||
|
LicenseService licenseService,
|
||||||
|
ServerApiClient serverApiClient,
|
||||||
|
LogtoManagementClient logtoClient) {
|
||||||
|
this.tenantService = tenantService;
|
||||||
|
this.licenseService = licenseService;
|
||||||
|
this.serverApiClient = serverApiClient;
|
||||||
|
this.logtoClient = logtoClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Inner records ---
|
||||||
|
|
||||||
|
public record DashboardData(
|
||||||
|
String name, String slug, String tier, String status,
|
||||||
|
boolean serverHealthy, String serverStatus, String serverEndpoint,
|
||||||
|
String licenseTier, long licenseDaysRemaining,
|
||||||
|
Map<String, Object> limits, Map<String, Object> features
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public record LicenseData(
|
||||||
|
UUID id, String tier, Map<String, Object> features, Map<String, Object> limits,
|
||||||
|
Instant issuedAt, Instant expiresAt, String token, long daysRemaining
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public record TenantSettingsData(
|
||||||
|
String name, String slug, String tier, String status,
|
||||||
|
String serverEndpoint, Instant createdAt
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
|
||||||
|
private TenantEntity resolveTenant() {
|
||||||
|
UUID tenantId = TenantContext.getTenantId();
|
||||||
|
return tenantService.getById(tenantId)
|
||||||
|
.orElseThrow(() -> new IllegalStateException("Tenant not found: " + tenantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private long daysUntil(Instant instant) {
|
||||||
|
if (instant == null) return 0;
|
||||||
|
long days = ChronoUnit.DAYS.between(Instant.now(), instant);
|
||||||
|
return Math.max(0, days);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Service methods ---
|
||||||
|
|
||||||
|
public DashboardData getDashboard() {
|
||||||
|
TenantEntity tenant = resolveTenant();
|
||||||
|
String endpoint = tenant.getServerEndpoint();
|
||||||
|
|
||||||
|
boolean serverHealthy = false;
|
||||||
|
String serverStatus = "NO_ENDPOINT";
|
||||||
|
if (endpoint != null && !endpoint.isBlank()) {
|
||||||
|
var health = serverApiClient.getHealth(endpoint);
|
||||||
|
serverHealthy = health.healthy();
|
||||||
|
serverStatus = health.status();
|
||||||
|
}
|
||||||
|
|
||||||
|
String licenseTier = null;
|
||||||
|
long licenseDaysRemaining = 0;
|
||||||
|
Map<String, Object> limits = Map.of();
|
||||||
|
Map<String, Object> features = Map.of();
|
||||||
|
|
||||||
|
var licenseOpt = licenseService.getActiveLicense(tenant.getId());
|
||||||
|
if (licenseOpt.isPresent()) {
|
||||||
|
LicenseEntity lic = licenseOpt.get();
|
||||||
|
licenseTier = lic.getTier();
|
||||||
|
licenseDaysRemaining = daysUntil(lic.getExpiresAt());
|
||||||
|
limits = lic.getLimits() != null ? lic.getLimits() : Map.of();
|
||||||
|
features = lic.getFeatures() != null ? lic.getFeatures() : Map.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DashboardData(
|
||||||
|
tenant.getName(), tenant.getSlug(),
|
||||||
|
tenant.getTier().name(), tenant.getStatus().name(),
|
||||||
|
serverHealthy, serverStatus, endpoint,
|
||||||
|
licenseTier, licenseDaysRemaining,
|
||||||
|
limits, features
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LicenseData getLicense() {
|
||||||
|
TenantEntity tenant = resolveTenant();
|
||||||
|
return licenseService.getActiveLicense(tenant.getId())
|
||||||
|
.map(lic -> new LicenseData(
|
||||||
|
lic.getId(), lic.getTier(),
|
||||||
|
lic.getFeatures() != null ? lic.getFeatures() : Map.of(),
|
||||||
|
lic.getLimits() != null ? lic.getLimits() : Map.of(),
|
||||||
|
lic.getIssuedAt(), lic.getExpiresAt(),
|
||||||
|
lic.getToken(), daysUntil(lic.getExpiresAt())
|
||||||
|
))
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getOidcConfig() {
|
||||||
|
TenantEntity tenant = resolveTenant();
|
||||||
|
String endpoint = tenant.getServerEndpoint();
|
||||||
|
if (endpoint == null || endpoint.isBlank()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
return serverApiClient.getOidcConfig(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateOidcConfig(Map<String, Object> config) {
|
||||||
|
TenantEntity tenant = resolveTenant();
|
||||||
|
String endpoint = tenant.getServerEndpoint();
|
||||||
|
if (endpoint == null || endpoint.isBlank()) {
|
||||||
|
throw new IllegalStateException("Tenant has no server endpoint configured");
|
||||||
|
}
|
||||||
|
serverApiClient.pushOidcConfig(endpoint, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Map<String, Object>> listTeamMembers() {
|
||||||
|
TenantEntity tenant = resolveTenant();
|
||||||
|
String orgId = tenant.getLogtoOrgId();
|
||||||
|
if (orgId == null || orgId.isBlank()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
return logtoClient.listOrganizationMembers(orgId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String inviteTeamMember(String email, String roleId) {
|
||||||
|
TenantEntity tenant = resolveTenant();
|
||||||
|
String orgId = tenant.getLogtoOrgId();
|
||||||
|
if (orgId == null || orgId.isBlank()) {
|
||||||
|
throw new IllegalStateException("Tenant has no Logto organization configured");
|
||||||
|
}
|
||||||
|
return logtoClient.createAndInviteUser(email, orgId, roleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeTeamMember(String userId) {
|
||||||
|
TenantEntity tenant = resolveTenant();
|
||||||
|
String orgId = tenant.getLogtoOrgId();
|
||||||
|
if (orgId == null || orgId.isBlank()) {
|
||||||
|
throw new IllegalStateException("Tenant has no Logto organization configured");
|
||||||
|
}
|
||||||
|
logtoClient.removeUserFromOrganization(orgId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void changeTeamMemberRole(String userId, String roleId) {
|
||||||
|
TenantEntity tenant = resolveTenant();
|
||||||
|
String orgId = tenant.getLogtoOrgId();
|
||||||
|
if (orgId == null || orgId.isBlank()) {
|
||||||
|
throw new IllegalStateException("Tenant has no Logto organization configured");
|
||||||
|
}
|
||||||
|
logtoClient.assignOrganizationRole(orgId, userId, roleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TenantSettingsData getSettings() {
|
||||||
|
TenantEntity tenant = resolveTenant();
|
||||||
|
return new TenantSettingsData(
|
||||||
|
tenant.getName(), tenant.getSlug(),
|
||||||
|
tenant.getTier().name(), tenant.getStatus().name(),
|
||||||
|
tenant.getServerEndpoint(), tenant.getCreatedAt()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user