Skip to content

Commit

Permalink
Merge pull request #58 from YAPP-Github/feature/MAFOO-42
Browse files Browse the repository at this point in the history
feat: ์‹ ๊ทœ ์œ ์ € ๊ฐ€์ž… ์‹œ ์Šฌ๋ž™ ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ ๋ณด๋‚ด๊ธฐ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
  • Loading branch information
gmkim20713 authored Aug 9, 2024
2 parents 7d5821b + f76e2be commit af6e965
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 22 deletions.
3 changes: 3 additions & 0 deletions user-service/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Test>().all {
Expand Down
8 changes: 6 additions & 2 deletions user-service/src/main/java/kr/mafoo/user/api/AuthApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -19,13 +21,15 @@ public interface AuthApi {
@Operation(summary = "์นด์นด์˜ค ๋กœ๊ทธ์ธ", description = "์นด์นด์˜ค ์ธ๊ฐ€ ์ฝ”๋“œ๋กœ ๋กœ๊ทธ์ธ(ํ† ํฐ ๋ฐœํ–‰)ํ•ฉ๋‹ˆ๋‹ค.")
@PostMapping("/login/kakao")
Mono<LoginResponse> loginWithKakao(
@RequestBody KakaoLoginRequest request
@RequestBody KakaoLoginRequest request,
ServerWebExchange exchange
);

@Operation(summary = "์• ํ”Œ ๋กœ๊ทธ์ธ" , description = "์• ํ”Œ ์ธ๊ฐ€ ์ฝ”๋“œ๋กœ ๋กœ๊ทธ์ธ(ํ† ํฐ ๋ฐœํ–‰)ํ•ฉ๋‹ˆ๋‹ค.")
@PostMapping("/login/apple")
Mono<LoginResponse> loginWithApple(
@RequestBody AppleLoginRequest request
@RequestBody AppleLoginRequest request,
ServerWebExchange exchange
);

@Operation(summary = "ํ† ํฐ ๊ฐฑ์‹ ", description = "๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์œผ๋กœ ๊ธฐ์กด ํ† ํฐ์„ ๊ฐฑ์‹ ํ•ฉ๋‹ˆ๋‹ค.")
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
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;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@RequiredArgsConstructor
Expand All @@ -16,23 +18,34 @@ public class AuthController implements AuthApi {
private final AuthService authService;

@Override
public Mono<LoginResponse> loginWithKakao(KakaoLoginRequest request) {
public Mono<LoginResponse> loginWithKakao(KakaoLoginRequest request, ServerWebExchange exchange) {
String userAgent = getUserAgent(exchange);
return authService
.loginWithKakao(request.accessToken())
.map(authToken -> new LoginResponse(authToken.accessToken(), authToken.refreshToken()));
.loginWithKakao(request.accessToken(), userAgent)
.map(this::toLoginResponse);
}

@Override
public Mono<LoginResponse> loginWithApple(AppleLoginRequest request) {
public Mono<LoginResponse> loginWithApple(AppleLoginRequest request, ServerWebExchange exchange) {
String userAgent = getUserAgent(exchange);
return authService
.loginWithApple(request.identityToken())
.map(authToken -> new LoginResponse(authToken.accessToken(), authToken.refreshToken()));
.loginWithApple(request.identityToken(), userAgent)
.map(this::toLoginResponse);
}

@Override
public Mono<LoginResponse> 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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,7 +23,6 @@ public class MemberEntity implements Persistable<String> {
@Column("name")
private String name;

@CreatedDate
@Column("created_at")
private LocalDateTime createdAt;

Expand Down Expand Up @@ -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;
}
Expand Down
19 changes: 10 additions & 9 deletions user-service/src/main/java/kr/mafoo/user/service/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.security.Key;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;
import java.util.Base64;
Expand All @@ -42,25 +41,27 @@ public class AuthService {
private final ObjectMapper objectMapper;

@Transactional
public Mono<AuthToken> loginWithKakao(String kakaoAccessToken) {
public Mono<AuthToken> loginWithKakao(String kakaoAccessToken, String userAgent) {
return getUserInfoWithKakaoToken(kakaoAccessToken)
.flatMap(kakaoLoginInfo -> getOrCreateMember(
IdentityProvider.KAKAO,
kakaoLoginInfo.id(),
kakaoLoginInfo.nickname(),
kakaoLoginInfo.profileImageUrl()
kakaoLoginInfo.profileImageUrl(),
userAgent
));
}

@Transactional
public Mono<AuthToken> loginWithApple(String identityToken) {
public Mono<AuthToken> loginWithApple(String identityToken, String userAgent) {
return getApplePublicKeys()
.flatMap(keyObj -> getUserInfoWithAppleAccessToken(keyObj.keys(), identityToken))
.flatMap(appleLoginInfo -> getOrCreateMember(
IdentityProvider.APPLE,
appleLoginInfo.id(),
NicknameGenerator.generate(),
null
null,
userAgent
));
}

Expand All @@ -75,20 +76,20 @@ public Mono<AuthToken> loginWithRefreshToken(String refreshToken){
});
}

private Mono<AuthToken> getOrCreateMember(IdentityProvider provider, String id, String username, String profileImageUrl) {
private Mono<AuthToken> getOrCreateMember(IdentityProvider provider, String id, String username, String profileImageUrl, String userAgent) {
return socialMemberRepository
.findByIdentityProviderAndId(provider, id)
.switchIfEmpty(createNewSocialMember(provider, id, username, profileImageUrl))
.switchIfEmpty(createNewSocialMember(provider, id, username, profileImageUrl, userAgent))
.map(socialMember -> {
String accessToken = jwtTokenService.generateAccessToken(socialMember.getMemberId());
String refreshToken = jwtTokenService.generateRefreshToken(socialMember.getMemberId());
return new AuthToken(accessToken, refreshToken);
});
}

private Mono<SocialMemberEntity> createNewSocialMember(IdentityProvider provider, String id, String username, String profileImageUrl) {
private Mono<SocialMemberEntity> createNewSocialMember(IdentityProvider provider, String id, String username, String profileImageUrl, String userAgent) {
return memberService
.createNewMember(username, profileImageUrl)
.createNewMember(username, profileImageUrl, userAgent)
.flatMap(newMember -> socialMemberRepository.save(
SocialMemberEntity.newSocialMember(provider, id, newMember.getId())
));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
import kr.mafoo.user.repository.MemberRepository;
import kr.mafoo.user.util.IdGenerator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Mono;

@Slf4j
@RequiredArgsConstructor
@Service
public class MemberService {
private final MemberRepository memberRepository;
private final SlackService slackService;

public Mono<Void> quitMemberByMemberId(String memberId) {
return memberRepository.deleteMemberById(memberId);
Expand All @@ -25,8 +28,19 @@ public Mono<MemberEntity> getMemberByMemberId(String memberId) {
}

@Transactional
public Mono<MemberEntity> createNewMember(String username, String profileImageUrl) {
public Mono<MemberEntity> createNewMember(String username, String profileImageUrl, String userAgent) {
MemberEntity memberEntity = MemberEntity.newMember(IdGenerator.generate(), username, profileImageUrl);
return memberRepository.save(memberEntity);

return memberRepository.save(memberEntity)
.flatMap(savedMember ->
slackService.sendNewMemberNotification(
memberEntity.getId(),
memberEntity.getName(),
memberEntity.getProfileImageUrl(),
memberEntity.getCreatedAt().toString(),
userAgent
)
.then(Mono.just(savedMember))
);
}
}
117 changes: 117 additions & 0 deletions user-service/src/main/java/kr/mafoo/user/service/SlackService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package kr.mafoo.user.service;

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 reactor.core.publisher.Mono;

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;
import static com.slack.api.model.block.composition.BlockCompositions.plainText;

@Service
@RequiredArgsConstructor
public class SlackService {

@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<TextObject> 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"));

ChatPostMessageRequest request = ChatPostMessageRequest
.builder()
.channel(errorChannel)
.blocks(
asBlocks(
divider(),
section(
section -> section.fields(textObjects)
)
))
.build();

methodsClient.chatPostMessage(request);
} catch (SlackApiException | IOException e) {
throw new RuntimeException("Can't send Slack Message.", e);
}
}

public Mono<Void> sendNewMemberNotification(String memberId, String memberName, String memberProfileImageUrl, String memberCreatedAt, String userAgent) {
return Mono.fromCallable(() -> {
List<LayoutBlock> layoutBlocks = new ArrayList<>();

layoutBlocks.add(
Blocks.header(
headerBlockBuilder ->
headerBlockBuilder.text(plainText("๐ŸŽ‰ ์‹ ๊ทœ ์‚ฌ์šฉ์ž ๊ฐ€์ž…"))));
layoutBlocks.add(divider());

MarkdownTextObject userIdMarkdown =
MarkdownTextObject.builder().text("`์‚ฌ์šฉ์ž ID`\n" + memberId).build();

MarkdownTextObject userNameMarkdown =
MarkdownTextObject.builder().text("`์‚ฌ์šฉ์ž ๋‹‰๋„ค์ž„`\n" + memberName).build();

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))));

MarkdownTextObject userUserAgentMarkdown =
MarkdownTextObject.builder().text("`๊ฐ€์ž… ํ™˜๊ฒฝ`\n" + userAgent).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();
}

}
7 changes: 7 additions & 0 deletions user-service/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,10 @@ management:
zipkin:
tracing:
endpoint: http://zipkin/api/v2/spans

slack:
webhook:
token: ${SLACK_TOKEN}
channel:
error: ${SLACK_ERROR_CHANNEL}
member: ${SLACK_MEMBER_CHANNEL}

0 comments on commit af6e965

Please sign in to comment.