diff --git a/devslab-kit-sample-app/src/main/java/kr/devslab/kit/sample/SampleApplication.java b/devslab-kit-sample-app/src/main/java/kr/devslab/kit/sample/SampleApplication.java
index e1f7e45..20d1c13 100644
--- a/devslab-kit-sample-app/src/main/java/kr/devslab/kit/sample/SampleApplication.java
+++ b/devslab-kit-sample-app/src/main/java/kr/devslab/kit/sample/SampleApplication.java
@@ -1,16 +1,55 @@
package kr.devslab.kit.sample;
+import kr.devslab.kit.access.core.repository.JpaPlatformPermissionRepository;
+import kr.devslab.kit.access.core.repository.JpaPlatformRoleRepository;
+import kr.devslab.kit.access.core.service.PermissionAdminService;
+import kr.devslab.kit.access.core.service.RoleAdminService;
+import kr.devslab.kit.access.core.service.RolePermissionService;
+import kr.devslab.kit.access.core.service.UserRoleService;
+import kr.devslab.kit.identity.core.repository.JpaPlatformUserAccountRepository;
+import kr.devslab.kit.identity.core.service.PlatformUserAccountAdminService;
+import kr.devslab.kit.tenant.TenantService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.AutoConfigurationPackage;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@SpringBootApplication(scanBasePackages = "kr.devslab.kit")
@AutoConfigurationPackage(basePackages = "kr.devslab.kit")
@EnableJpaRepositories(basePackages = "kr.devslab.kit")
+@EnableConfigurationProperties(SampleSeedProperties.class)
public class SampleApplication {
public static void main(String[] args) {
SpringApplication.run(SampleApplication.class, args);
}
+
+ @Bean
+ SampleSeedRunner sampleSeedRunner(
+ SampleSeedProperties props,
+ TenantService tenantService,
+ PermissionAdminService permissionAdminService,
+ JpaPlatformPermissionRepository permissionRepository,
+ RoleAdminService roleAdminService,
+ JpaPlatformRoleRepository roleRepository,
+ RolePermissionService rolePermissionService,
+ PlatformUserAccountAdminService userAdminService,
+ JpaPlatformUserAccountRepository userRepository,
+ UserRoleService userRoleService
+ ) {
+ return new SampleSeedRunner(
+ props,
+ tenantService,
+ permissionAdminService,
+ permissionRepository,
+ roleAdminService,
+ roleRepository,
+ rolePermissionService,
+ userAdminService,
+ userRepository,
+ userRoleService
+ );
+ }
}
diff --git a/devslab-kit-sample-app/src/main/java/kr/devslab/kit/sample/SampleSeedProperties.java b/devslab-kit-sample-app/src/main/java/kr/devslab/kit/sample/SampleSeedProperties.java
new file mode 100644
index 0000000..4936a0c
--- /dev/null
+++ b/devslab-kit-sample-app/src/main/java/kr/devslab/kit/sample/SampleSeedProperties.java
@@ -0,0 +1,26 @@
+package kr.devslab.kit.sample;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Settings for {@link SampleSeedRunner}. Only meaningful in the sample
+ * application — real consumers seed their own data however they like.
+ */
+@ConfigurationProperties(prefix = "sample.seed")
+public class SampleSeedProperties {
+
+ /** Disable for production-like environments where seed data is unwanted. */
+ private boolean enabled = true;
+ private String adminLoginId = "admin";
+ private String adminPassword = "admin";
+ private String adminEmail = "admin@example.com";
+
+ public boolean isEnabled() { return enabled; }
+ public void setEnabled(boolean enabled) { this.enabled = enabled; }
+ public String getAdminLoginId() { return adminLoginId; }
+ public void setAdminLoginId(String adminLoginId) { this.adminLoginId = adminLoginId; }
+ public String getAdminPassword() { return adminPassword; }
+ public void setAdminPassword(String adminPassword) { this.adminPassword = adminPassword; }
+ public String getAdminEmail() { return adminEmail; }
+ public void setAdminEmail(String adminEmail) { this.adminEmail = adminEmail; }
+}
diff --git a/devslab-kit-sample-app/src/main/java/kr/devslab/kit/sample/SampleSeedRunner.java b/devslab-kit-sample-app/src/main/java/kr/devslab/kit/sample/SampleSeedRunner.java
new file mode 100644
index 0000000..8eff875
--- /dev/null
+++ b/devslab-kit-sample-app/src/main/java/kr/devslab/kit/sample/SampleSeedRunner.java
@@ -0,0 +1,180 @@
+package kr.devslab.kit.sample;
+
+import java.util.List;
+import kr.devslab.kit.access.core.repository.JpaPlatformPermissionRepository;
+import kr.devslab.kit.access.core.repository.JpaPlatformRoleRepository;
+import kr.devslab.kit.access.core.service.PermissionAdminService;
+import kr.devslab.kit.access.core.service.RoleAdminService;
+import kr.devslab.kit.access.core.service.RolePermissionService;
+import kr.devslab.kit.access.core.service.UserRoleService;
+import kr.devslab.kit.core.id.PermissionId;
+import kr.devslab.kit.core.id.RoleId;
+import kr.devslab.kit.core.id.TenantId;
+import kr.devslab.kit.core.id.UserId;
+import kr.devslab.kit.identity.core.repository.JpaPlatformUserAccountRepository;
+import kr.devslab.kit.identity.core.service.PlatformUserAccountAdminService;
+import kr.devslab.kit.tenant.TenantMetadata;
+import kr.devslab.kit.tenant.TenantMode;
+import kr.devslab.kit.tenant.TenantService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * Idempotent first-boot seed for the sample app.
+ *
+ *
Provisions just enough for the admin UI to be usable end-to-end against
+ * this backend:
+ *
+ *
+ * - One tenant: {@code default}.
+ * - One role: {@code PLATFORM_ADMIN} bound to that tenant.
+ * - The {@code admin.*} permission set covering every admin-api endpoint.
+ * - One user: {@code admin / admin} (override via {@code SAMPLE_SEED_ADMIN_*}),
+ * holding {@code PLATFORM_ADMIN}.
+ *
+ *
+ * Every step is idempotent — repeated boots find each entity by code /
+ * loginId and skip the create, so re-running against an already-seeded
+ * database is a no-op.
+ *
+ *
Disable with {@code sample.seed.enabled=false} for production-like
+ * runs where seed data would be unwanted.
+ */
+public class SampleSeedRunner implements ApplicationRunner {
+
+ private static final Logger log = LoggerFactory.getLogger(SampleSeedRunner.class);
+
+ private static final String DEFAULT_TENANT_ID = "default";
+ private static final String ADMIN_ROLE_CODE = "PLATFORM_ADMIN";
+ private static final String ADMIN_ROLE_NAME = "Platform Admin";
+
+ private static final List ADMIN_PERMISSIONS = List.of(
+ new PermissionSeed("admin.user.read", "Read user accounts via /admin/api/v1/users"),
+ new PermissionSeed("admin.user.write", "Create / update / delete user accounts"),
+ new PermissionSeed("admin.role.read", "Read roles and their permission grants"),
+ new PermissionSeed("admin.role.write", "Create / rename / delete roles and grant permissions"),
+ new PermissionSeed("admin.permission.read", "Read permission catalogue"),
+ new PermissionSeed("admin.permission.write", "Create / update / delete permissions"),
+ new PermissionSeed("admin.group.read", "Read groups and memberships"),
+ new PermissionSeed("admin.group.write", "Create / rename / delete groups and manage memberships"),
+ new PermissionSeed("admin.menu.read", "Read menu tree"),
+ new PermissionSeed("admin.menu.write", "Create / update / delete menu items"),
+ new PermissionSeed("admin.tenant.read", "Read tenants"),
+ new PermissionSeed("admin.tenant.write", "Create / rename / change status / delete tenants"),
+ new PermissionSeed("admin.policy.test", "Run policy dry-run via /admin/api/v1/policies/test"),
+ new PermissionSeed("admin.audit.read", "Read audit logs with filters + paging"),
+ new PermissionSeed("admin.diagnostics.run", "Run diagnostics probes (login-test, permission-check, menu-visibility)"),
+ new PermissionSeed("admin.settings.read", "Read DevslabKitProperties via /admin/api/v1/settings")
+ );
+
+ private final SampleSeedProperties props;
+ private final TenantService tenantService;
+ private final PermissionAdminService permissionAdminService;
+ private final JpaPlatformPermissionRepository permissionRepository;
+ private final RoleAdminService roleAdminService;
+ private final JpaPlatformRoleRepository roleRepository;
+ private final RolePermissionService rolePermissionService;
+ private final PlatformUserAccountAdminService userAdminService;
+ private final JpaPlatformUserAccountRepository userRepository;
+ private final UserRoleService userRoleService;
+
+ public SampleSeedRunner(
+ SampleSeedProperties props,
+ TenantService tenantService,
+ PermissionAdminService permissionAdminService,
+ JpaPlatformPermissionRepository permissionRepository,
+ RoleAdminService roleAdminService,
+ JpaPlatformRoleRepository roleRepository,
+ RolePermissionService rolePermissionService,
+ PlatformUserAccountAdminService userAdminService,
+ JpaPlatformUserAccountRepository userRepository,
+ UserRoleService userRoleService
+ ) {
+ this.props = props;
+ this.tenantService = tenantService;
+ this.permissionAdminService = permissionAdminService;
+ this.permissionRepository = permissionRepository;
+ this.roleAdminService = roleAdminService;
+ this.roleRepository = roleRepository;
+ this.rolePermissionService = rolePermissionService;
+ this.userAdminService = userAdminService;
+ this.userRepository = userRepository;
+ this.userRoleService = userRoleService;
+ }
+
+ @Override
+ @Transactional
+ public void run(ApplicationArguments args) {
+ if (!props.isEnabled()) {
+ log.info("[sample-seed] disabled via sample.seed.enabled=false — skipping");
+ return;
+ }
+
+ TenantId tenant = ensureTenant();
+ RoleId adminRole = ensureAdminRole(tenant);
+ ensureAdminPermissionsAndGrants(adminRole);
+ UserId adminUser = ensureAdminUser(tenant);
+ userRoleService.assign(adminUser, adminRole, tenant); // idempotent
+
+ log.info("[sample-seed] complete: tenant={} role={} user={} (login as {} / {})",
+ tenant.value(), ADMIN_ROLE_CODE, adminUser.value(),
+ props.getAdminLoginId(), props.getAdminPassword());
+ }
+
+ private TenantId ensureTenant() {
+ TenantId id = TenantId.of(DEFAULT_TENANT_ID);
+ if (tenantService.findById(id).isPresent()) {
+ return id;
+ }
+ TenantMetadata created = tenantService.create(id, "Default Tenant", TenantMode.SINGLE);
+ log.info("[sample-seed] created tenant {}", created.id().value());
+ return id;
+ }
+
+ private RoleId ensureAdminRole(TenantId tenant) {
+ return roleRepository.findByTenantIdAndCode(tenant.value(), ADMIN_ROLE_CODE)
+ .map(entity -> RoleId.of(entity.getId()))
+ .orElseGet(() -> {
+ var role = roleAdminService.create(tenant, ADMIN_ROLE_CODE, ADMIN_ROLE_NAME);
+ log.info("[sample-seed] created role {}", ADMIN_ROLE_CODE);
+ return role.id();
+ });
+ }
+
+ private void ensureAdminPermissionsAndGrants(RoleId adminRole) {
+ for (PermissionSeed perm : ADMIN_PERMISSIONS) {
+ PermissionId permId = permissionRepository.findByCode(perm.code())
+ .map(entity -> PermissionId.of(entity.getId()))
+ .orElseGet(() -> {
+ permissionAdminService.create(perm.code(), perm.description());
+ log.info("[sample-seed] created permission {}", perm.code());
+ return permissionRepository.findByCode(perm.code())
+ .map(e -> PermissionId.of(e.getId()))
+ .orElseThrow();
+ });
+ rolePermissionService.grant(adminRole, permId); // idempotent
+ }
+ }
+
+ private UserId ensureAdminUser(TenantId tenant) {
+ return userRepository.findByTenantIdAndLoginId(tenant.value(), props.getAdminLoginId())
+ .map(entity -> UserId.of(entity.getId()))
+ .orElseGet(() -> {
+ var view = userAdminService.create(
+ tenant,
+ props.getAdminLoginId(),
+ props.getAdminEmail(),
+ props.getAdminPassword(),
+ "LOCAL"
+ );
+ log.info("[sample-seed] created user {}", view.loginId());
+ return view.id();
+ });
+ }
+
+ private record PermissionSeed(String code, String description) {
+ }
+}
diff --git a/devslab-kit-sample-app/src/main/resources/application.yaml b/devslab-kit-sample-app/src/main/resources/application.yaml
index b922759..4c9ddc6 100644
--- a/devslab-kit-sample-app/src/main/resources/application.yaml
+++ b/devslab-kit-sample-app/src/main/resources/application.yaml
@@ -2,6 +2,21 @@ spring:
application:
name: devslab-kit-sample-app
+ # Connect to the same Postgres / Redis that
+ # devslab-kit-admin-ui's docker-compose stands up by default. Override the
+ # DEVSLAB_* env vars when you need to point elsewhere (CI, staging, etc).
+ datasource:
+ url: ${DEVSLAB_DATASOURCE_URL:jdbc:postgresql://localhost:5432/devslab_kit}
+ username: ${DEVSLAB_DATASOURCE_USERNAME:devslab}
+ password: ${DEVSLAB_DATASOURCE_PASSWORD:devslab}
+ data:
+ redis:
+ host: ${DEVSLAB_REDIS_HOST:localhost}
+ port: ${DEVSLAB_REDIS_PORT:6379}
+
+ jpa:
+ open-in-view: false
+
devslab:
kit:
tenant:
@@ -9,3 +24,30 @@ devslab:
mode: single
default-tenant-id: default
resolver: fixed
+
+ identity:
+ jwt:
+ # 32+ byte key required by jjwt HS256. Override in prod via
+ # DEVSLAB_JWT_SECRET — the default below is sufficient for local
+ # development and the integration test setup, NOT for production.
+ secret: ${DEVSLAB_JWT_SECRET:dev-only-32byte-jwt-key-please-override!}
+ issuer: ${DEVSLAB_JWT_ISSUER:devslab-kit-sample}
+ ttl: ${DEVSLAB_JWT_TTL:PT8H}
+
+ audit:
+ enabled: true
+ async-queue-capacity: ${DEVSLAB_AUDIT_QUEUE:1024}
+
+ menu:
+ enabled: true
+ cache-ttl: ${DEVSLAB_MENU_CACHE_TTL:PT5M}
+
+# Sample-app-only seed runner: provisions a default tenant + admin user with
+# PLATFORM_ADMIN role + admin.* permissions on first boot, so the admin UI
+# can log in immediately for end-to-end testing. Disable in production.
+sample:
+ seed:
+ enabled: ${SAMPLE_SEED_ENABLED:true}
+ admin-login-id: ${SAMPLE_SEED_ADMIN_LOGIN_ID:admin}
+ admin-password: ${SAMPLE_SEED_ADMIN_PASSWORD:admin}
+ admin-email: ${SAMPLE_SEED_ADMIN_EMAIL:admin@example.com}