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
@@ -0,0 +1,13 @@
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;

public record CreateTenantRequest(
@NotBlank @Size(max = 64) String id,
@NotBlank @Size(max = 128) String name,
@NotNull TenantMode mode
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package kr.devslab.kit.admin.tenant;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record RenameTenantRequest(@NotBlank @Size(max = 128) String name) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package kr.devslab.kit.admin.tenant;

import jakarta.validation.Valid;
import java.util.List;
import kr.devslab.kit.admin.AdminApiPaths;
import kr.devslab.kit.core.id.TenantId;
import kr.devslab.kit.tenant.TenantMetadata;
import kr.devslab.kit.tenant.TenantService;
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.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;

@RestController
@RequestMapping(AdminApiPaths.BASE + "/tenants")
public class TenantAdminController {

private final TenantService tenantService;

public TenantAdminController(TenantService tenantService) {
this.tenantService = tenantService;
}

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

@GetMapping
public List<TenantMetadata> list() {
return tenantService.findAll();
}

@GetMapping("/{id}")
public ResponseEntity<TenantMetadata> get(@PathVariable String id) {
return tenantService.findById(TenantId.of(id))
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}

@PutMapping("/{id}")
public ResponseEntity<Void> rename(@PathVariable String id, @Valid @RequestBody RenameTenantRequest req) {
tenantService.rename(TenantId.of(id), req.name());
return ResponseEntity.noContent().build();
}

@PutMapping("/{id}/activate")
public ResponseEntity<Void> activate(@PathVariable String id) {
tenantService.activate(TenantId.of(id));
return ResponseEntity.noContent().build();
}

@PutMapping("/{id}/deactivate")
public ResponseEntity<Void> deactivate(@PathVariable String id) {
tenantService.deactivate(TenantId.of(id));
return ResponseEntity.noContent().build();
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable String id) {
tenantService.delete(TenantId.of(id));
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package kr.devslab.kit.autoconfigure;

import jakarta.persistence.EntityManager;
import java.time.Clock;
import kr.devslab.kit.tenant.TenantContextHolder;
import kr.devslab.kit.tenant.TenantResolver;
import kr.devslab.kit.tenant.TenantService;
import kr.devslab.kit.tenant.core.DefaultTenantContextHolder;
import kr.devslab.kit.tenant.core.FixedTenantResolver;
import kr.devslab.kit.tenant.core.HeaderTenantResolver;
import kr.devslab.kit.tenant.core.SubdomainTenantResolver;
import kr.devslab.kit.tenant.core.repository.JpaPlatformTenantRepository;
import kr.devslab.kit.tenant.core.service.DefaultTenantService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
Expand Down Expand Up @@ -48,4 +55,12 @@ public TenantResolver tenantResolver(DevslabKitProperties properties) {
);
};
}

@Bean
@ConditionalOnClass(EntityManager.class)
@ConditionalOnBean(JpaPlatformTenantRepository.class)
@ConditionalOnMissingBean
public TenantService tenantService(JpaPlatformTenantRepository repository, Clock clock) {
return new DefaultTenantService(repository, clock);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package kr.devslab.kit.tenant;

import java.time.Instant;
import java.util.Objects;
import kr.devslab.kit.core.id.TenantId;

public record TenantMetadata(
TenantId id,
String name,
TenantMode mode,
boolean active,
Instant createdAt
) {

public TenantMetadata {
Objects.requireNonNull(id, "TenantMetadata id must not be null");
Objects.requireNonNull(mode, "TenantMetadata mode 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");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package kr.devslab.kit.tenant;

import java.util.List;
import java.util.Optional;
import kr.devslab.kit.core.id.TenantId;

public interface TenantService {

TenantMetadata create(TenantId id, String name, TenantMode mode);

void rename(TenantId id, String newName);

void deactivate(TenantId id);

void activate(TenantId id);

void delete(TenantId id);

Optional<TenantMetadata> findById(TenantId id);

List<TenantMetadata> findAll();
}
5 changes: 5 additions & 0 deletions devslab-kit-tenant-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ description = "devslab-kit :: tenant default implementation"
dependencies {
api(project(":devslab-kit-tenant-api"))

api("org.springframework.boot:spring-boot-starter-data-jpa")

compileOnly("org.springframework:spring-web")
compileOnly("jakarta.servlet:jakarta.servlet-api")

compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package kr.devslab.kit.tenant.core.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.Instant;
import kr.devslab.kit.tenant.TenantMode;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Table(name = "platform_tenant")
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PlatformTenantEntity {

@Id
@Column(name = "id", nullable = false, updatable = false, length = 64)
private String id;

@Column(name = "name", nullable = false, length = 128)
private String name;

@Enumerated(EnumType.STRING)
@Column(name = "mode", nullable = false, length = 16)
private TenantMode mode;

@Column(name = "active", nullable = false)
private boolean active;

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

public PlatformTenantEntity(String id, String name, TenantMode mode, boolean active, Instant createdAt) {
this.id = id;
this.name = name;
this.mode = mode;
this.active = active;
this.createdAt = createdAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package kr.devslab.kit.tenant.core.repository;

import kr.devslab.kit.tenant.core.entity.PlatformTenantEntity;
import org.springframework.data.jpa.repository.JpaRepository;

public interface JpaPlatformTenantRepository extends JpaRepository<PlatformTenantEntity, String> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package kr.devslab.kit.tenant.core.service;

import java.time.Clock;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
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 kr.devslab.kit.tenant.core.entity.PlatformTenantEntity;
import kr.devslab.kit.tenant.core.repository.JpaPlatformTenantRepository;
import org.springframework.transaction.annotation.Transactional;

public class DefaultTenantService implements TenantService {

private final JpaPlatformTenantRepository repository;
private final Clock clock;

public DefaultTenantService(JpaPlatformTenantRepository repository, Clock clock) {
this.repository = repository;
this.clock = clock;
}

@Override
@Transactional
public TenantMetadata create(TenantId id, String name, TenantMode mode) {
if (repository.findById(id.value()).isPresent()) {
throw new IllegalStateException("Tenant already exists: " + id);
}
PlatformTenantEntity entity = new PlatformTenantEntity(
id.value(), name, mode, true, Instant.now(clock));
repository.save(entity);
return toMetadata(entity);
}

@Override
@Transactional
public void rename(TenantId id, String newName) {
PlatformTenantEntity entity = repository.findById(id.value())
.orElseThrow(() -> new IllegalArgumentException("Tenant not found: " + id));
entity.setName(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) {
PlatformTenantEntity entity = repository.findById(id.value())
.orElseThrow(() -> new IllegalArgumentException("Tenant not found: " + id));
entity.setActive(true);
}

@Override
@Transactional
public void delete(TenantId id) {
repository.deleteById(id.value());
}

@Override
@Transactional(readOnly = true)
public Optional<TenantMetadata> findById(TenantId id) {
return repository.findById(id.value()).map(this::toMetadata);
}

@Override
@Transactional(readOnly = true)
public List<TenantMetadata> findAll() {
return repository.findAll().stream().map(this::toMetadata).toList();
}

private TenantMetadata toMetadata(PlatformTenantEntity e) {
return new TenantMetadata(
TenantId.of(e.getId()),
e.getName(),
e.getMode(),
e.isActive(),
e.getCreatedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS platform_tenant (
id varchar(64) PRIMARY KEY,
name varchar(128) NOT NULL,
mode varchar(16) NOT NULL,
active boolean NOT NULL DEFAULT true,
created_at timestamp with time zone NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_platform_tenant_active ON platform_tenant(active);
Loading