diff --git a/.github/workflows/dev-ci-cd.yml b/.github/workflows/dev-ci-cd.yml index 692b2f5..dab4fd9 100644 --- a/.github/workflows/dev-ci-cd.yml +++ b/.github/workflows/dev-ci-cd.yml @@ -46,27 +46,16 @@ jobs: needs: test runs-on: ubuntu-24.04 permissions: - contents: write + contents: read packages: write steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - fetch-depth: 0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - - name: Set up Kustomize - run: | - KUSTOMIZE_VERSION=5.7.1 - curl -sSL -o /tmp/kustomize.tar.gz \ - "https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv${KUSTOMIZE_VERSION}/kustomize_v${KUSTOMIZE_VERSION}_linux_amd64.tar.gz" - tar -xzf /tmp/kustomize.tar.gz -C /tmp - sudo mv /tmp/kustomize /usr/local/bin/kustomize - kustomize version - - name: Normalize image name run: | echo "IMAGE_NAME=ghcr.io/$(echo "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')/project-auth-server" >> "$GITHUB_ENV" @@ -83,26 +72,8 @@ jobs: uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: . - file: ./Dockerfile + file: ./deploy/docker/Dockerfile push: true tags: | ${{ env.IMAGE_NAME }}:dev ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} - - - name: Update dev image tag for Argo CD - run: | - cd k8s/dev - kustomize edit set image "${IMAGE_NAME}=${IMAGE_NAME}:${IMAGE_TAG}" - - - name: Commit and push manifest update - run: | - if git diff --quiet -- k8s/dev/kustomization.yaml; then - echo "No manifest change detected" - exit 0 - fi - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add k8s/dev/kustomization.yaml - git commit -m "chore: update auth-server dev image to ${IMAGE_TAG}" - git push diff --git a/README.md b/README.md index 2c79972..146323b 100644 --- a/README.md +++ b/README.md @@ -1,539 +1,176 @@ # Project-Auth-Server -인증과 인가를 담당하는 서버입니다. - -## 현재 브랜치 범위 - -- 회원 도메인 구성 -- 회원가입 API 구현 -- 로그인 API 및 JWT 발급 기반 구현 -- JPA 기반 사용자 영속성 어댑터 구성 -- Flyway 기반 스키마 마이그레이션 추가 -- Keycloak 기반 OAuth2 소셜 로그인 시작점 구성 -- Keycloak 로그인 성공 후 auth-server 내부 JWT 재발급 흐름 추가 -- `local`, `dev`, `prod` 환경 설정 분리 -- `.env` 실행 설정 파일과 로컬 `docker compose` 실행 기준 추가 -- auth-server 정적 로그인 페이지 추가 -- RS256 기반 JWT 발급과 JWK Set 공개 -- `api-server`, `worker` 검증 준비용 well-known 메타데이터 추가 - -## 모듈 구성 - -- `bootstrap` - - 애플리케이션 실행 모듈입니다. - - Spring Boot 설정과 런타임 구성을 담당합니다. -- `presentation` - - Controller, 요청/응답 DTO, Mapper를 담당합니다. - - 외부 요청을 애플리케이션 유스케이스로 전달하는 진입점입니다. -- `application` - - 유스케이스, 커맨드/결과 DTO, 포트를 담당합니다. - - 도메인 규칙을 조합해 실제 요청 흐름을 처리합니다. -- `domain` - - 엔티티, 값 객체, 도메인 서비스, 도메인 예외를 담당합니다. - - 특정 프레임워크나 저장소 구현에 의존하지 않습니다. -- `infrastructure` - - 영속성 어댑터, 외부 시스템 연동, 보안 연동 구현을 담당합니다. - - Keycloak, DB, Kafka 같은 외부 자원과 맞닿는 계층입니다. -- `common` - - 공통 응답 모델과 외부 계층에서 함께 사용하는 공통 요소를 담당합니다. - -## 모듈 내부 패키지 규칙 - -- `bootstrap` - - 애플리케이션 조립과 설정만 둡니다. - - 예: `config/auth`, `config/openapi` -- `application` - - 기능별 유스케이스를 기준으로 둡니다. - - 유스케이스 안에서 `port/in`, `port/out`으로 구분합니다. - - 예: `auth/login/port/in`, `auth/login/port/out`, `user/signup/port/in`, `user/signup/port/out`, `support/exception` -- `domain` - - 도메인 개념별로 묶습니다. - - 예: `user/model`, `user/exception`, `user/service` -- `presentation` - - 외부 진입점도 기능별로 나눕니다. - - 예: `auth/controller`, `auth/dto`, `user/controller`, `support/exception` -- `infrastructure` - - 기술 구현과 대상 자원 기준으로 나눕니다. - - 예: `persistence/user/entity`, `persistence/user/repository`, `persistence/user/mapper`, `security/password`, `security/token` - -새 기능을 추가할 때는 먼저 `기능(auth, user 등)`을 고르고, 그 안에서 `controller`, `dto`, `service`, `exception`처럼 책임에 맞는 위치에 배치하는 것을 기본 규칙으로 삼습니다. - -## 현재 구현 방향 - -- `bootstrap -> presentation, infrastructure` -- `presentation -> application, common` -- `infrastructure -> application, domain` -- `application -> domain` -- `domain -> none` -- `common -> none` - -- 실행 가능한 Spring Boot 모듈은 `bootstrap` 하나만 둡니다. -- 현재 브랜치에서는 PostgreSQL + JPA + Flyway 기준으로 회원가입/로그인 흐름을 검증합니다. -- OAuth2는 Keycloak을 OIDC 공급자로 사용하고, Google/GitHub 브로커는 `kc_idp_hint`로 분기합니다. -- 응답은 공통 응답 형식으로 감싸서 반환합니다. -- JWT는 auth-server가 RS256으로 직접 발급하고 `issuer`, `key pair`, `expiration`은 설정값으로 관리합니다. -- JPA는 `ddl-auto=validate`로만 두고, 스키마 변경은 Flyway 스크립트로 관리합니다. -- 실행 설정 파일은 `bootstrap` 모듈에 두고, 실제 값은 프로파일별 yml과 환경 변수로 분리합니다. - -## 현재 브랜치에서 확인할 수 있는 기능 - -- 회원 도메인, 값 객체, 회원가입/로그인 유스케이스 -- 회원가입 API, 로그인 API, 공통 응답 구조 -- Keycloak Google/GitHub 소셜 로그인 진입 API -- OAuth2 로그인 성공 후 auth-server 자체 JWT 재발급 -- JPA 사용자 엔티티, Spring Data JPA 저장소, 영속성 매퍼 -- Flyway 마이그레이션 스크립트 -- Swagger 기반 API 문서 -- Postman 컬렉션 기반 수동 검증 -- PostgreSQL + Keycloak 로컬 인프라 구성 -- auth-server 로그인 페이지(`/login`) -- JWT 공개키 노출 엔드포인트 - - `GET /.well-known/openid-configuration` - - `GET /.well-known/jwks.json` - -## 환경 설정 전략 - -프로파일은 `local`, `dev`, `prod`로 분리합니다. - -- `application.yml` - - 공통 설정만 둡니다. - - 프로파일 공통 JPA, OpenAPI, OAuth2 registration id 같은 값을 관리합니다. -- `application-local.yml` - - 로컬 개발용 기본값을 둡니다. - - 기본 datasource는 H2를 사용합니다. - - 필요하면 환경 변수로 PostgreSQL 연결값을 덮어쓸 수 있습니다. -- `application-dev.yml` - - 개발 환경에서 필요한 값을 환경 변수로 주입받습니다. -- `application-prod.yml` - - 운영 환경에서 필요한 값을 환경 변수로 주입받습니다. - -`.env` 파일은 Spring Boot가 직접 읽는 파일이라기보다, 로컬 셸이나 `docker compose`, IDE 실행 설정이 환경 변수로 주입할 수 있도록 돕는 실행 설정 파일로 사용합니다. - -- `.env.local` -- `.env.dev` -- `.env.prod` - -## 로컬 인프라 - -로컬에서는 루트의 `docker-compose.yml`로 PostgreSQL, Keycloak, Vault를 함께 띄웁니다. - -- PostgreSQL - - auth-server용 DB: `project_auth` - - Keycloak용 DB: `keycloak` -- Keycloak - - PostgreSQL을 외부 DB로 사용합니다. - - 로컬 realm import 파일은 `docs/keycloak/realm/project-auth-realm-local.json`에 둡니다. -- Vault - - dev mode로 기동합니다. - - `vault-init` 서비스가 transit engine과 로컬 signing key를 자동으로 준비합니다. - -같은 PostgreSQL 인스턴스를 쓰더라도 auth-server와 Keycloak은 DB를 분리합니다. 애플리케이션 테이블과 Keycloak 관리 테이블을 한 DB에 섞지 않는 것을 기본 기준으로 잡습니다. - -## 실행 방법 - -먼저 `.env.local` 값을 현재 로컬 환경에 맞게 확인한 뒤, 로컬 인프라를 실행합니다. - -```bash -docker compose --env-file .env.local up -d +인증과 인가를 담당하는 서버입니다. 이 저장소는 **애플리케이션 소스 코드와 로컬 개발 환경**을 중심으로 유지합니다. +Kubernetes, Argo CD, Sealed Secrets 같은 운영 선언은 이제 별도 GitOps 저장소인 `Project-Auth-GitOps`에서 관리합니다. + +## 아키텍처 정리 + +### 변경 전 구조 + +```mermaid +flowchart TD + Root["project-auth-server/"] + Root --> Bootstrap["bootstrap/"] + Root --> Domain["domain/"] + Root --> Application["application/"] + Root --> Presentation["presentation/"] + Root --> Infrastructure["infrastructure/"] + Root --> Common["common/"] + Root --> K8s["k8s/"] + Root --> Argo["argocd/"] + Root --> Docker["docker/"] + Root --> Dockerfile["Dockerfile"] + Root --> Compose["docker-compose.yml"] + Root --> Docs["docs/"] ``` -이후 현재 셸에 환경 변수를 올린 뒤 애플리케이션을 실행합니다. - -```bash -set -a -source .env.local -set +a - -./gradlew :bootstrap:bootRun +### 변경 전 문제점 + +- 소스 코드와 운영 배포 자산이 같은 저장소 루트에 섞여 있었습니다. +- `common` 모듈이 사실상 `ApiResult` 하나 때문에 존재해, 잡동사니 모듈로 커질 위험이 있었습니다. +- `application` 계층의 에러 코드가 HTTP status를 알고 있어 웹 의미가 새고 있었습니다. +- 운영 선언이 앱 repo와 GitOps repo에 동시에 남을 수 있어 source of truth가 흔들릴 여지가 있었습니다. + +### 변경 후 구조 + +```mermaid +flowchart TD + Root["project-auth-server/"] + Root --> Bootstrap["bootstrap/"] + Root --> Domain["domain/"] + Root --> Application["application/"] + Root --> Presentation["presentation/"] + Root --> Infrastructure["infrastructure/"] + Root --> Docs["docs/"] + Root --> Deploy["deploy/docker"] + Root --> Examples["examples/legacy/"] + Root --> Build["build files"] + + Docs --> ArchDocs["architecture/"] + Docs --> DevDocs["development/"] + Docs --> OpsDocs["operations/"] + Docs --> SecurityDocs["security/"] ``` -애플리케이션이 실행되면 기본 포트는 `8080`입니다. - -애플리케이션 실행 전에 대상 DB와 Keycloak이 먼저 떠 있어야 합니다. -로컬 Keycloak 설정 절차는 `docs/keycloak/LOCAL_SETUP.md` 문서를 기준으로 맞춥니다. -Vault Transit 로컬 확인 절차는 `docs/vault/LOCAL_SETUP.md`를 참고합니다. - -브라우저에서 `http://localhost:8080/login` 으로 들어가면 auth-server가 직접 제공하는 로그인 페이지를 확인할 수 있습니다. - -로컬에서 환경 변수를 따로 주지 않으면 H2 메모리 DB 기준으로 실행됩니다. 기존 `.env.local`에 `APP_DATASOURCE_*` 값이 들어 있으면 그 값이 우선 적용되어 PostgreSQL로 연결됩니다. - -필수 DB 설정은 아래 환경 변수로 제어합니다. - -- `APP_DATASOURCE_URL` -- `APP_DATASOURCE_USERNAME` -- `APP_DATASOURCE_PASSWORD` - -JWT 설정은 아래 환경 변수로 덮어쓸 수 있습니다. - -- `APP_SECURITY_JWT_ISSUER` -- `APP_SECURITY_JWT_ACTIVE_KEY_ID` -- `APP_SECURITY_JWT_GENERATE_KEY_PAIR_ON_STARTUP` -- `APP_SECURITY_JWT_ACCESS_TOKEN_EXPIRATION` -- `APP_SECURITY_JWT_KEYS_0_KEY_ID` -- `APP_SECURITY_JWT_KEYS_0_PUBLIC_KEY` -- `APP_SECURITY_JWT_KEYS_0_PRIVATE_KEY` -- `APP_SECURITY_JWT_KEYS_1_KEY_ID` -- `APP_SECURITY_JWT_KEYS_1_PUBLIC_KEY` -- `APP_SECURITY_JWT_KEYS_1_PRIVATE_KEY` -- `APP_SECURITY_JWT_VAULT_ENABLED` -- `APP_SECURITY_JWT_VAULT_ADDRESS` -- `APP_SECURITY_JWT_VAULT_TOKEN` -- `APP_SECURITY_JWT_VAULT_MOUNT_PATH` -- `APP_SECURITY_JWT_VAULT_TRANSIT_KEY_NAME` - -Keycloak OAuth2 설정은 아래 환경 변수로 제어합니다. - -- `APP_SECURITY_OAUTH2_KEYCLOAK_ISSUER_URI` -- `APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_ID` -- `APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET` -- `APP_SECURITY_OAUTH2_GOOGLE_IDP_HINT` -- `APP_SECURITY_OAUTH2_GITHUB_IDP_HINT` - -로컬 인프라용 환경 변수는 아래 파일에서 함께 관리합니다. - -- `.env.local` - - auth-server 로컬 실행 - - `docker compose` 로컬 인프라 실행 -- `.env.dev` - - 개발 환경 예시 -- `.env.prod` - - 운영 환경 예시 - -## 애플리케이션 이미지 빌드 - -루트의 [Dockerfile](/home/donghyeon/dev/Project-Auth-Server/Dockerfile)로 `auth-server` 이미지를 빌드할 수 있습니다. - -현재 Dockerfile은 멀티스테이지 빌드로 동작합니다. - -1. Gradle로 `:bootstrap:bootJar` 생성 -2. BuildKit cache mount로 Gradle 캐시 재사용 -3. Spring Boot `jarmode=tools`로 layered jar를 추출 -4. dependency / boot loader / application 레이어를 분리 복사 -5. 최종 JRE 이미지에 필요한 레이어만 복사 -6. non-root 사용자로 애플리케이션 실행 -7. `/actuator/health` 기반 Docker healthcheck 포함 - -로컬 빌드: - -```bash -docker build -t project-auth-server:local . -``` - -로컬 실행 예시: - -```bash -docker run --rm -p 8080:8080 \ - --env-file .env.local \ - -e SPRING_PROFILES_ACTIVE=local \ - project-auth-server:local +### 어떤 점이 완화되었는가 + +- 앱 repo는 소스와 로컬 개발 환경에 집중하고, 운영 선언은 GitOps repo로 분리했습니다. +- `ApiResult`와 성공 응답 코드를 `presentation`으로 이동해 HTTP 응답 의미를 표현 계층으로 모았습니다. +- `ErrorCode`에서 HTTP status를 제거하고, status 매핑은 `presentation`의 `ApiErrorHttpStatusMapper`가 담당하게 했습니다. +- `common` 모듈을 제거해 모호한 공통 모듈이 커질 위험을 줄였습니다. +- ArchUnit 테스트를 추가해 레이어 규칙을 실제 테스트로 검증하게 했습니다. + +## 현재 폴더 구조 + +```text +project-auth-server/ +├─ bootstrap/ +├─ domain/ +├─ application/ +├─ presentation/ +├─ infrastructure/ +├─ docs/ +│ ├─ architecture/ +│ ├─ development/ +│ ├─ operations/ +│ └─ security/ +├─ deploy/ +│ ├─ docker/ +│ └─ scripts/ +├─ examples/ +│ └─ legacy/ +└─ build files ``` -같은 이미지를 `dev`, `prod`에서 함께 사용하려면 런타임에 `SPRING_PROFILES_ACTIVE`만 다르게 주입하면 됩니다. +## 레이어 규칙 -```bash -docker run --rm -p 8080:8080 \ - -e SPRING_PROFILES_ACTIVE=dev \ - -e APP_DATASOURCE_URL=jdbc:postgresql://host:5432/project_auth \ - -e APP_DATASOURCE_USERNAME=project_auth \ - -e APP_DATASOURCE_PASSWORD=project_auth \ - project-auth-server:local -``` +### 규칙 1 -컨테이너에서 JVM 옵션이 필요하면 `JAVA_TOOL_OPTIONS`로 주입합니다. 현재 엔트리포인트는 `java -jar /app/application.jar` 형태라 JVM이 `JAVA_TOOL_OPTIONS`를 자동으로 읽습니다. +`domain`은 Spring, JPA, Controller를 모릅니다. -```bash -docker run --rm -p 8080:8080 \ - --env-file .env.local \ - -e SPRING_PROFILES_ACTIVE=local \ - -e JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75.0" \ - project-auth-server:local -``` +### 규칙 2 -Docker BuildKit이 비활성화된 환경이라면 아래처럼 켜서 빌드합니다. +`application`은 “무슨 일을 한다”를 담당합니다. -```bash -DOCKER_BUILDKIT=1 docker build -t project-auth-server:local . -``` +### 규칙 3 -개발/운영 환경에서는 `.env.*` 파일을 이미지에 포함하지 않고, 런타임 환경 변수 또는 Kubernetes `ConfigMap`/`Secret`으로 주입하는 것을 기본 기준으로 합니다. -현재 베이스 이미지는 digest까지 고정해 두어 재현성을 높였고, 이후 보안 패치 주기에 맞춰 digest를 갱신하는 방식으로 운영하는 것을 권장합니다. +`presentation`은 HTTP 입출력과 응답 계약만 담당합니다. -Actuator health endpoint는 아래 경로를 사용합니다. +### 규칙 4 -- `/actuator/health` -- `/actuator/health/liveness` -- `/actuator/health/readiness` -- `/livez` -- `/readyz` +`infrastructure`는 기술 구현체만 담당합니다. -## Dev CI/CD +### 규칙 5 -이 저장소는 [`.github/workflows/dev-ci-cd.yml`](/home/donghyeon/dev/Project-Auth-Server/.github/workflows/dev-ci-cd.yml) 기준으로 dev CI/CD를 구성합니다. +`bootstrap`이 전부 조립합니다. -- Pull Request to `develop` - - `./gradlew test` -- Push to `develop` - - `./gradlew test` - - `ghcr.io//project-auth-server:dev` - - `ghcr.io//project-auth-server:` - 두 태그로 이미지를 빌드/푸시 - - `k8s/dev/kustomization.yaml`의 `newTag`를 새 ``로 갱신 - - 같은 `develop` 브랜치에 manifest 변경 커밋 반영 - - Argo CD가 `develop`을 감시 중이면 새 태그를 sync +### 규칙 6 -현재 dev 배포 선언은 아래 파일로 관리합니다. +배포/운영 파일은 앱 소스 바깥 저장소 또는 `deploy/` 아래로 분리합니다. -- Kustomize: [k8s/dev/kustomization.yaml](/home/donghyeon/dev/Project-Auth-Server/k8s/dev/kustomization.yaml) -- Argo CD AppProject: [argocd/auth-dev-project.yaml](/home/donghyeon/dev/Project-Auth-Server/argocd/auth-dev-project.yaml) -- Argo CD Application: [argocd/dev-auth-server-application.yaml](/home/donghyeon/dev/Project-Auth-Server/argocd/dev-auth-server-application.yaml) +### 규칙 7 -auth-server dev 배포에는 [k8s/dev/db-migration-job.yaml](/home/donghyeon/dev/Project-Auth-Server/k8s/dev/db-migration-job.yaml)이 포함되어 있어, Argo CD sync 시 migration job이 먼저 실행되고 그 뒤 애플리케이션 Deployment가 따라오는 흐름을 기대합니다. +문서는 `architecture / development / operations / security`로 분리합니다. -민감값은 Git에 직접 올리지 않고, [k8s/dev/secret.yaml](/home/donghyeon/dev/Project-Auth-Server/k8s/dev/secret.yaml)에 키 구조만 유지한 채 placeholder 값만 둡니다. -현재 dev 구성은 secret까지 Argo CD가 직접 생성하는 방식이 아니라, 실제 secret은 namespace에 사전 생성하고 Argo CD는 그 참조만 배포하는 방식입니다. +이 규칙은 [LayerDependencyArchitectureTest.java](/home/donghyeon/dev/Project-Auth-Server/bootstrap/src/test/java/com/project/auth/architecture/LayerDependencyArchitectureTest.java)에서 ArchUnit으로 검증합니다. -현재 workflow는 아래 기준으로 정리되어 있습니다. +## 소스 모듈 -- runner: `ubuntu-24.04` -- major tag 대신 명시적 action version 사용 -- 현재 단일 아키 빌드만 하므로 QEMU 제거 -- 배포 기준 태그는 `dev`가 아니라 `` -- `dev`는 편의용 moving tag로만 유지 +- `bootstrap` + - 애플리케이션 진입점과 설정 조립을 담당합니다. +- `domain` + - 엔티티, 값 객체, 도메인 예외, 도메인 정책을 둡니다. +- `application` + - 유스케이스, 커맨드/결과 DTO, 포트를 둡니다. +- `presentation` + - Controller, 요청/응답 DTO, 응답 래퍼, HTTP 예외 매핑을 둡니다. +- `infrastructure` + - JPA, 외부 시스템, 보안/토큰 같은 기술 구현체를 둡니다. -dev namespace에서 먼저 필요한 secret은 아래 두 개입니다. +## 로컬 개발 환경 -1. GHCR pull secret +로컬 인프라는 [deploy/docker/docker-compose.yml](/home/donghyeon/dev/Project-Auth-Server/deploy/docker/docker-compose.yml)로 띄웁니다. ```bash -kubectl create secret docker-registry ghcr-regcred \ - --namespace auth-dev \ - --docker-server=ghcr.io \ - --docker-username= \ - --docker-password= +docker compose -f deploy/docker/docker-compose.yml --env-file .env.local up -d ``` -2. auth-server secret +애플리케이션 실행: ```bash -kubectl apply -f k8s/dev/namespace.yaml -kubectl apply -f k8s/dev/serviceaccount.yaml -kubectl apply -f k8s/dev/service.yaml -kubectl apply -f k8s/dev/configmap.yaml - -cp k8s/dev/secret.yaml /tmp/auth-server-secret.yaml -# /tmp/auth-server-secret.yaml 안의 change-me 값을 실제 값으로 교체 -kubectl apply -f /tmp/auth-server-secret.yaml -``` - -Argo CD는 클러스터에 별도 설치해야 합니다. 이 저장소는 Argo CD가 읽을 `Application` 선언만 함께 관리합니다. -적용 순서는 보통 `AppProject -> Application` 순서로 가져갑니다. - -현재 dev namespace는 Pod Security Admission 기준으로 아래 라벨을 사용합니다. - -- `enforce=baseline` -- `warn=restricted` -- `audit=restricted` - -Deployment는 이에 맞춰 `runAsNonRoot`, `seccompProfile: RuntimeDefault`, `allowPrivilegeEscalation: false`, `capabilities.drop: [ALL]`, `startupProbe`, `livenessProbe`, `readinessProbe`를 포함합니다. - -## Platform services for dev - -auth-server가 실제로 기동되려면 `Postgres`, `Keycloak`, `Vault`가 먼저 필요합니다. dev 기준 공용 platform 서비스 manifest는 아래 경로로 관리합니다. - -- Kustomize: [k8s/platform-dev/kustomization.yaml](/home/donghyeon/dev/Project-Auth-Server/k8s/platform-dev/kustomization.yaml) -- Argo CD Application: [argocd/dev-platform-application.yaml](/home/donghyeon/dev/Project-Auth-Server/argocd/dev-platform-application.yaml) - -`platform-dev`는 별도 `AppProject`를 두지 않고 기존 [argocd/auth-dev-project.yaml](/home/donghyeon/dev/Project-Auth-Server/argocd/auth-dev-project.yaml)을 함께 사용합니다. 현재 단계에서는 같은 저장소/같은 dev 환경에서 auth-server와 공용 platform 서비스를 같이 관리하는 편이 단순하고 충분합니다. - -현재 platform manifest는 아래 서비스 이름을 기준으로 앱과 연결됩니다. - -- `postgres.platform.svc.cluster.local` -- `keycloak.platform.svc.cluster.local:8081` -- `vault.platform.svc.cluster.local` - -민감값은 [k8s/platform-dev/secret.yaml](/home/donghyeon/dev/Project-Auth-Server/k8s/platform-dev/secret.yaml)에 placeholder만 두고, 실제 값으로 채운 뒤 namespace에 수동 적용하는 방식을 기준으로 합니다. - -## Flyway 운영 기준 - -Flyway는 스키마 변경을 추적하기 위해 이번 브랜치에서 적용했습니다. 다만 운영 환경에서 애플리케이션 Pod가 스케일 아웃될 때마다 마이그레이션을 시도하게 두는 구조는 지양합니다. - -이 프로젝트는 기본적으로 애플리케이션 시작 시 Flyway를 실행하지 않습니다. - -- 기본값: `APP_PERSISTENCE_MIGRATION_RUN_ON_STARTUP=false` -- 일반 애플리케이션 Pod: `false` -- 마이그레이션 전용 Job/배포 단계: `true` - -즉, 운영에서는 보통 아래 순서로 가져갑니다. - -1. 마이그레이션 전용 Job 또는 CI/CD 단계가 DB에 먼저 붙어서 Flyway를 실행 -2. 마이그레이션이 끝난 뒤 애플리케이션 Pod를 롤아웃 - -로컬에서 마이그레이션만 실행하고 싶다면, 같은 애플리케이션 이미지를 사용하더라도 아래처럼 별도 실행 컨텍스트로 분리하는 방식을 권장합니다. +set -a +source .env.local +set +a -```bash -APP_PERSISTENCE_MIGRATION_RUN_ON_STARTUP=true \ -SPRING_MAIN_WEB_APPLICATION_TYPE=none \ ./gradlew :bootstrap:bootRun ``` -이 방식은 “앱 서버가 뜰 때마다 Flyway를 돈다”가 아니라, “DB에 대해 한 번만 실행하는 마이그레이션 프로세스”를 따로 두는 운영 형태를 연습하기 위한 기준입니다. - -Kubernetes에서 같은 이미지를 마이그레이션 전용 Job으로 분리하는 예시는 `docs/k8s/auth-db-migration-job.yaml` 파일을 참고하면 됩니다. - -## Keycloak 설정 기준 - -auth-server는 Google/GitHub와 직접 연결하지 않고 Keycloak과만 연결합니다. 따라서 소셜 로그인용 Client ID와 Secret은 Keycloak에 등록해야 합니다. - -- auth-server에 넣는 값 - - Keycloak realm issuer - - Keycloak client id - - Keycloak client secret -- Keycloak에 넣는 값 - - Google OAuth Client ID / Secret - - GitHub OAuth App Client ID / Secret - -현재 로컬 기준으로 auth-server는 아래 Keycloak 설정을 기대합니다. - -- realm: `project-auth` -- client id: `project-auth-server` -- client secret: `project-auth-server-secret` -- Google provider alias: `google` -- GitHub provider alias: `github` - -자세한 순서는 `docs/keycloak/LOCAL_SETUP.md`를 참고하면 됩니다. - -## API 문서 - -- Swagger UI: `http://localhost:8080/swagger-ui.html` -- OpenAPI JSON: `http://localhost:8080/v3/api-docs` - -현재 브랜치에서는 회원가입 API와 로그인 API를 기준으로 문서를 확인할 수 있습니다. - -추가로 아래 OAuth2 시작 API도 문서에서 확인할 수 있습니다. - -- `GET /api/v1/auth/oauth2/keycloak/google` -- `GET /api/v1/auth/oauth2/keycloak/github` - -위 두 API는 Keycloak 인증 화면으로 리다이렉트되며, 인증이 완료되면 auth-server가 내부 JWT를 다시 발급한 JSON 응답을 반환합니다. - -## JWT 공개키 검증 기준 - -이 브랜치부터 auth-server는 대칭키가 아니라 RS256으로 JWT를 발급합니다. 그리고 키 회전을 위해 “현재 서명 키”와 “이전 검증 키”를 함께 관리할 수 있게 구성합니다. - -- 로컬 - - 공개키/개인키를 따로 주지 않으면 시작 시 임시 RSA 키 쌍을 생성합니다. - - 따라서 재시작 전후 토큰이 계속 유효해야 하는 상황이면 `.env.local`에 키를 직접 넣어야 합니다. -- `dev`, `prod` - - `APP_SECURITY_JWT_ACTIVE_KEY_ID`로 현재 서명 키를 지정합니다. - - `APP_SECURITY_JWT_KEYS_N_*` 묶음으로 현재 키와 이전 키를 함께 넣습니다. - - 현재 활성 키는 private/public key를 모두 가져야 하고, 이전 키는 public key만 있어도 됩니다. - - `APP_SECURITY_JWT_GENERATE_KEY_PAIR_ON_STARTUP=false`를 유지합니다. - -공개 메타데이터는 아래 엔드포인트로 노출합니다. - -- OpenID metadata: `http://localhost:8080/.well-known/openid-configuration` -- JWK Set: `http://localhost:8080/.well-known/jwks.json` - -`api-server`나 `worker`가 Spring Security Resource Server로 검증하려면 보통 아래처럼 붙이면 됩니다. - -```yaml -spring: - security: - oauth2: - resourceserver: - jwt: - issuer-uri: http://localhost:8080 -``` - -위 설정을 사용하면 auth-server의 `issuer`와 `jwks_uri`를 기준으로 공개키를 자동 조회해 JWT 서명을 검증할 수 있습니다. +주요 로컬 참고 문서: -로컬에서 고정 키를 쓰고 싶으면 X.509 public key와 PKCS#8 private key를 Base64 또는 PEM 형식으로 환경 변수에 넣으면 됩니다. 관련 설정 위치는 아래 파일을 참고합니다. +- [docs/development/keycloak/LOCAL_SETUP.md](/home/donghyeon/dev/Project-Auth-Server/docs/development/keycloak/LOCAL_SETUP.md) +- [docs/development/vault-local-setup.md](/home/donghyeon/dev/Project-Auth-Server/docs/development/vault-local-setup.md) +- [docs/development/postman/auth-core.postman_collection.json](/home/donghyeon/dev/Project-Auth-Server/docs/development/postman/auth-core.postman_collection.json) -- `.env.local` -- `.env.dev` -- `.env.prod` +## 이미지 빌드 -OpenSSL로 키를 생성한 뒤 Base64로 환경 변수에 넣으려면 보통 아래 순서로 준비합니다. +이미지 빌드는 [deploy/docker/Dockerfile](/home/donghyeon/dev/Project-Auth-Server/deploy/docker/Dockerfile)을 사용합니다. ```bash -openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048 -openssl rsa -pubout -in private_key.pem -out public_key.pem - -base64 -w 0 private_key.pem -base64 -w 0 public_key.pem +docker build -f deploy/docker/Dockerfile -t project-auth-server:local . ``` -활성 키는 `APP_SECURITY_JWT_ACTIVE_KEY_ID`로 고르고, 각 키의 material은 `APP_SECURITY_JWT_KEYS_N_PUBLIC_KEY`, `APP_SECURITY_JWT_KEYS_N_PRIVATE_KEY`로 넣습니다. 이전 키는 검증 전용이므로 private key 없이 public key만 남겨둘 수 있습니다. - -예를 들어 키를 회전하는 시점에는 이런 식으로 운영합니다. - -- `APP_SECURITY_JWT_ACTIVE_KEY_ID=auth-rsa-2` -- `APP_SECURITY_JWT_KEYS_0_KEY_ID=auth-rsa-1` -- `APP_SECURITY_JWT_KEYS_0_PUBLIC_KEY=...` -- `APP_SECURITY_JWT_KEYS_0_PRIVATE_KEY=` 비워둠 -- `APP_SECURITY_JWT_KEYS_1_KEY_ID=auth-rsa-2` -- `APP_SECURITY_JWT_KEYS_1_PUBLIC_KEY=...` -- `APP_SECURITY_JWT_KEYS_1_PRIVATE_KEY=...` +현재 Dockerfile은: -이 상태에서는 새 토큰은 `auth-rsa-2`로 발급하고, 기존 `auth-rsa-1`로 서명된 토큰은 만료될 때까지 계속 검증할 수 있습니다. +1. `application.jar` +2. `migration.jar` -현재 구현은 `ConfiguredJwtSigningKeySource`가 환경 변수 기반으로 키를 읽습니다. 이후 Secret 관리가 더 고도화되면 같은 `JwtSigningKeySource` 인터페이스를 구현하는 방식으로 Vault, AWS KMS, GCP KMS, HSM 연동으로 확장할 수 있습니다. 즉 이번 브랜치에서는 KMS/HSM 자체를 붙이기보다, 그 방향으로 갈 수 있도록 키 소스를 분리해 둔 상태입니다. +두 개를 함께 만들고, 마이그레이션 전용 바이너리도 같이 포함합니다. -## Refresh Token 정책 기준 +## 운영 자산 관리 원칙 -현재 auth-server는 refresh token을 아직 발급하지 않고, access token만 JSON 응답으로 반환합니다. 따라서 아래 항목은 이번 브랜치 범위에 포함하지 않습니다. - -- refresh token 발급 -- 재발급 API -- HttpOnly cookie 저장 전략 -- refresh token 회전 및 폐기 정책 - -이유는 이 항목들이 단순 토큰 필드 추가 수준이 아니라, 아래 설계를 함께 요구하기 때문입니다. - -- 저장 위치 - - DB, Redis, stateless token 중 무엇을 기준으로 할지 -- 재발급 계약 - - API 응답 모델, 오류 코드, 만료/폐기 규칙 -- 브라우저 보안 정책 - - `HttpOnly`, `Secure`, `SameSite`, CSRF 대응 -- 로그아웃 및 세션 무효화 기준 - -이번 브랜치에서는 먼저 access token 서명 구조를 Vault Transit까지 확장 가능한 형태로 정리하고, refresh token은 별도 브랜치에서 다루는 것을 기본 방침으로 잡습니다. 순서는 아래처럼 가져갑니다. - -1. Vault Transit 기반 access token 서명 구조 정리 -2. refresh token 저장/재발급 정책 확정 -3. cookie 전략과 브라우저 보안 정책 확정 -4. 재발급 API와 로그아웃/폐기 흐름 구현 - -## Postman 사용 방법 - -`docs/postman/auth-core.postman_collection.json` 파일을 Postman에 import 하면 바로 테스트할 수 있습니다. - -컬렉션에는 아래 요청이 포함되어 있습니다. - -- 회원가입 성공 -- 로그인 성공 -- 회원가입 중복 이메일 -- 로그인 인증 실패 -- 회원가입 요청값 검증 실패 - -기본 변수는 `baseUrl=http://localhost:8080` 으로 설정되어 있습니다. - -소셜 로그인은 브라우저 리다이렉트 기반이므로 Postman보다 브라우저에서 테스트하는 편이 맞습니다. - -## 테스트 - -```bash -./gradlew test -``` +이 repo는 더 이상 Kubernetes/Argo CD 운영 선언을 source of truth로 들고 있지 않습니다. -테스트 범위는 다음을 포함합니다. +- 앱 repo: 소스 코드, 로컬 docker 개발 환경 +- GitOps repo (`Project-Auth-GitOps`): Kubernetes manifest, Argo CD application, SealedSecret, 환경별 overlay -- 회원가입 유스케이스 단위 테스트 -- 로그인 유스케이스 단위 테스트 -- OAuth2 로그인 유스케이스 단위 테스트 -- H2 기반 사용자 영속성 통합 테스트 -- OAuth2 시작 컨트롤러 테스트 -- 로그인 컨트롤러 테스트 -- 회원가입 컨트롤러 테스트 -- Swagger OpenAPI 노출 통합 테스트 +즉 운영 변경은 `Project-Auth-GitOps`에서 관리하고, 이 repo의 CI는 이미지 빌드와 푸시까지만 담당합니다. diff --git a/application/src/main/java/com/project/auth/application/auth/exception/AuthErrorCode.java b/application/src/main/java/com/project/auth/application/auth/exception/AuthErrorCode.java index 24a1788..ad8dcb8 100644 --- a/application/src/main/java/com/project/auth/application/auth/exception/AuthErrorCode.java +++ b/application/src/main/java/com/project/auth/application/auth/exception/AuthErrorCode.java @@ -3,27 +3,20 @@ import com.project.auth.application.support.exception.ErrorCode; public enum AuthErrorCode implements ErrorCode { - INVALID_CREDENTIALS(401, "AUTH-001", "이메일 또는 비밀번호가 올바르지 않습니다."), - OAUTH_USER_INFO_INVALID(400, "AUTH-002", "소셜 로그인 사용자 정보가 올바르지 않습니다."), - OAUTH_ACCOUNT_CONFLICT(409, "AUTH-003", "동일한 이메일의 기존 계정이 있어 소셜 로그인을 진행할 수 없습니다."), - OAUTH_LOGIN_FAILED(401, "AUTH-004", "소셜 로그인에 실패했습니다."), - UNSUPPORTED_OAUTH_PROVIDER(400, "AUTH-005", "지원하지 않는 OAuth2 공급자입니다."); + INVALID_CREDENTIALS("AUTH-001", "이메일 또는 비밀번호가 올바르지 않습니다."), + OAUTH_USER_INFO_INVALID("AUTH-002", "소셜 로그인 사용자 정보가 올바르지 않습니다."), + OAUTH_ACCOUNT_CONFLICT("AUTH-003", "동일한 이메일의 기존 계정이 있어 소셜 로그인을 진행할 수 없습니다."), + OAUTH_LOGIN_FAILED("AUTH-004", "소셜 로그인에 실패했습니다."), + UNSUPPORTED_OAUTH_PROVIDER("AUTH-005", "지원하지 않는 OAuth2 공급자입니다."); - private final int status; private final String code; private final String message; - AuthErrorCode(int status, String code, String message) { - this.status = status; + AuthErrorCode(String code, String message) { this.code = code; this.message = message; } - @Override - public int status() { - return status; - } - @Override public String code() { return code; diff --git a/application/src/main/java/com/project/auth/application/support/exception/CommonErrorCode.java b/application/src/main/java/com/project/auth/application/support/exception/CommonErrorCode.java index c56d233..ee3bfee 100644 --- a/application/src/main/java/com/project/auth/application/support/exception/CommonErrorCode.java +++ b/application/src/main/java/com/project/auth/application/support/exception/CommonErrorCode.java @@ -1,24 +1,17 @@ package com.project.auth.application.support.exception; public enum CommonErrorCode implements ErrorCode { - INVALID_INPUT(400, "COMMON-001", "요청 값이 올바르지 않습니다."), - INTERNAL_SERVER_ERROR(500, "COMMON-999", "예상하지 못한 오류가 발생했습니다."); + INVALID_INPUT("COMMON-001", "요청 값이 올바르지 않습니다."), + INTERNAL_SERVER_ERROR("COMMON-999", "예상하지 못한 오류가 발생했습니다."); - private final int status; private final String code; private final String message; - CommonErrorCode(int status, String code, String message) { - this.status = status; + CommonErrorCode(String code, String message) { this.code = code; this.message = message; } - @Override - public int status() { - return status; - } - @Override public String code() { return code; diff --git a/application/src/main/java/com/project/auth/application/support/exception/ErrorCode.java b/application/src/main/java/com/project/auth/application/support/exception/ErrorCode.java index 32a59c8..6e0f581 100644 --- a/application/src/main/java/com/project/auth/application/support/exception/ErrorCode.java +++ b/application/src/main/java/com/project/auth/application/support/exception/ErrorCode.java @@ -2,8 +2,6 @@ public interface ErrorCode { - int status(); - String code(); String message(); diff --git a/application/src/main/java/com/project/auth/application/user/exception/UserErrorCode.java b/application/src/main/java/com/project/auth/application/user/exception/UserErrorCode.java index c1cb8da..5138863 100644 --- a/application/src/main/java/com/project/auth/application/user/exception/UserErrorCode.java +++ b/application/src/main/java/com/project/auth/application/user/exception/UserErrorCode.java @@ -3,26 +3,19 @@ import com.project.auth.application.support.exception.ErrorCode; public enum UserErrorCode implements ErrorCode { - USER_EMAIL_INVALID(400, "USER-001", "유효한 이메일 형식이 아닙니다."), - USER_PASSWORD_INVALID(400, "USER-002", "비밀번호는 8자 이상 50자 이하여야 합니다."), - USER_NAME_INVALID(400, "USER-003", "이름은 2자 이상 20자 이하여야 합니다."), - USER_EMAIL_ALREADY_EXISTS(409, "USER-004", "이미 가입된 이메일입니다."); + USER_EMAIL_INVALID("USER-001", "유효한 이메일 형식이 아닙니다."), + USER_PASSWORD_INVALID("USER-002", "비밀번호는 8자 이상 50자 이하여야 합니다."), + USER_NAME_INVALID("USER-003", "이름은 2자 이상 20자 이하여야 합니다."), + USER_EMAIL_ALREADY_EXISTS("USER-004", "이미 가입된 이메일입니다."); - private final int status; private final String code; private final String message; - UserErrorCode(int status, String code, String message) { - this.status = status; + UserErrorCode(String code, String message) { this.code = code; this.message = message; } - @Override - public int status() { - return status; - } - @Override public String code() { return code; diff --git a/argocd/auth-dev-project.yaml b/argocd/auth-dev-project.yaml deleted file mode 100644 index 887a899..0000000 --- a/argocd/auth-dev-project.yaml +++ /dev/null @@ -1,52 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: AppProject -metadata: - name: auth-dev - namespace: argocd - finalizers: - - resources-finalizer.argocd.argoproj.io - -spec: - description: Project Auth Server dev deployment project - - sourceRepos: - - https://github.com/DongHyeonka/Project-Auth-Server.git - - destinations: - - namespace: auth-dev - server: https://kubernetes.default.svc - - namespace: platform - server: https://kubernetes.default.svc - - clusterResourceWhitelist: - - group: "" - kind: Namespace - - namespaceResourceWhitelist: - - group: "" - kind: ConfigMap - - group: "" - kind: Secret - - group: "" - kind: Service - - group: "" - kind: ServiceAccount - - group: "" - kind: PersistentVolumeClaim - - group: "apps" - kind: Deployment - - group: "apps" - kind: StatefulSet - - group: "apps" - kind: ReplicaSet - - group: "autoscaling" - kind: HorizontalPodAutoscaler - - group: "batch" - kind: Job - - group: "networking.k8s.io" - kind: Ingress - - group: "policy" - kind: PodDisruptionBudget - - orphanedResources: - warn: true diff --git a/argocd/dev-auth-server-application.yaml b/argocd/dev-auth-server-application.yaml deleted file mode 100644 index fd3d660..0000000 --- a/argocd/dev-auth-server-application.yaml +++ /dev/null @@ -1,36 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: auth-server-dev - namespace: argocd - finalizers: - - resources-finalizer.argocd.argoproj.io -spec: - project: auth-dev - - source: - repoURL: https://github.com/DongHyeonka/Project-Auth-Server.git - targetRevision: develop - path: k8s/dev - - destination: - server: https://kubernetes.default.svc - namespace: auth-dev - - syncPolicy: - automated: - enabled: true - prune: true - selfHeal: true - syncOptions: - - CreateNamespace=true - - PruneLast=true - - ApplyOutOfSyncOnly=true - retry: - limit: 5 - backoff: - duration: 5s - factor: 2 - maxDuration: 3m - - revisionHistoryLimit: 5 diff --git a/argocd/dev-platform-application.yaml b/argocd/dev-platform-application.yaml deleted file mode 100644 index 8bc6bd2..0000000 --- a/argocd/dev-platform-application.yaml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: platform-dev - namespace: argocd - finalizers: - - resources-finalizer.argocd.argoproj.io -spec: - project: auth-dev - source: - repoURL: https://github.com/DongHyeonka/Project-Auth-Server.git - targetRevision: develop - path: k8s/platform-dev - destination: - server: https://kubernetes.default.svc - namespace: platform - syncPolicy: - automated: - enabled: true - prune: true - selfHeal: true - syncOptions: - - CreateNamespace=true - - PruneLast=true - - ApplyOutOfSyncOnly=true - retry: - limit: 5 - backoff: - duration: 5s - factor: 2 - maxDuration: 3m - revisionHistoryLimit: 5 diff --git a/bootstrap/build.gradle b/bootstrap/build.gradle index a308122..54c2bc0 100644 --- a/bootstrap/build.gradle +++ b/bootstrap/build.gradle @@ -3,7 +3,6 @@ import org.gradle.api.JavaVersion dependencies { implementation project(':application') - implementation project(':common') implementation project(':presentation') implementation project(':infrastructure') @@ -21,6 +20,7 @@ dependencies { runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'com.tngtech.archunit:archunit-junit5:1.4.1' testImplementation project(':domain') testRuntimeOnly 'com.h2database:h2' } diff --git a/bootstrap/src/main/java/com/project/auth/config/auth/security/OAuth2LoginFailureHandler.java b/bootstrap/src/main/java/com/project/auth/config/auth/security/OAuth2LoginFailureHandler.java index e4e41e5..5e4333a 100644 --- a/bootstrap/src/main/java/com/project/auth/config/auth/security/OAuth2LoginFailureHandler.java +++ b/bootstrap/src/main/java/com/project/auth/config/auth/security/OAuth2LoginFailureHandler.java @@ -2,7 +2,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.project.auth.application.auth.exception.AuthErrorCode; -import com.project.auth.common.response.ApiResult; +import com.project.auth.presentation.support.exception.ApiErrorHttpStatusMapper; +import com.project.auth.presentation.support.response.ApiResult; import org.springframework.http.MediaType; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; @@ -26,7 +27,7 @@ public void onAuthenticationFailure( HttpServletResponse response, AuthenticationException exception ) throws IOException { - response.setStatus(AuthErrorCode.OAUTH_LOGIN_FAILED.status()); + response.setStatus(ApiErrorHttpStatusMapper.map(AuthErrorCode.OAUTH_LOGIN_FAILED).value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding(StandardCharsets.UTF_8.name()); objectMapper.writeValue( diff --git a/bootstrap/src/test/java/com/project/auth/architecture/LayerDependencyArchitectureTest.java b/bootstrap/src/test/java/com/project/auth/architecture/LayerDependencyArchitectureTest.java new file mode 100644 index 0000000..3ce511f --- /dev/null +++ b/bootstrap/src/test/java/com/project/auth/architecture/LayerDependencyArchitectureTest.java @@ -0,0 +1,54 @@ +package com.project.auth.architecture; + +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; + +@AnalyzeClasses( + packages = {"com.project.auth", "com.project.authmigration"}, + importOptions = {ImportOption.DoNotIncludeTests.class} +) +class LayerDependencyArchitectureTest { + + @ArchTest + static final ArchRule domain_must_not_depend_on_spring_web_or_jpa = + noClasses() + .that().resideInAnyPackage("com.project.auth.domain..") + .should().dependOnClassesThat().resideInAnyPackage( + "org.springframework..", + "org.hibernate..", + "jakarta.persistence..", + "jakarta.servlet.." + ); + + @ArchTest + static final ArchRule application_must_not_depend_on_presentation_or_infrastructure = + noClasses() + .that().resideInAnyPackage("com.project.auth.application..") + .should().dependOnClassesThat().resideInAnyPackage( + "com.project.auth.presentation..", + "com.project.auth.infrastructure..", + "jakarta.servlet..", + "org.springframework.web.." + ); + + @ArchTest + static final ArchRule presentation_must_not_depend_on_infrastructure = + noClasses() + .that().resideInAnyPackage("com.project.auth.presentation..") + .should().dependOnClassesThat().resideInAnyPackage("com.project.auth.infrastructure.."); + + @ArchTest + static final ArchRule bootstrap_is_the_only_layer_that_may_depend_on_config_packages = + noClasses() + .that().resideInAnyPackage( + "com.project.auth.domain..", + "com.project.auth.application..", + "com.project.auth.presentation..", + "com.project.auth.infrastructure.." + ) + .should().dependOnClassesThat().resideInAnyPackage("com.project.auth.config.."); +} diff --git a/common/build.gradle b/common/build.gradle deleted file mode 100644 index 47e62b1..0000000 --- a/common/build.gradle +++ /dev/null @@ -1,3 +0,0 @@ -dependencies { - -} \ No newline at end of file diff --git a/Dockerfile b/deploy/docker/Dockerfile similarity index 97% rename from Dockerfile rename to deploy/docker/Dockerfile index 84f2a79..62677ba 100644 --- a/Dockerfile +++ b/deploy/docker/Dockerfile @@ -9,7 +9,6 @@ COPY gradle gradle COPY settings.gradle build.gradle gradle.properties ./ COPY bootstrap/build.gradle bootstrap/build.gradle COPY application/build.gradle application/build.gradle -COPY common/build.gradle common/build.gradle COPY domain/build.gradle domain/build.gradle COPY infrastructure/build.gradle infrastructure/build.gradle COPY presentation/build.gradle presentation/build.gradle @@ -20,7 +19,6 @@ RUN --mount=type=cache,target=/root/.gradle \ COPY bootstrap/src bootstrap/src COPY application/src application/src -COPY common/src common/src COPY domain/src domain/src COPY infrastructure/src infrastructure/src COPY presentation/src presentation/src diff --git a/docker-compose.yml b/deploy/docker/docker-compose.yml similarity index 88% rename from docker-compose.yml rename to deploy/docker/docker-compose.yml index 273f621..7e7975c 100644 --- a/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -19,7 +19,7 @@ services: KEYCLOAK_DB_PASSWORD: ${KEYCLOAK_DB_PASSWORD:-keycloak} volumes: - postgres_data:/var/lib/postgresql/data - - ./docker/postgres/init:/docker-entrypoint-initdb.d:ro + - ./resources/postgres/init:/docker-entrypoint-initdb.d:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_SUPERUSER:-postgres} -d ${POSTGRES_DEFAULT_DB:-postgres}"] interval: 10s @@ -27,7 +27,7 @@ services: retries: 10 keycloak: - image: quay.io/keycloak/keycloak:latest + image: quay.io/keycloak/keycloak:26.5.5 container_name: project-auth-keycloak restart: unless-stopped command: @@ -45,11 +45,11 @@ services: KC_DB_USERNAME: ${KEYCLOAK_DB_USER:-keycloak} KC_DB_PASSWORD: ${KEYCLOAK_DB_PASSWORD:-keycloak} KC_HEALTH_ENABLED: "true" - KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-admin} - KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin} + KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN:-admin} + KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin} volumes: - keycloak_data:/opt/keycloak/data - - ./docs/keycloak/realm:/opt/keycloak/data/import:ro + - ../../docs/development/keycloak/realm:/opt/keycloak/data/import:ro vault: image: hashicorp/vault:1.18 @@ -90,7 +90,7 @@ services: - /bin/sh - /vault-init/01-init-transit.sh volumes: - - ./docker/vault/init:/vault-init:ro + - ./resources/vault/init:/vault-init:ro volumes: postgres_data: diff --git a/docker/postgres/init/01-init-project-auth-databases.sh b/deploy/docker/resources/postgres/init/01-init-project-auth-databases.sh similarity index 100% rename from docker/postgres/init/01-init-project-auth-databases.sh rename to deploy/docker/resources/postgres/init/01-init-project-auth-databases.sh diff --git a/docker/vault/init/01-init-transit.sh b/deploy/docker/resources/vault/init/01-init-transit.sh similarity index 100% rename from docker/vault/init/01-init-transit.sh rename to deploy/docker/resources/vault/init/01-init-transit.sh diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 0000000..6072bb1 --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,3 @@ +# Architecture + +계층 구조와 패키지 규칙, ArchUnit 검증 기준을 정리하는 문서 위치입니다. diff --git a/docs/keycloak/LOCAL_SETUP.md b/docs/development/keycloak/LOCAL_SETUP.md similarity index 96% rename from docs/keycloak/LOCAL_SETUP.md rename to docs/development/keycloak/LOCAL_SETUP.md index f753473..b787a3b 100644 --- a/docs/keycloak/LOCAL_SETUP.md +++ b/docs/development/keycloak/LOCAL_SETUP.md @@ -2,7 +2,7 @@ ## 기본 전제 -- `docker compose`는 루트 `docker-compose.yml`을 사용합니다. +- `docker compose`는 `deploy/docker/docker-compose.yml`을 사용합니다. - 실제 소셜 로그인용 `Google`/`GitHub` Client ID와 Secret은 auth-server가 아니라 Keycloak에 등록합니다. - auth-server는 Keycloak realm의 OIDC client 정보만 사용합니다. diff --git a/docs/keycloak/realm/project-auth-realm-local.json b/docs/development/keycloak/realm/project-auth-realm-local.json similarity index 100% rename from docs/keycloak/realm/project-auth-realm-local.json rename to docs/development/keycloak/realm/project-auth-realm-local.json diff --git a/docs/postman/auth-core.postman_collection.json b/docs/development/postman/auth-core.postman_collection.json similarity index 100% rename from docs/postman/auth-core.postman_collection.json rename to docs/development/postman/auth-core.postman_collection.json diff --git a/docs/vault/LOCAL_SETUP.md b/docs/development/vault-local-setup.md similarity index 95% rename from docs/vault/LOCAL_SETUP.md rename to docs/development/vault-local-setup.md index c5285fa..d9fb7b5 100644 --- a/docs/vault/LOCAL_SETUP.md +++ b/docs/development/vault-local-setup.md @@ -2,7 +2,7 @@ ## 기본 전제 -- 루트 `docker-compose.yml`에 `vault`, `vault-init` 서비스가 포함되어 있습니다. +- `deploy/docker/docker-compose.yml`에 `vault`, `vault-init` 서비스가 포함되어 있습니다. - 현재 단계는 Vault Transit 개념과 로컬 서명 흐름을 익히는 단계입니다. - 아직 auth-server가 Vault를 실제 signer로 사용하지 않아도, transit 엔진과 키를 먼저 준비해 둘 수 있습니다. diff --git a/docs/k8s/auth-db-migration-job.yaml b/docs/k8s/auth-db-migration-job.yaml deleted file mode 100644 index dada37b..0000000 --- a/docs/k8s/auth-db-migration-job.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: batch/v1 -kind: Job -metadata: - name: auth-db-migration -spec: - backoffLimit: 1 - template: - spec: - restartPolicy: Never - containers: - - name: auth-db-migration - image: your-registry/project-auth-server:latest - imagePullPolicy: IfNotPresent - env: - - name: SPRING_MAIN_WEB_APPLICATION_TYPE - value: none - - name: APP_PERSISTENCE_MIGRATION_RUN_ON_STARTUP - value: "true" - - name: APP_DATASOURCE_URL - valueFrom: - secretKeyRef: - name: auth-database - key: url - - name: APP_DATASOURCE_USERNAME - valueFrom: - secretKeyRef: - name: auth-database - key: username - - name: APP_DATASOURCE_PASSWORD - valueFrom: - secretKeyRef: - name: auth-database - key: password diff --git a/k8s/dev/configmap.yaml b/k8s/dev/configmap.yaml deleted file mode 100644 index 3b1cae7..0000000 --- a/k8s/dev/configmap.yaml +++ /dev/null @@ -1,25 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: auth-server-config -data: - SPRING_PROFILES_ACTIVE: dev - APP_DOCS_TITLE: Project Auth Server API - APP_DOCS_DESCRIPTION: dev auth-server OpenAPI - APP_DOCS_VERSION: v1 - APP_DATASOURCE_URL: jdbc:postgresql://postgres.platform.svc.cluster.local:5432/project_auth - APP_PERSISTENCE_MIGRATION_RUN_ON_STARTUP: "false" - APP_SECURITY_OAUTH2_KEYCLOAK_ISSUER_URI: http://keycloak.platform.svc.cluster.local:8081/realms/project-auth - APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_ID: project-auth-server - APP_SECURITY_OAUTH2_GOOGLE_REGISTRATION_ID: keycloak-google - APP_SECURITY_OAUTH2_GOOGLE_IDP_HINT: google - APP_SECURITY_OAUTH2_GITHUB_REGISTRATION_ID: keycloak-github - APP_SECURITY_OAUTH2_GITHUB_IDP_HINT: github - APP_SECURITY_JWT_ISSUER: http://auth-server.auth-dev.svc.cluster.local:8080 - APP_SECURITY_JWT_ACTIVE_KEY_ID: dev-vault-rsa-1 - APP_SECURITY_JWT_GENERATE_KEY_PAIR_ON_STARTUP: "false" - APP_SECURITY_JWT_ACCESS_TOKEN_EXPIRATION: PT30M - APP_SECURITY_JWT_VAULT_ENABLED: "true" - APP_SECURITY_JWT_VAULT_ADDRESS: http://vault.platform.svc.cluster.local:8200 - APP_SECURITY_JWT_VAULT_MOUNT_PATH: transit - APP_SECURITY_JWT_VAULT_TRANSIT_KEY_NAME: project-auth-jwt diff --git a/k8s/dev/db-migration-job.yaml b/k8s/dev/db-migration-job.yaml deleted file mode 100644 index c02b0fe..0000000 --- a/k8s/dev/db-migration-job.yaml +++ /dev/null @@ -1,69 +0,0 @@ -apiVersion: batch/v1 -kind: Job -metadata: - name: auth-db-migration - annotations: - argocd.argoproj.io/hook: PreSync - argocd.argoproj.io/hook-delete-policy: BeforeHookCreation,HookSucceeded - argocd.argoproj.io/sync-wave: "-1" -spec: - backoffLimit: 1 - template: - metadata: - labels: - app: auth-db-migration - spec: - serviceAccountName: auth-server - automountServiceAccountToken: false - restartPolicy: Never - securityContext: - runAsNonRoot: true - runAsUser: 10001 - runAsGroup: 10001 - seccompProfile: - type: RuntimeDefault - containers: - - name: auth-db-migration - image: ghcr.io/donghyeonka/project-auth-server - imagePullPolicy: IfNotPresent - command: - - java - - -jar - - /app/migration.jar - env: - - name: SPRING_MAIN_WEB_APPLICATION_TYPE - value: none - - name: APP_PERSISTENCE_MIGRATION_RUN_ON_STARTUP - value: "true" - - name: APP_PERSISTENCE_MIGRATION_LOCATION - value: classpath:db/migration - - name: APP_DATASOURCE_URL - valueFrom: - configMapKeyRef: - name: auth-server-config - key: APP_DATASOURCE_URL - - name: APP_DATASOURCE_USERNAME - valueFrom: - secretKeyRef: - name: auth-server-secret - key: APP_DATASOURCE_USERNAME - - name: APP_DATASOURCE_PASSWORD - valueFrom: - secretKeyRef: - name: auth-server-secret - key: APP_DATASOURCE_PASSWORD - - name: APP_DATASOURCE_DRIVER_CLASS_NAME - value: org.postgresql.Driver - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - readOnlyRootFilesystem: false - resources: - requests: - cpu: 100m - memory: 256Mi - limits: - cpu: 500m - memory: 512Mi diff --git a/k8s/dev/deployment.yaml b/k8s/dev/deployment.yaml deleted file mode 100644 index 8e132fd..0000000 --- a/k8s/dev/deployment.yaml +++ /dev/null @@ -1,76 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: auth-server - annotations: - argocd.argoproj.io/sync-wave: "0" - -spec: - replicas: 1 - selector: - matchLabels: - app: auth-server - - template: - metadata: - labels: - app: auth-server - - spec: - serviceAccountName: auth-server - automountServiceAccountToken: false - securityContext: - runAsNonRoot: true - runAsUser: 10001 - runAsGroup: 10001 - seccompProfile: - type: RuntimeDefault - containers: - - name: auth-server - image: ghcr.io/donghyeonka/project-auth-server - imagePullPolicy: IfNotPresent - ports: - - containerPort: 8080 - name: http - envFrom: - - configMapRef: - name: auth-server-config - - secretRef: - name: auth-server-secret - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - readOnlyRootFilesystem: false - resources: - requests: - cpu: 250m - memory: 512Mi - limits: - cpu: 1000m - memory: 1024Mi - readinessProbe: - httpGet: - path: /actuator/health/readiness - port: http - initialDelaySeconds: 20 - timeoutSeconds: 3 - failureThreshold: 3 - periodSeconds: 10 - livenessProbe: - httpGet: - path: /actuator/health/liveness - port: http - initialDelaySeconds: 30 - timeoutSeconds: 3 - failureThreshold: 5 - periodSeconds: 15 - startupProbe: - httpGet: - path: /actuator/health/liveness - port: http - initialDelaySeconds: 10 - timeoutSeconds: 3 - periodSeconds: 10 - failureThreshold: 12 diff --git a/k8s/dev/kustomization.yaml b/k8s/dev/kustomization.yaml deleted file mode 100644 index 10aea9a..0000000 --- a/k8s/dev/kustomization.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -namespace: auth-dev - -resources: -- namespace.yaml -- serviceaccount.yaml -- configmap.yaml -- service.yaml -- db-migration-job.yaml -- deployment.yaml - -images: -- name: ghcr.io/donghyeonka/project-auth-server - newName: ghcr.io/donghyeonka/project-auth-server - newTag: 5648fd2 diff --git a/k8s/dev/namespace.yaml b/k8s/dev/namespace.yaml deleted file mode 100644 index 5f3207a..0000000 --- a/k8s/dev/namespace.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: auth-dev - labels: - pod-security.kubernetes.io/enforce: baseline - pod-security.kubernetes.io/enforce-version: latest - pod-security.kubernetes.io/warn: restricted - pod-security.kubernetes.io/warn-version: latest - pod-security.kubernetes.io/audit: restricted - pod-security.kubernetes.io/audit-version: latest diff --git a/k8s/dev/secret.yaml b/k8s/dev/secret.yaml deleted file mode 100644 index 91e1740..0000000 --- a/k8s/dev/secret.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: auth-server-secret - namespace: auth-dev -type: Opaque -stringData: - APP_DATASOURCE_USERNAME: project_auth - APP_DATASOURCE_PASSWORD: change-me - APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET: change-me - APP_SECURITY_JWT_VAULT_TOKEN: change-me diff --git a/k8s/dev/service.yaml b/k8s/dev/service.yaml deleted file mode 100644 index 7adde35..0000000 --- a/k8s/dev/service.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: auth-server -spec: - selector: - app: auth-server - ports: - - name: http - port: 8080 - targetPort: 8080 - type: ClusterIP diff --git a/k8s/dev/serviceaccount.yaml b/k8s/dev/serviceaccount.yaml deleted file mode 100644 index 2172948..0000000 --- a/k8s/dev/serviceaccount.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: auth-server -automountServiceAccountToken: false -imagePullSecrets: - - name: ghcr-regcred diff --git a/k8s/platform-dev/configmap.yaml b/k8s/platform-dev/configmap.yaml deleted file mode 100644 index c538e3f..0000000 --- a/k8s/platform-dev/configmap.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: platform-config -data: - POSTGRES_SUPERUSER: postgres - POSTGRES_DEFAULT_DB: postgres - AUTH_DB_NAME: project_auth - AUTH_DB_USER: project_auth - KEYCLOAK_DB_NAME: keycloak - KEYCLOAK_DB_USER: keycloak - KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME: admin - KEYCLOAK_CLIENT_ID: project-auth-server - AUTH_SERVER_BASE_URL: http://auth-server.auth-dev.svc.cluster.local:8080 - VAULT_TRANSIT_KEY_NAME: project-auth-jwt diff --git a/k8s/platform-dev/files/keycloak/project-auth-realm.json b/k8s/platform-dev/files/keycloak/project-auth-realm.json deleted file mode 100644 index 422766e..0000000 --- a/k8s/platform-dev/files/keycloak/project-auth-realm.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "realm": "project-auth", - "enabled": true, - "displayName": "Project Auth", - "sslRequired": "NONE", - "registrationAllowed": false, - "loginWithEmailAllowed": true, - "duplicateEmailsAllowed": false, - "resetPasswordAllowed": true, - "clients": [ - { - "clientId": "project-auth-server", - "name": "project-auth-server", - "enabled": true, - "protocol": "openid-connect", - "publicClient": false, - "standardFlowEnabled": true, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false - } - ] -} diff --git a/k8s/platform-dev/files/postgres/01-init-project-auth-databases.sh b/k8s/platform-dev/files/postgres/01-init-project-auth-databases.sh deleted file mode 100644 index effac6b..0000000 --- a/k8s/platform-dev/files/postgres/01-init-project-auth-databases.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh -set -eu - -psql -v ON_ERROR_STOP=1 --username "${POSTGRES_USER}" --dbname "${POSTGRES_DB}" <<-EOSQL - CREATE USER ${AUTH_DB_USER} WITH PASSWORD '${AUTH_DB_PASSWORD}'; - CREATE DATABASE ${AUTH_DB_NAME} OWNER ${AUTH_DB_USER}; - - CREATE USER ${KEYCLOAK_DB_USER} WITH PASSWORD '${KEYCLOAK_DB_PASSWORD}'; - CREATE DATABASE ${KEYCLOAK_DB_NAME} OWNER ${KEYCLOAK_DB_USER}; -EOSQL diff --git a/k8s/platform-dev/files/vault/01-init-transit.sh b/k8s/platform-dev/files/vault/01-init-transit.sh deleted file mode 100644 index f98a138..0000000 --- a/k8s/platform-dev/files/vault/01-init-transit.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh - -set -eu - -until vault status -address="${VAULT_ADDR}" >/dev/null 2>&1; do - sleep 2 -done - -vault secrets enable transit || true -vault write "transit/keys/${VAULT_TRANSIT_KEY_NAME}" type="rsa-2048" || true -vault read "transit/keys/${VAULT_TRANSIT_KEY_NAME}" diff --git a/k8s/platform-dev/keycloak-client-sync-job.yaml b/k8s/platform-dev/keycloak-client-sync-job.yaml deleted file mode 100644 index 78d3093..0000000 --- a/k8s/platform-dev/keycloak-client-sync-job.yaml +++ /dev/null @@ -1,67 +0,0 @@ -apiVersion: batch/v1 -kind: Job -metadata: - name: keycloak-client-sync -spec: - backoffLimit: 5 - template: - metadata: - labels: - app: keycloak-client-sync - spec: - restartPolicy: OnFailure - containers: - - name: keycloak-client-sync - image: quay.io/keycloak/keycloak:26.5.5 - command: - - /bin/sh - - -c - - | - set -eu - - until /opt/keycloak/bin/kcadm.sh config credentials \ - --server http://keycloak.platform.svc.cluster.local:8081 \ - --realm master \ - --user "$KC_BOOTSTRAP_ADMIN_USERNAME" \ - --password "$KC_BOOTSTRAP_ADMIN_PASSWORD" >/dev/null 2>&1; do - sleep 5 - done - - CLIENT_UUID=$(/opt/keycloak/bin/kcadm.sh get clients \ - -r project-auth \ - -q clientId="$KEYCLOAK_CLIENT_ID" | sed -n 's/.*"id" : "\([^"]*\)".*/\1/p' | head -n 1) - - test -n "$CLIENT_UUID" - - /opt/keycloak/bin/kcadm.sh update "clients/${CLIENT_UUID}" \ - -r project-auth \ - -s "secret=$KEYCLOAK_CLIENT_SECRET" \ - -s "baseUrl=$AUTH_SERVER_BASE_URL" \ - -s 'redirectUris=["'"$AUTH_SERVER_BASE_URL"'/login/oauth2/code/keycloak-google","'"$AUTH_SERVER_BASE_URL"'/login/oauth2/code/keycloak-github"]' \ - -s 'webOrigins=["'"$AUTH_SERVER_BASE_URL"'"]' - env: - - name: KC_BOOTSTRAP_ADMIN_USERNAME - valueFrom: - configMapKeyRef: - name: platform-config - key: KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME - - name: KC_BOOTSTRAP_ADMIN_PASSWORD - valueFrom: - secretKeyRef: - name: platform-secret - key: KEYCLOAK_BOOTSTRAP_ADMIN_PASSWORD - - name: KEYCLOAK_CLIENT_ID - valueFrom: - configMapKeyRef: - name: platform-config - key: KEYCLOAK_CLIENT_ID - - name: KEYCLOAK_CLIENT_SECRET - valueFrom: - secretKeyRef: - name: platform-secret - key: KEYCLOAK_CLIENT_SECRET - - name: AUTH_SERVER_BASE_URL - valueFrom: - configMapKeyRef: - name: platform-config - key: AUTH_SERVER_BASE_URL diff --git a/k8s/platform-dev/keycloak-deployment.yaml b/k8s/platform-dev/keycloak-deployment.yaml deleted file mode 100644 index f5f094f..0000000 --- a/k8s/platform-dev/keycloak-deployment.yaml +++ /dev/null @@ -1,102 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: keycloak -spec: - replicas: 1 - selector: - matchLabels: - app: keycloak - template: - metadata: - labels: - app: keycloak - spec: - automountServiceAccountToken: false - securityContext: - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault - containers: - - name: keycloak - image: quay.io/keycloak/keycloak:26.5.5 - args: - - start-dev - - --import-realm - - --http-port=8080 - ports: - - containerPort: 8080 - name: http - - containerPort: 9000 - name: management - env: - - name: KC_DB - value: postgres - - name: KC_DB_URL - value: jdbc:postgresql://postgres.platform.svc.cluster.local:5432/keycloak - - name: KC_DB_USERNAME - valueFrom: - configMapKeyRef: - name: platform-config - key: KEYCLOAK_DB_USER - - name: KC_DB_PASSWORD - valueFrom: - secretKeyRef: - name: platform-secret - key: KEYCLOAK_DB_PASSWORD - - name: KC_HEALTH_ENABLED - value: "true" - - name: KC_BOOTSTRAP_ADMIN_USERNAME - valueFrom: - configMapKeyRef: - name: platform-config - key: KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME - - name: KC_BOOTSTRAP_ADMIN_PASSWORD - valueFrom: - secretKeyRef: - name: platform-secret - key: KEYCLOAK_BOOTSTRAP_ADMIN_PASSWORD - volumeMounts: - - name: keycloak-realm-import - mountPath: /opt/keycloak/data/import/project-auth-realm.json - subPath: project-auth-realm.json - readinessProbe: - httpGet: - path: /health/ready - port: management - initialDelaySeconds: 30 - periodSeconds: 10 - timeoutSeconds: 3 - failureThreshold: 6 - livenessProbe: - httpGet: - path: /health/live - port: management - initialDelaySeconds: 60 - periodSeconds: 15 - timeoutSeconds: 3 - failureThreshold: 6 - startupProbe: - httpGet: - path: /health/ready - port: management - initialDelaySeconds: 20 - periodSeconds: 10 - timeoutSeconds: 3 - failureThreshold: 30 - resources: - requests: - cpu: 250m - memory: 768Mi - limits: - cpu: 1000m - memory: 1536Mi - securityContext: - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - volumes: - - name: keycloak-realm-import - configMap: - name: keycloak-realm-import diff --git a/k8s/platform-dev/keycloak-service.yaml b/k8s/platform-dev/keycloak-service.yaml deleted file mode 100644 index 93d577b..0000000 --- a/k8s/platform-dev/keycloak-service.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: keycloak -spec: - selector: - app: keycloak - ports: - - name: http - port: 8081 - targetPort: 8080 - type: ClusterIP diff --git a/k8s/platform-dev/kustomization.yaml b/k8s/platform-dev/kustomization.yaml deleted file mode 100644 index d61e827..0000000 --- a/k8s/platform-dev/kustomization.yaml +++ /dev/null @@ -1,30 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -namespace: platform - -resources: - - namespace.yaml - - configmap.yaml - - postgres-service.yaml - - postgres-statefulset.yaml - - keycloak-service.yaml - - keycloak-deployment.yaml - - keycloak-client-sync-job.yaml - - vault-service.yaml - - vault-deployment.yaml - - vault-init-job.yaml - -generatorOptions: - disableNameSuffixHash: true - -configMapGenerator: - - name: postgres-init-script - files: - - files/postgres/01-init-project-auth-databases.sh - - name: keycloak-realm-import - files: - - files/keycloak/project-auth-realm.json - - name: vault-init-script - files: - - files/vault/01-init-transit.sh diff --git a/k8s/platform-dev/namespace.yaml b/k8s/platform-dev/namespace.yaml deleted file mode 100644 index 4aa1d0c..0000000 --- a/k8s/platform-dev/namespace.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: platform - labels: - pod-security.kubernetes.io/enforce: baseline - pod-security.kubernetes.io/enforce-version: latest - pod-security.kubernetes.io/warn: restricted - pod-security.kubernetes.io/warn-version: latest - pod-security.kubernetes.io/audit: restricted - pod-security.kubernetes.io/audit-version: latest diff --git a/k8s/platform-dev/postgres-service.yaml b/k8s/platform-dev/postgres-service.yaml deleted file mode 100644 index b0601cf..0000000 --- a/k8s/platform-dev/postgres-service.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: postgres -spec: - clusterIP: None - selector: - app: postgres - ports: - - name: postgres - port: 5432 - targetPort: 5432 - type: ClusterIP diff --git a/k8s/platform-dev/postgres-statefulset.yaml b/k8s/platform-dev/postgres-statefulset.yaml deleted file mode 100644 index b32b2f9..0000000 --- a/k8s/platform-dev/postgres-statefulset.yaml +++ /dev/null @@ -1,123 +0,0 @@ -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: postgres -spec: - serviceName: postgres - replicas: 1 - selector: - matchLabels: - app: postgres - template: - metadata: - labels: - app: postgres - spec: - securityContext: - fsGroup: 999 - seccompProfile: - type: RuntimeDefault - containers: - - name: postgres - image: postgres:16-alpine - ports: - - containerPort: 5432 - name: postgres - env: - - name: PGDATA - value: /var/lib/postgresql/data/pgdata - - name: POSTGRES_USER - valueFrom: - configMapKeyRef: - name: platform-config - key: POSTGRES_SUPERUSER - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: platform-secret - key: POSTGRES_SUPERUSER_PASSWORD - - name: POSTGRES_DB - valueFrom: - configMapKeyRef: - name: platform-config - key: POSTGRES_DEFAULT_DB - - name: AUTH_DB_NAME - valueFrom: - configMapKeyRef: - name: platform-config - key: AUTH_DB_NAME - - name: AUTH_DB_USER - valueFrom: - configMapKeyRef: - name: platform-config - key: AUTH_DB_USER - - name: AUTH_DB_PASSWORD - valueFrom: - secretKeyRef: - name: platform-secret - key: AUTH_DB_PASSWORD - - name: KEYCLOAK_DB_NAME - valueFrom: - configMapKeyRef: - name: platform-config - key: KEYCLOAK_DB_NAME - - name: KEYCLOAK_DB_USER - valueFrom: - configMapKeyRef: - name: platform-config - key: KEYCLOAK_DB_USER - - name: KEYCLOAK_DB_PASSWORD - valueFrom: - secretKeyRef: - name: platform-secret - key: KEYCLOAK_DB_PASSWORD - securityContext: - volumeMounts: - - name: postgres-data - mountPath: /var/lib/postgresql/data - - name: postgres-init-script - mountPath: /docker-entrypoint-initdb.d/01-init-project-auth-databases.sh - subPath: 01-init-project-auth-databases.sh - - name: postgres-run - mountPath: /var/run/postgresql - readinessProbe: - exec: - command: - - sh - - -c - - pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" - initialDelaySeconds: 10 - periodSeconds: 10 - timeoutSeconds: 3 - livenessProbe: - exec: - command: - - sh - - -c - - pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" - initialDelaySeconds: 20 - periodSeconds: 15 - timeoutSeconds: 3 - resources: - requests: - cpu: 250m - memory: 512Mi - limits: - cpu: 1000m - memory: 1024Mi - volumes: - - name: postgres-init-script - configMap: - name: postgres-init-script - defaultMode: 0555 - - name: postgres-run - emptyDir: {} - volumeClaimTemplates: - - metadata: - name: postgres-data - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 5Gi diff --git a/k8s/platform-dev/secret.yaml b/k8s/platform-dev/secret.yaml deleted file mode 100644 index ecf7f59..0000000 --- a/k8s/platform-dev/secret.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: platform-secret -type: Opaque -stringData: - POSTGRES_SUPERUSER_PASSWORD: change-me - AUTH_DB_PASSWORD: change-me - KEYCLOAK_DB_PASSWORD: change-me - KEYCLOAK_BOOTSTRAP_ADMIN_PASSWORD: change-me - KEYCLOAK_CLIENT_SECRET: change-me - VAULT_DEV_ROOT_TOKEN_ID: change-me diff --git a/k8s/platform-dev/vault-deployment.yaml b/k8s/platform-dev/vault-deployment.yaml deleted file mode 100644 index 0e33ccd..0000000 --- a/k8s/platform-dev/vault-deployment.yaml +++ /dev/null @@ -1,83 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: vault -spec: - replicas: 1 - selector: - matchLabels: - app: vault - template: - metadata: - labels: - app: vault - spec: - containers: - - name: vault - image: hashicorp/vault:1.18 - args: - - server - - -dev - - -dev-root-token-id=$(VAULT_DEV_ROOT_TOKEN_ID) - - -dev-listen-address=0.0.0.0:8200 - ports: - - containerPort: 8200 - name: http - env: - - name: VAULT_ADDR - value: http://127.0.0.1:8200 - - name: VAULT_TOKEN - valueFrom: - secretKeyRef: - name: platform-secret - key: VAULT_DEV_ROOT_TOKEN_ID - - name: VAULT_DEV_ROOT_TOKEN_ID - valueFrom: - secretKeyRef: - name: platform-secret - key: VAULT_DEV_ROOT_TOKEN_ID - - name: VAULT_TRANSIT_KEY_NAME - valueFrom: - configMapKeyRef: - name: platform-config - key: VAULT_TRANSIT_KEY_NAME - lifecycle: - postStart: - exec: - command: - - /bin/sh - - /vault-init/01-init-transit.sh - volumeMounts: - - name: vault-init-script - mountPath: /vault-init/01-init-transit.sh - subPath: 01-init-transit.sh - readinessProbe: - exec: - command: - - sh - - -c - - vault status -address=http://127.0.0.1:8200 >/dev/null - initialDelaySeconds: 10 - periodSeconds: 10 - timeoutSeconds: 5 - livenessProbe: - exec: - command: - - sh - - -c - - vault status -address=http://127.0.0.1:8200 >/dev/null - initialDelaySeconds: 20 - periodSeconds: 15 - timeoutSeconds: 5 - resources: - requests: - cpu: 100m - memory: 256Mi - limits: - cpu: 500m - memory: 512Mi - volumes: - - name: vault-init-script - configMap: - name: vault-init-script - defaultMode: 0555 diff --git a/k8s/platform-dev/vault-init-job.yaml b/k8s/platform-dev/vault-init-job.yaml deleted file mode 100644 index 43f5186..0000000 --- a/k8s/platform-dev/vault-init-job.yaml +++ /dev/null @@ -1,40 +0,0 @@ -apiVersion: batch/v1 -kind: Job -metadata: - name: vault-init -spec: - backoffLimit: 5 - template: - metadata: - labels: - app: vault-init - spec: - restartPolicy: OnFailure - containers: - - name: vault-init - image: hashicorp/vault:1.18 - command: - - /bin/sh - - /vault-init/01-init-transit.sh - env: - - name: VAULT_ADDR - value: http://vault.platform.svc.cluster.local:8200 - - name: VAULT_TOKEN - valueFrom: - secretKeyRef: - name: platform-secret - key: VAULT_DEV_ROOT_TOKEN_ID - - name: VAULT_TRANSIT_KEY_NAME - valueFrom: - configMapKeyRef: - name: platform-config - key: VAULT_TRANSIT_KEY_NAME - volumeMounts: - - name: vault-init-script - mountPath: /vault-init/01-init-transit.sh - subPath: 01-init-transit.sh - volumes: - - name: vault-init-script - configMap: - name: vault-init-script - defaultMode: 0555 diff --git a/k8s/platform-dev/vault-service.yaml b/k8s/platform-dev/vault-service.yaml deleted file mode 100644 index 97a411a..0000000 --- a/k8s/platform-dev/vault-service.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: vault -spec: - selector: - app: vault - ports: - - name: http - port: 8200 - targetPort: 8200 - type: ClusterIP diff --git a/presentation/build.gradle b/presentation/build.gradle index a1d0c80..feed446 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -1,6 +1,5 @@ dependencies { implementation project(':application') - implementation project(':common') implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' compileOnly 'org.springframework.security:spring-security-oauth2-client' diff --git a/presentation/src/main/java/com/project/auth/presentation/auth/controller/AuthLoginController.java b/presentation/src/main/java/com/project/auth/presentation/auth/controller/AuthLoginController.java index 05ee8ea..4b8e59d 100644 --- a/presentation/src/main/java/com/project/auth/presentation/auth/controller/AuthLoginController.java +++ b/presentation/src/main/java/com/project/auth/presentation/auth/controller/AuthLoginController.java @@ -2,12 +2,12 @@ import com.project.auth.application.auth.login.LoginResult; import com.project.auth.application.auth.login.port.in.LoginUseCase; -import com.project.auth.application.support.code.SuccessCode; -import com.project.auth.common.response.ApiResult; import com.project.auth.presentation.auth.docs.AuthLoginApiDocs; import com.project.auth.presentation.auth.dto.LoginRequest; import com.project.auth.presentation.auth.dto.LoginResponse; import com.project.auth.presentation.auth.mapper.AuthPresentationMapper; +import com.project.auth.presentation.support.response.ApiResult; +import com.project.auth.presentation.support.response.ApiSuccessCode; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; @@ -21,7 +21,7 @@ @Tag(name = "Auth", description = "로그인과 토큰 발급 API") public class AuthLoginController implements AuthLoginApiDocs { - private static final SuccessCode LOGIN_SUCCEEDED = SuccessCode.AUTH_LOGIN_SUCCEEDED; + private static final ApiSuccessCode LOGIN_SUCCEEDED = ApiSuccessCode.AUTH_LOGIN_SUCCEEDED; private final LoginUseCase loginUseCase; private final AuthPresentationMapper authPresentationMapper; diff --git a/presentation/src/main/java/com/project/auth/presentation/auth/controller/AuthOAuth2Controller.java b/presentation/src/main/java/com/project/auth/presentation/auth/controller/AuthOAuth2Controller.java index de408f5..f0926fe 100644 --- a/presentation/src/main/java/com/project/auth/presentation/auth/controller/AuthOAuth2Controller.java +++ b/presentation/src/main/java/com/project/auth/presentation/auth/controller/AuthOAuth2Controller.java @@ -2,12 +2,12 @@ import com.project.auth.application.auth.login.LoginResult; import com.project.auth.application.auth.oauth.login.port.in.OAuthLoginUseCase; -import com.project.auth.application.support.code.SuccessCode; -import com.project.auth.common.response.ApiResult; import com.project.auth.presentation.auth.docs.AuthOAuth2ApiDocs; import com.project.auth.presentation.auth.dto.LoginResponse; import com.project.auth.presentation.auth.mapper.AuthPresentationMapper; import com.project.auth.presentation.auth.mapper.OAuth2AuthenticationCommandMapper; +import com.project.auth.presentation.support.response.ApiResult; +import com.project.auth.presentation.support.response.ApiSuccessCode; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.HttpHeaders; @@ -26,7 +26,7 @@ @Tag(name = "Auth", description = "OAuth2 소셜 로그인 API") public class AuthOAuth2Controller implements AuthOAuth2ApiDocs { - private static final SuccessCode LOGIN_SUCCEEDED = SuccessCode.AUTH_LOGIN_SUCCEEDED; + private static final ApiSuccessCode LOGIN_SUCCEEDED = ApiSuccessCode.AUTH_LOGIN_SUCCEEDED; private final OAuthLoginUseCase oAuthLoginUseCase; private final AuthPresentationMapper authPresentationMapper; diff --git a/presentation/src/main/java/com/project/auth/presentation/auth/docs/AuthLoginApiDocs.java b/presentation/src/main/java/com/project/auth/presentation/auth/docs/AuthLoginApiDocs.java index 9f23990..dca5081 100644 --- a/presentation/src/main/java/com/project/auth/presentation/auth/docs/AuthLoginApiDocs.java +++ b/presentation/src/main/java/com/project/auth/presentation/auth/docs/AuthLoginApiDocs.java @@ -1,8 +1,8 @@ package com.project.auth.presentation.auth.docs; -import com.project.auth.common.response.ApiResult; import com.project.auth.presentation.auth.dto.LoginRequest; import com.project.auth.presentation.auth.dto.LoginResponse; +import com.project.auth.presentation.support.response.ApiResult; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; diff --git a/presentation/src/main/java/com/project/auth/presentation/support/exception/ApiErrorHttpStatusMapper.java b/presentation/src/main/java/com/project/auth/presentation/support/exception/ApiErrorHttpStatusMapper.java new file mode 100644 index 0000000..81ce90a --- /dev/null +++ b/presentation/src/main/java/com/project/auth/presentation/support/exception/ApiErrorHttpStatusMapper.java @@ -0,0 +1,44 @@ +package com.project.auth.presentation.support.exception; + +import com.project.auth.application.auth.exception.AuthErrorCode; +import com.project.auth.application.support.exception.CommonErrorCode; +import com.project.auth.application.support.exception.ErrorCode; +import com.project.auth.application.user.exception.UserErrorCode; +import org.springframework.http.HttpStatus; + +public final class ApiErrorHttpStatusMapper { + + private ApiErrorHttpStatusMapper() { + } + + public static HttpStatus map(ErrorCode errorCode) { + return switch (errorCode) { + case CommonErrorCode commonErrorCode -> mapCommon(commonErrorCode); + case AuthErrorCode authErrorCode -> mapAuth(authErrorCode); + case UserErrorCode userErrorCode -> mapUser(userErrorCode); + default -> HttpStatus.INTERNAL_SERVER_ERROR; + }; + } + + private static HttpStatus mapCommon(CommonErrorCode errorCode) { + return switch (errorCode) { + case INVALID_INPUT -> HttpStatus.BAD_REQUEST; + case INTERNAL_SERVER_ERROR -> HttpStatus.INTERNAL_SERVER_ERROR; + }; + } + + private static HttpStatus mapAuth(AuthErrorCode errorCode) { + return switch (errorCode) { + case INVALID_CREDENTIALS, OAUTH_LOGIN_FAILED -> HttpStatus.UNAUTHORIZED; + case OAUTH_USER_INFO_INVALID, UNSUPPORTED_OAUTH_PROVIDER -> HttpStatus.BAD_REQUEST; + case OAUTH_ACCOUNT_CONFLICT -> HttpStatus.CONFLICT; + }; + } + + private static HttpStatus mapUser(UserErrorCode errorCode) { + return switch (errorCode) { + case USER_EMAIL_INVALID, USER_PASSWORD_INVALID, USER_NAME_INVALID -> HttpStatus.BAD_REQUEST; + case USER_EMAIL_ALREADY_EXISTS -> HttpStatus.CONFLICT; + }; + } +} diff --git a/presentation/src/main/java/com/project/auth/presentation/support/exception/GlobalExceptionHandler.java b/presentation/src/main/java/com/project/auth/presentation/support/exception/GlobalExceptionHandler.java index 3bd1ec7..ac729f5 100644 --- a/presentation/src/main/java/com/project/auth/presentation/support/exception/GlobalExceptionHandler.java +++ b/presentation/src/main/java/com/project/auth/presentation/support/exception/GlobalExceptionHandler.java @@ -2,8 +2,7 @@ import com.project.auth.application.support.exception.BusinessException; import com.project.auth.application.support.exception.CommonErrorCode; -import com.project.auth.common.response.ApiResult; -import org.springframework.http.HttpStatus; +import com.project.auth.presentation.support.response.ApiResult; import org.springframework.http.ResponseEntity; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -25,7 +24,7 @@ public ResponseEntity>> handleValidationException( errors.put(fieldError.getField(), fieldError.getDefaultMessage()); } - return ResponseEntity.status(HttpStatus.valueOf(CommonErrorCode.INVALID_INPUT.status())) + return ResponseEntity.status(ApiErrorHttpStatusMapper.map(CommonErrorCode.INVALID_INPUT)) .body(ApiResult.failure( CommonErrorCode.INVALID_INPUT.code(), CommonErrorCode.INVALID_INPUT.message(), @@ -35,13 +34,13 @@ public ResponseEntity>> handleValidationException( @ExceptionHandler(BusinessException.class) public ResponseEntity> handleBusinessException(BusinessException exception) { - return ResponseEntity.status(HttpStatus.valueOf(exception.getErrorCode().status())) + return ResponseEntity.status(ApiErrorHttpStatusMapper.map(exception.getErrorCode())) .body(ApiResult.failure(exception.getErrorCode().code(), exception.getMessage())); } @ExceptionHandler(Exception.class) public ResponseEntity> handleUnexpectedException(Exception exception) { - return ResponseEntity.status(HttpStatus.valueOf(CommonErrorCode.INTERNAL_SERVER_ERROR.status())) + return ResponseEntity.status(ApiErrorHttpStatusMapper.map(CommonErrorCode.INTERNAL_SERVER_ERROR)) .body(ApiResult.failure( CommonErrorCode.INTERNAL_SERVER_ERROR.code(), CommonErrorCode.INTERNAL_SERVER_ERROR.message() diff --git a/common/src/main/java/com/project/auth/common/response/ApiResult.java b/presentation/src/main/java/com/project/auth/presentation/support/response/ApiResult.java similarity index 92% rename from common/src/main/java/com/project/auth/common/response/ApiResult.java rename to presentation/src/main/java/com/project/auth/presentation/support/response/ApiResult.java index 5dc772d..59115f7 100644 --- a/common/src/main/java/com/project/auth/common/response/ApiResult.java +++ b/presentation/src/main/java/com/project/auth/presentation/support/response/ApiResult.java @@ -1,4 +1,4 @@ -package com.project.auth.common.response; +package com.project.auth.presentation.support.response; public record ApiResult( boolean success, diff --git a/application/src/main/java/com/project/auth/application/support/code/SuccessCode.java b/presentation/src/main/java/com/project/auth/presentation/support/response/ApiSuccessCode.java similarity index 75% rename from application/src/main/java/com/project/auth/application/support/code/SuccessCode.java rename to presentation/src/main/java/com/project/auth/presentation/support/response/ApiSuccessCode.java index 8b3e3d6..16742b3 100644 --- a/application/src/main/java/com/project/auth/application/support/code/SuccessCode.java +++ b/presentation/src/main/java/com/project/auth/presentation/support/response/ApiSuccessCode.java @@ -1,13 +1,13 @@ -package com.project.auth.application.support.code; +package com.project.auth.presentation.support.response; -public enum SuccessCode { +public enum ApiSuccessCode { USER_SIGNED_UP("USER_SIGNED_UP", "회원가입이 완료되었습니다."), AUTH_LOGIN_SUCCEEDED("AUTH_LOGIN_SUCCEEDED", "로그인이 완료되었습니다."); private final String code; private final String message; - SuccessCode(String code, String message) { + ApiSuccessCode(String code, String message) { this.code = code; this.message = message; } diff --git a/presentation/src/main/java/com/project/auth/presentation/user/controller/UserSignUpController.java b/presentation/src/main/java/com/project/auth/presentation/user/controller/UserSignUpController.java index da9b8ff..7c0fef2 100644 --- a/presentation/src/main/java/com/project/auth/presentation/user/controller/UserSignUpController.java +++ b/presentation/src/main/java/com/project/auth/presentation/user/controller/UserSignUpController.java @@ -1,13 +1,13 @@ package com.project.auth.presentation.user.controller; -import com.project.auth.application.support.code.SuccessCode; import com.project.auth.application.user.signup.SignUpResult; import com.project.auth.application.user.signup.port.in.SignUpUseCase; -import com.project.auth.common.response.ApiResult; import com.project.auth.presentation.user.docs.UserSignUpApiDocs; import com.project.auth.presentation.user.dto.SignUpRequest; import com.project.auth.presentation.user.dto.SignUpResponse; import com.project.auth.presentation.user.mapper.UserPresentationMapper; +import com.project.auth.presentation.support.response.ApiResult; +import com.project.auth.presentation.support.response.ApiSuccessCode; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import org.springframework.http.HttpStatus; @@ -22,7 +22,7 @@ @Tag(name = "Users", description = "회원 가입과 사용자 코어 API") public class UserSignUpController implements UserSignUpApiDocs { - private static final SuccessCode USER_SIGNED_UP = SuccessCode.USER_SIGNED_UP; + private static final ApiSuccessCode USER_SIGNED_UP = ApiSuccessCode.USER_SIGNED_UP; private final SignUpUseCase signUpUseCase; private final UserPresentationMapper userPresentationMapper; diff --git a/presentation/src/main/java/com/project/auth/presentation/user/docs/UserSignUpApiDocs.java b/presentation/src/main/java/com/project/auth/presentation/user/docs/UserSignUpApiDocs.java index 5858332..37920da 100644 --- a/presentation/src/main/java/com/project/auth/presentation/user/docs/UserSignUpApiDocs.java +++ b/presentation/src/main/java/com/project/auth/presentation/user/docs/UserSignUpApiDocs.java @@ -1,8 +1,8 @@ package com.project.auth.presentation.user.docs; -import com.project.auth.common.response.ApiResult; import com.project.auth.presentation.user.dto.SignUpRequest; import com.project.auth.presentation.user.dto.SignUpResponse; +import com.project.auth.presentation.support.response.ApiResult; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; diff --git a/presentation/src/test/java/com/project/auth/presentation/auth/controller/AuthLoginControllerTest.java b/presentation/src/test/java/com/project/auth/presentation/auth/controller/AuthLoginControllerTest.java index 6eefe26..79eba0a 100644 --- a/presentation/src/test/java/com/project/auth/presentation/auth/controller/AuthLoginControllerTest.java +++ b/presentation/src/test/java/com/project/auth/presentation/auth/controller/AuthLoginControllerTest.java @@ -2,9 +2,9 @@ import com.project.auth.application.auth.login.LoginResult; import com.project.auth.application.auth.login.port.in.LoginUseCase; -import com.project.auth.application.support.code.SuccessCode; import com.project.auth.presentation.auth.dto.LoginRequest; import com.project.auth.presentation.auth.mapper.AuthPresentationMapper; +import com.project.auth.presentation.support.response.ApiSuccessCode; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; @@ -37,7 +37,7 @@ void loginReturnsOkResponse() { assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).isNotNull(); assertThat(response.getBody().success()).isTrue(); - assertThat(response.getBody().code()).isEqualTo(SuccessCode.AUTH_LOGIN_SUCCEEDED.code()); + assertThat(response.getBody().code()).isEqualTo(ApiSuccessCode.AUTH_LOGIN_SUCCEEDED.code()); assertThat(response.getBody().data()).isNotNull(); assertThat(response.getBody().data().user().email()).isEqualTo("tester@example.com"); assertThat(response.getBody().data().token().issuer()).isEqualTo("project-auth-server"); diff --git a/presentation/src/test/java/com/project/auth/presentation/user/controller/UserSignUpControllerTest.java b/presentation/src/test/java/com/project/auth/presentation/user/controller/UserSignUpControllerTest.java index 4310d49..bfceff3 100644 --- a/presentation/src/test/java/com/project/auth/presentation/user/controller/UserSignUpControllerTest.java +++ b/presentation/src/test/java/com/project/auth/presentation/user/controller/UserSignUpControllerTest.java @@ -1,8 +1,8 @@ package com.project.auth.presentation.user.controller; -import com.project.auth.application.support.code.SuccessCode; import com.project.auth.application.user.signup.SignUpResult; import com.project.auth.application.user.signup.port.in.SignUpUseCase; +import com.project.auth.presentation.support.response.ApiSuccessCode; import com.project.auth.presentation.user.dto.SignUpRequest; import com.project.auth.presentation.user.mapper.UserPresentationMapper; import org.junit.jupiter.api.Test; @@ -32,7 +32,7 @@ void signUpReturnsCreatedResponse() { assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); assertThat(response.getBody()).isNotNull(); assertThat(response.getBody().success()).isTrue(); - assertThat(response.getBody().code()).isEqualTo(SuccessCode.USER_SIGNED_UP.code()); + assertThat(response.getBody().code()).isEqualTo(ApiSuccessCode.USER_SIGNED_UP.code()); assertThat(response.getBody().data()).isNotNull(); assertThat(response.getBody().data().email()).isEqualTo("tester@example.com"); } diff --git a/settings.gradle b/settings.gradle index 7498556..a1c8358 100644 --- a/settings.gradle +++ b/settings.gradle @@ -43,4 +43,3 @@ include("domain") include("application") include("presentation") include("infrastructure") -include("common")