From 503dd42abd6a9ae220e5036cf4d6d4aa4f940751 Mon Sep 17 00:00:00 2001 From: jjoonleo Date: Sun, 10 May 2026 15:24:42 +0900 Subject: [PATCH] Add public privacy policy page --- .../ontime_back/config/SecurityConfig.java | 2 +- .../AccountDeletionPageController.java | 5 +- .../controller/PrivacyPolicyController.java | 295 ++++++++++++++++++ .../global/jwt/JwtAuthenticationFilter.java | 2 +- .../ontime_back/ControllerTestSupport.java | 3 +- .../AccountDeletionPageControllerTest.java | 2 +- .../PrivacyPolicyControllerTest.java | 42 +++ .../jwt/JwtAuthenticationFilterTest.java | 12 +- 8 files changed, 352 insertions(+), 11 deletions(-) create mode 100644 ontime-back/src/main/java/devkor/ontime_back/controller/PrivacyPolicyController.java create mode 100644 ontime-back/src/test/java/devkor/ontime_back/controller/PrivacyPolicyControllerTest.java diff --git a/ontime-back/src/main/java/devkor/ontime_back/config/SecurityConfig.java b/ontime-back/src/main/java/devkor/ontime_back/config/SecurityConfig.java index e061a36..5acee5f 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/config/SecurityConfig.java +++ b/ontime-back/src/main/java/devkor/ontime_back/config/SecurityConfig.java @@ -72,7 +72,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .headers(headers -> headers .frameOptions(frameOptions -> frameOptions.disable())) .authorizeHttpRequests(auth -> auth - .requestMatchers("/", "/account-deletion", "/css/**", "/images/**", "/js/**", "/favicon.ico", "/h2-console/**").permitAll() + .requestMatchers("/", "/account-deletion", "/privacy-policy", "/css/**", "/images/**", "/js/**", "/favicon.ico", "/h2-console/**").permitAll() .requestMatchers("/health", "/actuator/health/**", "/oauth2/sign-up", "oauth2/success", "login/success", "/oauth2/google/login", "/oauth2/kakao/login", "/oauth2/apple/login", "/sign-up", "/*/additional-info").permitAll() .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**", "/webjars/**", "/swagger-ui.html").permitAll() .requestMatchers("/error").permitAll() diff --git a/ontime-back/src/main/java/devkor/ontime_back/controller/AccountDeletionPageController.java b/ontime-back/src/main/java/devkor/ontime_back/controller/AccountDeletionPageController.java index 2d62db9..bcfa05f 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/controller/AccountDeletionPageController.java +++ b/ontime-back/src/main/java/devkor/ontime_back/controller/AccountDeletionPageController.java @@ -133,8 +133,9 @@ public ResponseEntity getAccountDeletionPage() {

Privacy Policy

- Once the public OnTime privacy policy is hosted, it should be linked from this page and listed in - Google Play Console together with this account deletion request URL. + The public OnTime privacy policy is available at + https://ontime-back.duckdns.org/privacy-policy. + It can be listed in Google Play Console together with this account deletion request URL.

diff --git a/ontime-back/src/main/java/devkor/ontime_back/controller/PrivacyPolicyController.java b/ontime-back/src/main/java/devkor/ontime_back/controller/PrivacyPolicyController.java new file mode 100644 index 0000000..008b9b9 --- /dev/null +++ b/ontime-back/src/main/java/devkor/ontime_back/controller/PrivacyPolicyController.java @@ -0,0 +1,295 @@ +package devkor.ontime_back.controller; + +import org.springframework.http.CacheControl; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.concurrent.TimeUnit; + +@RestController +public class PrivacyPolicyController { + + @GetMapping(value = "/privacy-policy", produces = MediaType.TEXT_HTML_VALUE) + public ResponseEntity getPrivacyPolicy() { + return ResponseEntity.ok() + .contentType(MediaType.TEXT_HTML) + .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic()) + .body(""" + + + + + + + + OnTime Privacy Policy + + + +
+
+

OnTime Privacy Policy

+

App name: OnTime

+

Developer/entity: ejun

+

Contact email: jjoonleo@gmail.com

+

Effective date: May 10, 2026

+
+ +
+

+ OnTime is provided by ejun. This Privacy Policy explains how OnTime collects, uses, + shares, protects, retains, and deletes data when you use the OnTime app. +

+

+ For privacy questions or requests, contact + jjoonleo@gmail.com. +

+ +

Data OnTime Collects Or Accesses

+

+ OnTime collects or accesses the following data to provide accounts, schedules, + preparation reminders, alarms, notifications, and support features. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DataExamplesPurpose
Account dataEmail address, display name, password for email sign-up, Google sign-in token, Apple identity token, Apple authorization code, and Apple-provided name or email when available.Create and authenticate accounts, keep users signed in, support social sign-in, and load profile information.
Schedule/preparation dataSchedule names and times, place information, movement time, spare time, notes, schedule state, lateness time, preparation steps, preparation durations, and step order.Create, update, display, finish, and delete schedules and preparation plans.
Alarm/notification dataAlarm settings, notification permission state, device ID, FCM token, platform, app version, OS version, supported alarm providers, alarm status reports, armed or skipped schedule IDs, and alarm failure reason.Deliver schedule reminders and alarm notifications, register the current device, restore alarms, and diagnose alarm coverage.
FeedbackOptional account deletion feedback or other feedback messages.Process user feedback and account deletion requests.
Local app dataCached user, schedule, place, preparation, alarm, and token data stored on the device.Keep app state available locally and support app operation.
Technical/diagnostic dataNetwork request metadata, server logs, error metadata, and security-related operational records.Operate, secure, debug, and maintain the service.
+

+ OnTime does not request app-owned access to location, contacts, camera, microphone, + phone, SMS, storage, calendar, nearby-device, or Bluetooth permissions in the current + Android release manifest. OnTime uses notification, exact alarm, full-screen intent, + boot completion, vibration, Firebase messaging, and network-related permissions to + provide schedule reminders and alarm functionality. +

+ +

How OnTime Uses Data

+

OnTime uses collected data to:

+
    +
  • Create, authenticate, and manage user accounts.
  • +
  • Support email/password, Google, and Apple sign-in.
  • +
  • Create, update, finish, delete, and display schedules.
  • +
  • Create and update default and schedule-specific preparation steps.
  • +
  • Send schedule reminders, preparation notifications, and alarm notifications.
  • +
  • Register and unregister the current device for alarm and notification delivery.
  • +
  • Process optional feedback and account deletion feedback.
  • +
  • Maintain security, prevent abuse, debug failures, and operate the service.
  • +
+ +

Third-Party Services And Processors

+

+ OnTime uses third-party services and SDKs where needed for core app behavior, including + Google Sign-In for Google account authentication, Apple Sign-In for Apple account + authentication, Firebase Core and Firebase Cloud Messaging for app initialization and + push notification delivery, and OnTime backend/API infrastructure for account, + schedule, preparation, alarm, notification, feedback, and deletion request processing. +

+ +

Data Sharing

+

+ OnTime shares data with service providers only as needed to provide app functionality, + authentication, notifications, hosting, security, operations, and support. OnTime does + not use in-app advertising in the current release build. +

+ +

Secure Data Handling

+

+ OnTime uses HTTPS API communication, token-based authentication, local secure token + storage, release-log restrictions, and redaction practices to protect personal and + sensitive data. Release builds must not log tokens, authorization headers, request + bodies, response bodies, personal schedule payloads, full alarm payloads, OAuth values, + or FCM tokens. +

+ +

Data Retention

+

+ OnTime keeps account, schedule, preparation, alarm, notification, feedback, and + technical data for as long as needed to provide the service, maintain security, meet + legal obligations, resolve disputes, and enforce agreements. +

+

+ When an OnTime account is deleted, account data and user-owned app data are deleted, + including associated schedules, preparation data, notification schedules, user settings, + alarm settings, alarm status, device records, FCM tokens, and session tokens. +

+

+ If a user submits optional account deletion feedback, OnTime may retain that feedback + for up to 1 year to review service quality and deletion-related support issues. +

+

+ Operational logs, monitoring records, and security records may be retained for up to 90 + days for service operation, debugging, security, and abuse-prevention purposes, unless a + longer period is required for legal compliance or an active security investigation. +

+

+ Backup copies that contain deleted account data are removed according to the normal + backup rotation and are retained for no longer than 30 days, unless a longer period is + required by law or an active security investigation. +

+ +

Account And Data Deletion

+

+ Users can request account deletion from within the OnTime app. On successful deletion, + the app signs the user out. +

+

+ Users can also request account deletion outside the app at + https://ontime-back.duckdns.org/account-deletion. +

+

+ For Google and Apple social accounts, the backend attempts to revoke the stored provider + token before deleting the local OnTime account. If provider token revocation fails, the + backend still deletes the local OnTime account. Deleting an OnTime account does not + delete the user's Google account or Apple ID. +

+ +

Children

+

+ OnTime is not directed to children. If you believe a child has provided personal data to + OnTime, contact jjoonleo@gmail.com so the + request can be reviewed. +

+ +

Changes To This Policy

+

+ OnTime may update this Privacy Policy to reflect changes in app behavior, legal + requirements, or service providers. The effective date above will be updated when the + policy changes. +

+
+
+ + + """); + } +} diff --git a/ontime-back/src/main/java/devkor/ontime_back/global/jwt/JwtAuthenticationFilter.java b/ontime-back/src/main/java/devkor/ontime_back/global/jwt/JwtAuthenticationFilter.java index 380db47..9e40c59 100644 --- a/ontime-back/src/main/java/devkor/ontime_back/global/jwt/JwtAuthenticationFilter.java +++ b/ontime-back/src/main/java/devkor/ontime_back/global/jwt/JwtAuthenticationFilter.java @@ -32,7 +32,7 @@ @Slf4j public class JwtAuthenticationFilter extends OncePerRequestFilter { - private static final List NO_CHECK_URLS = List.of("/login", "/health", "/actuator/health", "/swagger-ui", "/sign-up", "/account-deletion", "/v3/api-docs", "/oauth2/google/login", "/oauth2/kakao/login", "/oauth2/apple/login"); + private static final List NO_CHECK_URLS = List.of("/login", "/health", "/actuator/health", "/swagger-ui", "/sign-up", "/account-deletion", "/privacy-policy", "/v3/api-docs", "/oauth2/google/login", "/oauth2/kakao/login", "/oauth2/apple/login"); private final JwtTokenProvider jwtTokenProvider; private final UserRepository userRepository; diff --git a/ontime-back/src/test/java/devkor/ontime_back/ControllerTestSupport.java b/ontime-back/src/test/java/devkor/ontime_back/ControllerTestSupport.java index cc11859..60cd6e3 100644 --- a/ontime-back/src/test/java/devkor/ontime_back/ControllerTestSupport.java +++ b/ontime-back/src/test/java/devkor/ontime_back/ControllerTestSupport.java @@ -26,7 +26,8 @@ FirebaseTokenController.class, AlarmController.class, SocialAuthController.class, - AccountDeletionPageController.class + AccountDeletionPageController.class, + PrivacyPolicyController.class } ) public abstract class ControllerTestSupport { diff --git a/ontime-back/src/test/java/devkor/ontime_back/controller/AccountDeletionPageControllerTest.java b/ontime-back/src/test/java/devkor/ontime_back/controller/AccountDeletionPageControllerTest.java index e4a2cac..2615a25 100644 --- a/ontime-back/src/test/java/devkor/ontime_back/controller/AccountDeletionPageControllerTest.java +++ b/ontime-back/src/test/java/devkor/ontime_back/controller/AccountDeletionPageControllerTest.java @@ -31,6 +31,6 @@ void getAccountDeletionPage() throws Exception { .andExpect(content().string(containsString("retain that feedback for up to"))) .andExpect(content().string(containsString("Operational logs, monitoring records, and security records may be retained for up to 90 days"))) .andExpect(content().string(containsString("retained for no longer than 30 days"))) - .andExpect(content().string(containsString("privacy policy"))); + .andExpect(content().string(containsString("https://ontime-back.duckdns.org/privacy-policy"))); } } diff --git a/ontime-back/src/test/java/devkor/ontime_back/controller/PrivacyPolicyControllerTest.java b/ontime-back/src/test/java/devkor/ontime_back/controller/PrivacyPolicyControllerTest.java new file mode 100644 index 0000000..37e3486 --- /dev/null +++ b/ontime-back/src/test/java/devkor/ontime_back/controller/PrivacyPolicyControllerTest.java @@ -0,0 +1,42 @@ +package devkor.ontime_back.controller; + +import devkor.ontime_back.ControllerTestSupport; +import devkor.ontime_back.TestSecurityConfig; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@Import(TestSecurityConfig.class) +class PrivacyPolicyControllerTest extends ControllerTestSupport { + + @DisplayName("개인정보 처리방침 페이지를 로그인 없이 HTML로 조회한다.") + @Test + void getPrivacyPolicy() throws Exception { + mockMvc.perform(get("/privacy-policy")) + .andExpect(status().isOk()) + .andExpect(header().string("Cache-Control", containsString("max-age=3600"))) + .andExpect(content().contentTypeCompatibleWith("text/html")) + .andExpect(content().string(containsString("OnTime Privacy Policy"))) + .andExpect(content().string(containsString("App name: OnTime"))) + .andExpect(content().string(containsString("Developer/entity: ejun"))) + .andExpect(content().string(containsString("jjoonleo@gmail.com"))) + .andExpect(content().string(containsString("Effective date: May 10, 2026"))) + .andExpect(content().string(containsString("https://ontime-back.duckdns.org/account-deletion"))) + .andExpect(content().string(containsString("Account data"))) + .andExpect(content().string(containsString("Schedule/preparation data"))) + .andExpect(content().string(containsString("Alarm/notification data"))) + .andExpect(content().string(containsString("Feedback"))) + .andExpect(content().string(containsString("Local app data"))) + .andExpect(content().string(containsString("Technical/diagnostic data"))) + .andExpect(content().string(containsString("account data and user-owned app data are deleted"))) + .andExpect(content().string(containsString("for up to 1 year"))) + .andExpect(content().string(containsString("for up to 90"))) + .andExpect(content().string(containsString("retained for no longer than 30 days"))); + } +} diff --git a/ontime-back/src/test/java/devkor/ontime_back/global/jwt/JwtAuthenticationFilterTest.java b/ontime-back/src/test/java/devkor/ontime_back/global/jwt/JwtAuthenticationFilterTest.java index 100028b..bc90ee2 100644 --- a/ontime-back/src/test/java/devkor/ontime_back/global/jwt/JwtAuthenticationFilterTest.java +++ b/ontime-back/src/test/java/devkor/ontime_back/global/jwt/JwtAuthenticationFilterTest.java @@ -3,7 +3,8 @@ import devkor.ontime_back.repository.UserRepository; import jakarta.servlet.FilterChain; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -13,14 +14,15 @@ class JwtAuthenticationFilterTest { - @DisplayName("계정 삭제 요청 페이지는 액세스 토큰 없이 JWT 필터를 통과한다.") - @Test - void skipsAccountDeletionPage() throws Exception { + @DisplayName("공개 HTML 페이지는 액세스 토큰 없이 JWT 필터를 통과한다.") + @ParameterizedTest + @ValueSource(strings = {"/account-deletion", "/privacy-policy"}) + void skipsPublicHtmlPages(String path) throws Exception { JwtTokenProvider jwtTokenProvider = mock(JwtTokenProvider.class); UserRepository userRepository = mock(UserRepository.class); FilterChain filterChain = mock(FilterChain.class); JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtTokenProvider, userRepository); - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/account-deletion"); + MockHttpServletRequest request = new MockHttpServletRequest("GET", path); MockHttpServletResponse response = new MockHttpServletResponse(); filter.doFilter(request, response, filterChain);