feat: add tenant service, controller, and DTOs with TDD
CRUD operations for tenants with slug-based lookup, tier management, and audit logging. Integration tests verify 201/409/401 responses.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
package net.siegeln.cameleer.saas.tenant;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import net.siegeln.cameleer.saas.tenant.dto.CreateTenantRequest;
|
||||
import net.siegeln.cameleer.saas.tenant.dto.TenantResponse;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
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.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/tenants")
|
||||
public class TenantController {
|
||||
|
||||
private final TenantService tenantService;
|
||||
|
||||
public TenantController(TenantService tenantService) {
|
||||
this.tenantService = tenantService;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<TenantResponse> create(@Valid @RequestBody CreateTenantRequest request,
|
||||
Authentication authentication) {
|
||||
try {
|
||||
// Extract actor ID from authentication credentials (Phase 1: userId stored as credentials)
|
||||
UUID actorId = authentication.getCredentials() instanceof UUID uid
|
||||
? uid : UUID.fromString(authentication.getCredentials().toString());
|
||||
|
||||
var entity = tenantService.create(request, actorId);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(toResponse(entity));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.status(HttpStatus.CONFLICT).build();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<TenantResponse> getById(@PathVariable UUID id) {
|
||||
return tenantService.getById(id)
|
||||
.map(entity -> ResponseEntity.ok(toResponse(entity)))
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@GetMapping("/by-slug/{slug}")
|
||||
public ResponseEntity<TenantResponse> getBySlug(@PathVariable String slug) {
|
||||
return tenantService.getBySlug(slug)
|
||||
.map(entity -> ResponseEntity.ok(toResponse(entity)))
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
private TenantResponse toResponse(TenantEntity entity) {
|
||||
return new TenantResponse(
|
||||
entity.getId(),
|
||||
entity.getName(),
|
||||
entity.getSlug(),
|
||||
entity.getTier().name(),
|
||||
entity.getStatus().name(),
|
||||
entity.getCreatedAt(),
|
||||
entity.getUpdatedAt()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package net.siegeln.cameleer.saas.tenant;
|
||||
|
||||
import net.siegeln.cameleer.saas.audit.AuditAction;
|
||||
import net.siegeln.cameleer.saas.audit.AuditService;
|
||||
import net.siegeln.cameleer.saas.tenant.dto.CreateTenantRequest;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class TenantService {
|
||||
|
||||
private final TenantRepository tenantRepository;
|
||||
private final AuditService auditService;
|
||||
|
||||
public TenantService(TenantRepository tenantRepository, AuditService auditService) {
|
||||
this.tenantRepository = tenantRepository;
|
||||
this.auditService = auditService;
|
||||
}
|
||||
|
||||
public TenantEntity create(CreateTenantRequest request, UUID actorId) {
|
||||
if (tenantRepository.existsBySlug(request.slug())) {
|
||||
throw new IllegalArgumentException("Slug already taken");
|
||||
}
|
||||
|
||||
var entity = new TenantEntity();
|
||||
entity.setName(request.name());
|
||||
entity.setSlug(request.slug());
|
||||
entity.setTier(request.tier() != null ? Tier.valueOf(request.tier()) : Tier.LOW);
|
||||
entity.setStatus(TenantStatus.PROVISIONING);
|
||||
|
||||
var saved = tenantRepository.save(entity);
|
||||
|
||||
auditService.log(actorId, null, saved.getId(),
|
||||
AuditAction.TENANT_CREATE, saved.getSlug(),
|
||||
null, null, "SUCCESS", null);
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
public Optional<TenantEntity> getById(UUID id) {
|
||||
return tenantRepository.findById(id);
|
||||
}
|
||||
|
||||
public Optional<TenantEntity> getBySlug(String slug) {
|
||||
return tenantRepository.findBySlug(slug);
|
||||
}
|
||||
|
||||
public Optional<TenantEntity> getByLogtoOrgId(String logtoOrgId) {
|
||||
return tenantRepository.findByLogtoOrgId(logtoOrgId);
|
||||
}
|
||||
|
||||
public List<TenantEntity> listActive() {
|
||||
return tenantRepository.findByStatus(TenantStatus.ACTIVE);
|
||||
}
|
||||
|
||||
public TenantEntity activate(UUID tenantId, UUID actorId) {
|
||||
var entity = tenantRepository.findById(tenantId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Tenant not found"));
|
||||
entity.setStatus(TenantStatus.ACTIVE);
|
||||
var saved = tenantRepository.save(entity);
|
||||
|
||||
auditService.log(actorId, null, tenantId,
|
||||
AuditAction.TENANT_UPDATE, entity.getSlug(),
|
||||
null, null, "SUCCESS", null);
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
public TenantEntity suspend(UUID tenantId, UUID actorId) {
|
||||
var entity = tenantRepository.findById(tenantId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Tenant not found"));
|
||||
entity.setStatus(TenantStatus.SUSPENDED);
|
||||
var saved = tenantRepository.save(entity);
|
||||
|
||||
auditService.log(actorId, null, tenantId,
|
||||
AuditAction.TENANT_SUSPEND, entity.getSlug(),
|
||||
null, null, "SUCCESS", null);
|
||||
|
||||
return saved;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package net.siegeln.cameleer.saas.tenant.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public record CreateTenantRequest(
|
||||
@NotBlank @Size(max = 255) String name,
|
||||
@NotBlank @Size(max = 100) @Pattern(regexp = "^[a-z0-9][a-z0-9-]*[a-z0-9]$", message = "Slug must be lowercase alphanumeric with hyphens") String slug,
|
||||
String tier
|
||||
) {}
|
||||
@@ -0,0 +1,14 @@
|
||||
package net.siegeln.cameleer.saas.tenant.dto;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
public record TenantResponse(
|
||||
UUID id,
|
||||
String name,
|
||||
String slug,
|
||||
String tier,
|
||||
String status,
|
||||
Instant createdAt,
|
||||
Instant updatedAt
|
||||
) {}
|
||||
Reference in New Issue
Block a user