Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
652691a
feat: AdminAuthService 추가 및 어드민 인증 로직 구현
Whale0928 Dec 22, 2025
903b6e3
feat: AdminAuthService 추가 및 어드민 인증 로직 구현
Whale0928 Dec 22, 2025
7ef3a13
refactor: 통합 테스트 관련 클래스 및 테스트 케이스 제거
Whale0928 Dec 22, 2025
18be9dc
chore: 배포 워크플로우 트리거 조건 수정
Whale0928 Dec 23, 2025
440ccb3
feat: 관리자 사용자 도메인 및 역할 관리 기능 추가
Whale0928 Dec 23, 2025
eb20e72
feat: 어드민 인증 로직 확장 및 토큰 갱신/탈퇴 API 추가
Whale0928 Dec 23, 2025
6bc4866
feat: Admin JWT 인증 및 필터 로직 추가
Whale0928 Dec 23, 2025
3f5400a
feat: 어드민 인증 통합 테스트 및 테스트용 팩토리 클래스 추가
Whale0928 Dec 23, 2025
82bf6ff
refactor: 어드민 모듈 통합 테스트 구조 및 설정 개선
Whale0928 Dec 23, 2025
2496e36
feat: 어드민 인증 관련 RestDocs 문서 및 테스트 추가
Whale0928 Dec 23, 2025
9384bbc
chore: CI 파이프라인 매트릭스 전략 및 테스트 단계 개선
Whale0928 Dec 23, 2025
e4ca0f7
chore: 버전 업데이트 1.0.4 → 1.0.5
Whale0928 Dec 23, 2025
0a7b202
feat: 어드민 회원가입 RestDocs 문서 및 테스트 추가
Whale0928 Dec 23, 2025
52f1427
feat: 어드민 회원가입 API 추가
Whale0928 Dec 23, 2025
5752376
refactor: 어드민 회원가입 통합 테스트 파일 정리 및 통합
Whale0928 Dec 23, 2025
a7c9e32
refactor: AdminSignupResponse 불필요한 static 메서드 제거 및 생성자 호출로 변경
Whale0928 Dec 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions .github/workflows/deploy_v2_development_product_api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@ name: deploy v2 development product api

on:
workflow_dispatch:
push:
workflow_run:
workflows: [ "product ci pipeline" ]
types:
- completed
branches:
- main
paths:
- 'bottlenote-batch/**'
- 'bottlenote-mono/**'
- 'bottlenote-observability/**'
- 'bottlenote-product-api/**'
#push:
# branches:
# - main
# paths:
# - 'bottlenote-batch/**'
# - 'bottlenote-mono/**'
# - 'bottlenote-observability/**'
# - 'bottlenote-product-api/**'

concurrency:
group: "deploy-product"
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/github-pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ jobs:
destination: ./_site
- name: Upload artifact
uses: actions/upload-pages-artifact@v3

deploy:
environment:
name: github-pages
Expand Down
22 changes: 13 additions & 9 deletions .github/workflows/product_ci_pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,11 @@ jobs:
with:
submodules: true
token: ${{ secrets.GIT_ACCESS_TOKEN }}

- name: setup jdk
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'

- name: setup gradle cache
uses: actions/cache@v3
with:
Expand Down Expand Up @@ -151,7 +149,14 @@ jobs:
needs: prepare
timeout-minutes: 20
runs-on: ubuntu-latest

strategy:
fail-fast: true
matrix:
include:
- name: product
task: integration_test
- name: admin
task: admin_integration_test
services:
docker:
image: docker:24.0
Expand All @@ -160,7 +165,6 @@ jobs:
- 1234:1234
env:
DOCKER_TLS_CERTDIR: ""

steps:
- name: checkout code
uses: actions/checkout@v4
Expand Down Expand Up @@ -191,17 +195,17 @@ jobs:
*/build
key: workspace-${{ github.sha }}

- name: run integration tests
run: ./gradlew integration_test
- name: run ${{ matrix.name }} integration tests
run: ./gradlew ${{ matrix.task }}

- name: upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: integration-test-results-${{ github.run_number }}
name: ${{ matrix.name }}-integration-test-results-${{ github.run_number }}
path: |
*/build/reports/tests/integration_test/
*/build/test-results/integration_test/
*/build/reports/tests/${{ matrix.task }}/
*/build/test-results/${{ matrix.task }}/

