From 81eed9a08a157674fc416f23d7a4d8e88af2a035 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Fri, 9 Feb 2024 13:33:37 +0900 Subject: [PATCH 1/6] [#22] feat: Add FCM service and config --- .gitignore | 1 + build.gradle | 1 + .../java/org/dnd/timeet/config/FCMConfig.java | 41 ++++++++++++++ .../application/FCMNotificationService.java | 55 +++++++++++++++++++ .../fcm/domain/FCMNotificationRequestDto.java | 22 ++++++++ .../org/dnd/timeet/member/domain/Member.java | 3 + 6 files changed, 123 insertions(+) create mode 100644 src/main/java/org/dnd/timeet/config/FCMConfig.java create mode 100644 src/main/java/org/dnd/timeet/fcm/application/FCMNotificationService.java create mode 100644 src/main/java/org/dnd/timeet/fcm/domain/FCMNotificationRequestDto.java diff --git a/.gitignore b/.gitignore index 74f33d8..f8fb2e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +timeet-firebase-adminsdk.json HELP.md .gradle build/ diff --git a/build.gradle b/build.gradle index 33ba110..5c4732a 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,7 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' implementation("org.springframework.boot:spring-boot-devtools") implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'com.google.firebase:firebase-admin:9.2.0' compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/org/dnd/timeet/config/FCMConfig.java b/src/main/java/org/dnd/timeet/config/FCMConfig.java new file mode 100644 index 0000000..ba1f36d --- /dev/null +++ b/src/main/java/org/dnd/timeet/config/FCMConfig.java @@ -0,0 +1,41 @@ +package org.dnd.timeet.config; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; + +@Configuration +public class FCMConfig { + + @Bean + FirebaseMessaging firebaseMessaging() throws IOException { + ClassPathResource resource = new ClassPathResource("timeet-firebase-adminsdk.json"); + + InputStream refreshToken = resource.getInputStream(); + + FirebaseApp firebaseApp = null; + List firebaseAppList = FirebaseApp.getApps(); + + if (firebaseAppList != null && !firebaseAppList.isEmpty()) { + for (FirebaseApp app : firebaseAppList) { + if (app.getName().equals(FirebaseApp.DEFAULT_APP_NAME)) { + firebaseApp = app; + } + } + } else { + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(refreshToken)).build(); + firebaseApp = FirebaseApp.initializeApp(options); + } + return FirebaseMessaging.getInstance(firebaseApp); + } + + +} diff --git a/src/main/java/org/dnd/timeet/fcm/application/FCMNotificationService.java b/src/main/java/org/dnd/timeet/fcm/application/FCMNotificationService.java new file mode 100644 index 0000000..556d75b --- /dev/null +++ b/src/main/java/org/dnd/timeet/fcm/application/FCMNotificationService.java @@ -0,0 +1,55 @@ +package org.dnd.timeet.fcm.application; + +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.Notification; +import java.util.Collections; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.dnd.timeet.common.exception.InternalServerError; +import org.dnd.timeet.common.exception.NotFoundError; +import org.dnd.timeet.fcm.domain.FCMNotificationRequestDto; +import org.dnd.timeet.member.domain.Member; +import org.dnd.timeet.member.domain.MemberRepository; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class FCMNotificationService { + + private final FirebaseMessaging firebaseMessaging; + private final MemberRepository memberRepository; + + public void sendNotificationByToken(FCMNotificationRequestDto requestDto) { + Optional member = memberRepository.findById(requestDto.getTargetMemberId()); + + if (member.isEmpty()) { + throw new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("MemberId", "Member not found")); + } + + if (member.get().getFcmToken() == null) { + throw new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("fcmToken", "fcmToken not exist")); + } + + Notification notification = Notification.builder() + .setTitle(requestDto.getTitle()) + .setBody(requestDto.getBody()) + .build(); + + Message message = Message.builder() + .setToken(member.get().getFcmToken()) + .setNotification(notification) + .build(); + + try { + firebaseMessaging.send(message); + } catch (Exception e) { + throw new InternalServerError(InternalServerError.ErrorCode.INTERNAL_SERVER_ERROR, + Collections.singletonMap("fcmSend", "Fail to send fcm")); + } + + + } +} diff --git a/src/main/java/org/dnd/timeet/fcm/domain/FCMNotificationRequestDto.java b/src/main/java/org/dnd/timeet/fcm/domain/FCMNotificationRequestDto.java new file mode 100644 index 0000000..710f56c --- /dev/null +++ b/src/main/java/org/dnd/timeet/fcm/domain/FCMNotificationRequestDto.java @@ -0,0 +1,22 @@ +package org.dnd.timeet.fcm.domain; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class FCMNotificationRequestDto { + + private Long targetMemberId; + private String title; + private String body; + + @Builder + public FCMNotificationRequestDto(Long targetMemberId, String title, String body) { + this.targetMemberId = targetMemberId; + this.title = title; + this.body = body; + } + +} diff --git a/src/main/java/org/dnd/timeet/member/domain/Member.java b/src/main/java/org/dnd/timeet/member/domain/Member.java index 9d684c8..1783892 100644 --- a/src/main/java/org/dnd/timeet/member/domain/Member.java +++ b/src/main/java/org/dnd/timeet/member/domain/Member.java @@ -39,6 +39,9 @@ public class Member extends BaseEntity { @Column(length = 50, nullable = false) private OAuth2Provider provider; + @Column(length = 255) + private String fcmToken; + // MEMO : 필수값들이므로 final 붙임 @Builder public Member(MemberRole role, String name, String imageUrl, Long oauthId, OAuth2Provider provider) { From ffcd451fa0c8949cf8fe3d04a047a1bc8217112a Mon Sep 17 00:00:00 2001 From: FacerAin Date: Sun, 11 Feb 2024 12:28:55 +0900 Subject: [PATCH 2/6] [#22] feat: Add upsertFcmToken --- .../timeet/member/application/MemberService.java | 15 +++++++++++++++ .../java/org/dnd/timeet/member/domain/Member.java | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/src/main/java/org/dnd/timeet/member/application/MemberService.java b/src/main/java/org/dnd/timeet/member/application/MemberService.java index 0fd9236..5912a34 100644 --- a/src/main/java/org/dnd/timeet/member/application/MemberService.java +++ b/src/main/java/org/dnd/timeet/member/application/MemberService.java @@ -1,7 +1,11 @@ package org.dnd.timeet.member.application; +import java.util.Collections; +import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.dnd.timeet.common.exception.NotFoundError; +import org.dnd.timeet.member.domain.Member; import org.dnd.timeet.member.domain.MemberRepository; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -14,4 +18,15 @@ public class MemberService { private final PasswordEncoder passwordEncoder; private final MemberRepository memberRepository; + + public void upsertFcmToken(Long id, String fcmToken) { + Optional member = memberRepository.findById(id); + if (member.isPresent()) { + member.get().setFcmToken(fcmToken); + } else { + throw new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("MemberId", "Member not found")); + } + + } } \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/member/domain/Member.java b/src/main/java/org/dnd/timeet/member/domain/Member.java index 1783892..557176d 100644 --- a/src/main/java/org/dnd/timeet/member/domain/Member.java +++ b/src/main/java/org/dnd/timeet/member/domain/Member.java @@ -51,4 +51,8 @@ public Member(MemberRole role, String name, String imageUrl, Long oauthId, OAuth this.oauthId = oauthId; this.provider = provider; } + + public void setFcmToken(String fcmToken) { + this.fcmToken = fcmToken; + } } From b3454eadee27bc8c41aadf21ef7c10cea858af67 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Sun, 11 Feb 2024 12:34:27 +0900 Subject: [PATCH 3/6] [#22] feat: Update build workflow for firebase config --- .github/workflows/build.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dcdb8a3..2626605 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,6 @@ name: Build -on: [pull_request, workflow_dispatch] +on: [ pull_request, workflow_dispatch ] jobs: @@ -17,6 +17,10 @@ jobs: java-version: 17 - name: Run Checkstyle run: ./gradlew checkstyleMain checkstyleTest + - name: Make firebase-adminsdk.json + run: | + touch src/main/resources/timeet-firebase-adminsdk.json + echo "${{ secrets.FIREBASE_ADMINSDK }}" > src/main/resources/firebase-adminsdk.json - name: Make application-prod.yml run: | touch src/main/resources/application-prod.yml From 78e45d9d6e31d6cefd823fca3030265655d41238 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Sun, 11 Feb 2024 12:34:46 +0900 Subject: [PATCH 4/6] [#22] feat: Update build workflow for firebase config --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2626605..777fef6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: - name: Make firebase-adminsdk.json run: | touch src/main/resources/timeet-firebase-adminsdk.json - echo "${{ secrets.FIREBASE_ADMINSDK }}" > src/main/resources/firebase-adminsdk.json + echo "${{ secrets.FIREBASE_ADMINSDK }}" > src/main/resources/timeet-firebase-adminsdk.json - name: Make application-prod.yml run: | touch src/main/resources/application-prod.yml From 662ecf5a97a157884dd2191a55b5b938a8b521a4 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Sun, 11 Feb 2024 12:39:45 +0900 Subject: [PATCH 5/6] [#22] feat: Update build workflow for firebase config --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 777fef6..7ba5d31 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: - name: Make firebase-adminsdk.json run: | touch src/main/resources/timeet-firebase-adminsdk.json - echo "${{ secrets.FIREBASE_ADMINSDK }}" > src/main/resources/timeet-firebase-adminsdk.json + echo "${{ secrets.FIREBASE_ADMINSDK }}" | base64 --decode > src/main/resources/timeet-firebase-adminsdk.json - name: Make application-prod.yml run: | touch src/main/resources/application-prod.yml From f80cd783a0c82d18571b3e58b16aa911d5a02774 Mon Sep 17 00:00:00 2001 From: FacerAin Date: Thu, 15 Feb 2024 23:11:19 +0900 Subject: [PATCH 6/6] [#22] feat: Add patch fcmToken controller --- .../common/security/annotation/ReqUser.java | 14 ++++++++++++++ .../fcm/application/FCMNotificationService.java | 15 +++++---------- .../timeet/member/application/MemberService.java | 14 ++++++-------- .../member/controller/MemberController.java | 13 +++++++++++++ .../org/dnd/timeet/member/domain/Member.java | 1 + .../timeet/member/dto/RegisterFcmRequest.java | 16 ++++++++++++++++ 6 files changed, 55 insertions(+), 18 deletions(-) create mode 100644 src/main/java/org/dnd/timeet/common/security/annotation/ReqUser.java create mode 100644 src/main/java/org/dnd/timeet/member/dto/RegisterFcmRequest.java diff --git a/src/main/java/org/dnd/timeet/common/security/annotation/ReqUser.java b/src/main/java/org/dnd/timeet/common/security/annotation/ReqUser.java new file mode 100644 index 0000000..07c8819 --- /dev/null +++ b/src/main/java/org/dnd/timeet/common/security/annotation/ReqUser.java @@ -0,0 +1,14 @@ +package org.dnd.timeet.common.security.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.security.core.annotation.AuthenticationPrincipal; + +@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") +public @interface ReqUser { + +} diff --git a/src/main/java/org/dnd/timeet/fcm/application/FCMNotificationService.java b/src/main/java/org/dnd/timeet/fcm/application/FCMNotificationService.java index 556d75b..ba76173 100644 --- a/src/main/java/org/dnd/timeet/fcm/application/FCMNotificationService.java +++ b/src/main/java/org/dnd/timeet/fcm/application/FCMNotificationService.java @@ -4,7 +4,6 @@ import com.google.firebase.messaging.Message; import com.google.firebase.messaging.Notification; import java.util.Collections; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.dnd.timeet.common.exception.InternalServerError; import org.dnd.timeet.common.exception.NotFoundError; @@ -21,14 +20,10 @@ public class FCMNotificationService { private final MemberRepository memberRepository; public void sendNotificationByToken(FCMNotificationRequestDto requestDto) { - Optional member = memberRepository.findById(requestDto.getTargetMemberId()); - - if (member.isEmpty()) { - throw new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, - Collections.singletonMap("MemberId", "Member not found")); - } - - if (member.get().getFcmToken() == null) { + Member member = memberRepository.findById(requestDto.getTargetMemberId()) + .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("MemberId", "Member not found"))); + if (member.getFcmToken() == null) { throw new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, Collections.singletonMap("fcmToken", "fcmToken not exist")); } @@ -39,7 +34,7 @@ public void sendNotificationByToken(FCMNotificationRequestDto requestDto) { .build(); Message message = Message.builder() - .setToken(member.get().getFcmToken()) + .setToken(member.getFcmToken()) .setNotification(notification) .build(); diff --git a/src/main/java/org/dnd/timeet/member/application/MemberService.java b/src/main/java/org/dnd/timeet/member/application/MemberService.java index 5912a34..e0c6910 100644 --- a/src/main/java/org/dnd/timeet/member/application/MemberService.java +++ b/src/main/java/org/dnd/timeet/member/application/MemberService.java @@ -2,7 +2,6 @@ import java.util.Collections; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.dnd.timeet.common.exception.NotFoundError; import org.dnd.timeet.member.domain.Member; @@ -19,14 +18,13 @@ public class MemberService { private final PasswordEncoder passwordEncoder; private final MemberRepository memberRepository; + @Transactional public void upsertFcmToken(Long id, String fcmToken) { - Optional member = memberRepository.findById(id); - if (member.isPresent()) { - member.get().setFcmToken(fcmToken); - } else { - throw new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, - Collections.singletonMap("MemberId", "Member not found")); - } + Member member = memberRepository.findById(id) + .orElseThrow(() -> new NotFoundError(NotFoundError.ErrorCode.RESOURCE_NOT_FOUND, + Collections.singletonMap("MemberId", "Member not found"))); + member.setFcmToken(fcmToken); + } } \ No newline at end of file diff --git a/src/main/java/org/dnd/timeet/member/controller/MemberController.java b/src/main/java/org/dnd/timeet/member/controller/MemberController.java index 6065921..5168434 100644 --- a/src/main/java/org/dnd/timeet/member/controller/MemberController.java +++ b/src/main/java/org/dnd/timeet/member/controller/MemberController.java @@ -1,8 +1,14 @@ package org.dnd.timeet.member.controller; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.dnd.timeet.common.security.annotation.ReqUser; import org.dnd.timeet.member.application.MemberService; +import org.dnd.timeet.member.domain.Member; +import org.dnd.timeet.member.dto.RegisterFcmRequest; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -14,5 +20,12 @@ public class MemberController { private final MemberService memberService; + @PatchMapping + @Operation(summary = "fcmToken 등록", description = "fcmToken을 등록한다.") + public void registerFcmToken(@RequestBody RegisterFcmRequest registerFcmRequest, + @ReqUser Member member) { + memberService.upsertFcmToken(member.getId(), registerFcmRequest.getFcmToken()); + + } } diff --git a/src/main/java/org/dnd/timeet/member/domain/Member.java b/src/main/java/org/dnd/timeet/member/domain/Member.java index 557176d..3a865be 100644 --- a/src/main/java/org/dnd/timeet/member/domain/Member.java +++ b/src/main/java/org/dnd/timeet/member/domain/Member.java @@ -52,6 +52,7 @@ public Member(MemberRole role, String name, String imageUrl, Long oauthId, OAuth this.provider = provider; } + public void setFcmToken(String fcmToken) { this.fcmToken = fcmToken; } diff --git a/src/main/java/org/dnd/timeet/member/dto/RegisterFcmRequest.java b/src/main/java/org/dnd/timeet/member/dto/RegisterFcmRequest.java new file mode 100644 index 0000000..61594c8 --- /dev/null +++ b/src/main/java/org/dnd/timeet/member/dto/RegisterFcmRequest.java @@ -0,0 +1,16 @@ +package org.dnd.timeet.member.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Schema(description = "fcmToken 등록 요청") +@Getter +@Setter +@NoArgsConstructor +public class RegisterFcmRequest { + + @Schema(description = "fcmToken", nullable = false, example = "fcmToken") + private String fcmToken; +}