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