Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
package kr.devslab.kit.admin.tenant;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import kr.devslab.kit.tenant.TenantMode;

/**
* Wire shape for {@code POST /admin/api/v1/tenants}.
*
* <p>The admin UI sends just {@code id} + {@code name} when provisioning
* a tenant, so {@code mode} is optional here. When omitted, the controller
* uses {@link TenantMode#SINGLE} as the default — the right value for
* single-application tenant deployments.
*/
public record CreateTenantRequest(
@NotBlank @Size(max = 64) String id,
@NotBlank @Size(max = 128) String name,
@NotNull TenantMode mode
TenantMode mode
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import kr.devslab.kit.admin.AdminApiPaths;
import kr.devslab.kit.core.id.TenantId;
import kr.devslab.kit.tenant.TenantMetadata;
import kr.devslab.kit.tenant.TenantMode;
import kr.devslab.kit.tenant.TenantService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
Expand All @@ -28,7 +29,8 @@ public TenantAdminController(TenantService tenantService) {

@PostMapping
public ResponseEntity<TenantMetadata> create(@Valid @RequestBody CreateTenantRequest req) {
TenantMetadata created = tenantService.create(TenantId.of(req.id()), req.name(), req.mode());
TenantMode mode = req.mode() == null ? TenantMode.SINGLE : req.mode();
TenantMetadata created = tenantService.create(TenantId.of(req.id()), req.name(), mode);
return ResponseEntity.status(201).body(created);
}

Expand All @@ -50,6 +52,20 @@ public ResponseEntity<Void> rename(@PathVariable String id, @Valid @RequestBody
return ResponseEntity.noContent().build();
}

/**
* Generic lifecycle setter used by the admin UI. The
* {@code /activate} / {@code /deactivate} shorthands below
* dispatch through {@link TenantService} default methods.
*/
@PutMapping("/{id}/status")
public ResponseEntity<Void> updateStatus(
@PathVariable String id,
@Valid @RequestBody UpdateTenantStatusRequest req
) {
tenantService.setStatus(TenantId.of(id), req.status());
return ResponseEntity.noContent().build();
}

@PutMapping("/{id}/activate")
public ResponseEntity<Void> activate(@PathVariable String id) {
tenantService.activate(TenantId.of(id));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package kr.devslab.kit.admin.tenant;

import jakarta.validation.constraints.NotNull;
import kr.devslab.kit.tenant.TenantStatus;

/**
* Wire shape for {@code PUT /admin/api/v1/tenants/{id}/status}.
*/
public record UpdateTenantStatusRequest(@NotNull TenantStatus status) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ public record TenantMetadata(
TenantId id,
String name,
TenantMode mode,
boolean active,
TenantStatus status,
Instant createdAt
) {

public TenantMetadata {
Objects.requireNonNull(id, "TenantMetadata id must not be null");
Objects.requireNonNull(mode, "TenantMetadata mode must not be null");
Objects.requireNonNull(status, "TenantMetadata status must not be null");
Objects.requireNonNull(createdAt, "TenantMetadata createdAt must not be null");
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("TenantMetadata name must not be null or blank");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,21 @@ public interface TenantService {

void rename(TenantId id, String newName);

void deactivate(TenantId id);

void activate(TenantId id);
/**
* Move a tenant to the given lifecycle state. The activate / deactivate
* shorthands below are kept for callers that don't need the full enum.
*/
void setStatus(TenantId id, TenantStatus status);

/** Convenience for {@code setStatus(id, TenantStatus.SUSPENDED)}. */
default void deactivate(TenantId id) {
setStatus(id, TenantStatus.SUSPENDED);
}

/** Convenience for {@code setStatus(id, TenantStatus.ACTIVE)}. */
default void activate(TenantId id) {
setStatus(id, TenantStatus.ACTIVE);
}

void delete(TenantId id);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package kr.devslab.kit.tenant;

/**
* Lifecycle state of a {@link TenantMetadata}.
*
* <p>Three states:
*
* <ul>
* <li>{@link #ACTIVE} — normal operation. Authentication, tenant-scoped
* data access, and admin operations all succeed.</li>
* <li>{@link #SUSPENDED} — temporarily disabled. Existing users can't log
* in but data and config are preserved; an admin can promote back to
* {@code ACTIVE} at any time.</li>
* <li>{@link #ARCHIVED} — soft-deleted. Stays in the system for audit
* and reference but is considered terminal.</li>
* </ul>
*
* <p>The vocabulary lines up with the admin UI's tenant page so the wire
* shape and the UI tag set match without translation.
*/
public enum TenantStatus {
ACTIVE,
SUSPENDED,
ARCHIVED
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import jakarta.persistence.Table;
import java.time.Instant;
import kr.devslab.kit.tenant.TenantMode;
import kr.devslab.kit.tenant.TenantStatus;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -31,17 +32,18 @@ public class PlatformTenantEntity {
@Column(name = "mode", nullable = false, length = 16)
private TenantMode mode;

@Column(name = "active", nullable = false)
private boolean active;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 16)
private TenantStatus status;

@Column(name = "created_at", nullable = false)
private Instant createdAt;

public PlatformTenantEntity(String id, String name, TenantMode mode, boolean active, Instant createdAt) {
public PlatformTenantEntity(String id, String name, TenantMode mode, TenantStatus status, Instant createdAt) {
this.id = id;
this.name = name;
this.mode = mode;
this.active = active;
this.status = status;
this.createdAt = createdAt;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import kr.devslab.kit.tenant.TenantMetadata;
import kr.devslab.kit.tenant.TenantMode;
import kr.devslab.kit.tenant.TenantService;
import kr.devslab.kit.tenant.TenantStatus;
import kr.devslab.kit.tenant.core.entity.PlatformTenantEntity;
import kr.devslab.kit.tenant.core.repository.JpaPlatformTenantRepository;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -29,7 +30,7 @@ public TenantMetadata create(TenantId id, String name, TenantMode mode) {
throw new IllegalStateException("Tenant already exists: " + id);
}
PlatformTenantEntity entity = new PlatformTenantEntity(
id.value(), name, mode, true, Instant.now(clock));
id.value(), name, mode, TenantStatus.ACTIVE, Instant.now(clock));
repository.save(entity);
return toMetadata(entity);
}
Expand All @@ -44,18 +45,13 @@ public void rename(TenantId id, String newName) {

@Override
@Transactional
public void deactivate(TenantId id) {
PlatformTenantEntity entity = repository.findById(id.value())
.orElseThrow(() -> new IllegalArgumentException("Tenant not found: " + id));
entity.setActive(false);
}

@Override
@Transactional
public void activate(TenantId id) {
public void setStatus(TenantId id, TenantStatus status) {
if (status == null) {
throw new IllegalArgumentException("status must not be null");
}
PlatformTenantEntity entity = repository.findById(id.value())
.orElseThrow(() -> new IllegalArgumentException("Tenant not found: " + id));
entity.setActive(true);
entity.setStatus(status);
}

@Override
Expand All @@ -81,7 +77,7 @@ private TenantMetadata toMetadata(PlatformTenantEntity e) {
TenantId.of(e.getId()),
e.getName(),
e.getMode(),
e.isActive(),
e.getStatus(),
e.getCreatedAt()
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- Replace the boolean `active` flag with a richer TenantStatus enum
-- (ACTIVE / SUSPENDED / ARCHIVED). Existing rows are back-filled in place:
-- active = true -> ACTIVE
-- active = false -> SUSPENDED (treat as "paused", not as "soft-deleted")
--
-- Done in three steps so the rewrite stays online-safe:
-- 1. add nullable status column,
-- 2. back-fill from active,
-- 3. swap constraints + indexes and drop active.

ALTER TABLE platform_tenant
ADD COLUMN status VARCHAR(16);

UPDATE platform_tenant
SET status = CASE WHEN active THEN 'ACTIVE' ELSE 'SUSPENDED' END
WHERE status IS NULL;

ALTER TABLE platform_tenant
ALTER COLUMN status SET NOT NULL;

DROP INDEX IF EXISTS idx_platform_tenant_active;
CREATE INDEX IF NOT EXISTS idx_platform_tenant_status ON platform_tenant(status);

ALTER TABLE platform_tenant
DROP COLUMN active;
Loading