diff --git a/.gitignore b/.gitignore index d1ee503..19a129b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ http/http-client.private.env.json ### ignore application.yml ### src/main/resources/application-dev.yml src/main/resources/application-local.yml -src/main/resources/firebase/firebase_service_key.json +src/main/resources/firebase/firebase-key.json .gradle/* build/* diff --git a/build.gradle b/build.gradle index be71db9..b64c238 100644 --- a/build.gradle +++ b/build.gradle @@ -68,6 +68,10 @@ dependencies { implementation 'org.springframework.cloud:spring-cloud-aws-context:2.2.6.RELEASE' implementation 'org.springframework.cloud:spring-cloud-aws-autoconfigure:2.2.6.RELEASE' + // Firebase + implementation 'com.google.firebase:firebase-admin:6.8.1' + implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.2.2' + // Secheduler implementation 'org.springframework.boot:spring-boot-starter-quartz' diff --git a/src/main/java/com/daon/onjung/core/utility/CookieUtil.java b/src/main/java/com/daon/onjung/core/utility/CookieUtil.java deleted file mode 100644 index 5703e79..0000000 --- a/src/main/java/com/daon/onjung/core/utility/CookieUtil.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.daon.onjung.core.utility; - -import com.daon.onjung.core.exception.error.ErrorCode; -import com.daon.onjung.core.exception.type.CommonException; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import java.util.Arrays; -import java.util.Optional; - -/** - * Cookie 관련 유틸리티 클래스 - */ -public class CookieUtil { - - /** - * Request에 있는 Cookie 중 name에 해당하는 값을 찾아 반환한다. - * - * @param request HttpServletRequest - * @param name 찾을 Cookie 이름 - * @return Optional - */ - public static Optional refineCookie(HttpServletRequest request, String name) { - Cookie[] cookies = request.getCookies(); - - if (cookies == null) { - throw new CommonException(ErrorCode.INVALID_HEADER_ERROR); - } - - return Arrays.stream(cookies) - .filter(cookie -> cookie.getName().equals(name)) - .findFirst().map(Cookie::getValue); - } - - /** - * Response에 Cookie를 추가한다. - * - * @param response HttpServletResponse - * @param cookieDomain Cookie 도메인 - * @param name Cookie 이름 - * @param value Cookie 값 - */ - public static void addCookie(HttpServletResponse response, String cookieDomain, String name, String value) { - Cookie cookie = new Cookie(name, value); - cookie.setDomain(cookieDomain); - cookie.setPath("/"); - response.addCookie(cookie); - } - - /** - * Response에 Secure Cookie를 추가한다. - * - * @param response HttpServletResponse - * @param cookieDomain Cookie 도메인 - * @param name Cookie 이름 - * @param value Cookie 값 - * @param maxAge Cookie 만료 시간 - */ - public static void addSecureCookie(HttpServletResponse response, String cookieDomain, String name, String value, Integer maxAge) { - Cookie cookie = new Cookie(name, value); - cookie.setDomain(cookieDomain); - cookie.setPath("/"); - cookie.setSecure(true); - cookie.setHttpOnly(true); - cookie.setMaxAge(maxAge); - response.addCookie(cookie); - } - - /** - * Request에 있는 Cookie 중 name에 해당하는 값을 삭제한다. - * - * @param request HttpServletRequest - * @param response HttpServletResponse - * @param name 삭제할 Cookie 이름 - */ - public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) { - Cookie[] cookies = request.getCookies(); - - if (cookies == null) { - return; - } - - for (Cookie cookie : cookies) { - if (cookie.getName().equals(name)) { - Cookie removedCookie = new Cookie(name, null); - removedCookie.setPath("/"); - removedCookie.setMaxAge(0); - removedCookie.setHttpOnly(true); - - if (cookie.getSecure()) { - removedCookie.setSecure(true); - } - - response.addCookie(removedCookie); - } - } - } -} diff --git a/src/main/java/com/daon/onjung/core/utility/FirebaseUtil.java b/src/main/java/com/daon/onjung/core/utility/FirebaseUtil.java new file mode 100644 index 0000000..bef747c --- /dev/null +++ b/src/main/java/com/daon/onjung/core/utility/FirebaseUtil.java @@ -0,0 +1,83 @@ +package com.daon.onjung.core.utility; + +import com.daon.onjung.core.exception.error.ErrorCode; +import com.daon.onjung.core.exception.type.CommonException; +import com.google.auth.oauth2.GoogleCredentials; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.minidev.json.JSONObject; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.List; +import java.util.Map; + +@Component +@RequiredArgsConstructor +@Slf4j +public class FirebaseUtil { + + @Value("${google.firebase.config.path}") + private String firebaseConfigPath; + + @Value("${google.access-token.request.url}") + private String GOOGLE_ACCESS_TOKEN_URL; + + @Value("${google.notification.url}") + private String GOOGLE_NOTIFICATION_URL; + + private static final String BEARER_PREFIX = "Bearer " ; + private static final String JSON_BODY_KEY_TITLE = "title"; + private static final String JSON_BODY_KEY_BODY = "body"; + private static final String JSON_BODY_KEY_TOKEN = "token"; + private static final String JSON_BODY_KEY_NOTIFICATION = "notification"; + private static final String JSON_BODY_KEY_MESSAGE = "message"; + private static final String JSON_BODY_KEY_VALIDATE_ONLY = "validate_only"; + + public String createFirebaseRequestUrl() { + return UriComponentsBuilder.fromHttpUrl(GOOGLE_NOTIFICATION_URL) + .toUriString(); + } + + public HttpHeaders createFirebaseRequestHeaders() { + + String accessToken = null; + + try { + GoogleCredentials credentials = GoogleCredentials + .fromStream(new ClassPathResource(firebaseConfigPath).getInputStream()) + .createScoped(List.of(GOOGLE_ACCESS_TOKEN_URL)); + + credentials.refreshIfExpired(); + + accessToken = BEARER_PREFIX + credentials.getAccessToken().getTokenValue(); + } catch (Exception e) { + log.error("Google Access Token을 가져오는데 실패했습니다.", e); + throw new CommonException(ErrorCode.EXTERNAL_SERVER_ERROR); } + + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Type", "application/json"); + headers.add("Authorization", accessToken); + return headers; + } + + public String createFirebaseRequestBody(String deviceToken, String storeName) { + JSONObject messageBody = new JSONObject(); + + JSONObject notificationJson = new JSONObject(); + notificationJson.put(JSON_BODY_KEY_TITLE, storeName + " 가게의 식권에 당첨됐어요!"); + notificationJson.put(JSON_BODY_KEY_BODY, "축하합니다! " + storeName + " 가게의 식권에 당첨되셨습니다. 지금 바로 확인해보세요!"); + + JSONObject messageJson = new JSONObject(); + messageJson.put(JSON_BODY_KEY_TOKEN, deviceToken); + messageJson.put(JSON_BODY_KEY_NOTIFICATION, notificationJson); + + messageBody.put(JSON_BODY_KEY_MESSAGE, messageJson); + messageBody.put(JSON_BODY_KEY_VALIDATE_ONLY, false); + + return messageBody.toJSONString(); + } +} diff --git a/src/main/java/com/daon/onjung/core/utility/HeaderUtil.java b/src/main/java/com/daon/onjung/core/utility/HeaderUtil.java index 5834fc1..bca5935 100644 --- a/src/main/java/com/daon/onjung/core/utility/HeaderUtil.java +++ b/src/main/java/com/daon/onjung/core/utility/HeaderUtil.java @@ -17,13 +17,4 @@ public static Optional refineHeader(HttpServletRequest request, String h return Optional.of(unpreparedToken.substring(prefix.length())); } -// public static Optional refineHeader(StompHeaderAccessor request, String header, String prefix) { -// String unpreparedToken = request.getFirstNativeHeader(header); -// -// if (!StringUtils.hasText(unpreparedToken) || !unpreparedToken.startsWith(prefix)) { -// return Optional.empty(); -// } -// -// return Optional.of(unpreparedToken.substring(prefix.length())); -// } } diff --git a/src/main/java/com/daon/onjung/core/utility/HttpServletUtil.java b/src/main/java/com/daon/onjung/core/utility/HttpServletUtil.java index e1511ef..1256395 100644 --- a/src/main/java/com/daon/onjung/core/utility/HttpServletUtil.java +++ b/src/main/java/com/daon/onjung/core/utility/HttpServletUtil.java @@ -17,73 +17,8 @@ @RequiredArgsConstructor public class HttpServletUtil { - @Value("${web-engine.client-url}") - private String clientUrl; - - @Value("${web-engine.cookie-domain}") - private String cookieDomain; - - @Value("${json-web-token.refresh-token-expire-period}") - private Long refreshTokenExpirePeriod; - private final ObjectMapper objectMapper; - public void onSuccessRedirectResponseWithJWTCookie( - HttpServletResponse response, - DefaultJsonWebTokenDto tokenDto - ) throws IOException { - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); - response.setStatus(HttpStatus.CREATED.value()); - - CookieUtil.addCookie( - response, - cookieDomain, - Constants.ACCESS_TOKEN, - tokenDto.getAccessToken() - ); - CookieUtil.addSecureCookie( - response, - cookieDomain, - Constants.REFRESH_TOKEN, - tokenDto.getRefreshToken(), - (int) (refreshTokenExpirePeriod / 1000L) - ); - - response.sendRedirect(String.format("%s/%s", clientUrl, "profile")); - } - - public void onSuccessBodyResponseWithJWTCookie( - HttpServletResponse response, - DefaultJsonWebTokenDto tokenDto - ) throws IOException { - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); - response.setStatus(HttpStatus.CREATED.value()); - - CookieUtil.addCookie( - response, - cookieDomain, - Constants.ACCESS_TOKEN, - tokenDto.getAccessToken() - ); - CookieUtil.addSecureCookie( - response, - cookieDomain, - Constants.REFRESH_TOKEN, - tokenDto.getRefreshToken(), - (int) (refreshTokenExpirePeriod / 1000L) - ); - - Map result = new HashMap<>(); - - result.put("success", true); - result.put("data", null); - result.put("error", null); - - response.getWriter().write(objectMapper.writeValueAsString(result)); - } - public void onSuccessBodyResponseWithJWTBody( HttpServletResponse response, DefaultJsonWebTokenDto tokenDto diff --git a/src/main/java/com/daon/onjung/core/utility/PasswordUtil.java b/src/main/java/com/daon/onjung/core/utility/PasswordUtil.java deleted file mode 100644 index 22b6e8c..0000000 --- a/src/main/java/com/daon/onjung/core/utility/PasswordUtil.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.daon.onjung.core.utility; - -/** - * Utility class for password-related operations - */ -public class PasswordUtil { - - /** - * Generate a random password - * @return The generated password - */ - public static String generatePassword(Integer length) { - StringBuilder password = new StringBuilder(); - - // 비밀번호 생성 - for (int i = 0; i < length; i++) { - int random = (int) (Math.random() * 4); - switch (random) { - case 0: - password.append((char) ((int) (Math.random() * 26) + 65)); - break; - case 1: - password.append((char) ((int) (Math.random() * 26) + 97)); - break; - case 2: - password.append((int) (Math.random() * 10)); - break; - case 3: - password.append("!@#$%^&*".charAt((int) (Math.random() * 8))); - break; - } - } - - return password.toString(); - } - - /** - * Generate a random authentication code with the given length - * @param length The length of the authentication code - * @return The generated authentication code - */ - public static String generateAuthCode(Integer length) { - StringBuilder authCode = new StringBuilder(); - - // 숫자로만 구성된 인증 코드 생성 - for (int i = 0; i < length; i++) { - authCode.append((int) (Math.random() * 10)); - } - - return authCode.toString(); - } -} diff --git a/src/main/java/com/daon/onjung/event/application/service/ProcessCompletedEventService.java b/src/main/java/com/daon/onjung/event/application/service/ProcessCompletedEventService.java index 0e76926..5682657 100644 --- a/src/main/java/com/daon/onjung/event/application/service/ProcessCompletedEventService.java +++ b/src/main/java/com/daon/onjung/event/application/service/ProcessCompletedEventService.java @@ -7,6 +7,7 @@ import com.daon.onjung.core.exception.error.ErrorCode; import com.daon.onjung.core.exception.type.CommonException; import com.daon.onjung.core.utility.BankUtil; +import com.daon.onjung.core.utility.FirebaseUtil; import com.daon.onjung.core.utility.RestClientUtil; import com.daon.onjung.event.application.usecase.ProcessCompletedEventUseCase; import com.daon.onjung.event.domain.Event; @@ -44,6 +45,7 @@ public class ProcessCompletedEventService implements ProcessCompletedEventUseCas private final RestClientUtil restClientUtil; private final BankUtil bankUtil; + private final FirebaseUtil firebaseUtil; private final ApplicationEventPublisher applicationEventPublisher; @@ -89,8 +91,8 @@ public void execute(Long eventId) { applicationEventPublisher.publishEvent( EventScheduled.builder() .eventId(newEvent.getId()) - .scheduledTime(newEvent.getEndDate().plusDays(1).atStartOfDay()) -// .scheduledTime(LocalDateTime.now().plusMinutes(1)) // 테스트용 1분 뒤 +// .scheduledTime(newEvent.getEndDate().plusDays(1).atStartOfDay()) + .scheduledTime(LocalDateTime.now().plusMinutes(1)) // 테스트용 1분 뒤 .build() ); @@ -154,6 +156,16 @@ public void execute(Long eventId) { ); ticketRepository.save(ticket); + // 푸시 알림 발송 + url = firebaseUtil.createFirebaseRequestUrl(); + headers = firebaseUtil.createFirebaseRequestHeaders(); + String deviceToken = userRepository.findById(userId). + orElseThrow(() -> new CommonException(ErrorCode.NOT_FOUND_RESOURCE)).getDeviceToken(); + String storeName = store.getName(); + String firebaseRequestBody = firebaseUtil.createFirebaseRequestBody(deviceToken, storeName); + + restClientUtil.sendPostMethod(url, headers, firebaseRequestBody); + // 발급된 유저 기록 및 발급된 티켓 수 증가 issuedUserIds.add(userId); issuedTickets++; diff --git a/src/main/java/com/daon/onjung/security/application/service/SignUpOwnerByDefaultService.java b/src/main/java/com/daon/onjung/security/application/service/SignUpOwnerByDefaultService.java index 589072a..e845979 100644 --- a/src/main/java/com/daon/onjung/security/application/service/SignUpOwnerByDefaultService.java +++ b/src/main/java/com/daon/onjung/security/application/service/SignUpOwnerByDefaultService.java @@ -118,8 +118,8 @@ public void execute(MultipartFile logo, applicationEventPublisher.publishEvent( EventScheduled.builder() .eventId(event.getId()) - .scheduledTime(event.getEndDate().plusDays(1).atStartOfDay()) -// .scheduledTime(LocalDateTime.now().plusMinutes(1)) // 테스트용 1분 뒤 +// .scheduledTime(event.getEndDate().plusDays(1).atStartOfDay()) + .scheduledTime(LocalDateTime.now().plusMinutes(1)) // 테스트용 1분 뒤 .build() ); diff --git a/src/main/java/com/daon/onjung/security/handler/logout/DefaultLogoutSuccessHandler.java b/src/main/java/com/daon/onjung/security/handler/logout/DefaultLogoutSuccessHandler.java index cbed2df..3711ed0 100644 --- a/src/main/java/com/daon/onjung/security/handler/logout/DefaultLogoutSuccessHandler.java +++ b/src/main/java/com/daon/onjung/security/handler/logout/DefaultLogoutSuccessHandler.java @@ -1,8 +1,6 @@ package com.daon.onjung.security.handler.logout; -import com.daon.onjung.core.constant.Constants; import com.daon.onjung.core.exception.error.ErrorCode; -import com.daon.onjung.core.utility.CookieUtil; import com.daon.onjung.core.utility.HttpServletUtil; import com.daon.onjung.security.handler.common.AbstractFailureHandler; import jakarta.servlet.http.HttpServletRequest; @@ -33,16 +31,6 @@ public void onLogoutSuccess( return; } - // User-Agent 헤더를 통해 요청이 브라우저에서 온 것인지 확인 - String userAgent = request.getHeader("User-Agent"); - - // 브라우저에서 온 요청인 경우 쿠키를 삭제함 - if (userAgent != null && userAgent.contains("Mozilla")) { - CookieUtil.deleteCookie(request, response, Constants.ACCESS_TOKEN); - CookieUtil.deleteCookie(request, response, Constants.REFRESH_TOKEN); - CookieUtil.deleteCookie(request, response, "JSESSIONID"); - } - httpServletUtil.onSuccessBodyResponse(response, HttpStatus.OK); }