diff --git a/README.ko.md b/README.ko.md index 0c193d6..519cf08 100644 --- a/README.ko.md +++ b/README.ko.md @@ -55,6 +55,14 @@ Spring Boot 3.3–3.5 사용 중인 앱용. 스타터의 [`3.x` 브랜치](https | [`api-log-mybatis-demo`](api-log-mybatis-demo/) | **MyBatis 백엔드** — Spring MVC + `RestApiClientUtil` + `MybatisApiLogWriter`. 번들 `ApiLogMapper`는 request_id 조회용, `recent` / `by-event` 쿼리는 데모가 커스텀 `ApiLogQueryMapper` (xml) 추가. 이미 MyBatis 쓰고 JPA 안 원하는 팀용. | [![Maven Central](https://img.shields.io/maven-central/v/kr.devslab/api-log-mybatis)](https://central.sonatype.com/artifact/kr.devslab/api-log-mybatis) | | [`api-log-r2dbc-demo`](api-log-r2dbc-demo/) | **R2DBC 백엔드 (리액티브)** — WebFlux + `ReactiveApiClientUtil` (`Mono` 기반) + `R2dbcApiLogWriter`. 리더는 `DatabaseClient`로 `Flux` 스트리밍. HTTP 경로 전체 논블로킹; api-log 쓰기도 논블로킹. 요청 경로에 JDBC가 전혀 없는 WebFlux 앱용. | [![Maven Central](https://img.shields.io/maven-central/v/kr.devslab/api-log-r2dbc)](https://central.sonatype.com/artifact/kr.devslab/api-log-r2dbc) | +### devslab-kit + +Spring Boot 4 플랫폼 스타터 — 인증, RBAC + 그룹 + ABAC, 멀티테넌시, 동적 메뉴, 감사 로깅, 관리자 REST API를 모두 자동 구성으로. 전체 문서: [devslab-kit.devslab.kr](https://devslab-kit.devslab.kr). + +| 데모 | 보여주는 것 | Maven Central | +| --- | --- | --- | +| [`devslab-kit-demo`](devslab-kit-demo/) | **플랫폼 코드가 전혀 없는** 소비자 앱 — 스타터만 추가하면 인증, RBAC + ABAC, 멀티테넌시, 동적 메뉴, 감사, 최초 관리자 부트스트랩, 관리자 REST API가 올라옴. PostgreSQL + Redis(분산 캐시); `bootRun`은 Docker Compose, 테스트는 Testcontainers + `@ServiceConnection`. | [![Maven Central](https://img.shields.io/maven-central/v/kr.devslab/devslab-kit-spring-boot-starter)](https://central.sonatype.com/artifact/kr.devslab/devslab-kit-spring-boot-starter) | + ## 컨벤션 - 각 데모는 **독립 Gradle 프로젝트** — 자체 `settings.gradle.kts`, `build.gradle.kts`, `gradlew`를 가짐. 루트 빌드를 공유하지 않으므로 의존성 버전이나 JDK 타겟이 독립적으로 변할 수 있음. diff --git a/README.md b/README.md index 57ac3f5..02658de 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,14 @@ Async API-call logging into PostgreSQL JSONB via the [api-log](https://github.co | [`api-log-mybatis-demo`](api-log-mybatis-demo/) | **MyBatis backend** — Spring MVC + `RestApiClientUtil` + `MybatisApiLogWriter`. Uses the bundled `ApiLogMapper` for by-request lookup, plus a custom `ApiLogQueryMapper` (xml) for `recent` / `by-event` queries. For teams already on MyBatis who don't want JPA. | [![Maven Central](https://img.shields.io/maven-central/v/kr.devslab/api-log-mybatis)](https://central.sonatype.com/artifact/kr.devslab/api-log-mybatis) | | [`api-log-r2dbc-demo`](api-log-r2dbc-demo/) | **R2DBC backend (reactive)** — WebFlux + `ReactiveApiClientUtil` (`Mono`-based) + `R2dbcApiLogWriter`. Reader uses `DatabaseClient` for streaming `Flux`. Entire HTTP path is non-blocking; api-log writes also non-blocking. For WebFlux apps that have no JDBC anywhere on the request path. | [![Maven Central](https://img.shields.io/maven-central/v/kr.devslab/api-log-r2dbc)](https://central.sonatype.com/artifact/kr.devslab/api-log-r2dbc) | +### devslab-kit + +Spring Boot 4 platform starter — authentication, RBAC + groups + ABAC, multi-tenancy, dynamic menus, audit logging, and an admin REST API, all from auto-configuration. Full docs at [devslab-kit.devslab.kr](https://devslab-kit.devslab.kr). + +| Demo | Showcases | Maven Central | +| --- | --- | --- | +| [`devslab-kit-demo`](devslab-kit-demo/) | A plain consumer app with **no platform code of its own** — adding the starter brings up auth, RBAC + ABAC, multi-tenancy, dynamic menus, audit, the first-admin bootstrap, and the admin REST API. PostgreSQL + Redis (distributed cache); Docker Compose for `bootRun`, Testcontainers + `@ServiceConnection` for tests. | [![Maven Central](https://img.shields.io/maven-central/v/kr.devslab/devslab-kit-spring-boot-starter)](https://central.sonatype.com/artifact/kr.devslab/devslab-kit-spring-boot-starter) | + ## Conventions - Each demo is a **standalone Gradle project** — its own `settings.gradle.kts`, `build.gradle.kts`, and `gradlew`. Demos do not share a root build, so their dependency versions and JDK targets can drift independently. diff --git a/devslab-kit-demo/README.ko.md b/devslab-kit-demo/README.ko.md new file mode 100644 index 0000000..6a6b921 --- /dev/null +++ b/devslab-kit-demo/README.ko.md @@ -0,0 +1,69 @@ +# devslab-kit-demo + +> **[devslab-kit](https://github.com/devslab-kr/devslab-kit)**(Spring Boot 4 플랫폼 스타터)를 PostgreSQL + Redis에서 사용하는 최소 앱. + +[English](./README.md) + +이 앱에는 **플랫폼 코드가 전혀 없습니다.** 스타터를 추가하고 데이터베이스를 가리키기만 +하면 인증, RBAC + 그룹 + ABAC, 멀티테넌시, 동적 메뉴, 감사 로깅, 최초 관리자 부트스트랩, +관리자 REST API가 모두 자동 구성으로 제공됩니다. + +> **설정 불필요.** 앱 자체 패키지(`kr.devslab.example.*`)에 평범한 +> `@SpringBootApplication` 하나면 충분합니다 — +> [`DevslabKitDemoApplication`](src/main/java/kr/devslab/example/devslabkit/DevslabKitDemoApplication.java) +> 참고. `scanBasePackages`·`@EntityScan`·`@EnableJpaRepositories` 모두 불필요: +> 스타터의 자동 구성이 kit의 JPA 엔티티·리포지토리와 관리자 REST API를 직접 +> 등록하며, 소비자가 스캔 범위를 넓히는 게 아니라 스타터가 알아서 넓힙니다. + +## 요구 사항 + +- Java 21+ +- Docker (Compose / Testcontainers로 PostgreSQL + Redis 기동) + +## 실행 + +```bash +./gradlew bootRun +``` + +`spring-boot-docker-compose`가 `compose.yaml`(Postgres + Redis)을 자동 기동하고, kit이 +Flyway 마이그레이션을 실행하며, 최초 관리자 부트스트랩이 `default` 테넌트에 `admin`/`admin` +사용자를 시드합니다. + +로그인해서 JWT 받기: + +```bash +curl -s localhost:8080/admin/api/v1/auth/login \ + -H 'Content-Type: application/json' \ + -d '{"tenantId":"default","loginId":"admin","password":"admin"}' +``` + +[관리자 콘솔](https://github.com/devslab-kr/devslab-kit-admin-ui)을 +`http://localhost:8080`에 연결하면 같은 API 위에서 UI를 쓸 수 있습니다. + +## 테스트 실행 (Docker만 필요) + +```bash +./gradlew test +``` + +Testcontainers가 일회용 Postgres + Redis를 띄우고, 테스트가 전체 컨텍스트를 부팅해 +플랫폼 빈이 배선됐는지 확인합니다. + +## 이 데모가 보여주는 것 + +- **스타터만 추가하면 됩니다.** `build.gradle.kts`는 + `kr.devslab:devslab-kit-spring-boot-starter`와 플랫폼이 사용하는 Spring 스타터(web, + security, JPA, Flyway, data-redis)를 선언합니다 — kit은 런타임 선택을 강요하지 않습니다. +- **코드가 아니라 설정.** 모든 것은 `application.yaml`의 `devslab.kit.*`로 제어합니다 — + 테넌트 모드/리졸버, JWT 시크릿, 캐시 백엔드, 부트스트랩 관리자. + [설정 레퍼런스](https://devslab-kit.devslab.kr/ko/reference/configuration/) 참고. +- **property 한 줄로 분산 캐시.** `devslab.kit.cache.type: redis`로 두면 사용자별 메뉴 + 트리(그리고 직접 추가한 `@Cacheable`)가 Redis에 JSON으로 캐시됩니다 — `Serializable`도, + 직렬화기 설정도 필요 없습니다. `in-memory`로 바꾸면 Redis를 뺄 수 있습니다. +- **최초 관리자 부트스트랩.** 빈 데이터베이스가 영구 백도어 없이 첫 부팅에 사용 가능한 + 관리자 로그인에 도달합니다. + +## 문서 + +전체 문서: **[devslab-kit.devslab.kr](https://devslab-kit.devslab.kr)**. diff --git a/devslab-kit-demo/README.md b/devslab-kit-demo/README.md new file mode 100644 index 0000000..de07c4b --- /dev/null +++ b/devslab-kit-demo/README.md @@ -0,0 +1,71 @@ +# devslab-kit-demo + +> A minimal app that consumes **[devslab-kit](https://github.com/devslab-kr/devslab-kit)** — the Spring Boot 4 platform starter — on PostgreSQL + Redis. + +[한국어](./README.ko.md) + +The app has **no platform code of its own**. Adding the starter and pointing it at +a database gives you authentication, RBAC + groups + ABAC, multi-tenancy, dynamic +menus, audit logging, a first-admin bootstrap, and an admin REST API — all from +auto-configuration. + +> **No wiring required.** A plain `@SpringBootApplication` in the app's own package +> (`kr.devslab.example.*`) is all it takes — see +> [`DevslabKitDemoApplication`](src/main/java/kr/devslab/example/devslabkit/DevslabKitDemoApplication.java). +> No `scanBasePackages`, no `@EntityScan`, no `@EnableJpaRepositories`: the starter's +> auto-configuration registers the kit's JPA entities, repositories, and the admin +> REST API itself, broadening scanning rather than making you widen it. + +## Prerequisites + +- Java 21+ +- Docker (for PostgreSQL + Redis via Compose / Testcontainers) + +## Run + +```bash +./gradlew bootRun +``` + +`spring-boot-docker-compose` auto-starts `compose.yaml` (Postgres + Redis), the kit +runs its Flyway migrations, and the first-admin bootstrap seeds an `admin`/`admin` +user in the `default` tenant. + +Log in to get a JWT: + +```bash +curl -s localhost:8080/admin/api/v1/auth/login \ + -H 'Content-Type: application/json' \ + -d '{"tenantId":"default","loginId":"admin","password":"admin"}' +``` + +Point the [admin console](https://github.com/devslab-kr/devslab-kit-admin-ui) at +`http://localhost:8080` for a UI over the same API. + +## Run the tests (Docker only) + +```bash +./gradlew test +``` + +Testcontainers starts throwaway Postgres + Redis; the test boots the full context +and asserts the platform beans are wired. + +## What this demo shows + +- **Just add the starter.** `build.gradle.kts` declares + `kr.devslab:devslab-kit-spring-boot-starter` plus the Spring starters the + platform uses (web, security, JPA, Flyway, data-redis) — the kit stays + unopinionated about your runtime. +- **Config, not code.** Everything is driven from `application.yaml` under + `devslab.kit.*` — tenant mode/resolver, the JWT secret, the cache backend, and + the bootstrap admin. See the [configuration reference](https://devslab-kit.devslab.kr/reference/configuration/). +- **Distributed cache via one property.** `devslab.kit.cache.type: redis` makes the + per-user menu tree (and any `@Cacheable` you add) cache as JSON in Redis — no + `Serializable`, no serializer wiring. Flip to `in-memory` to drop Redis. +- **First-admin bootstrap.** A fresh database reaches a usable admin login on first + boot, without a permanent backdoor. + +## Docs + +Full documentation: **[devslab-kit.devslab.kr](https://devslab-kit.devslab.kr)**. diff --git a/devslab-kit-demo/build.gradle.kts b/devslab-kit-demo/build.gradle.kts new file mode 100644 index 0000000..297199b --- /dev/null +++ b/devslab-kit-demo/build.gradle.kts @@ -0,0 +1,60 @@ +plugins { + java + id("org.springframework.boot") version "4.0.6" + id("io.spring.dependency-management") version "1.1.7" +} + +group = "kr.devslab.example" +version = "0.0.1-SNAPSHOT" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenCentral() + // For local testing before 0.1.0 is on Maven Central: + // (1) in the devslab-kit repo run `./gradlew publishToMavenLocal` + // (2) uncomment the line below + // mavenLocal() +} + +dependencies { + // The devslab-kit starter pulls in the whole platform's auto-configuration. + implementation("kr.devslab:devslab-kit-spring-boot-starter:0.1.0") + + // devslab-kit is deliberately unopinionated about which Spring starters you + // bring — a consumer wires the runtime it actually wants. This is the set the + // platform needs to come fully alive: web + security (admin REST API), JPA + + // Flyway (the kit ships its schema migrations on the classpath), and + // data-redis (only used when devslab.kit.cache.type=redis). + implementation("org.springframework.boot:spring-boot-starter-webmvc") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-jdbc") + implementation("org.springframework.boot:spring-boot-starter-flyway") + implementation("org.springframework.boot:spring-boot-starter-data-redis") + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("org.flywaydb:flyway-database-postgresql") + + runtimeOnly("org.postgresql:postgresql") + + // Auto-starts compose.yaml (Postgres + Redis) on `bootRun`. + developmentOnly("org.springframework.boot:spring-boot-docker-compose") + + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.boot:spring-boot-testcontainers") + // Spring Boot 4 manages Testcontainers 2.x, whose artifacts were renamed to + // the `testcontainers-*` prefix (`testcontainers-junit-jupiter`, + // `testcontainers-postgresql`); the old 1.x IDs (`junit-jupiter`, `postgresql`) + // are absent from the SB4 BOM, so leaving them unversioned fails to resolve. + testImplementation("org.testcontainers:testcontainers-junit-jupiter") + testImplementation("org.testcontainers:testcontainers-postgresql") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.withType { + useJUnitPlatform() +} diff --git a/devslab-kit-demo/compose.yaml b/devslab-kit-demo/compose.yaml new file mode 100644 index 0000000..3e4778b --- /dev/null +++ b/devslab-kit-demo/compose.yaml @@ -0,0 +1,13 @@ +services: + postgres: + image: 'postgres:16-alpine' + environment: + - 'POSTGRES_DB=demo' + - 'POSTGRES_USER=demo' + - 'POSTGRES_PASSWORD=demo' + ports: + - '5432:5432' + redis: + image: 'redis:7-alpine' + ports: + - '6379:6379' diff --git a/devslab-kit-demo/docker-compose.yml b/devslab-kit-demo/docker-compose.yml new file mode 100644 index 0000000..1910bb9 --- /dev/null +++ b/devslab-kit-demo/docker-compose.yml @@ -0,0 +1,32 @@ +# Single-service compose file for the local "run it yourself" path. +# +# The CI / `./gradlew test` path does NOT use this file — Testcontainers spins +# up its own ephemeral PostgreSQL on a random port. This compose file exists +# so a human can do: +# +# docker compose up -d db +# ./gradlew bootRun +# +# ...without installing Postgres on their laptop. The credentials and port +# below match what application.yml reads. + +services: + db: + image: postgres:16-alpine + container_name: easy-paging-postgres-demo-db + environment: + POSTGRES_DB: easypaging + POSTGRES_USER: easypaging + POSTGRES_PASSWORD: easypaging + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U easypaging -d easypaging"] + interval: 5s + timeout: 3s + retries: 10 + volumes: + - pgdata:/var/lib/postgresql/data + +volumes: + pgdata: diff --git a/devslab-kit-demo/gradle.properties b/devslab-kit-demo/gradle.properties new file mode 100644 index 0000000..a31b0e0 --- /dev/null +++ b/devslab-kit-demo/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8 +org.gradle.parallel=true +org.gradle.caching=true diff --git a/devslab-kit-demo/gradle/wrapper/gradle-wrapper.jar b/devslab-kit-demo/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..b1b8ef5 Binary files /dev/null and b/devslab-kit-demo/gradle/wrapper/gradle-wrapper.jar differ diff --git a/devslab-kit-demo/gradle/wrapper/gradle-wrapper.properties b/devslab-kit-demo/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..df6a6ad --- /dev/null +++ b/devslab-kit-demo/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/devslab-kit-demo/gradlew b/devslab-kit-demo/gradlew new file mode 100755 index 0000000..b9bb139 --- /dev/null +++ b/devslab-kit-demo/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/devslab-kit-demo/gradlew.bat b/devslab-kit-demo/gradlew.bat new file mode 100644 index 0000000..24c62d5 --- /dev/null +++ b/devslab-kit-demo/gradlew.bat @@ -0,0 +1,82 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/devslab-kit-demo/settings.gradle.kts b/devslab-kit-demo/settings.gradle.kts new file mode 100644 index 0000000..be3a5e0 --- /dev/null +++ b/devslab-kit-demo/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "devslab-kit-demo" diff --git a/devslab-kit-demo/src/main/java/kr/devslab/example/devslabkit/DevslabKitDemoApplication.java b/devslab-kit-demo/src/main/java/kr/devslab/example/devslabkit/DevslabKitDemoApplication.java new file mode 100644 index 0000000..b9b0efa --- /dev/null +++ b/devslab-kit-demo/src/main/java/kr/devslab/example/devslabkit/DevslabKitDemoApplication.java @@ -0,0 +1,23 @@ +package kr.devslab.example.devslabkit; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * A minimal consumer of {@code devslab-kit-spring-boot-starter}. + * + *

This is the whole point of the demo: a plain + * {@code @SpringBootApplication} in the app's own package + * ({@code kr.devslab.example.devslabkit}, not under {@code kr.devslab.kit}) — no + * {@code scanBasePackages}, no {@code @EntityScan}, no {@code @EnableJpaRepositories}. + * The starter's auto-configuration registers the platform's services, JPA entities + * and repositories, the admin REST API, and the first-admin bootstrap on its own, + * broadening scanning rather than requiring the consumer to widen it. + */ +@SpringBootApplication +public class DevslabKitDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(DevslabKitDemoApplication.class, args); + } +} diff --git a/devslab-kit-demo/src/main/resources/application.yaml b/devslab-kit-demo/src/main/resources/application.yaml new file mode 100644 index 0000000..cf01d8c --- /dev/null +++ b/devslab-kit-demo/src/main/resources/application.yaml @@ -0,0 +1,53 @@ +spring: + application: + name: devslab-kit-demo + + # compose.yaml (Postgres + Redis) is started automatically on `bootRun` by + # spring-boot-docker-compose; these defaults point at it. + datasource: + url: ${DEVSLAB_DATASOURCE_URL:jdbc:postgresql://localhost:5432/demo} + username: ${DEVSLAB_DATASOURCE_USERNAME:demo} + password: ${DEVSLAB_DATASOURCE_PASSWORD:demo} + data: + redis: + host: ${DEVSLAB_REDIS_HOST:localhost} + port: ${DEVSLAB_REDIS_PORT:6379} + + jpa: + open-in-view: false + +devslab: + kit: + # Single-tenant: the context still always resolves a tenant (the `default` + # one), so the code path is identical to a multi-tenant deployment. + tenant: + mode: single + resolver: fixed + default-tenant-id: default + + identity: + jwt: + # 32+ byte key for HS256. Fine for a local demo; inject a real secret in + # production via DEVSLAB_JWT_SECRET. + secret: ${DEVSLAB_JWT_SECRET:demo-only-32byte-jwt-signing-key!!} + ttl: PT8H + + # Distributed cache backed by the compose Redis. Flip to `in-memory` to drop + # the Redis dependency, or `none` to disable caching entirely. + cache: + type: ${DEVSLAB_CACHE_TYPE:redis} + ttl: PT10M + + # First-admin bootstrap: provisions the `default` tenant, a PLATFORM_ADMIN + # role with the admin.* permissions, and an admin/admin user on first boot — + # so you can log in to the admin API immediately. The forced password change + # is off here for demo convenience; do NOT ship this shape to production. + bootstrap: + enabled: true + admin-login-id: admin + admin-password: admin + must-change-password: false + +logging: + level: + kr.devslab: DEBUG diff --git a/devslab-kit-demo/src/test/java/kr/devslab/example/devslabkit/DevslabKitDemoApplicationTests.java b/devslab-kit-demo/src/test/java/kr/devslab/example/devslabkit/DevslabKitDemoApplicationTests.java new file mode 100644 index 0000000..64c993f --- /dev/null +++ b/devslab-kit-demo/src/test/java/kr/devslab/example/devslabkit/DevslabKitDemoApplicationTests.java @@ -0,0 +1,59 @@ +package kr.devslab.example.devslabkit; + +import static org.assertj.core.api.Assertions.assertThat; + +import kr.devslab.kit.access.PermissionChecker; +import kr.devslab.kit.audit.AuditEventPublisher; +import kr.devslab.kit.identity.PasswordHasher; +import kr.devslab.kit.menu.MenuProvider; +import kr.devslab.kit.tenant.TenantResolver; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; + +/** + * Proves the starter wires the whole platform into a plain consumer app: the + * context boots against real Postgres + Redis (Testcontainers) and the key + * platform beans are present and usable. + */ +@Import(TestcontainersConfiguration.class) +@SpringBootTest +class DevslabKitDemoApplicationTests { + + @Autowired + private TenantResolver tenantResolver; + + @Autowired + private PermissionChecker permissionChecker; + + @Autowired + private MenuProvider menuProvider; + + @Autowired + private AuditEventPublisher auditEventPublisher; + + @Autowired + private PasswordHasher passwordHasher; + + @Test + void platformBeansAreAutoConfigured() { + assertThat(tenantResolver).isNotNull(); + assertThat(permissionChecker).isNotNull(); + assertThat(menuProvider).isNotNull(); + assertThat(auditEventPublisher).isNotNull(); + assertThat(passwordHasher).isNotNull(); + } + + @Test + void singleTenantResolvesTheDefaultTenant() { + assertThat(tenantResolver.resolve().tenantId().value()).isEqualTo("default"); + } + + @Test + void bcryptPasswordHasherRoundTrips() { + String hash = passwordHasher.hash("hunter2"); + assertThat(passwordHasher.matches("hunter2", hash)).isTrue(); + assertThat(passwordHasher.matches("nope", hash)).isFalse(); + } +} diff --git a/devslab-kit-demo/src/test/java/kr/devslab/example/devslabkit/TestDevslabKitDemoApplication.java b/devslab-kit-demo/src/test/java/kr/devslab/example/devslabkit/TestDevslabKitDemoApplication.java new file mode 100644 index 0000000..6903fe9 --- /dev/null +++ b/devslab-kit-demo/src/test/java/kr/devslab/example/devslabkit/TestDevslabKitDemoApplication.java @@ -0,0 +1,16 @@ +package kr.devslab.example.devslabkit; + +import org.springframework.boot.SpringApplication; + +/** + * Run the demo locally with Testcontainers-provided Postgres + Redis instead of + * the compose stack: {@code ./gradlew bootTestRun}. + */ +public class TestDevslabKitDemoApplication { + + public static void main(String[] args) { + SpringApplication.from(DevslabKitDemoApplication::main) + .with(TestcontainersConfiguration.class) + .run(args); + } +} diff --git a/devslab-kit-demo/src/test/java/kr/devslab/example/devslabkit/TestcontainersConfiguration.java b/devslab-kit-demo/src/test/java/kr/devslab/example/devslabkit/TestcontainersConfiguration.java new file mode 100644 index 0000000..9b8c1e1 --- /dev/null +++ b/devslab-kit-demo/src/test/java/kr/devslab/example/devslabkit/TestcontainersConfiguration.java @@ -0,0 +1,32 @@ +package kr.devslab.example.devslabkit; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.postgresql.PostgreSQLContainer; +import org.testcontainers.utility.DockerImageName; + +/** + * Throwaway Postgres + Redis for the test (and for {@code bootTestRun}). + * {@code @ServiceConnection} wires each container's connection details into the + * Spring context automatically — no URLs to copy. + * + *

Testcontainers 2.x (managed by Spring Boot 4) moved {@code PostgreSQLContainer} + * to {@code org.testcontainers.postgresql}. + */ +@TestConfiguration(proxyBeanMethods = false) +class TestcontainersConfiguration { + + @Bean + @ServiceConnection + PostgreSQLContainer postgresContainer() { + return new PostgreSQLContainer(DockerImageName.parse("postgres:16-alpine")); + } + + @Bean + @ServiceConnection(name = "redis") + GenericContainer redisContainer() { + return new GenericContainer<>(DockerImageName.parse("redis:7-alpine")).withExposedPorts(6379); + } +}