From bd9d1b9bc17edad5526cbaf817995c49de108c3e Mon Sep 17 00:00:00 2001 From: dev-ant Date: Tue, 17 Feb 2026 12:13:30 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EC=A0=95=EB=B3=B4=20Kafka=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=88=98=EC=8B=A0=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/config/KafkaListenerConfig.java | 9 + .../common/constant/UserUpdateConstant.java | 1 + .../common/exception/GlobalExceptionCode.java | 5 + .../the/eternity/das/user/entity/User.java | 28 +- .../das/user/entity/UserRepository.java | 2 + .../application/UserVerificationService.java | 389 ++++++++++++++++++ .../UserVerificationHistoryListResponse.java | 11 + .../UserVerificationHistoryResponse.java | 33 ++ .../UserVerificationInfoResponse.java | 40 ++ .../UserVerificationTokenIssueResponse.java | 9 + .../UserVerificationTokenResponse.java | 15 + .../verification/entity/UserVerification.java | 79 ++++ .../entity/UserVerificationHistory.java | 49 +++ .../UserVerificationHistoryRepository.java | 9 + .../entity/UserVerificationRepository.java | 11 + .../entity/UserVerificationToken.java | 70 ++++ .../UserVerificationTokenRepository.java | 19 + .../enums/VerificationFailureReason.java | 9 + .../entity/enums/VerificationTokenStatus.java | 8 + .../kafka/UserVerificationKafkaConsumer.java | 24 ++ .../kafka/UserVerificationVerifyMessage.java | 12 + .../UserVerificationController.java | 69 ++++ src/main/resources/application-sample.yml | 15 + ...13110000__add_user_verification_tables.sql | 53 +++ 24 files changed, 968 insertions(+), 1 deletion(-) create mode 100644 src/main/java/until/the/eternity/das/common/config/KafkaListenerConfig.java create mode 100644 src/main/java/until/the/eternity/das/verification/application/UserVerificationService.java create mode 100644 src/main/java/until/the/eternity/das/verification/dto/response/UserVerificationHistoryListResponse.java create mode 100644 src/main/java/until/the/eternity/das/verification/dto/response/UserVerificationHistoryResponse.java create mode 100644 src/main/java/until/the/eternity/das/verification/dto/response/UserVerificationInfoResponse.java create mode 100644 src/main/java/until/the/eternity/das/verification/dto/response/UserVerificationTokenIssueResponse.java create mode 100644 src/main/java/until/the/eternity/das/verification/dto/response/UserVerificationTokenResponse.java create mode 100644 src/main/java/until/the/eternity/das/verification/entity/UserVerification.java create mode 100644 src/main/java/until/the/eternity/das/verification/entity/UserVerificationHistory.java create mode 100644 src/main/java/until/the/eternity/das/verification/entity/UserVerificationHistoryRepository.java create mode 100644 src/main/java/until/the/eternity/das/verification/entity/UserVerificationRepository.java create mode 100644 src/main/java/until/the/eternity/das/verification/entity/UserVerificationToken.java create mode 100644 src/main/java/until/the/eternity/das/verification/entity/UserVerificationTokenRepository.java create mode 100644 src/main/java/until/the/eternity/das/verification/entity/enums/VerificationFailureReason.java create mode 100644 src/main/java/until/the/eternity/das/verification/entity/enums/VerificationTokenStatus.java create mode 100644 src/main/java/until/the/eternity/das/verification/kafka/UserVerificationKafkaConsumer.java create mode 100644 src/main/java/until/the/eternity/das/verification/kafka/UserVerificationVerifyMessage.java create mode 100644 src/main/java/until/the/eternity/das/verification/presentation/UserVerificationController.java create mode 100644 src/main/resources/db/migration/V20260213110000__add_user_verification_tables.sql diff --git a/src/main/java/until/the/eternity/das/common/config/KafkaListenerConfig.java b/src/main/java/until/the/eternity/das/common/config/KafkaListenerConfig.java new file mode 100644 index 0000000..30b1d29 --- /dev/null +++ b/src/main/java/until/the/eternity/das/common/config/KafkaListenerConfig.java @@ -0,0 +1,9 @@ +package until.the.eternity.das.common.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; + +@Configuration +@EnableKafka +public class KafkaListenerConfig { +} diff --git a/src/main/java/until/the/eternity/das/common/constant/UserUpdateConstant.java b/src/main/java/until/the/eternity/das/common/constant/UserUpdateConstant.java index 1220e9a..3c817b5 100644 --- a/src/main/java/until/the/eternity/das/common/constant/UserUpdateConstant.java +++ b/src/main/java/until/the/eternity/das/common/constant/UserUpdateConstant.java @@ -3,4 +3,5 @@ public class UserUpdateConstant { public static final String INFO = "USER_INFO_UPDATE_EVENT"; public static final String PASSWORD = "PASSWORD_UPDATE_EVENT"; + public static final String USER_VERIFICATION_VERIFY = "USER_VERIFICATION_VERIFY_EVENT"; } diff --git a/src/main/java/until/the/eternity/das/common/exception/GlobalExceptionCode.java b/src/main/java/until/the/eternity/das/common/exception/GlobalExceptionCode.java index de2dc5a..4b80c69 100644 --- a/src/main/java/until/the/eternity/das/common/exception/GlobalExceptionCode.java +++ b/src/main/java/until/the/eternity/das/common/exception/GlobalExceptionCode.java @@ -39,6 +39,11 @@ public enum GlobalExceptionCode implements ExceptionCode { // User USER_INFO_UPDATE_FAILED(INTERNAL_SERVER_ERROR, "사용자 정보 수정에 실패했습니다. 잠시 후 다시 시도해주세요."), + USER_VERIFICATION_GENERATE_FAILED(INTERNAL_SERVER_ERROR, "인증 코드 생성에 실패했습니다. 잠시 후 다시 시도해주세요."), + USER_VERIFICATION_TOKEN_NOT_FOUND(HttpStatus.BAD_REQUEST, "발급된 인증 토큰이 존재하지 않습니다."), + USER_VERIFICATION_TOKEN_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "이미 유효한 인증 토큰이 존재합니다. 재발급을 이용해주세요."), + USER_VERIFICATION_COOLDOWN_ACTIVE(HttpStatus.BAD_REQUEST, "최근 7일 이내 인증 성공 이력이 있어 토큰 발급이 불가능합니다."), + USER_VERIFICATION_INVALID(HttpStatus.BAD_REQUEST, "유효하지 않거나 만료된 인증 코드입니다."), // OAUTH NOT_SUPPORTED_PROVIDER(HttpStatus.BAD_REQUEST, "지원하지 않는 소셜로그인입니다"); diff --git a/src/main/java/until/the/eternity/das/user/entity/User.java b/src/main/java/until/the/eternity/das/user/entity/User.java index 6570e6b..eba5840 100644 --- a/src/main/java/until/the/eternity/das/user/entity/User.java +++ b/src/main/java/until/the/eternity/das/user/entity/User.java @@ -68,6 +68,18 @@ public class User { @Enumerated(EnumType.STRING) private InactivatedType inactivatedType; + @Column(name = "server_name", length = 20) + @Comment("게임 서버명") + private String serverName; + + @Column(name = "is_verified", nullable = false) + @Comment("사용자 인증 상태") + private boolean verified; + + @Column(name = "verified_at") + @Comment("최근 인증 성공 시각") + private LocalDateTime verifiedAt; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "role_id") private Role role; @@ -90,4 +102,18 @@ public void updateUserStatus(Status status) { this.inactivatedAt = LocalDateTime.now(); } -} \ No newline at end of file + public void updateServerName(String serverName) { + this.serverName = serverName; + } + + public void updateVerificationStatus(boolean verified, LocalDateTime verifiedAt) { + this.verified = verified; + this.verifiedAt = verifiedAt; + } + + public void updateGameProfile(String nickname, String serverName) { + this.nickname = nickname; + this.serverName = serverName; + } + +} diff --git a/src/main/java/until/the/eternity/das/user/entity/UserRepository.java b/src/main/java/until/the/eternity/das/user/entity/UserRepository.java index b08fbe8..21aa44b 100644 --- a/src/main/java/until/the/eternity/das/user/entity/UserRepository.java +++ b/src/main/java/until/the/eternity/das/user/entity/UserRepository.java @@ -10,6 +10,8 @@ public interface UserRepository extends JpaRepository { boolean existsByNickname(String nickname); + boolean existsByNicknameAndIdNot(String nickname, Long id); + User save(User user); Optional findByEmail(String email); diff --git a/src/main/java/until/the/eternity/das/verification/application/UserVerificationService.java b/src/main/java/until/the/eternity/das/verification/application/UserVerificationService.java new file mode 100644 index 0000000..913926c --- /dev/null +++ b/src/main/java/until/the/eternity/das/verification/application/UserVerificationService.java @@ -0,0 +1,389 @@ +package until.the.eternity.das.verification.application; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Collections; +import java.util.List; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import until.the.eternity.das.common.aop.ActiveUserRequired; +import until.the.eternity.das.common.exception.CustomException; +import until.the.eternity.das.common.exception.GlobalExceptionCode; +import until.the.eternity.das.user.entity.User; +import until.the.eternity.das.user.entity.UserRepository; +import until.the.eternity.das.verification.dto.response.UserVerificationHistoryResponse; +import until.the.eternity.das.verification.dto.response.UserVerificationHistoryListResponse; +import until.the.eternity.das.verification.dto.response.UserVerificationInfoResponse; +import until.the.eternity.das.verification.dto.response.UserVerificationTokenIssueResponse; +import until.the.eternity.das.verification.dto.response.UserVerificationTokenResponse; +import until.the.eternity.das.verification.entity.UserVerification; +import until.the.eternity.das.verification.entity.UserVerificationHistory; +import until.the.eternity.das.verification.entity.UserVerificationHistoryRepository; +import until.the.eternity.das.verification.entity.UserVerificationRepository; +import until.the.eternity.das.verification.entity.UserVerificationToken; +import until.the.eternity.das.verification.entity.UserVerificationTokenRepository; +import until.the.eternity.das.verification.entity.enums.VerificationFailureReason; +import until.the.eternity.das.verification.entity.enums.VerificationTokenStatus; +import until.the.eternity.das.verification.kafka.UserVerificationVerifyMessage; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UserVerificationService { + + private static final int MAX_GENERATION_RETRY = 20; + private static final int NONCE_BYTES_LENGTH = 16; + private static final String HMAC_ALGORITHM = "HmacSHA256"; + + private final UserRepository userRepository; + private final UserVerificationTokenRepository userVerificationTokenRepository; + private final UserVerificationRepository userVerificationRepository; + private final UserVerificationHistoryRepository userVerificationHistoryRepository; + private final ObjectMapper objectMapper; + + private final SecureRandom secureRandom = new SecureRandom(); + + @Value("${app.verification.prefix:메모노기_}") + private String verificationPrefix; + + @Value("${app.verification.length:20}") + private int verificationLength; + + @Value("${app.verification.validity-minutes:60}") + private long verificationValidityMinutes; + + @Value("${app.verification.cooldown-days:7}") + private long verificationCooldownDays; + + @Value("${app.verification.hash-secret:change-this-secret}") + private String verificationHashSecret; + + @Transactional + @ActiveUserRequired + public UserVerificationTokenIssueResponse issueToken(Long userId) { + return issueTokenInternal(userId, false); + } + + @Transactional + @ActiveUserRequired + public UserVerificationTokenIssueResponse reissueToken(Long userId) { + return issueTokenInternal(userId, true); + } + + @Transactional(readOnly = true) + @ActiveUserRequired + public UserVerificationTokenResponse getMyToken(Long userId) { + UserVerificationToken token = userVerificationTokenRepository + .findTopByUserIdOrderByIssuedAtDesc(userId) + .orElseThrow(() -> new CustomException(GlobalExceptionCode.USER_VERIFICATION_TOKEN_NOT_FOUND)); + + return toTokenResponse(token); + } + + @Transactional(readOnly = true) + @ActiveUserRequired + public UserVerificationInfoResponse getMyVerificationInfo(Long userId) { + ensureUserExists(userId); + return UserVerificationInfoResponse.of(userId, userVerificationRepository.findByUserId(userId).orElse(null)); + } + + @Transactional(readOnly = true) + @ActiveUserRequired + public UserVerificationHistoryListResponse getMyVerificationHistories(Long userId, String sort, Integer limit) { + ensureUserExists(userId); + + int normalizedLimit = normalizeLimit(limit); + String normalizedSort = normalizeSort(sort); + + List items = userVerificationHistoryRepository.findTop100ByUserIdOrderByVerifiedAtDesc(userId) + .stream() + .map(UserVerificationHistoryResponse::of) + .toList(); + + if ("oldest".equals(normalizedSort)) { + items = items.stream().collect(java.util.stream.Collectors.toList()); + Collections.reverse(items); + } + + if (items.size() > normalizedLimit) { + items = items.subList(0, normalizedLimit); + } + + return new UserVerificationHistoryListResponse( + normalizedSort, + normalizedLimit, + items.size(), + items + ); + } + + @Transactional(readOnly = true) + public UserVerificationInfoResponse getUserVerificationInfo(Long userId) { + ensureUserExists(userId); + return UserVerificationInfoResponse.of(userId, userVerificationRepository.findByUserId(userId).orElse(null)); + } + + @Transactional + public void verifyFromKafkaPayload(String payload) { + UserVerificationVerifyMessage event = parseMessage(payload); + if (event == null) { + return; + } + + LocalDateTime now = LocalDateTime.now(); + + if (event.verificationValue() == null || event.verificationValue().isBlank()) { + saveFailureHistory(null, null, event.serverName(), event.characterName(), now, VerificationFailureReason.INVALID_MESSAGE); + return; + } + + UserVerificationToken token = userVerificationTokenRepository.findByTokenValue(event.verificationValue()).orElse(null); + + if (token == null) { + saveFailureHistory(null, null, event.serverName(), event.characterName(), now, VerificationFailureReason.TOKEN_NOT_FOUND); + return; + } + + User user = token.getUser(); + + if (token.isRevoked()) { + saveFailureHistory(user, token, event.serverName(), event.characterName(), now, VerificationFailureReason.TOKEN_REVOKED); + return; + } + + if (token.isExpired(now)) { + saveFailureHistory(user, token, event.serverName(), event.characterName(), now, VerificationFailureReason.TOKEN_EXPIRED); + return; + } + + if (token.isVerified()) { + saveFailureHistory(user, token, event.serverName(), event.characterName(), now, VerificationFailureReason.TOKEN_ALREADY_VERIFIED); + return; + } + + if (event.serverName() == null || event.serverName().isBlank() + || event.characterName() == null || event.characterName().isBlank()) { + saveFailureHistory(user, token, event.serverName(), event.characterName(), now, VerificationFailureReason.INVALID_MESSAGE); + return; + } + + invalidatePreviousOwner(event.serverName(), event.characterName(), user.getId()); + + UserVerification verification = userVerificationRepository.findByUserId(user.getId()) + .orElseGet(() -> UserVerification.builder().user(user).verificationCount(0).verified(false).build()); + + verification.markVerified(event.serverName(), event.characterName(), now, token.getId()); + token.markVerified(now); + + user.updateServerName(event.serverName()); + user.updateVerificationStatus(true, now); + + userVerificationRepository.save(verification); + + userVerificationHistoryRepository.save( + UserVerificationHistory.builder() + .user(user) + .serverName(event.serverName()) + .characterName(event.characterName()) + .verifiedAt(now) + .verificationSuccess(true) + .token(token) + .build() + ); + } + + private UserVerificationTokenIssueResponse issueTokenInternal(Long userId, boolean reissue) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(GlobalExceptionCode.USER_NOT_EXISTS)); + + LocalDateTime now = LocalDateTime.now(); + + UserVerification verification = userVerificationRepository.findByUserId(userId).orElse(null); + if (verification != null && verification.hasRecentSuccess(now.minusDays(verificationCooldownDays))) { + throw new CustomException(GlobalExceptionCode.USER_VERIFICATION_COOLDOWN_ACTIVE); + } + + UserVerificationToken activeToken = userVerificationTokenRepository + .findTopByUserIdAndRevokedFalseAndVerifiedFalseAndExpiresAtAfterOrderByIssuedAtDesc(userId, now) + .orElse(null); + + if (!reissue && activeToken != null) { + throw new CustomException(GlobalExceptionCode.USER_VERIFICATION_TOKEN_ALREADY_EXISTS); + } + + if (reissue && activeToken != null) { + activeToken.revoke(now); + } + + String verificationValue = generateUniqueVerificationValue(userId, now); + LocalDateTime expiresAt = now.plusMinutes(verificationValidityMinutes); + + userVerificationTokenRepository.save( + UserVerificationToken.builder() + .user(user) + .issuedAt(now) + .expiresAt(expiresAt) + .revoked(false) + .verified(false) + .tokenValue(verificationValue) + .build() + ); + + return new UserVerificationTokenIssueResponse(verificationPrefix + verificationValue, expiresAt); + } + + private void invalidatePreviousOwner(String serverName, String characterName, Long currentUserId) { + UserVerification existingOwner = userVerificationRepository + .findByServerNameAndCharacterNameAndVerifiedTrue(serverName, characterName) + .orElse(null); + + if (existingOwner == null || existingOwner.getUser().getId().equals(currentUserId)) { + return; + } + + existingOwner.invalidate(); + existingOwner.getUser().updateVerificationStatus(false, null); + } + + private UserVerificationVerifyMessage parseMessage(String payload) { + try { + return objectMapper.readValue(payload, UserVerificationVerifyMessage.class); + } catch (JsonProcessingException e) { + log.warn("Invalid kafka payload for verification. payload={}", payload); + saveFailureHistory(null, null, null, null, LocalDateTime.now(), VerificationFailureReason.INVALID_MESSAGE); + return null; + } + } + + private void saveFailureHistory( + User user, + UserVerificationToken token, + String serverName, + String characterName, + LocalDateTime now, + VerificationFailureReason reason + ) { + userVerificationHistoryRepository.save( + UserVerificationHistory.builder() + .user(user) + .serverName(serverName) + .characterName(characterName) + .verifiedAt(now) + .verificationSuccess(false) + .failureReason(reason) + .token(token) + .build() + ); + } + + private UserVerificationTokenResponse toTokenResponse(UserVerificationToken token) { + LocalDateTime now = LocalDateTime.now(); + long expiresInSeconds = Math.max(0, Duration.between(now, token.getExpiresAt()).getSeconds()); + VerificationTokenStatus status = resolveTokenStatus(token, now); + + return new UserVerificationTokenResponse( + token.getId(), + verificationPrefix + token.getTokenValue(), + status.name(), + token.getIssuedAt(), + token.getExpiresAt(), + expiresInSeconds, + token.isRevoked(), + token.isVerified() + ); + } + + private VerificationTokenStatus resolveTokenStatus(UserVerificationToken token, LocalDateTime now) { + if (token.isVerified()) { + return VerificationTokenStatus.VERIFIED; + } + if (token.isRevoked()) { + return VerificationTokenStatus.REVOKED; + } + if (token.isExpired(now)) { + return VerificationTokenStatus.EXPIRED; + } + return VerificationTokenStatus.ACTIVE; + } + + private int normalizeLimit(Integer limit) { + if (limit == null) { + return 20; + } + if (limit < 1) { + return 1; + } + return Math.min(limit, 100); + } + + private String normalizeSort(String sort) { + if (sort == null || sort.isBlank()) { + return "latest"; + } + return "oldest".equalsIgnoreCase(sort) ? "oldest" : "latest"; + } + + private void ensureUserExists(Long userId) { + if (!userRepository.existsById(userId)) { + throw new CustomException(GlobalExceptionCode.USER_NOT_EXISTS); + } + } + + private String generateUniqueVerificationValue(Long userId, LocalDateTime issuedAt) { + long issuedAtMillis = issuedAt.toInstant(ZoneOffset.UTC).toEpochMilli(); + + for (int retry = 0; retry < MAX_GENERATION_RETRY; retry++) { + String candidate = buildHashedVerificationValue(userId, issuedAtMillis); + if (!userVerificationTokenRepository.existsByTokenValue(candidate)) { + return candidate; + } + } + + throw new CustomException(GlobalExceptionCode.USER_VERIFICATION_GENERATE_FAILED); + } + + private String buildHashedVerificationValue(Long userId, long issuedAtMillis) { + byte[] nonce = new byte[NONCE_BYTES_LENGTH]; + secureRandom.nextBytes(nonce); + + String payload = userId + ":" + issuedAtMillis + ":" + toHex(nonce); + String digest = hmacSha256Hex(payload, verificationHashSecret); + + if (verificationLength > digest.length()) { + throw new CustomException(GlobalExceptionCode.USER_VERIFICATION_GENERATE_FAILED); + } + + return digest.substring(0, verificationLength); + } + + private String hmacSha256Hex(String message, String secret) { + try { + Mac mac = Mac.getInstance(HMAC_ALGORITHM); + SecretKeySpec secretKeySpec = + new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM); + mac.init(secretKeySpec); + + return toHex(mac.doFinal(message.getBytes(StandardCharsets.UTF_8))); + } catch (Exception e) { + throw new CustomException(GlobalExceptionCode.USER_VERIFICATION_GENERATE_FAILED); + } + } + + private String toHex(byte[] bytes) { + StringBuilder builder = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + builder.append(String.format("%02X", b)); + } + return builder.toString(); + } +} diff --git a/src/main/java/until/the/eternity/das/verification/dto/response/UserVerificationHistoryListResponse.java b/src/main/java/until/the/eternity/das/verification/dto/response/UserVerificationHistoryListResponse.java new file mode 100644 index 0000000..461b5cd --- /dev/null +++ b/src/main/java/until/the/eternity/das/verification/dto/response/UserVerificationHistoryListResponse.java @@ -0,0 +1,11 @@ +package until.the.eternity.das.verification.dto.response; + +import java.util.List; + +public record UserVerificationHistoryListResponse( + String sort, + int limit, + int count, + List items +) { +} diff --git a/src/main/java/until/the/eternity/das/verification/dto/response/UserVerificationHistoryResponse.java b/src/main/java/until/the/eternity/das/verification/dto/response/UserVerificationHistoryResponse.java new file mode 100644 index 0000000..7639c87 --- /dev/null +++ b/src/main/java/until/the/eternity/das/verification/dto/response/UserVerificationHistoryResponse.java @@ -0,0 +1,33 @@ +package until.the.eternity.das.verification.dto.response; + +import java.time.LocalDateTime; +import until.the.eternity.das.verification.entity.UserVerificationHistory; + +public record UserVerificationHistoryResponse( + Long historyId, + String serverName, + String characterName, + LocalDateTime verifiedAt, + boolean verificationSuccess, + String resultCode, + String resultMessage, + String failureReason, + Long tokenId +) { + + public static UserVerificationHistoryResponse of(UserVerificationHistory history) { + String failureReason = history.getFailureReason() == null ? null : history.getFailureReason().name(); + + return new UserVerificationHistoryResponse( + history.getId(), + history.getServerName(), + history.getCharacterName(), + history.getVerifiedAt(), + history.isVerificationSuccess(), + history.isVerificationSuccess() ? "SUCCESS" : "FAIL", + history.isVerificationSuccess() ? "인증 성공" : "인증 실패", + failureReason, + history.getToken() == null ? null : history.getToken().getId() + ); + } +} diff --git a/src/main/java/until/the/eternity/das/verification/dto/response/UserVerificationInfoResponse.java b/src/main/java/until/the/eternity/das/verification/dto/response/UserVerificationInfoResponse.java new file mode 100644 index 0000000..e3877b8 --- /dev/null +++ b/src/main/java/until/the/eternity/das/verification/dto/response/UserVerificationInfoResponse.java @@ -0,0 +1,40 @@ +package until.the.eternity.das.verification.dto.response; + +import java.time.LocalDateTime; +import until.the.eternity.das.verification.entity.UserVerification; + +public record UserVerificationInfoResponse( + Long userId, + boolean verified, + String verificationState, + String serverName, + String characterName, + String verificationIdentity, + LocalDateTime lastVerifiedAt, + int verificationCount, + Long latestTokenId +) { + + public static UserVerificationInfoResponse of(Long userId, UserVerification verification) { + if (verification == null) { + return new UserVerificationInfoResponse(userId, false, "UNVERIFIED", null, null, null, null, 0, null); + } + + String identity = null; + if (verification.getServerName() != null && verification.getCharacterName() != null) { + identity = verification.getServerName() + ":" + verification.getCharacterName(); + } + + return new UserVerificationInfoResponse( + userId, + verification.isVerified(), + verification.isVerified() ? "VERIFIED" : "UNVERIFIED", + verification.getServerName(), + verification.getCharacterName(), + identity, + verification.getLastVerifiedAt(), + verification.getVerificationCount(), + verification.getLatestTokenId() + ); + } +} diff --git a/src/main/java/until/the/eternity/das/verification/dto/response/UserVerificationTokenIssueResponse.java b/src/main/java/until/the/eternity/das/verification/dto/response/UserVerificationTokenIssueResponse.java new file mode 100644 index 0000000..9dde8ed --- /dev/null +++ b/src/main/java/until/the/eternity/das/verification/dto/response/UserVerificationTokenIssueResponse.java @@ -0,0 +1,9 @@ +package until.the.eternity.das.verification.dto.response; + +import java.time.LocalDateTime; + +public record UserVerificationTokenIssueResponse( + String verificationCode, + LocalDateTime expiresAt +) { +} diff --git a/src/main/java/until/the/eternity/das/verification/dto/response/UserVerificationTokenResponse.java b/src/main/java/until/the/eternity/das/verification/dto/response/UserVerificationTokenResponse.java new file mode 100644 index 0000000..320c7d3 --- /dev/null +++ b/src/main/java/until/the/eternity/das/verification/dto/response/UserVerificationTokenResponse.java @@ -0,0 +1,15 @@ +package until.the.eternity.das.verification.dto.response; + +import java.time.LocalDateTime; + +public record UserVerificationTokenResponse( + Long tokenId, + String verificationCode, + String tokenStatus, + LocalDateTime issuedAt, + LocalDateTime expiresAt, + Long expiresInSeconds, + boolean revoked, + boolean verified +) { +} diff --git a/src/main/java/until/the/eternity/das/verification/entity/UserVerification.java b/src/main/java/until/the/eternity/das/verification/entity/UserVerification.java new file mode 100644 index 0000000..4cf4639 --- /dev/null +++ b/src/main/java/until/the/eternity/das/verification/entity/UserVerification.java @@ -0,0 +1,79 @@ +package until.the.eternity.das.verification.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import until.the.eternity.das.user.entity.User; + +@Entity +@Table(name = "user_verification") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class UserVerification { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false, unique = true) + private User user; + + @Column(name = "server_name", length = 20) + private String serverName; + + @Column(name = "character_name", length = 100) + private String characterName; + + @Column(name = "last_verified_at") + private LocalDateTime lastVerifiedAt; + + @Column(name = "verification_count", nullable = false) + private int verificationCount; + + @Column(name = "latest_token_id") + private Long latestTokenId; + + @Column(name = "is_verified", nullable = false) + private boolean verified; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + public boolean hasRecentSuccess(LocalDateTime threshold) { + return lastVerifiedAt != null && !lastVerifiedAt.isBefore(threshold); + } + + public boolean matchesIdentity(String serverName, String characterName) { + return this.serverName != null && this.characterName != null + && this.serverName.equals(serverName) + && this.characterName.equals(characterName); + } + + public void markVerified(String serverName, String characterName, LocalDateTime now, Long latestTokenId) { + if (matchesIdentity(serverName, characterName)) { + this.verificationCount = this.verificationCount + 1; + } else { + this.verificationCount = 1; + } + + this.serverName = serverName; + this.characterName = characterName; + this.lastVerifiedAt = now; + this.latestTokenId = latestTokenId; + this.verified = true; + } + + public void invalidate() { + this.verified = false; + } +} diff --git a/src/main/java/until/the/eternity/das/verification/entity/UserVerificationHistory.java b/src/main/java/until/the/eternity/das/verification/entity/UserVerificationHistory.java new file mode 100644 index 0000000..0a05295 --- /dev/null +++ b/src/main/java/until/the/eternity/das/verification/entity/UserVerificationHistory.java @@ -0,0 +1,49 @@ +package until.the.eternity.das.verification.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; +import until.the.eternity.das.user.entity.User; +import until.the.eternity.das.verification.entity.enums.VerificationFailureReason; + +@Entity +@Table(name = "user_verification_history") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class UserVerificationHistory { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @Column(name = "server_name", length = 20) + private String serverName; + + @Column(name = "character_name", length = 100) + private String characterName; + + @Column(name = "verified_at", nullable = false) + private LocalDateTime verifiedAt; + + @Column(name = "verification_success", nullable = false) + private boolean verificationSuccess; + + @Enumerated(EnumType.STRING) + @Column(name = "failure_reason", length = 100) + private VerificationFailureReason failureReason; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "token_id") + private UserVerificationToken token; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; +} diff --git a/src/main/java/until/the/eternity/das/verification/entity/UserVerificationHistoryRepository.java b/src/main/java/until/the/eternity/das/verification/entity/UserVerificationHistoryRepository.java new file mode 100644 index 0000000..651fd04 --- /dev/null +++ b/src/main/java/until/the/eternity/das/verification/entity/UserVerificationHistoryRepository.java @@ -0,0 +1,9 @@ +package until.the.eternity.das.verification.entity; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserVerificationHistoryRepository extends JpaRepository { + + List findTop100ByUserIdOrderByVerifiedAtDesc(Long userId); +} diff --git a/src/main/java/until/the/eternity/das/verification/entity/UserVerificationRepository.java b/src/main/java/until/the/eternity/das/verification/entity/UserVerificationRepository.java new file mode 100644 index 0000000..2cc4373 --- /dev/null +++ b/src/main/java/until/the/eternity/das/verification/entity/UserVerificationRepository.java @@ -0,0 +1,11 @@ +package until.the.eternity.das.verification.entity; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserVerificationRepository extends JpaRepository { + + Optional findByUserId(Long userId); + + Optional findByServerNameAndCharacterNameAndVerifiedTrue(String serverName, String characterName); +} diff --git a/src/main/java/until/the/eternity/das/verification/entity/UserVerificationToken.java b/src/main/java/until/the/eternity/das/verification/entity/UserVerificationToken.java new file mode 100644 index 0000000..eb944d4 --- /dev/null +++ b/src/main/java/until/the/eternity/das/verification/entity/UserVerificationToken.java @@ -0,0 +1,70 @@ +package until.the.eternity.das.verification.entity; + +import jakarta.persistence.*; +import java.time.LocalDateTime; +import lombok.*; +import org.hibernate.annotations.Comment; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import until.the.eternity.das.user.entity.User; + +@Entity +@Table(name = "user_verification_token") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class UserVerificationToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(name = "issued_at", nullable = false) + private LocalDateTime issuedAt; + + @Column(name = "expires_at", nullable = false) + private LocalDateTime expiresAt; + + @Column(name = "revoked", nullable = false) + private boolean revoked; + + @Column(name = "revoked_at") + private LocalDateTime revokedAt; + + @Column(name = "token_value", nullable = false, unique = true, length = 64) + @Comment("메모노기_ 접두사를 제외한 토큰 값") + private String tokenValue; + + @Column(name = "verified", nullable = false) + private boolean verified; + + @Column(name = "verified_at") + private LocalDateTime verifiedAt; + + @CreationTimestamp + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + public boolean isExpired(LocalDateTime now) { + return expiresAt.isBefore(now); + } + + public void revoke(LocalDateTime now) { + this.revoked = true; + this.revokedAt = now; + } + + public void markVerified(LocalDateTime now) { + this.verified = true; + this.verifiedAt = now; + } +} diff --git a/src/main/java/until/the/eternity/das/verification/entity/UserVerificationTokenRepository.java b/src/main/java/until/the/eternity/das/verification/entity/UserVerificationTokenRepository.java new file mode 100644 index 0000000..e792a3b --- /dev/null +++ b/src/main/java/until/the/eternity/das/verification/entity/UserVerificationTokenRepository.java @@ -0,0 +1,19 @@ +package until.the.eternity.das.verification.entity; + +import java.time.LocalDateTime; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserVerificationTokenRepository extends JpaRepository { + + Optional findTopByUserIdAndRevokedFalseAndVerifiedFalseAndExpiresAtAfterOrderByIssuedAtDesc( + Long userId, + LocalDateTime now + ); + + Optional findTopByUserIdOrderByIssuedAtDesc(Long userId); + + Optional findByTokenValue(String tokenValue); + + boolean existsByTokenValue(String tokenValue); +} diff --git a/src/main/java/until/the/eternity/das/verification/entity/enums/VerificationFailureReason.java b/src/main/java/until/the/eternity/das/verification/entity/enums/VerificationFailureReason.java new file mode 100644 index 0000000..4c9ff4c --- /dev/null +++ b/src/main/java/until/the/eternity/das/verification/entity/enums/VerificationFailureReason.java @@ -0,0 +1,9 @@ +package until.the.eternity.das.verification.entity.enums; + +public enum VerificationFailureReason { + TOKEN_NOT_FOUND, + TOKEN_EXPIRED, + TOKEN_REVOKED, + TOKEN_ALREADY_VERIFIED, + INVALID_MESSAGE +} diff --git a/src/main/java/until/the/eternity/das/verification/entity/enums/VerificationTokenStatus.java b/src/main/java/until/the/eternity/das/verification/entity/enums/VerificationTokenStatus.java new file mode 100644 index 0000000..b2bb499 --- /dev/null +++ b/src/main/java/until/the/eternity/das/verification/entity/enums/VerificationTokenStatus.java @@ -0,0 +1,8 @@ +package until.the.eternity.das.verification.entity.enums; + +public enum VerificationTokenStatus { + ACTIVE, + VERIFIED, + REVOKED, + EXPIRED +} diff --git a/src/main/java/until/the/eternity/das/verification/kafka/UserVerificationKafkaConsumer.java b/src/main/java/until/the/eternity/das/verification/kafka/UserVerificationKafkaConsumer.java new file mode 100644 index 0000000..f597b22 --- /dev/null +++ b/src/main/java/until/the/eternity/das/verification/kafka/UserVerificationKafkaConsumer.java @@ -0,0 +1,24 @@ +package until.the.eternity.das.verification.kafka; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; +import until.the.eternity.das.verification.application.UserVerificationService; + +@Slf4j +@Component +@RequiredArgsConstructor +public class UserVerificationKafkaConsumer { + + private final UserVerificationService userVerificationService; + + @KafkaListener( + topics = "${app.kafka.topics.user-verification-verify:USER_VERIFICATION_VERIFY_EVENT}", + groupId = "${spring.kafka.consumer.group-id:devnogi-auth-user-verification-consumer}" + ) + public void consumeVerificationMessage(String payload) { + log.debug("Received user verification kafka message. payload={}", payload); + userVerificationService.verifyFromKafkaPayload(payload); + } +} diff --git a/src/main/java/until/the/eternity/das/verification/kafka/UserVerificationVerifyMessage.java b/src/main/java/until/the/eternity/das/verification/kafka/UserVerificationVerifyMessage.java new file mode 100644 index 0000000..e0270e2 --- /dev/null +++ b/src/main/java/until/the/eternity/das/verification/kafka/UserVerificationVerifyMessage.java @@ -0,0 +1,12 @@ +package until.the.eternity.das.verification.kafka; + +import java.time.Instant; + +public record UserVerificationVerifyMessage( + String characterName, + String serverName, + String verificationValue, + String message, + Instant dateSend +) { +} diff --git a/src/main/java/until/the/eternity/das/verification/presentation/UserVerificationController.java b/src/main/java/until/the/eternity/das/verification/presentation/UserVerificationController.java new file mode 100644 index 0000000..15d031e --- /dev/null +++ b/src/main/java/until/the/eternity/das/verification/presentation/UserVerificationController.java @@ -0,0 +1,69 @@ +package until.the.eternity.das.verification.presentation; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import until.the.eternity.das.common.response.CommonResponse; +import until.the.eternity.das.verification.application.UserVerificationService; +import until.the.eternity.das.verification.dto.response.UserVerificationHistoryListResponse; +import until.the.eternity.das.verification.dto.response.UserVerificationInfoResponse; +import until.the.eternity.das.verification.dto.response.UserVerificationTokenIssueResponse; +import until.the.eternity.das.verification.dto.response.UserVerificationTokenResponse; + +@RestController +@RequestMapping("/api/user/verification") +@RequiredArgsConstructor +public class UserVerificationController { + + private final UserVerificationService userVerificationService; + + @PostMapping("/token") + public ResponseEntity> issueToken( + @AuthenticationPrincipal Long id + ) { + return ResponseEntity.ok(CommonResponse.success(userVerificationService.issueToken(id))); + } + + @GetMapping("/token") + public ResponseEntity> getMyToken( + @AuthenticationPrincipal Long id + ) { + return ResponseEntity.ok(CommonResponse.success(userVerificationService.getMyToken(id))); + } + + @PostMapping("/token/reissue") + public ResponseEntity> reissueToken( + @AuthenticationPrincipal Long id + ) { + return ResponseEntity.ok(CommonResponse.success(userVerificationService.reissueToken(id))); + } + + @GetMapping("/info") + public ResponseEntity> getMyVerificationInfo( + @AuthenticationPrincipal Long id + ) { + return ResponseEntity.ok(CommonResponse.success(userVerificationService.getMyVerificationInfo(id))); + } + + @GetMapping("/history") + public ResponseEntity> getMyVerificationHistory( + @AuthenticationPrincipal Long id, + @RequestParam(defaultValue = "latest") String sort, + @RequestParam(defaultValue = "20") Integer limit + ) { + return ResponseEntity.ok(CommonResponse.success(userVerificationService.getMyVerificationHistories(id, sort, limit))); + } + + @GetMapping("/users/{userId}/info") + public ResponseEntity> getUserVerificationInfo( + @PathVariable Long userId + ) { + return ResponseEntity.ok(CommonResponse.success(userVerificationService.getUserVerificationInfo(userId))); + } +} diff --git a/src/main/resources/application-sample.yml b/src/main/resources/application-sample.yml index fb0e65e..fae4421 100644 --- a/src/main/resources/application-sample.yml +++ b/src/main/resources/application-sample.yml @@ -98,6 +98,10 @@ spring: key-serializer: org.apache.kafka.common.serialization.StringSerializer # 값을 JSON 형태로 보내기 위한 설정 value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + consumer: + group-id: ${KAFKA_CONSUMER_GROUP_ID:devnogi-auth-user-verification-consumer} + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.apache.kafka.common.serialization.StringDeserializer resources: static-locations: classpath:/static/ mvc: @@ -150,6 +154,17 @@ decorator: p6spy: enable-logging: false +app: + verification: + prefix: ${USER_VERIFICATION_PREFIX:메모노기_} + length: ${USER_VERIFICATION_LENGTH:20} + validity-minutes: ${USER_VERIFICATION_VALIDITY_MINUTES:60} + cooldown-days: ${USER_VERIFICATION_COOLDOWN_DAYS:7} + hash-secret: ${USER_VERIFICATION_HASH_SECRET:change-this-secret} + kafka: + topics: + user-verification-verify: ${KAFKA_TOPIC_USER_VERIFICATION_VERIFY:USER_VERIFICATION_VERIFY_EVENT} + --- spring: config: diff --git a/src/main/resources/db/migration/V20260213110000__add_user_verification_tables.sql b/src/main/resources/db/migration/V20260213110000__add_user_verification_tables.sql new file mode 100644 index 0000000..cb97f88 --- /dev/null +++ b/src/main/resources/db/migration/V20260213110000__add_user_verification_tables.sql @@ -0,0 +1,53 @@ +ALTER TABLE users + ADD COLUMN server_name VARCHAR(20) NULL COMMENT '게임 서버명', + ADD COLUMN is_verified BOOLEAN NOT NULL DEFAULT FALSE COMMENT '사용자 인증 상태', + ADD COLUMN verified_at DATETIME NULL COMMENT '최근 인증 성공 시각'; + +CREATE TABLE user_verification_token ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '인증 토큰 ID', + user_id BIGINT NOT NULL COMMENT '회원 ID', + issued_at DATETIME NOT NULL COMMENT '발급 시각', + expires_at DATETIME NOT NULL COMMENT '만료 시각', + revoked BOOLEAN NOT NULL DEFAULT FALSE COMMENT '폐기 여부', + revoked_at DATETIME NULL COMMENT '폐기 시각', + token_value VARCHAR(64) NOT NULL UNIQUE COMMENT '발급값(메모노기_ 접두 제외)', + verified BOOLEAN NOT NULL DEFAULT FALSE COMMENT '인증 성공 여부', + verified_at DATETIME NULL COMMENT '인증 성공 시각', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시각', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정 시각', + CONSTRAINT fk_user_verification_token_user_id FOREIGN KEY (user_id) REFERENCES users(id) +) COMMENT='회원 인증 토큰 정보'; + +CREATE TABLE user_verification ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '회원 인증 정보 ID', + user_id BIGINT NOT NULL UNIQUE COMMENT '회원 ID', + server_name VARCHAR(20) NULL COMMENT '서버명', + character_name VARCHAR(100) NULL COMMENT '캐릭터명', + last_verified_at DATETIME NULL COMMENT '최근 인증 일시', + verification_count INT NOT NULL DEFAULT 0 COMMENT '누적 인증 횟수', + latest_token_id BIGINT NULL COMMENT '최근 인증 토큰 ID', + is_verified BOOLEAN NOT NULL DEFAULT FALSE COMMENT '현재 유효 인증 여부', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시각', + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정 시각', + CONSTRAINT fk_user_verification_user_id FOREIGN KEY (user_id) REFERENCES users(id), + CONSTRAINT fk_user_verification_latest_token_id FOREIGN KEY (latest_token_id) REFERENCES user_verification_token(id) +) COMMENT='회원 인증 요약 정보'; + +CREATE TABLE user_verification_history ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '회원 인증 이력 ID', + user_id BIGINT NULL COMMENT '회원 ID', + server_name VARCHAR(20) NULL COMMENT '서버명', + character_name VARCHAR(100) NULL COMMENT '캐릭터명', + verified_at DATETIME NOT NULL COMMENT '검증 처리 시각', + verification_success BOOLEAN NOT NULL COMMENT '인증 성공 여부', + failure_reason VARCHAR(100) NULL COMMENT '실패 사유', + token_id BIGINT NULL COMMENT '토큰 ID', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시각', + CONSTRAINT fk_user_verification_history_user_id FOREIGN KEY (user_id) REFERENCES users(id), + CONSTRAINT fk_user_verification_history_token_id FOREIGN KEY (token_id) REFERENCES user_verification_token(id) +) COMMENT='회원 인증 시도 이력'; + +CREATE INDEX idx_user_verification_token_user_id ON user_verification_token(user_id); +CREATE INDEX idx_user_verification_token_expires_at ON user_verification_token(expires_at); +CREATE INDEX idx_user_verification_server_character_verified ON user_verification(server_name, character_name, is_verified); +CREATE INDEX idx_user_verification_history_user_id_verified_at ON user_verification_history(user_id, verified_at DESC); From 91f000125ff89cf828daadd86a1108a39e80c5f9 Mon Sep 17 00:00:00 2001 From: dev-ant Date: Tue, 17 Feb 2026 12:18:32 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20docker-compose-local=20Kafka=20env?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose-local.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docker-compose-local.yml diff --git a/docker-compose-local.yml b/docker-compose-local.yml new file mode 100644 index 0000000..1c109de --- /dev/null +++ b/docker-compose-local.yml @@ -0,0 +1,34 @@ +version: "3.8" + +services: + auth-server: + build: + context: . + dockerfile: Dockerfile + image: devnogi-auth-server:local + container_name: devnogi-auth-server-local + ports: + - "${SERVER_PORT:-8091}:${SERVER_PORT:-8091}" + env_file: + - .env + environment: + SPRING_PROFILES_ACTIVE: default + SERVER_PORT: ${SERVER_PORT:-8091} + DB_IP: ${DB_IP:-host.docker.internal} + DB_PORT: ${DB_PORT:-3316} + DB_SCHEMA: ${DB_SCHEMA:-devnogi} + DB_USER: ${DB_USER:-devnogi} + DB_PASSWORD: ${DB_PASSWORD:-devnogi0529!} + KAFKA_BOOTSTRAP_SERVERS: ${KAFKA_BOOTSTRAP_SERVERS:-119.194.235.11:9102} + KAFKA_TOPIC_USER_VERIFICATION_VERIFY: ${KAFKA_TOPIC_USER_VERIFICATION_VERIFY:-USER_VERIFICATION_VERIFY_EVENT} + KAFKA_CONSUMER_GROUP_ID: ${KAFKA_CONSUMER_GROUP_ID:-devnogi-auth-user-verification-consumer-local} + JAVA_OPTS: -Xms256m -Xmx512m + restart: unless-stopped + extra_hosts: + - "host.docker.internal:host-gateway" + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:${SERVER_PORT:-8091}/actuator/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 90s