product-ci-final-build:
needs: [ unit-tests, rule-tests, integration-tests ]
Expand Down
2 changes: 1 addition & 1 deletion bottlenote-admin-api/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.4
1.0.5
6 changes: 3 additions & 3 deletions bottlenote-admin-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ val asciidoctorExt: Configuration by configurations.creating

dependencies {
implementation(project(":bottlenote-mono"))
testImplementation(project(":bottlenote-mono").dependencyProject.sourceSets["test"].output)

implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
Expand All @@ -31,9 +33,6 @@ dependencies {

// Test - Testcontainers
testImplementation(libs.bundles.testcontainers.complete)

// Test - mono 모듈 TestFactory 참조
testImplementation(project(":bottlenote-mono").dependencyProject.sourceSets.test.get().output)
}

sourceSets {
Expand Down Expand Up @@ -76,3 +75,4 @@ tasks.asciidoctor {
}
baseDirFollowsSourceFile()
}
tasks.register("prepareKotlinBuildScriptModel") {}
6 changes: 6 additions & 0 deletions bottlenote-admin-api/src/docs/asciidoc/admin-api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ include::api/overview/global-exception.adoc[]

'''

== Auth API

include::api/admin-auth/auth.adoc[]

'''

== Alcohol API

include::api/admin-alcohols/alcohols.adoc[]
72 changes: 72 additions & 0 deletions bottlenote-admin-api/src/docs/asciidoc/api/admin-auth/auth.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
=== 로그인 ===

- 관리자 계정으로 로그인하여 액세스 토큰과 리프레시 토큰을 발급받습니다.

[source]
----
POST /admin/api/v1/auth/login
----

[discrete]
==== 요청 파라미터 ====

[discrete]
include::{snippets}/admin/auth/login/request-fields.adoc[]
include::{snippets}/admin/auth/login/http-request.adoc[]

[discrete]
==== 응답 파라미터 ====

[discrete]
include::{snippets}/admin/auth/login/response-fields.adoc[]
include::{snippets}/admin/auth/login/http-response.adoc[]

'''

=== 토큰 갱신 ===

- 리프레시 토큰을 사용하여 새로운 액세스 토큰을 발급받습니다.

[source]
----
POST /admin/api/v1/auth/refresh
----

[discrete]
==== 요청 파라미터 ====

[discrete]
include::{snippets}/admin/auth/refresh/request-fields.adoc[]
include::{snippets}/admin/auth/refresh/http-request.adoc[]

[discrete]
==== 응답 파라미터 ====

[discrete]
include::{snippets}/admin/auth/refresh/response-fields.adoc[]
include::{snippets}/admin/auth/refresh/http-response.adoc[]

'''

=== 탈퇴 ===

- 인증된 관리자 계정을 탈퇴 처리합니다.

[source]
----
DELETE /admin/api/v1/auth/withdraw
----

[discrete]
==== 요청 헤더 ====

[discrete]
include::{snippets}/admin/auth/withdraw/request-headers.adoc[]
include::{snippets}/admin/auth/withdraw/http-request.adoc[]

[discrete]
==== 응답 파라미터 ====

[discrete]
include::{snippets}/admin/auth/withdraw/response-fields.adoc[]
include::{snippets}/admin/auth/withdraw/http-response.adoc[]
2 changes: 2 additions & 0 deletions bottlenote-admin-api/src/main/kotlin/app/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package app

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.domain.EntityScan
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication
import java.util.*

@ConfigurationPropertiesScan(basePackages = ["app"])
@EntityScan(basePackages = ["app"])
@SpringBootApplication(scanBasePackages = ["app"])
class AdminApplication
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package app.bottlenote.auth.config

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder

@ConfigurationProperties(prefix = "root.admin")
data class RootAdminProperties(
val email: String,
private val password: String
) {
/**
* 주입받은 인코더를 사용하여 비밀번호를 암호화하여 반환합니다.
*/
fun getEncodedPassword(encoder: BCryptPasswordEncoder): String {
return encoder.encode(password)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package app.bottlenote.auth.persentaton

import app.bottlenote.auth.config.RootAdminProperties
import app.bottlenote.global.data.response.GlobalResponse
import app.bottlenote.global.security.SecurityContextUtil
import app.bottlenote.user.dto.request.AdminSignupRequest
import app.bottlenote.user.dto.response.TokenItem
import app.bottlenote.user.exception.UserException
import app.bottlenote.user.exception.UserExceptionCode
import app.bottlenote.user.service.AdminAuthService
import jakarta.validation.Valid
import org.slf4j.LoggerFactory
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.context.event.EventListener
import org.springframework.http.ResponseEntity
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/auth")
class AuthController(
private val authService: AdminAuthService,
private val rootAdminProperties: RootAdminProperties,
private val encoder: BCryptPasswordEncoder
) {
private val log = LoggerFactory.getLogger(javaClass)

@EventListener(ApplicationReadyEvent::class)
fun onApplicationReady() {
val rootAdminIsAlive = authService.rootAdminIsAlive()
log.info("루트 어드민 존재 여부: {}", rootAdminIsAlive)
if (!rootAdminIsAlive) {
log.info("루트 어드민 초기 생성 로직 호출")
val email = rootAdminProperties.email
val encodedPassword = rootAdminProperties.getEncodedPassword(encoder)
authService.initRootAdmin(email, encodedPassword)
}
}

@PostMapping("/login")
fun login(@RequestBody request: LoginRequest): ResponseEntity<*> {
val tokenItem: TokenItem = authService.login(request.email, request.password)
return GlobalResponse.ok(tokenItem)
}

@PostMapping("/refresh")
fun refresh(@RequestBody request: RefreshRequest): ResponseEntity<*> {
val tokenItem: TokenItem = authService.refresh(request.refreshToken)
return GlobalResponse.ok(tokenItem)
}

@PostMapping("/signup")
fun signup(@RequestBody @Valid request: AdminSignupRequest): ResponseEntity<*> {
val requesterId = SecurityContextUtil.getAdminUserIdByContext()
.orElseThrow { UserException(UserExceptionCode.REQUIRED_USER_ID) }
val response = authService.signup(requesterId, request)
return GlobalResponse.ok(response)
}

@DeleteMapping("/withdraw")
fun withdraw(): ResponseEntity<*> {
val adminId = SecurityContextUtil.getAdminUserIdByContext()
.orElseThrow { UserException(UserExceptionCode.REQUIRED_USER_ID) }
authService.withdraw(adminId)
return GlobalResponse.ok(mapOf("message" to "탈퇴 처리되었습니다."))
}

data class LoginRequest(
val email: String,
val password: String
)

data class RefreshRequest(
val refreshToken: String
)
}
Original file line number Diff line number Diff line change
@@ -1,32 +1,43 @@
package app.global.security

import app.bottlenote.global.security.jwt.AdminJwtAuthenticationFilter
import app.bottlenote.global.security.jwt.AdminJwtAuthenticationManager
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.CorsConfigurationSource
import org.springframework.web.cors.UrlBasedCorsConfigurationSource

@Configuration
@EnableWebSecurity
class SecurityConfig {
class SecurityConfig(
private val adminJwtAuthenticationManager: AdminJwtAuthenticationManager
) {

@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
return http
.csrf { it.disable() }
.cors { corsConfigurationSource() }
.cors { it.configurationSource(corsConfigurationSource()) }
.sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
.formLogin { it.disable() }
.httpBasic { it.disable() }
.authorizeHttpRequests { auth ->
run {
auth.anyRequest().permitAll()

}
auth
.requestMatchers("/auth/login", "/auth/refresh").permitAll()
.requestMatchers("/actuator/**").permitAll()
.anyRequest().authenticated()
}
.addFilterBefore(
AdminJwtAuthenticationFilter(adminJwtAuthenticationManager),
UsernamePasswordAuthenticationFilter::class.java
)
.build()
}

Expand Down
8 changes: 5 additions & 3 deletions bottlenote-admin-api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,25 +58,27 @@ schedules:
sync:
enable: false

root:
admin:
email: ${ROOT_ADMIN_EMAIL:email@email.com}
password: ${ROOT_ADMIN_PASSWORD:email@email.com}

--- # default/local 프로파일 (동일 설정)
spring:
config:
activate:
on-profile: default,local

logging:
level:
root: info
app.bottlenote: info

management:
tracing:
enabled: false
otlp:
metrics:
export:
enabled: false

server:
tomcat:
threads:
Expand Down
Loading