From a9346d5baacf33711feaff0c713e121e50568005 Mon Sep 17 00:00:00 2001 From: zlon Date: Tue, 7 Apr 2026 12:52:29 +0900 Subject: [PATCH 1/6] =?UTF-8?q?refactor=20:=20gateway=20=EA=B6=8C=ED=95=9C?= =?UTF-8?q?=20role=20=EB=B9=84=EA=B5=90=EB=AC=B8=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 21 ++++- .../gatewayserver/config/SecurityConfig.java | 48 +++++++++- .../config/UserHeaderFilter.java | 88 +++++++++++-------- .../src/main/resources/application.yaml | 2 +- .../userservice/domain/entity/User.java | 12 ++- .../client/ShipmentFeignClient.java | 3 +- .../config/JpaAuditingConfig.java | 9 ++ .../infrastructure/config/SecurityConfig.java | 2 +- .../init/MasterUserInitializer.java | 42 +++++++++ .../controller/AuthController.java | 1 - .../src/main/resources/application.yaml | 14 +-- 11 files changed, 190 insertions(+), 52 deletions(-) create mode 100644 user-service/src/main/java/com/shipflow/userservice/infrastructure/config/JpaAuditingConfig.java create mode 100644 user-service/src/main/java/com/shipflow/userservice/infrastructure/init/MasterUserInitializer.java diff --git a/docker-compose.yml b/docker-compose.yml index 984794f..4f1f100 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -168,8 +168,16 @@ services: - "8080" environment: <<: *service-env + KEYCLOAK_REALM: ${KEYCLOAK_REALM} + KEYCLOAK_REALM_USER: ${KEYCLOAK_REALM_USER} + KEYCLOAK_ADMIN_USERNAME: ${KEYCLOAK_ADMIN_USERNAME} + KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} + KEYCLOAK_ADMIN_CLIENT_ID: ${KEYCLOAK_ADMIN_CLIENT_ID} + KEYCLOAK_LOGIN_CLIENT_ID: ${KEYCLOAK_LOGIN_CLIENT_ID} depends_on: <<: *service-depends-on + keycloak : + condition: service_healthy rabbitmq: image: rabbitmq:3-management @@ -210,8 +218,13 @@ services: container_name: shipflow-keycloak restart: unless-stopped environment: - KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN} + realm: ${KEYCLOAK_REALM} + user-realm: ${KEYCLOAK_REALM_USER} + environment: + KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN_USERNAME} KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} + admin-client-id: ${KEYCLOAK_ADMIN_CLIENT_ID} + login-client-id: ${KEYCLOAK_LOGIN_CLIENT_ID} KC_DB: postgres KC_DB_URL: jdbc:postgresql://keycloak-postgres:5432/keycloak KC_DB_USERNAME: keycloak @@ -222,6 +235,12 @@ services: - "9001:8080" depends_on: - keycloak-postgres + healthcheck: + test: [ "CMD-SHELL", "exec 3<>/dev/tcp/localhost/8080" ] + interval: 10s + timeout: 5s + retries: 20 + start_period: 120s volumes: shipflow_postgres_data: diff --git a/gateway-server/src/main/java/com/shipflow/gatewayserver/config/SecurityConfig.java b/gateway-server/src/main/java/com/shipflow/gatewayserver/config/SecurityConfig.java index d0c46b0..0b60798 100644 --- a/gateway-server/src/main/java/com/shipflow/gatewayserver/config/SecurityConfig.java +++ b/gateway-server/src/main/java/com/shipflow/gatewayserver/config/SecurityConfig.java @@ -2,15 +2,33 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter; import org.springframework.security.web.server.SecurityWebFilterChain; +import reactor.core.publisher.Mono; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.springframework.http.HttpMethod.*; @Configuration @EnableWebFluxSecurity +@EnableReactiveMethodSecurity public class SecurityConfig { private static final String[] WHITELIST = { @@ -30,7 +48,6 @@ public class SecurityConfig { public static final String AILOG = "/api/ai/**"; public static final String SLACK = "/api/slack/**"; - @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { return http @@ -77,9 +94,36 @@ public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { ) .oauth2ResourceServer(oauth -> oauth - .jwt(Customizer.withDefaults()) + .jwt(jwt -> jwt.jwtAuthenticationConverter(grantedAuthoritiesExtractor())) ) .build(); } + + @Bean + public Converter> grantedAuthoritiesExtractor() { + Converter> delegate = jwt -> { + JwtGrantedAuthoritiesConverter defaultConverter = new JwtGrantedAuthoritiesConverter(); + Collection defaultAuthorities = defaultConverter.convert(jwt); + + Map realmAccess = jwt.getClaimAsMap("realm_access"); + List realmRoles = List.of(); + + if (realmAccess != null && realmAccess.get("roles") instanceof List roles) { + realmRoles = roles.stream() + .map(Object::toString) + .map(role -> role.startsWith("ROLE_") ? role : "ROLE_" + role) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } + + return Stream.concat(defaultAuthorities.stream(), realmRoles.stream()) + .collect(Collectors.toSet()); + }; + + return new ReactiveJwtAuthenticationConverterAdapter(jwt -> { + Collection authorities = delegate.convert(jwt); + return new JwtAuthenticationToken(jwt, authorities, jwt.getSubject()); + }); + } } \ No newline at end of file diff --git a/gateway-server/src/main/java/com/shipflow/gatewayserver/config/UserHeaderFilter.java b/gateway-server/src/main/java/com/shipflow/gatewayserver/config/UserHeaderFilter.java index d207643..a73cca3 100644 --- a/gateway-server/src/main/java/com/shipflow/gatewayserver/config/UserHeaderFilter.java +++ b/gateway-server/src/main/java/com/shipflow/gatewayserver/config/UserHeaderFilter.java @@ -1,5 +1,7 @@ package com.shipflow.gatewayserver.config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -19,56 +21,68 @@ @Component public class UserHeaderFilter implements GlobalFilter { + private static final Logger log = LoggerFactory.getLogger(UserHeaderFilter.class); + private static final String USER_ID_HEADER = "X-User-Id"; private static final String USER_ROLE_HEADER = "X-User-Role"; @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { - // 모든 요청에서 먼저 헤더 제거 (핵심!) - ServerWebExchange sanitizedExchange = exchange.mutate() - .request(exchange.getRequest().mutate() - .headers(headers -> { - headers.remove(USER_ID_HEADER); - headers.remove(USER_ROLE_HEADER); - }) - .build()) - .build(); - - return sanitizedExchange.getPrincipal() - .ofType(Authentication.class) - .flatMap(auth -> { - - if (auth instanceof JwtAuthenticationToken jwtAuth) { - Jwt jwt = jwtAuth.getToken(); - - String userId = jwt.getSubject(); - String role = extractRole(jwt); + log.info("[UserHeaderFilter] entered. path={}, method={}", + exchange.getRequest().getURI().getPath(), + exchange.getRequest().getMethod()); - if (userId == null || userId.isBlank() || role == null || role.isBlank()) { - return Mono.error(new BusinessException(GateErrorCode.MISSING_ROLES)); - } - - // JWT 값으로만 재주입 - ServerHttpRequest mutated = sanitizedExchange.getRequest().mutate() + ServerWebExchange sanitizedExchange = exchange.mutate() + .request(exchange.getRequest().mutate() .headers(headers -> { - headers.add(USER_ID_HEADER, userId); - headers.add(USER_ROLE_HEADER, role); + headers.remove(USER_ID_HEADER); + headers.remove(USER_ROLE_HEADER); }) - .build(); + .build()) + .build(); - return chain.filter( - sanitizedExchange.mutate().request(mutated).build() - ); - } + return sanitizedExchange.getPrincipal() + .doOnNext(p -> log.info("[UserHeaderFilter] principal={}", p.getClass().getName())) + .switchIfEmpty(Mono.fromRunnable(() -> + log.warn("[UserHeaderFilter] principal is empty") + )) + .ofType(Authentication.class) + .doOnNext(auth -> log.info("[UserHeaderFilter] auth class={}", auth.getClass().getName())) + .flatMap(auth -> { + if (auth instanceof JwtAuthenticationToken jwtAuth) { + Jwt jwt = jwtAuth.getToken(); + + String userId = jwt.getSubject(); + String role = extractRole(jwt); + + log.info("[UserHeaderFilter] JWT subject={}", userId); + log.info("[UserHeaderFilter] JWT role={}", role); + log.info("[UserHeaderFilter] realm_access={}", jwt.getClaimAsMap("realm_access")); + + if (userId == null || userId.isBlank() || role == null || role.isBlank()) { + return Mono.error(new BusinessException(GateErrorCode.MISSING_ROLES)); + } + + ServerHttpRequest mutated = sanitizedExchange.getRequest().mutate() + .headers(headers -> { + headers.add(USER_ID_HEADER, userId); + headers.add(USER_ROLE_HEADER, role); + }) + .build(); + + return chain.filter( + sanitizedExchange.mutate().request(mutated).build() + ); + } - return chain.filter(sanitizedExchange); - }) - .switchIfEmpty(chain.filter(sanitizedExchange)); + log.warn("[UserHeaderFilter] Authentication exists but not JwtAuthenticationToken"); + return chain.filter(sanitizedExchange); + }) + .switchIfEmpty(chain.filter(sanitizedExchange)); } - - private String extractRole(Jwt jwt) { //role 추출 + private String extractRole(Jwt jwt) { Map realmAccess = jwt.getClaimAsMap("realm_access"); if (realmAccess == null) { throw new BusinessException(GateErrorCode.MISSING_REALM_ACCESS); diff --git a/gateway-server/src/main/resources/application.yaml b/gateway-server/src/main/resources/application.yaml index 81670a0..99ff34e 100644 --- a/gateway-server/src/main/resources/application.yaml +++ b/gateway-server/src/main/resources/application.yaml @@ -49,7 +49,7 @@ spring: oauth2: resourceserver: jwt: - issuer-uri: http://localhost:9001/realms/shipflow + issuer-uri: http://keycloak:8080/realms/shipflow eureka: client: diff --git a/user-service/src/main/java/com/shipflow/userservice/domain/entity/User.java b/user-service/src/main/java/com/shipflow/userservice/domain/entity/User.java index 349c947..83161af 100644 --- a/user-service/src/main/java/com/shipflow/userservice/domain/entity/User.java +++ b/user-service/src/main/java/com/shipflow/userservice/domain/entity/User.java @@ -58,8 +58,12 @@ public User(UUID id, String username, String name, String slackId) { this.name = name; this.slackId = slackId; this.status = UserStatus.PENDING; - this.createdAt = LocalDateTime.now(); + + LocalDateTime now = LocalDateTime.now(); + this.createdAt = now; this.createdBy = id; + this.updatedAt = now; + this.updatedBy = id; } public User(UUID id, String username, String name, String slackId, UUID hubId, UUID companyId) { @@ -68,6 +72,12 @@ public User(UUID id, String username, String name, String slackId, UUID hubId, U this.companyId = companyId; } + public User(UUID id, String username, String name, String slackId, UserRole role, UserStatus status) { + this(id, username, name, slackId); + this.status = status; + this.role = role; + } + public void approve(UserRole role){ //승인 if (this.status != UserStatus.PENDING) { throw new BusinessException(UserErrorCode.INVALID_USER_STATUS); diff --git a/user-service/src/main/java/com/shipflow/userservice/infrastructure/client/ShipmentFeignClient.java b/user-service/src/main/java/com/shipflow/userservice/infrastructure/client/ShipmentFeignClient.java index e543f9b..4333da8 100644 --- a/user-service/src/main/java/com/shipflow/userservice/infrastructure/client/ShipmentFeignClient.java +++ b/user-service/src/main/java/com/shipflow/userservice/infrastructure/client/ShipmentFeignClient.java @@ -3,11 +3,12 @@ import java.util.UUID; import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(name = "shipment-service") public interface ShipmentFeignClient { - @PatchMapping("/internal/shipments/{userId}") + @DeleteMapping("/internal/shipment-managers/users/{userId}") ClientApiResponse patchManager(@PathVariable UUID userId); } diff --git a/user-service/src/main/java/com/shipflow/userservice/infrastructure/config/JpaAuditingConfig.java b/user-service/src/main/java/com/shipflow/userservice/infrastructure/config/JpaAuditingConfig.java new file mode 100644 index 0000000..3a83bdd --- /dev/null +++ b/user-service/src/main/java/com/shipflow/userservice/infrastructure/config/JpaAuditingConfig.java @@ -0,0 +1,9 @@ +package com.shipflow.userservice.infrastructure.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaAuditingConfig { +} \ No newline at end of file diff --git a/user-service/src/main/java/com/shipflow/userservice/infrastructure/config/SecurityConfig.java b/user-service/src/main/java/com/shipflow/userservice/infrastructure/config/SecurityConfig.java index a89be54..6205c56 100644 --- a/user-service/src/main/java/com/shipflow/userservice/infrastructure/config/SecurityConfig.java +++ b/user-service/src/main/java/com/shipflow/userservice/infrastructure/config/SecurityConfig.java @@ -31,6 +31,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti @Bean public JwtDecoder jwtDecoder() { - return JwtDecoders.fromIssuerLocation("http://localhost:9001/realms/shipflow"); + return JwtDecoders.fromIssuerLocation("http://keycloak:8080/realms/shipflow"); } } \ No newline at end of file diff --git a/user-service/src/main/java/com/shipflow/userservice/infrastructure/init/MasterUserInitializer.java b/user-service/src/main/java/com/shipflow/userservice/infrastructure/init/MasterUserInitializer.java new file mode 100644 index 0000000..5148183 --- /dev/null +++ b/user-service/src/main/java/com/shipflow/userservice/infrastructure/init/MasterUserInitializer.java @@ -0,0 +1,42 @@ +package com.shipflow.userservice.infrastructure.init; + +import com.shipflow.userservice.domain.entity.User; +import com.shipflow.userservice.domain.model.UserRole; +import com.shipflow.userservice.domain.model.UserStatus; +import com.shipflow.userservice.domain.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Configuration +@RequiredArgsConstructor +public class MasterUserInitializer { + + private final UserRepository userRepository; + + @Bean + public ApplicationRunner initMasterUser() { + return args -> createMasterUserIfNotExists(); + } + + @Transactional + public void createMasterUserIfNotExists() { + String username = "master"; + + boolean exists = userRepository.findByUsername(username).isPresent(); + if (exists) { + return; + } + + UUID masterId = UUID.fromString("0c6a758d-afe4-47a4-9f09-df82c6e99653"); + LocalDateTime now = LocalDateTime.now(); + + User master = new User(masterId, "master", "master", "master-admin", UserRole.MASTER, UserStatus.APPROVED); + userRepository.save(master); + } +} \ No newline at end of file diff --git a/user-service/src/main/java/com/shipflow/userservice/presentation/controller/AuthController.java b/user-service/src/main/java/com/shipflow/userservice/presentation/controller/AuthController.java index b4ed089..b189a96 100644 --- a/user-service/src/main/java/com/shipflow/userservice/presentation/controller/AuthController.java +++ b/user-service/src/main/java/com/shipflow/userservice/presentation/controller/AuthController.java @@ -48,7 +48,6 @@ public ResponseEntity login(@RequestBody LoginReqDto request) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - System.out.println("[UserService] login endpoint called"); MultiValueMap form = new LinkedMultiValueMap<>(); form.add("grant_type", "password"); diff --git a/user-service/src/main/resources/application.yaml b/user-service/src/main/resources/application.yaml index 4e87287..25b48d8 100644 --- a/user-service/src/main/resources/application.yaml +++ b/user-service/src/main/resources/application.yaml @@ -20,7 +20,7 @@ spring: oauth2: resourceserver: jwt: - issuer-uri: http://localhost:9001/realms/shipflow + issuer-uri: http://keycloak:8080/realms/shipflow cloud: openfeign: @@ -31,13 +31,13 @@ spring: readTimeout: 5000 keycloak: - server-url: http://localhost:9001 - realm: master - user-realm: shipflow - admin-client-id: admin-cli - admin-username: ${KEYCLOAK_ADMIN} + server-url: http://keycloak:8080 + realm: ${KEYCLOAK_REALM} + user-realm: ${KEYCLOAK_REALM_USER} + admin-client-id: ${KEYCLOAK_ADMIN_CLIENT_ID} + admin-username: ${KEYCLOAK_ADMIN_USERNAME} admin-password: ${KEYCLOAK_ADMIN_PASSWORD} - login-client-id: shipflow-api + login-client-id: ${KEYCLOAK_LOGIN_CLIENT_ID} eureka: client: From 87c76a6c55afb60bad1d3176e010bd71636249a8 Mon Sep 17 00:00:00 2001 From: zlon Date: Tue, 7 Apr 2026 13:33:34 +0900 Subject: [PATCH 2/6] =?UTF-8?q?refactor=20:=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 8 ++++++++ docker-compose.yml | 1 - .../gatewayserver/config/UserHeaderFilter.java | 13 +++---------- .../infrastructure/config/SecurityConfig.java | 5 ----- .../infrastructure/init/MasterUserInitializer.java | 13 ++++++++++--- 5 files changed, 21 insertions(+), 19 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4757a00 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +.gradle +**/build +**/target +node_modules +.idea +.vscode +*.log \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 4f1f100..3be87b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -220,7 +220,6 @@ services: environment: realm: ${KEYCLOAK_REALM} user-realm: ${KEYCLOAK_REALM_USER} - environment: KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN_USERNAME} KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD} admin-client-id: ${KEYCLOAK_ADMIN_CLIENT_ID} diff --git a/gateway-server/src/main/java/com/shipflow/gatewayserver/config/UserHeaderFilter.java b/gateway-server/src/main/java/com/shipflow/gatewayserver/config/UserHeaderFilter.java index a73cca3..3d4de37 100644 --- a/gateway-server/src/main/java/com/shipflow/gatewayserver/config/UserHeaderFilter.java +++ b/gateway-server/src/main/java/com/shipflow/gatewayserver/config/UserHeaderFilter.java @@ -1,7 +1,7 @@ package com.shipflow.gatewayserver.config; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.shipflow.gatewayserver.exception.BusinessException; +import com.shipflow.gatewayserver.exception.GateErrorCode; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.http.server.reactive.ServerHttpRequest; @@ -15,14 +15,11 @@ import java.util.List; import java.util.Map; -import com.shipflow.gatewayserver.exception.BusinessException; -import com.shipflow.gatewayserver.exception.GateErrorCode; +import static reactor.netty.http.HttpConnectionLiveness.log; @Component public class UserHeaderFilter implements GlobalFilter { - private static final Logger log = LoggerFactory.getLogger(UserHeaderFilter.class); - private static final String USER_ID_HEADER = "X-User-Id"; private static final String USER_ROLE_HEADER = "X-User-Role"; @@ -56,10 +53,6 @@ public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { String userId = jwt.getSubject(); String role = extractRole(jwt); - log.info("[UserHeaderFilter] JWT subject={}", userId); - log.info("[UserHeaderFilter] JWT role={}", role); - log.info("[UserHeaderFilter] realm_access={}", jwt.getClaimAsMap("realm_access")); - if (userId == null || userId.isBlank() || role == null || role.isBlank()) { return Mono.error(new BusinessException(GateErrorCode.MISSING_ROLES)); } diff --git a/user-service/src/main/java/com/shipflow/userservice/infrastructure/config/SecurityConfig.java b/user-service/src/main/java/com/shipflow/userservice/infrastructure/config/SecurityConfig.java index 6205c56..90b0dfc 100644 --- a/user-service/src/main/java/com/shipflow/userservice/infrastructure/config/SecurityConfig.java +++ b/user-service/src/main/java/com/shipflow/userservice/infrastructure/config/SecurityConfig.java @@ -28,9 +28,4 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti return http.build(); } - - @Bean - public JwtDecoder jwtDecoder() { - return JwtDecoders.fromIssuerLocation("http://keycloak:8080/realms/shipflow"); - } } \ No newline at end of file diff --git a/user-service/src/main/java/com/shipflow/userservice/infrastructure/init/MasterUserInitializer.java b/user-service/src/main/java/com/shipflow/userservice/infrastructure/init/MasterUserInitializer.java index 5148183..68f3de7 100644 --- a/user-service/src/main/java/com/shipflow/userservice/infrastructure/init/MasterUserInitializer.java +++ b/user-service/src/main/java/com/shipflow/userservice/infrastructure/init/MasterUserInitializer.java @@ -10,7 +10,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; import java.util.UUID; @Configuration @@ -20,8 +19,17 @@ public class MasterUserInitializer { private final UserRepository userRepository; @Bean + @Transactional public ApplicationRunner initMasterUser() { - return args -> createMasterUserIfNotExists(); + return args -> { + String username = "master"; + if (userRepository.findByUsername(username).isPresent()) { + return; + } + UUID masterId = UUID.fromString("0c6a758d-afe4-47a4-9f09-df82c6e99653"); + User master = new User(masterId, "master", "master", "master-admin", UserRole.MASTER, UserStatus.APPROVED); + userRepository.save(master); + }; } @Transactional @@ -34,7 +42,6 @@ public void createMasterUserIfNotExists() { } UUID masterId = UUID.fromString("0c6a758d-afe4-47a4-9f09-df82c6e99653"); - LocalDateTime now = LocalDateTime.now(); User master = new User(masterId, "master", "master", "master-admin", UserRole.MASTER, UserStatus.APPROVED); userRepository.save(master); From a31c4464d48b8e57f88724fc46506e25442b2af2 Mon Sep 17 00:00:00 2001 From: zlon Date: Tue, 7 Apr 2026 16:46:38 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feature=20:=20=ED=99=98=EA=B2=BD=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=98=88=EC=8B=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.env.example b/.env.example index 3622b6e..826f025 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,20 @@ POSTGRES_DB= POSTGRES_USER= POSTGRES_PASSWORD= + +DB_HOST= +DB_PORT= +DB_NAME= +DB_USER= +DB_PASSWORD= + +KEYCLOAK_REALM= +KEYCLOAK_REALM_USER= +KEYCLOAK_ADMIN_USERNAME= +KEYCLOAK_ADMIN_PASSWORD= +KEYCLOAK_LOGIN_CLIENT_ID= +KEYCLOAK_ADMIN_CLIENT_ID= +KEYCLOAK_DB_PASSWORD= + RABBITMQ_USERNAME= RABBITMQ_PASSWORD= \ No newline at end of file From 3c90f2b92b4cfb1ed4f68780a779e56b4bdd5f39 Mon Sep 17 00:00:00 2001 From: zlon Date: Tue, 7 Apr 2026 17:03:33 +0900 Subject: [PATCH 4/6] =?UTF-8?q?refactor=20:=20healthcheck=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3ae88af..b38803f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -241,7 +241,7 @@ services: depends_on: - keycloak-postgres healthcheck: - test: [ "CMD-SHELL", "exec 3<>/dev/tcp/localhost/8080" ] + test: [ "CMD-SHELL", "nc -z localhost 8080" ] interval: 10s timeout: 5s retries: 20 From 5d0e1e57b29c0461d941aa0677b3c4a94d5030a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A7=80=EC=9B=90?= <140367464+zlonce@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:52:23 +0900 Subject: [PATCH 5/6] Update ci.yml --- .github/workflows/ci.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9064eb7..fbb880c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,18 @@ jobs: RABBITMQ_USERNAME: ${{ secrets.RABBITMQ_USERNAME }} RABBITMQ_PASSWORD: ${{ secrets.RABBITMQ_PASSWORD }} + KEYCLOAK_DB_PASSWORD: ${{ secrets.KEYCLOAK_DB_PASSWORD }} + KEYCLOAK_ADMIN_USERNAME: ${{ secrets.KEYCLOAK_ADMIN_USERNAME }} + KEYCLOAK_ADMIN_PASSWORD: ${{ secrets.KEYCLOAK_ADMIN_PASSWORD }} + KEYCLOAK_REALM: ${{ secrets.KEYCLOAK_REALM }} + KEYCLOAK_REALM_USER: ${{ secrets.KEYCLOAK_REALM_USER }} + KEYCLOAK_ADMIN_CLIENT_ID: ${{ secrets.KEYCLOAK_ADMIN_CLIENT_ID }} + KEYCLOAK_LOGIN_CLIENT_ID: ${{ secrets.KEYCLOAK_LOGIN_CLIENT_ID }} + + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + + steps: - name: Checkout uses: actions/checkout@v4 @@ -24,9 +36,9 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Build and start services - run: docker compose up -d --build --wait --wait-timeout 180 + run: docker compose up -d --build --wait --wait-timeout 500 timeout-minutes: 25 - name: Tear down if: always() - run: docker compose down -v \ No newline at end of file + run: docker compose down -v From 7de072b1e4538692f51480f3e467b2865f4ad103 Mon Sep 17 00:00:00 2001 From: zlon Date: Tue, 7 Apr 2026 18:58:31 +0900 Subject: [PATCH 6/6] =?UTF-8?q?refactor=20:=20keycloak=20db=20health=20?= =?UTF-8?q?=EA=B8=B0=EB=8B=A4=EB=A6=AC=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b38803f..3608755 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -215,6 +215,12 @@ services: - "5433:5432" volumes: - shipflow_keycloak_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U keycloak -d keycloak"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 20s keycloak: build: @@ -239,13 +245,14 @@ services: ports: - "9001:8080" depends_on: - - keycloak-postgres + keycloak-postgres: + condition: service_healthy healthcheck: - test: [ "CMD-SHELL", "nc -z localhost 8080" ] - interval: 10s - timeout: 5s - retries: 20 - start_period: 120s + test: ["CMD-SHELL", "bash -c '