diff --git a/application/app-api/src/main/java/core/startup/mealtoktok/api/auth/AuthApi.java b/application/app-api/src/main/java/core/startup/mealtoktok/api/auth/AuthApi.java index 535b645..1f14187 100644 --- a/application/app-api/src/main/java/core/startup/mealtoktok/api/auth/AuthApi.java +++ b/application/app-api/src/main/java/core/startup/mealtoktok/api/auth/AuthApi.java @@ -1,7 +1,7 @@ package core.startup.mealtoktok.api.auth; -import core.startup.mealtoktok.api.dto.SignupRequest; -import core.startup.mealtoktok.api.dto.OAuthLoginResponse; +import core.startup.mealtoktok.api.auth.dto.SignupRequest; +import core.startup.mealtoktok.api.auth.dto.OAuthLoginResponse; import core.startup.mealtoktok.common.dto.Response; import core.startup.mealtoktok.domain.auth.AuthService; import core.startup.mealtoktok.domain.auth.JwtTokens; diff --git a/application/app-api/src/main/java/core/startup/mealtoktok/api/auth/AuthApiDocs.java b/application/app-api/src/main/java/core/startup/mealtoktok/api/auth/AuthApiDocs.java index 5ae8bee..da167dd 100644 --- a/application/app-api/src/main/java/core/startup/mealtoktok/api/auth/AuthApiDocs.java +++ b/application/app-api/src/main/java/core/startup/mealtoktok/api/auth/AuthApiDocs.java @@ -1,13 +1,12 @@ package core.startup.mealtoktok.api.auth; -import core.startup.mealtoktok.api.dto.SignupRequest; -import core.startup.mealtoktok.api.dto.OAuthLoginResponse; +import core.startup.mealtoktok.api.auth.dto.SignupRequest; +import core.startup.mealtoktok.api.auth.dto.OAuthLoginResponse; import core.startup.mealtoktok.common.dto.Response; import core.startup.mealtoktok.domain.auth.OAuthTokens; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; -import org.springframework.web.bind.annotation.RequestParam; @Tag(name = "인증 API") public interface AuthApiDocs { diff --git a/application/app-api/src/main/java/core/startup/mealtoktok/api/dto/OAuthLoginResponse.java b/application/app-api/src/main/java/core/startup/mealtoktok/api/auth/dto/OAuthLoginResponse.java similarity index 78% rename from application/app-api/src/main/java/core/startup/mealtoktok/api/dto/OAuthLoginResponse.java rename to application/app-api/src/main/java/core/startup/mealtoktok/api/auth/dto/OAuthLoginResponse.java index 8755922..48a1557 100644 --- a/application/app-api/src/main/java/core/startup/mealtoktok/api/dto/OAuthLoginResponse.java +++ b/application/app-api/src/main/java/core/startup/mealtoktok/api/auth/dto/OAuthLoginResponse.java @@ -1,4 +1,4 @@ -package core.startup.mealtoktok.api.dto; +package core.startup.mealtoktok.api.auth.dto; public record OAuthLoginResponse( String link diff --git a/application/app-api/src/main/java/core/startup/mealtoktok/api/dto/SignupRequest.java b/application/app-api/src/main/java/core/startup/mealtoktok/api/auth/dto/SignupRequest.java similarity index 91% rename from application/app-api/src/main/java/core/startup/mealtoktok/api/dto/SignupRequest.java rename to application/app-api/src/main/java/core/startup/mealtoktok/api/auth/dto/SignupRequest.java index da719d1..c2dbfc7 100644 --- a/application/app-api/src/main/java/core/startup/mealtoktok/api/dto/SignupRequest.java +++ b/application/app-api/src/main/java/core/startup/mealtoktok/api/auth/dto/SignupRequest.java @@ -1,4 +1,4 @@ -package core.startup.mealtoktok.api.dto; +package core.startup.mealtoktok.api.auth.dto; import core.startup.mealtoktok.domain.auth.OAuthTokens; import core.startup.mealtoktok.domain.user.AddressWithCoordinate; diff --git a/application/app-api/src/main/java/core/startup/mealtoktok/api/auth/exception/ExpiredTokenException.java b/application/app-api/src/main/java/core/startup/mealtoktok/api/auth/exception/ExpiredTokenException.java index b6958c9..3b1327d 100644 --- a/application/app-api/src/main/java/core/startup/mealtoktok/api/auth/exception/ExpiredTokenException.java +++ b/application/app-api/src/main/java/core/startup/mealtoktok/api/auth/exception/ExpiredTokenException.java @@ -4,7 +4,6 @@ import core.startup.mealtoktok.domain.auth.exception.AuthErrorCode; public class ExpiredTokenException extends WebException { - public static final ExpiredTokenException EXCEPTION = new ExpiredTokenException(); private ExpiredTokenException() { diff --git a/application/app-api/src/main/java/core/startup/mealtoktok/api/global/exception/GlobalControllerAdvice.java b/application/app-api/src/main/java/core/startup/mealtoktok/api/global/exception/GlobalControllerAdvice.java index 3e57a6e..a4fec73 100644 --- a/application/app-api/src/main/java/core/startup/mealtoktok/api/global/exception/GlobalControllerAdvice.java +++ b/application/app-api/src/main/java/core/startup/mealtoktok/api/global/exception/GlobalControllerAdvice.java @@ -15,24 +15,11 @@ @RestControllerAdvice public class GlobalControllerAdvice { - @ExceptionHandler(value = WebException.class) - public ResponseEntity customError(WebException e, HttpServletRequest request) { + @ExceptionHandler(value = CustomException.class) + public ResponseEntity customError(CustomException e, HttpServletRequest request) { return ResponseEntity .status(e.getStatus()) .body(Response.error(e.getErrorCode().getErrorReason(), request.getRequestURI(), e.getMessage())); } - @ExceptionHandler(value = DomainException.class) - public ResponseEntity domainError(DomainException e, HttpServletRequest request) { - return ResponseEntity - .status(e.getStatus()) - .body(Response.error(e.getErrorCode().getErrorReason(), request.getRequestURI(), e.getMessage())); - } - - @ExceptionHandler(value = InfraException.class) - public ResponseEntity infraError(InfraException e, HttpServletRequest request) { - return ResponseEntity - .status(e.getStatus()) - .body(Response.error(e.getErrorCode().getErrorReason(), request.getRequestURI(), e.getMessage())); - } } diff --git a/application/app-api/src/main/java/core/startup/mealtoktok/api/global/security/JwtAuthenticationEntryPoint.java b/application/app-api/src/main/java/core/startup/mealtoktok/api/global/security/JwtAuthenticationEntryPoint.java index b66d7fd..5799013 100644 --- a/application/app-api/src/main/java/core/startup/mealtoktok/api/global/security/JwtAuthenticationEntryPoint.java +++ b/application/app-api/src/main/java/core/startup/mealtoktok/api/global/security/JwtAuthenticationEntryPoint.java @@ -20,7 +20,6 @@ public JwtAuthenticationEntryPoint(@Qualifier("handlerExceptionResolver") Handle @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException){ - if (isExceptionInSecurityFilter(request)) { resolver.resolveException(request, response, null, (Exception) request.getAttribute("exception")); return; diff --git a/application/app-api/src/main/java/core/startup/mealtoktok/api/global/security/JwtExceptionHandleFilter.java b/application/app-api/src/main/java/core/startup/mealtoktok/api/global/security/JwtExceptionHandleFilter.java deleted file mode 100644 index 10613ac..0000000 --- a/application/app-api/src/main/java/core/startup/mealtoktok/api/global/security/JwtExceptionHandleFilter.java +++ /dev/null @@ -1,22 +0,0 @@ -package core.startup.mealtoktok.api.global.security; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; - -public class JwtExceptionHandleFilter extends OncePerRequestFilter { - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - try { - filterChain.doFilter(request, response); - } catch (Exception e) { - request.setAttribute("exception", e); - } finally { - filterChain.doFilter(request, response); - } - } -} diff --git a/application/app-api/src/main/java/core/startup/mealtoktok/api/global/security/SecurityConfig.java b/application/app-api/src/main/java/core/startup/mealtoktok/api/global/security/SecurityConfig.java index 991fb3f..5cab02c 100644 --- a/application/app-api/src/main/java/core/startup/mealtoktok/api/global/security/SecurityConfig.java +++ b/application/app-api/src/main/java/core/startup/mealtoktok/api/global/security/SecurityConfig.java @@ -43,7 +43,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti ); http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); -// http.addFilterBefore(new JwtExceptionHandleFilter(), JwtTokenFilter.class); return http.build(); } diff --git a/application/app-api/src/main/java/core/startup/mealtoktok/api/global/util/CookieUtils.java b/application/app-api/src/main/java/core/startup/mealtoktok/api/global/util/CookieUtils.java deleted file mode 100644 index 8c6c9a2..0000000 --- a/application/app-api/src/main/java/core/startup/mealtoktok/api/global/util/CookieUtils.java +++ /dev/null @@ -1,54 +0,0 @@ -package core.startup.mealtoktok.api.global.util; - -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.util.SerializationUtils; - -import java.util.Base64; -import java.util.Optional; - -public class CookieUtils { - - public static Optional getCookie(HttpServletRequest request, String name) { - Cookie[] cookies = request.getCookies(); - if (cookies != null && cookies.length > 0) { - for (Cookie cookie : cookies) { - if (cookie.getName().equals(name)) { - return Optional.of(cookie); - } - } - } - return Optional.empty(); - } - - public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) { - Cookie cookie = new Cookie(name, value); - cookie.setPath("/"); - cookie.setHttpOnly(true); - cookie.setMaxAge(maxAge); - response.addCookie(cookie); - } - - public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) { - Cookie[] cookies = request.getCookies(); - if (cookies != null && cookies.length > 0) { - for (Cookie cookie : cookies) { - if (cookie.getName().equals(name)) { - cookie.setValue(""); - cookie.setPath("/"); - cookie.setMaxAge(0); - response.addCookie(cookie); - } - } - } - } - - public static String serialize(Object object) { - return Base64.getUrlEncoder().encodeToString(SerializationUtils.serialize(object)); - } - - public static T deserialize(Cookie cookie, Class cls) { - return cls.cast(SerializationUtils.deserialize(Base64.getUrlDecoder().decode(cookie.getValue()))); - } -} diff --git a/application/app-api/src/main/java/core/startup/mealtoktok/api/global/util/HttpUtils.java b/application/app-api/src/main/java/core/startup/mealtoktok/api/global/util/HttpUtils.java deleted file mode 100644 index 21a04ca..0000000 --- a/application/app-api/src/main/java/core/startup/mealtoktok/api/global/util/HttpUtils.java +++ /dev/null @@ -1,15 +0,0 @@ -package core.startup.mealtoktok.api.global.util; - -import core.startup.mealtoktok.api.global.security.SecurityProperties; -import core.startup.mealtoktok.domain.auth.JwtTokens; -import org.springframework.http.HttpHeaders; - -public class HttpUtils { - - public static HttpHeaders setHeaders(JwtTokens jwtTokens) { - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.add(SecurityProperties.ACCESS_TOKEN_HEADER, jwtTokens.accessToken()); - httpHeaders.add(SecurityProperties.REFRESH_TOKEN_HEADER, jwtTokens.refreshToken()); - return httpHeaders; - } -} diff --git a/domain/src/main/java/core/startup/mealtoktok/domain/alarm/Alarm.java b/domain/src/main/java/core/startup/mealtoktok/domain/alarm/Alarm.java new file mode 100644 index 0000000..adac367 --- /dev/null +++ b/domain/src/main/java/core/startup/mealtoktok/domain/alarm/Alarm.java @@ -0,0 +1,7 @@ +package core.startup.mealtoktok.domain.alarm; + +public record Alarm ( + String title, + String body +) { +} diff --git a/domain/src/main/java/core/startup/mealtoktok/domain/alarm/AlarmSender.java b/domain/src/main/java/core/startup/mealtoktok/domain/alarm/AlarmSender.java new file mode 100644 index 0000000..85331b4 --- /dev/null +++ b/domain/src/main/java/core/startup/mealtoktok/domain/alarm/AlarmSender.java @@ -0,0 +1,7 @@ +package core.startup.mealtoktok.domain.alarm; + +import core.startup.mealtoktok.domain.user.User; + +public interface AlarmSender { + void send(User user, Alarm alarm); +} diff --git a/domain/src/main/java/core/startup/mealtoktok/domain/alarm/exception/AlarmErrorCode.java b/domain/src/main/java/core/startup/mealtoktok/domain/alarm/exception/AlarmErrorCode.java new file mode 100644 index 0000000..19166cf --- /dev/null +++ b/domain/src/main/java/core/startup/mealtoktok/domain/alarm/exception/AlarmErrorCode.java @@ -0,0 +1,21 @@ +package core.startup.mealtoktok.domain.alarm.exception; + +import core.startup.mealtoktok.common.exception.BaseErrorCode; +import core.startup.mealtoktok.common.exception.ErrorReason; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum AlarmErrorCode implements BaseErrorCode { + ALARM_SEND_FAIL(500, "ALARM_500_1", "알람 전송에 실패했습니다."); + + private final Integer status; + private final String errorCode; + private final String message; + + @Override + public ErrorReason getErrorReason() { + return ErrorReason.of(status, errorCode, message); + } +} diff --git a/infra/build.gradle b/infra/build.gradle index f659df2..f3d1edf 100644 --- a/infra/build.gradle +++ b/infra/build.gradle @@ -12,6 +12,9 @@ dependencies { implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-openfeign', version: '4.1.3' implementation 'org.springframework.cloud:spring-cloud-commons:4.1.4' + //FCM + implementation group: 'com.google.firebase', name: 'firebase-admin', version: '9.3.0' + //redis & cache implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-json' diff --git a/infra/src/main/java/core/startup/mealtoktok/infra/notification/FcmAlarmSender.java b/infra/src/main/java/core/startup/mealtoktok/infra/notification/FcmAlarmSender.java new file mode 100644 index 0000000..697da95 --- /dev/null +++ b/infra/src/main/java/core/startup/mealtoktok/infra/notification/FcmAlarmSender.java @@ -0,0 +1,46 @@ +package core.startup.mealtoktok.infra.notification; + +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.MulticastMessage; +import com.google.firebase.messaging.Notification; +import core.startup.mealtoktok.domain.alarm.Alarm; +import core.startup.mealtoktok.domain.alarm.AlarmSender; +import core.startup.mealtoktok.domain.user.User; +import core.startup.mealtoktok.infra.notification.exception.AlarmSendFailException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class FcmAlarmSender implements AlarmSender { + + private final FirebaseMessaging firebaseMessaging; + + @Override + public void send(User user, Alarm alarm) { + MulticastMessage message = makeMessage(user, alarm); + try { + firebaseMessaging.sendEachForMulticast(message); + } catch (FirebaseMessagingException e) { + log.error("userId : {} 님에게 알람 전송을 실패하였습니다.", user.getUserId(), e); + throw AlarmSendFailException.EXCEPTION; + } + } + + private MulticastMessage makeMessage(User user, Alarm alarm) { + Notification notification = Notification.builder() + .setTitle(alarm.title()) + .setBody(alarm.body()) + .build(); + + return MulticastMessage.builder() + .setNotification(notification) + .addAllTokens(user.getDeviceTokens()) + .build(); + } + + +} diff --git a/infra/src/main/java/core/startup/mealtoktok/infra/notification/config/FcmConfig.java b/infra/src/main/java/core/startup/mealtoktok/infra/notification/config/FcmConfig.java new file mode 100644 index 0000000..e474ee4 --- /dev/null +++ b/infra/src/main/java/core/startup/mealtoktok/infra/notification/config/FcmConfig.java @@ -0,0 +1,38 @@ +package core.startup.mealtoktok.infra.notification.config; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Base64; + +@Configuration +public class FcmConfig { + + @Value("${fcm.secret-key}") + private String fcmSecretKey; + + @Bean + public FirebaseApp firebaseApp() throws IOException { + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(getDecodedCredentials(fcmSecretKey)) + .build(); + return FirebaseApp.initializeApp(options); + } + + @Bean + public FirebaseMessaging firebaseMessaging(FirebaseApp firebaseApp) { + return FirebaseMessaging.getInstance(firebaseApp); + } + + private GoogleCredentials getDecodedCredentials(String encodedKey) throws IOException { + byte[] decodedKey = Base64.getDecoder().decode(encodedKey); + return GoogleCredentials.fromStream(new ByteArrayInputStream(decodedKey)); + } +} diff --git a/infra/src/main/java/core/startup/mealtoktok/infra/notification/exception/AlarmSendFailException.java b/infra/src/main/java/core/startup/mealtoktok/infra/notification/exception/AlarmSendFailException.java new file mode 100644 index 0000000..ba93dd3 --- /dev/null +++ b/infra/src/main/java/core/startup/mealtoktok/infra/notification/exception/AlarmSendFailException.java @@ -0,0 +1,13 @@ +package core.startup.mealtoktok.infra.notification.exception; + +import core.startup.mealtoktok.common.exception.InfraException; +import core.startup.mealtoktok.domain.alarm.exception.AlarmErrorCode; + +public class AlarmSendFailException extends InfraException { + + public static final AlarmSendFailException EXCEPTION = new AlarmSendFailException(); + + private AlarmSendFailException() { + super(AlarmErrorCode.ALARM_SEND_FAIL); + } +} diff --git a/infra/src/main/resources/application-infra.yml b/infra/src/main/resources/application-infra.yml index f3fb1d9..00d372a 100644 --- a/infra/src/main/resources/application-infra.yml +++ b/infra/src/main/resources/application-infra.yml @@ -6,6 +6,8 @@ spring: jpa: open-in-view: false +fcm: + secret-key: ${FCM_SECRET_KEY} --- spring: config: