From 4f29fe17b4eef51450a5800b4723eac3879e487d Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 3 Aug 2024 14:00:15 +0900 Subject: [PATCH 01/13] chore: add slack related configuration --- user-service/build.gradle.kts | 3 +++ .../kr/mafoo/user/config/SlackApiConfig.java | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 user-service/src/main/java/kr/mafoo/user/config/SlackApiConfig.java diff --git a/user-service/build.gradle.kts b/user-service/build.gradle.kts index 3cd1fdd..caa2d23 100644 --- a/user-service/build.gradle.kts +++ b/user-service/build.gradle.kts @@ -50,6 +50,9 @@ dependencies { implementation("io.micrometer:micrometer-tracing-bridge-otel:1.3.2") implementation("io.opentelemetry:opentelemetry-exporter-zipkin:1.40.0") implementation("io.micrometer:micrometer-registry-prometheus:1.13.2") + + implementation("com.slack.api:slack-api-client:1.40.3") + } tasks.withType().all { diff --git a/user-service/src/main/java/kr/mafoo/user/config/SlackApiConfig.java b/user-service/src/main/java/kr/mafoo/user/config/SlackApiConfig.java new file mode 100644 index 0000000..d82edd2 --- /dev/null +++ b/user-service/src/main/java/kr/mafoo/user/config/SlackApiConfig.java @@ -0,0 +1,20 @@ +package kr.mafoo.user.config; + +import com.slack.api.Slack; +import com.slack.api.methods.MethodsClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SlackApiConfig { + + @Value("${slack.webhook.token}") + private String token; + + @Bean + public MethodsClient getClient() { + Slack slackClient = Slack.getInstance(); + return slackClient.methods(token); + } +} \ No newline at end of file From 2c3f1a2334c5925bd9094fc0c3a5f736d2a5ff8b Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 3 Aug 2024 14:04:34 +0900 Subject: [PATCH 02/13] feat: create slack notification service --- .../user/slack/SlackNotificationService.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 user-service/src/main/java/kr/mafoo/user/slack/SlackNotificationService.java diff --git a/user-service/src/main/java/kr/mafoo/user/slack/SlackNotificationService.java b/user-service/src/main/java/kr/mafoo/user/slack/SlackNotificationService.java new file mode 100644 index 0000000..b0aafcc --- /dev/null +++ b/user-service/src/main/java/kr/mafoo/user/slack/SlackNotificationService.java @@ -0,0 +1,65 @@ +package kr.mafoo.user.slack; + +import com.slack.api.Slack; +import com.slack.api.methods.MethodsClient; +import com.slack.api.methods.SlackApiException; +import com.slack.api.methods.request.chat.ChatPostMessageRequest; +import com.slack.api.model.block.composition.TextObject; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static com.slack.api.model.block.Blocks.*; +import static com.slack.api.model.block.composition.BlockCompositions.markdownText; + +@Service +@RequiredArgsConstructor +public class SlackNotificationService { + + @Value(value = "${slack.webhook.token}") + private String token; + + @Value(value = "${slack.webhook.channel.error}") + private String errorChannel; + + @Value(value = "${slack.webhook.channel.member}") + private String memberChannel; + + public void sendErrorNotification(Throwable throwable, String method, String uri, String statusCode, long executionTime, String userAgent) { + try { + List textObjects = new ArrayList<>(); + + textObjects.add(markdownText(">*예상하지 못한 에러가 발생했습니다!*\n")); + textObjects.add(markdownText("\n")); + + textObjects.add(markdownText("*메소드:* \n`" + method + "`\n")); + textObjects.add(markdownText("*URI:* \n`" + uri + "`\n")); + textObjects.add(markdownText("*상태코드:* \n`" + statusCode + "`\n")); + textObjects.add(markdownText("*메세지:* \n`" + throwable.getMessage() + "`\n")); + textObjects.add(markdownText("*소요시간:* \n`" + executionTime + " ms`\n")); + textObjects.add(markdownText("*사용자:* \n`" + userAgent + "`\n")); + + MethodsClient methods = Slack.getInstance().methods(token); + ChatPostMessageRequest request = ChatPostMessageRequest + .builder() + .channel(errorChannel) + .blocks( + asBlocks( + divider(), + section( + section -> section.fields(textObjects) + ) + )) + .build(); + + methods.chatPostMessage(request); + } catch (SlackApiException | IOException e) { + throw new RuntimeException("Can't send Slack Message.", e); + } + } + +} From 3cc1cb36271b6859aad66b077d0602228671277a Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Fri, 9 Aug 2024 02:19:33 +0900 Subject: [PATCH 03/13] feat: implement new member creation slack alarm --- .../kr/mafoo/user/service/MemberService.java | 5 +++ .../user/slack/SlackNotificationService.java | 31 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/user-service/src/main/java/kr/mafoo/user/service/MemberService.java b/user-service/src/main/java/kr/mafoo/user/service/MemberService.java index 9db3bd7..e9cf39d 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/MemberService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/MemberService.java @@ -3,6 +3,7 @@ import kr.mafoo.user.domain.MemberEntity; import kr.mafoo.user.exception.MemberNotFoundException; import kr.mafoo.user.repository.MemberRepository; +import kr.mafoo.user.slack.SlackNotificationService; import kr.mafoo.user.util.IdGenerator; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -12,6 +13,7 @@ @Service public class MemberService { private final MemberRepository memberRepository; + private final SlackNotificationService slackNotificationService; public Mono quitMemberByMemberId(String memberId) { return memberRepository.deleteMemberById(memberId); @@ -25,6 +27,9 @@ public Mono getMemberByMemberId(String memberId) { public Mono createNewMember(String username, String profileImageUrl) { MemberEntity memberEntity = MemberEntity.newMember(IdGenerator.generate(), username, profileImageUrl); + + slackNotificationService.sendNewMemberNotification(memberEntity.getId(), memberEntity.getName(), memberEntity.getCreatedAt()); + return memberRepository.save(memberEntity); } } diff --git a/user-service/src/main/java/kr/mafoo/user/slack/SlackNotificationService.java b/user-service/src/main/java/kr/mafoo/user/slack/SlackNotificationService.java index b0aafcc..33a9a3d 100644 --- a/user-service/src/main/java/kr/mafoo/user/slack/SlackNotificationService.java +++ b/user-service/src/main/java/kr/mafoo/user/slack/SlackNotificationService.java @@ -10,6 +10,7 @@ import org.springframework.stereotype.Service; import java.io.IOException; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -62,4 +63,34 @@ public void sendErrorNotification(Throwable throwable, String method, String uri } } + public void sendNewMemberNotification(String memberId, String memberName, LocalDateTime createdAt) { + try { + List textObjects = new ArrayList<>(); + + textObjects.add(markdownText(">*새로운 사용자가 가입했습니다!*\n")); + textObjects.add(markdownText("\n")); + + textObjects.add(markdownText("*사용자 ID:* \n`" + memberId + "`\n")); + textObjects.add(markdownText("*사용자 이름:* \n`" + memberName + "`\n")); + textObjects.add(markdownText("*생성일자:* \n`" + createdAt + "`\n")); + + MethodsClient methods = Slack.getInstance().methods(token); + ChatPostMessageRequest request = ChatPostMessageRequest + .builder() + .channel(memberChannel) + .blocks( + asBlocks( + divider(), + section( + section -> section.fields(textObjects) + ) + )) + .build(); + + methods.chatPostMessage(request); + } catch (SlackApiException | IOException e) { + throw new RuntimeException("Can't send Slack Message.", e); + } + } + } From 70d750f07799a63fde76bb1a9dfa3e231f112420 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Fri, 9 Aug 2024 13:25:01 +0900 Subject: [PATCH 04/13] fix: switch inapplicable annotation * fix: remove @CreatedDate annotation --- .../src/main/java/kr/mafoo/user/domain/MemberEntity.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/user-service/src/main/java/kr/mafoo/user/domain/MemberEntity.java b/user-service/src/main/java/kr/mafoo/user/domain/MemberEntity.java index 8f7c11e..fc1ee2b 100644 --- a/user-service/src/main/java/kr/mafoo/user/domain/MemberEntity.java +++ b/user-service/src/main/java/kr/mafoo/user/domain/MemberEntity.java @@ -3,7 +3,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Transient; import org.springframework.data.domain.Persistable; @@ -24,7 +23,6 @@ public class MemberEntity implements Persistable { @Column("name") private String name; - @CreatedDate @Column("created_at") private LocalDateTime createdAt; @@ -54,6 +52,7 @@ public static MemberEntity newMember(String id, String name, String profileImage member.id = id; member.name = name; member.profileImageUrl = profileImageUrl; + member.createdAt = LocalDateTime.now(); member.isNew = true; return member; } From d292276b76e66502992e9d078e4b05337c663d0d Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Fri, 9 Aug 2024 13:28:50 +0900 Subject: [PATCH 05/13] fix: fix createNewMember slack alarm sending logic to match webflux format --- .../java/kr/mafoo/user/service/MemberService.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/user-service/src/main/java/kr/mafoo/user/service/MemberService.java b/user-service/src/main/java/kr/mafoo/user/service/MemberService.java index e9cf39d..7755428 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/MemberService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/MemberService.java @@ -6,9 +6,11 @@ import kr.mafoo.user.slack.SlackNotificationService; import kr.mafoo.user.util.IdGenerator; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; +@Slf4j @RequiredArgsConstructor @Service public class MemberService { @@ -28,8 +30,15 @@ public Mono getMemberByMemberId(String memberId) { public Mono createNewMember(String username, String profileImageUrl) { MemberEntity memberEntity = MemberEntity.newMember(IdGenerator.generate(), username, profileImageUrl); - slackNotificationService.sendNewMemberNotification(memberEntity.getId(), memberEntity.getName(), memberEntity.getCreatedAt()); - - return memberRepository.save(memberEntity); + return memberRepository.save(memberEntity) + .flatMap(savedMember -> + slackNotificationService.sendNewMemberNotification( + memberEntity.getId(), + memberEntity.getName(), + memberEntity.getProfileImageUrl(), + memberEntity.getCreatedAt().toString() + ) + .then(Mono.just(savedMember)) + ); } } From 363666765dd8cd3dbc8f7aab67ff53fa602d2536 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Fri, 9 Aug 2024 13:33:33 +0900 Subject: [PATCH 06/13] refactor: add device info to new member slack alarm content --- .../main/java/kr/mafoo/user/api/AuthApi.java | 8 +- .../mafoo/user/controller/AuthController.java | 9 +- .../kr/mafoo/user/service/AuthService.java | 19 ++-- .../kr/mafoo/user/service/MemberService.java | 6 +- .../user/slack/SlackNotificationService.java | 107 +++++++++++++----- 5 files changed, 103 insertions(+), 46 deletions(-) diff --git a/user-service/src/main/java/kr/mafoo/user/api/AuthApi.java b/user-service/src/main/java/kr/mafoo/user/api/AuthApi.java index 2fe2fd6..e097d8c 100644 --- a/user-service/src/main/java/kr/mafoo/user/api/AuthApi.java +++ b/user-service/src/main/java/kr/mafoo/user/api/AuthApi.java @@ -9,7 +9,9 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Tag(name = "인증(로그인) 관련 API", description = "토큰 발행, 로그인 등 API") @@ -19,13 +21,15 @@ public interface AuthApi { @Operation(summary = "카카오 로그인", description = "카카오 인가 코드로 로그인(토큰 발행)합니다.") @PostMapping("/login/kakao") Mono loginWithKakao( - @RequestBody KakaoLoginRequest request + @RequestBody KakaoLoginRequest request, + ServerWebExchange exchange ); @Operation(summary = "애플 로그인" , description = "애플 인가 코드로 로그인(토큰 발행)합니다.") @PostMapping("/login/apple") Mono loginWithApple( - @RequestBody AppleLoginRequest request + @RequestBody AppleLoginRequest request, + ServerWebExchange exchange ); @Operation(summary = "토큰 갱신", description = "리프레시 토큰으로 기존 토큰을 갱신합니다.") diff --git a/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java b/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java index b274df3..1d4f7e3 100644 --- a/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java +++ b/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java @@ -8,6 +8,7 @@ import kr.mafoo.user.service.AuthService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @RequiredArgsConstructor @@ -16,16 +17,16 @@ public class AuthController implements AuthApi { private final AuthService authService; @Override - public Mono loginWithKakao(KakaoLoginRequest request) { + public Mono loginWithKakao(KakaoLoginRequest request, ServerWebExchange exchange) { return authService - .loginWithKakao(request.code()) + .loginWithKakao(request.code(), exchange) .map(authToken -> new LoginResponse(authToken.accessToken(), authToken.refreshToken())); } @Override - public Mono loginWithApple(AppleLoginRequest request) { + public Mono loginWithApple(AppleLoginRequest request, ServerWebExchange exchange) { return authService - .loginWithApple(request.identityToken()) + .loginWithApple(request.identityToken(), exchange) .map(authToken -> new LoginResponse(authToken.accessToken(), authToken.refreshToken())); } diff --git a/user-service/src/main/java/kr/mafoo/user/service/AuthService.java b/user-service/src/main/java/kr/mafoo/user/service/AuthService.java index 4ace3f6..9077ddc 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/AuthService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/AuthService.java @@ -21,6 +21,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.security.Key; @@ -41,25 +42,27 @@ public class AuthService { private final ObjectMapper objectMapper; - public Mono loginWithKakao(String code) { + public Mono loginWithKakao(String code, ServerWebExchange exchange) { return getKakaoTokenWithCode(code) .flatMap(this::getUserInfoWithKakaoToken) .flatMap(kakaoLoginInfo -> getOrCreateMember( IdentityProvider.KAKAO, kakaoLoginInfo.id(), kakaoLoginInfo.nickname(), - kakaoLoginInfo.profileImageUrl() + kakaoLoginInfo.profileImageUrl(), + exchange )); } - public Mono loginWithApple(String identityToken) { + public Mono loginWithApple(String identityToken, ServerWebExchange exchange) { return getApplePublicKeys() .flatMap(keyObj -> getUserInfoWithAppleAccessToken(keyObj.keys(), identityToken)) .flatMap(appleLoginInfo -> getOrCreateMember( IdentityProvider.APPLE, appleLoginInfo.id(), NicknameGenerator.generate(), - null + null, + exchange )); } @@ -73,10 +76,10 @@ public Mono loginWithRefreshToken(String refreshToken){ }); } - private Mono getOrCreateMember(IdentityProvider provider, String id, String username, String profileImageUrl) { + private Mono getOrCreateMember(IdentityProvider provider, String id, String username, String profileImageUrl, ServerWebExchange exchange) { return socialMemberRepository .findByIdentityProviderAndId(provider, id) - .switchIfEmpty(createNewSocialMember(provider, id, username, profileImageUrl)) + .switchIfEmpty(createNewSocialMember(provider, id, username, profileImageUrl, exchange)) .map(socialMember -> { String accessToken = jwtTokenService.generateAccessToken(socialMember.getMemberId()); String refreshToken = jwtTokenService.generateRefreshToken(socialMember.getMemberId()); @@ -84,9 +87,9 @@ private Mono getOrCreateMember(IdentityProvider provider, String id, }); } - private Mono createNewSocialMember(IdentityProvider provider, String id, String username, String profileImageUrl) { + private Mono createNewSocialMember(IdentityProvider provider, String id, String username, String profileImageUrl, ServerWebExchange exchange) { return memberService - .createNewMember(username, profileImageUrl) + .createNewMember(username, profileImageUrl, exchange) .flatMap(newMember -> socialMemberRepository.save( SocialMemberEntity.newSocialMember(provider, id, newMember.getId()) )); diff --git a/user-service/src/main/java/kr/mafoo/user/service/MemberService.java b/user-service/src/main/java/kr/mafoo/user/service/MemberService.java index 7755428..8edcd7a 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/MemberService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/MemberService.java @@ -8,6 +8,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Slf4j @@ -27,7 +28,7 @@ public Mono getMemberByMemberId(String memberId) { .switchIfEmpty(Mono.error(new MemberNotFoundException())); } - public Mono createNewMember(String username, String profileImageUrl) { + public Mono createNewMember(String username, String profileImageUrl, ServerWebExchange exchange) { MemberEntity memberEntity = MemberEntity.newMember(IdGenerator.generate(), username, profileImageUrl); return memberRepository.save(memberEntity) @@ -36,7 +37,8 @@ public Mono createNewMember(String username, String profileImageUr memberEntity.getId(), memberEntity.getName(), memberEntity.getProfileImageUrl(), - memberEntity.getCreatedAt().toString() + memberEntity.getCreatedAt().toString(), + exchange ) .then(Mono.just(savedMember)) ); diff --git a/user-service/src/main/java/kr/mafoo/user/slack/SlackNotificationService.java b/user-service/src/main/java/kr/mafoo/user/slack/SlackNotificationService.java index 33a9a3d..1b45e5e 100644 --- a/user-service/src/main/java/kr/mafoo/user/slack/SlackNotificationService.java +++ b/user-service/src/main/java/kr/mafoo/user/slack/SlackNotificationService.java @@ -1,35 +1,38 @@ package kr.mafoo.user.slack; -import com.slack.api.Slack; import com.slack.api.methods.MethodsClient; import com.slack.api.methods.SlackApiException; import com.slack.api.methods.request.chat.ChatPostMessageRequest; +import com.slack.api.model.block.Blocks; +import com.slack.api.model.block.LayoutBlock; +import com.slack.api.model.block.composition.MarkdownTextObject; import com.slack.api.model.block.composition.TextObject; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; import java.io.IOException; -import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import static com.slack.api.model.block.Blocks.*; import static com.slack.api.model.block.composition.BlockCompositions.markdownText; +import static com.slack.api.model.block.composition.BlockCompositions.plainText; @Service @RequiredArgsConstructor public class SlackNotificationService { - @Value(value = "${slack.webhook.token}") - private String token; - @Value(value = "${slack.webhook.channel.error}") private String errorChannel; @Value(value = "${slack.webhook.channel.member}") private String memberChannel; + private final MethodsClient methodsClient; + public void sendErrorNotification(Throwable throwable, String method, String uri, String statusCode, long executionTime, String userAgent) { try { List textObjects = new ArrayList<>(); @@ -44,7 +47,6 @@ public void sendErrorNotification(Throwable throwable, String method, String uri textObjects.add(markdownText("*소요시간:* \n`" + executionTime + " ms`\n")); textObjects.add(markdownText("*사용자:* \n`" + userAgent + "`\n")); - MethodsClient methods = Slack.getInstance().methods(token); ChatPostMessageRequest request = ChatPostMessageRequest .builder() .channel(errorChannel) @@ -57,39 +59,84 @@ public void sendErrorNotification(Throwable throwable, String method, String uri )) .build(); - methods.chatPostMessage(request); + methodsClient.chatPostMessage(request); } catch (SlackApiException | IOException e) { throw new RuntimeException("Can't send Slack Message.", e); } } - public void sendNewMemberNotification(String memberId, String memberName, LocalDateTime createdAt) { - try { - List textObjects = new ArrayList<>(); + public Mono sendNewMemberNotification(String memberId, String memberName, String memberProfileImageUrl, String memberCreatedAt, ServerWebExchange exchange) { + return Mono.fromCallable(() -> { + List layoutBlocks = new ArrayList<>(); - textObjects.add(markdownText(">*새로운 사용자가 가입했습니다!*\n")); - textObjects.add(markdownText("\n")); + layoutBlocks.add( + Blocks.header( + headerBlockBuilder -> + headerBlockBuilder.text(plainText("🎉 신규 사용자 가입")))); + layoutBlocks.add(divider()); - textObjects.add(markdownText("*사용자 ID:* \n`" + memberId + "`\n")); - textObjects.add(markdownText("*사용자 이름:* \n`" + memberName + "`\n")); - textObjects.add(markdownText("*생성일자:* \n`" + createdAt + "`\n")); + MarkdownTextObject userIdMarkdown = + MarkdownTextObject.builder().text("`사용자 ID`\n" + memberId).build(); - MethodsClient methods = Slack.getInstance().methods(token); - ChatPostMessageRequest request = ChatPostMessageRequest - .builder() - .channel(memberChannel) - .blocks( - asBlocks( - divider(), - section( - section -> section.fields(textObjects) - ) - )) - .build(); + MarkdownTextObject userNameMarkdown = + MarkdownTextObject.builder().text("`사용자 닉네임`\n" + memberName).build(); - methods.chatPostMessage(request); - } catch (SlackApiException | IOException e) { - throw new RuntimeException("Can't send Slack Message.", e); + layoutBlocks.add( + section( + section -> section.fields(List.of(userIdMarkdown, userNameMarkdown)))); + + MarkdownTextObject userProfileImageMarkdown = + MarkdownTextObject.builder().text("`프로필 이미지`\n" + memberProfileImageUrl).build(); + + MarkdownTextObject userCreatedAtMarkdown = + MarkdownTextObject.builder().text("`가입 일자`\n" + memberCreatedAt).build(); + + layoutBlocks.add( + section( + section -> section.fields(List.of(userProfileImageMarkdown, userCreatedAtMarkdown)))); + + String userAgent = exchange.getRequest().getHeaders().getFirst("User-Agent"); + String deviceInfo = extractDeviceInfo(userAgent); + + MarkdownTextObject userUserAgentMarkdown = + MarkdownTextObject.builder().text("`가입 환경`\n" + deviceInfo).build(); + + layoutBlocks.add( + section( + section -> section.fields(List.of(userUserAgentMarkdown)))); + + ChatPostMessageRequest chatPostMessageRequest = + ChatPostMessageRequest + .builder() + .text("신규 사용자 가입 알림") + .channel(memberChannel) + .blocks(layoutBlocks) + .build(); + + return methodsClient.chatPostMessage(chatPostMessageRequest); + }) + .then(); + } + + // 기기 정보를 추출하는 메소드 + private String extractDeviceInfo(String userAgent) { + if (userAgent == null) { + return "Unknown device"; + } + userAgent = userAgent.toLowerCase(); + + if (userAgent.contains("windows")) { + return "Windows PC"; + } else if (userAgent.contains("mac")) { + return "Mac"; + } else if (userAgent.contains("android")) { + return "Android Device"; + } else if (userAgent.contains("iphone") || userAgent.contains("ipad")) { + return "iOS Device"; + } else if (userAgent.contains("linux")) { + return "Linux"; + } else { + return "Unknown device"; } } From 480f31980b6ead3b986547ed7a3ecd113d78c681 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 3 Aug 2024 21:24:03 +0900 Subject: [PATCH 07/13] feat: add slack related info in application.yaml --- user-service/src/main/resources/application.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/user-service/src/main/resources/application.yaml b/user-service/src/main/resources/application.yaml index c627c58..d612854 100644 --- a/user-service/src/main/resources/application.yaml +++ b/user-service/src/main/resources/application.yaml @@ -42,3 +42,9 @@ management: zipkin: tracing: endpoint: http://zipkin/api/v2/spans + +slack: + webhook: + token: ${SLACK_TOKEN} + channel: + error: ${SLACK_ERROR_CHANNEL} From fba7cf227db81b5cbbe886f99c05be5c90a9dc4e Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Fri, 9 Aug 2024 14:58:39 +0900 Subject: [PATCH 08/13] chore: add slack member alarm related channel info to application.yaml --- user-service/src/main/resources/application.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/user-service/src/main/resources/application.yaml b/user-service/src/main/resources/application.yaml index d612854..594d857 100644 --- a/user-service/src/main/resources/application.yaml +++ b/user-service/src/main/resources/application.yaml @@ -48,3 +48,4 @@ slack: token: ${SLACK_TOKEN} channel: error: ${SLACK_ERROR_CHANNEL} + member: ${SLACK_MEMBER_CHANNEL} From 7be7944afa48f103679d130ff3c3d74c9de95d6f Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Fri, 9 Aug 2024 15:37:54 +0900 Subject: [PATCH 09/13] refactor: rename SlackNotificationService as SlackService and combine slack dir w/ service dir --- .../src/main/java/kr/mafoo/user/service/MemberService.java | 5 ++--- .../SlackService.java} | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) rename user-service/src/main/java/kr/mafoo/user/{slack/SlackNotificationService.java => service/SlackService.java} (98%) diff --git a/user-service/src/main/java/kr/mafoo/user/service/MemberService.java b/user-service/src/main/java/kr/mafoo/user/service/MemberService.java index 7c020ef..5c0338a 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/MemberService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/MemberService.java @@ -3,7 +3,6 @@ import kr.mafoo.user.domain.MemberEntity; import kr.mafoo.user.exception.MemberNotFoundException; import kr.mafoo.user.repository.MemberRepository; -import kr.mafoo.user.slack.SlackNotificationService; import kr.mafoo.user.util.IdGenerator; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -17,7 +16,7 @@ @Service public class MemberService { private final MemberRepository memberRepository; - private final SlackNotificationService slackNotificationService; + private final SlackService slackService; public Mono quitMemberByMemberId(String memberId) { return memberRepository.deleteMemberById(memberId); @@ -35,7 +34,7 @@ public Mono createNewMember(String username, String profileImageUr return memberRepository.save(memberEntity) .flatMap(savedMember -> - slackNotificationService.sendNewMemberNotification( + slackService.sendNewMemberNotification( memberEntity.getId(), memberEntity.getName(), memberEntity.getProfileImageUrl(), diff --git a/user-service/src/main/java/kr/mafoo/user/slack/SlackNotificationService.java b/user-service/src/main/java/kr/mafoo/user/service/SlackService.java similarity index 98% rename from user-service/src/main/java/kr/mafoo/user/slack/SlackNotificationService.java rename to user-service/src/main/java/kr/mafoo/user/service/SlackService.java index 1b45e5e..331b20d 100644 --- a/user-service/src/main/java/kr/mafoo/user/slack/SlackNotificationService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/SlackService.java @@ -1,4 +1,4 @@ -package kr.mafoo.user.slack; +package kr.mafoo.user.service; import com.slack.api.methods.MethodsClient; import com.slack.api.methods.SlackApiException; @@ -23,7 +23,7 @@ @Service @RequiredArgsConstructor -public class SlackNotificationService { +public class SlackService { @Value(value = "${slack.webhook.channel.error}") private String errorChannel; From 964da1bf85b81b9f53f4faa4ec52e2dda82ed994 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Fri, 9 Aug 2024 15:40:21 +0900 Subject: [PATCH 10/13] refactor: fix sendNewMemberNotification to return detailed user agent info --- .../kr/mafoo/user/service/SlackService.java | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/user-service/src/main/java/kr/mafoo/user/service/SlackService.java b/user-service/src/main/java/kr/mafoo/user/service/SlackService.java index 331b20d..ef2d637 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/SlackService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/SlackService.java @@ -96,10 +96,9 @@ public Mono sendNewMemberNotification(String memberId, String memberName, section -> section.fields(List.of(userProfileImageMarkdown, userCreatedAtMarkdown)))); String userAgent = exchange.getRequest().getHeaders().getFirst("User-Agent"); - String deviceInfo = extractDeviceInfo(userAgent); MarkdownTextObject userUserAgentMarkdown = - MarkdownTextObject.builder().text("`가입 환경`\n" + deviceInfo).build(); + MarkdownTextObject.builder().text("`가입 환경`\n" + userAgent).build(); layoutBlocks.add( section( @@ -118,26 +117,4 @@ public Mono sendNewMemberNotification(String memberId, String memberName, .then(); } - // 기기 정보를 추출하는 메소드 - private String extractDeviceInfo(String userAgent) { - if (userAgent == null) { - return "Unknown device"; - } - userAgent = userAgent.toLowerCase(); - - if (userAgent.contains("windows")) { - return "Windows PC"; - } else if (userAgent.contains("mac")) { - return "Mac"; - } else if (userAgent.contains("android")) { - return "Android Device"; - } else if (userAgent.contains("iphone") || userAgent.contains("ipad")) { - return "iOS Device"; - } else if (userAgent.contains("linux")) { - return "Linux"; - } else { - return "Unknown device"; - } - } - } From 415d76049c39ca071b9106cb78535b16e44b8a0f Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 10 Aug 2024 05:54:32 +0900 Subject: [PATCH 11/13] refactor: move User-Agent extraction from service to controller --- .../mafoo/user/controller/AuthController.java | 4 ++-- .../kr/mafoo/user/service/AuthService.java | 18 ++++++++---------- .../kr/mafoo/user/service/MemberService.java | 5 ++--- .../kr/mafoo/user/service/SlackService.java | 5 +---- 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java b/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java index 64a1177..26169de 100644 --- a/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java +++ b/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java @@ -19,14 +19,14 @@ public class AuthController implements AuthApi { @Override public Mono loginWithKakao(KakaoLoginRequest request, ServerWebExchange exchange) { return authService - .loginWithKakao(request.accessToken(), exchange) + .loginWithKakao(request.accessToken(), exchange.getRequest().getHeaders().getFirst("User-Agent")) .map(authToken -> new LoginResponse(authToken.accessToken(), authToken.refreshToken())); } @Override public Mono loginWithApple(AppleLoginRequest request, ServerWebExchange exchange) { return authService - .loginWithApple(request.identityToken(), exchange) + .loginWithApple(request.identityToken(), exchange.getRequest().getHeaders().getFirst("User-Agent")) .map(authToken -> new LoginResponse(authToken.accessToken(), authToken.refreshToken())); } diff --git a/user-service/src/main/java/kr/mafoo/user/service/AuthService.java b/user-service/src/main/java/kr/mafoo/user/service/AuthService.java index 3cc49e1..15a2374 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/AuthService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/AuthService.java @@ -22,10 +22,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; -import java.security.Key; import java.security.interfaces.RSAPublicKey; import java.util.Arrays; import java.util.Base64; @@ -43,19 +41,19 @@ public class AuthService { private final ObjectMapper objectMapper; @Transactional - public Mono loginWithKakao(String kakaoAccessToken, ServerWebExchange exchange) { + public Mono loginWithKakao(String kakaoAccessToken, String userAgent) { return getUserInfoWithKakaoToken(kakaoAccessToken) .flatMap(kakaoLoginInfo -> getOrCreateMember( IdentityProvider.KAKAO, kakaoLoginInfo.id(), kakaoLoginInfo.nickname(), kakaoLoginInfo.profileImageUrl(), - exchange + userAgent )); } @Transactional - public Mono loginWithApple(String identityToken, ServerWebExchange exchange) { + public Mono loginWithApple(String identityToken, String userAgent) { return getApplePublicKeys() .flatMap(keyObj -> getUserInfoWithAppleAccessToken(keyObj.keys(), identityToken)) .flatMap(appleLoginInfo -> getOrCreateMember( @@ -63,7 +61,7 @@ public Mono loginWithApple(String identityToken, ServerWebExchange ex appleLoginInfo.id(), NicknameGenerator.generate(), null, - exchange + userAgent )); } @@ -78,10 +76,10 @@ public Mono loginWithRefreshToken(String refreshToken){ }); } - private Mono getOrCreateMember(IdentityProvider provider, String id, String username, String profileImageUrl, ServerWebExchange exchange) { + private Mono getOrCreateMember(IdentityProvider provider, String id, String username, String profileImageUrl, String userAgent) { return socialMemberRepository .findByIdentityProviderAndId(provider, id) - .switchIfEmpty(createNewSocialMember(provider, id, username, profileImageUrl, exchange)) + .switchIfEmpty(createNewSocialMember(provider, id, username, profileImageUrl, userAgent)) .map(socialMember -> { String accessToken = jwtTokenService.generateAccessToken(socialMember.getMemberId()); String refreshToken = jwtTokenService.generateRefreshToken(socialMember.getMemberId()); @@ -89,9 +87,9 @@ private Mono getOrCreateMember(IdentityProvider provider, String id, }); } - private Mono createNewSocialMember(IdentityProvider provider, String id, String username, String profileImageUrl, ServerWebExchange exchange) { + private Mono createNewSocialMember(IdentityProvider provider, String id, String username, String profileImageUrl, String userAgent) { return memberService - .createNewMember(username, profileImageUrl, exchange) + .createNewMember(username, profileImageUrl, userAgent) .flatMap(newMember -> socialMemberRepository.save( SocialMemberEntity.newSocialMember(provider, id, newMember.getId()) )); diff --git a/user-service/src/main/java/kr/mafoo/user/service/MemberService.java b/user-service/src/main/java/kr/mafoo/user/service/MemberService.java index 5c0338a..63af4cb 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/MemberService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/MemberService.java @@ -7,7 +7,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import org.springframework.web.server.ServerWebExchange; import org.springframework.transaction.annotation.Transactional; import reactor.core.publisher.Mono; @@ -29,7 +28,7 @@ public Mono getMemberByMemberId(String memberId) { } @Transactional - public Mono createNewMember(String username, String profileImageUrl, ServerWebExchange exchange) { + public Mono createNewMember(String username, String profileImageUrl, String userAgent) { MemberEntity memberEntity = MemberEntity.newMember(IdGenerator.generate(), username, profileImageUrl); return memberRepository.save(memberEntity) @@ -39,7 +38,7 @@ public Mono createNewMember(String username, String profileImageUr memberEntity.getName(), memberEntity.getProfileImageUrl(), memberEntity.getCreatedAt().toString(), - exchange + userAgent ) .then(Mono.just(savedMember)) ); diff --git a/user-service/src/main/java/kr/mafoo/user/service/SlackService.java b/user-service/src/main/java/kr/mafoo/user/service/SlackService.java index ef2d637..81f10d4 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/SlackService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/SlackService.java @@ -10,7 +10,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.io.IOException; @@ -65,7 +64,7 @@ public void sendErrorNotification(Throwable throwable, String method, String uri } } - public Mono sendNewMemberNotification(String memberId, String memberName, String memberProfileImageUrl, String memberCreatedAt, ServerWebExchange exchange) { + public Mono sendNewMemberNotification(String memberId, String memberName, String memberProfileImageUrl, String memberCreatedAt, String userAgent) { return Mono.fromCallable(() -> { List layoutBlocks = new ArrayList<>(); @@ -95,8 +94,6 @@ public Mono sendNewMemberNotification(String memberId, String memberName, section( section -> section.fields(List.of(userProfileImageMarkdown, userCreatedAtMarkdown)))); - String userAgent = exchange.getRequest().getHeaders().getFirst("User-Agent"); - MarkdownTextObject userUserAgentMarkdown = MarkdownTextObject.builder().text("`가입 환경`\n" + userAgent).build(); From 36c461aefa4c2a5528b3f4ceb71b9a60e84ccf07 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 10 Aug 2024 05:57:52 +0900 Subject: [PATCH 12/13] refactor: create getUserAgent method for AuthController --- .../java/kr/mafoo/user/controller/AuthController.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java b/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java index 26169de..84d1612 100644 --- a/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java +++ b/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java @@ -18,15 +18,17 @@ public class AuthController implements AuthApi { @Override public Mono loginWithKakao(KakaoLoginRequest request, ServerWebExchange exchange) { + String userAgent = getUserAgent(exchange); return authService - .loginWithKakao(request.accessToken(), exchange.getRequest().getHeaders().getFirst("User-Agent")) + .loginWithKakao(request.accessToken(), userAgent) .map(authToken -> new LoginResponse(authToken.accessToken(), authToken.refreshToken())); } @Override public Mono loginWithApple(AppleLoginRequest request, ServerWebExchange exchange) { + String userAgent = getUserAgent(exchange); return authService - .loginWithApple(request.identityToken(), exchange.getRequest().getHeaders().getFirst("User-Agent")) + .loginWithApple(request.identityToken(), userAgent) .map(authToken -> new LoginResponse(authToken.accessToken(), authToken.refreshToken())); } @@ -36,4 +38,8 @@ public Mono loginWithRefreshToken(TokenRefreshRequest request) { .loginWithRefreshToken(request.refreshToken()) .map(authToken -> new LoginResponse(authToken.accessToken(), authToken.refreshToken())); } + + private String getUserAgent(ServerWebExchange exchange) { + return exchange.getRequest().getHeaders().getFirst("User-Agent"); + } } From f76e2be28510c20a56bf8598c27c6c35c5df4189 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 10 Aug 2024 05:59:40 +0900 Subject: [PATCH 13/13] refactor: create toLoginResponse method for AuthController --- .../kr/mafoo/user/controller/AuthController.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java b/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java index 84d1612..57eb2be 100644 --- a/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java +++ b/user-service/src/main/java/kr/mafoo/user/controller/AuthController.java @@ -5,6 +5,7 @@ import kr.mafoo.user.controller.dto.request.KakaoLoginRequest; import kr.mafoo.user.controller.dto.request.TokenRefreshRequest; import kr.mafoo.user.controller.dto.response.LoginResponse; +import kr.mafoo.user.domain.AuthToken; import kr.mafoo.user.service.AuthService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.RestController; @@ -21,7 +22,7 @@ public Mono loginWithKakao(KakaoLoginRequest request, ServerWebEx String userAgent = getUserAgent(exchange); return authService .loginWithKakao(request.accessToken(), userAgent) - .map(authToken -> new LoginResponse(authToken.accessToken(), authToken.refreshToken())); + .map(this::toLoginResponse); } @Override @@ -29,17 +30,22 @@ public Mono loginWithApple(AppleLoginRequest request, ServerWebEx String userAgent = getUserAgent(exchange); return authService .loginWithApple(request.identityToken(), userAgent) - .map(authToken -> new LoginResponse(authToken.accessToken(), authToken.refreshToken())); + .map(this::toLoginResponse); } @Override public Mono loginWithRefreshToken(TokenRefreshRequest request) { return authService .loginWithRefreshToken(request.refreshToken()) - .map(authToken -> new LoginResponse(authToken.accessToken(), authToken.refreshToken())); + .map(this::toLoginResponse); } private String getUserAgent(ServerWebExchange exchange) { return exchange.getRequest().getHeaders().getFirst("User-Agent"); } + + private LoginResponse toLoginResponse(AuthToken authToken) { + return new LoginResponse(authToken.accessToken(), authToken.refreshToken()); + } + }