From feb8e78b0c31bdf6398d709d68206e3452f63e1c Mon Sep 17 00:00:00 2001 From: Wowieee4 Date: Mon, 4 May 2026 11:20:53 +0700 Subject: [PATCH] update backend --- walkguide-backend/demo/pom.xml | 3 +- ...ication.java => WalkGuideApplication.java} | 10 +- .../java/com/walkguide/config/DataSeeder.java | 13 +- .../com/walkguide/config/OpenApiConfig.java | 19 +- .../com/walkguide/config/SecurityConfig.java | 65 ++++-- .../walkguide/controller/AuthController.java | 65 ++++-- .../controller/GuardianController.java | 201 +++++++++++----- .../controller/PairingController.java | 51 ++++ .../walkguide/controller/UserController.java | 189 +++++++++++---- .../java/com/walkguide/dto/ApiResponse.java | 25 +- .../dto/request/AiConfigUpdateRequest.java | 11 + .../dto/request/FcmTokenRequest.java | 7 + .../dto/request/GeofenceConfigRequest.java | 10 + .../HardwareShortcutUpdateRequest.java | 10 + .../dto/request/InviteUserRequest.java | 11 + .../dto/request/LocationUpdateRequest.java | 11 + .../walkguide/dto/request/LoginRequest.java | 15 ++ .../dto/request/ObstacleLogRequest.java | 12 + .../dto/request/PairingResponseRequest.java | 8 + .../dto/request/RefreshTokenRequest.java | 6 + .../dto/request/RegisterRequest.java | 23 ++ .../dto/request/SendNotificationRequest.java | 10 + .../com/walkguide/dto/request/SosRequest.java | 9 + .../request/UserSettingsUpdateRequest.java | 11 + .../request/VoiceCommandUpdateRequest.java | 9 + .../dto/response/ActivityLogResponse.java | 14 ++ .../dto/response/AiConfigResponse.java | 14 ++ .../dto/response/AuthDataResponse.java | 14 ++ .../dto/response/DashboardResponse.java | 24 ++ .../dto/response/GeofenceResponse.java | 13 ++ .../response/HardwareShortcutResponse.java | 13 ++ .../dto/response/LocationResponse.java | 16 ++ .../dto/response/NotificationResponse.java | 17 ++ .../dto/response/ObstacleLogResponse.java | 17 ++ .../dto/response/PairingStatusResponse.java | 16 ++ .../dto/response/SosEventResponse.java | 16 ++ .../dto/response/UserSettingsResponse.java | 14 ++ .../dto/response/VoiceCommandResponse.java | 13 ++ .../com/walkguide/entity/ActivityLog.java | 42 ++++ .../java/com/walkguide/entity/AiConfig.java | 53 +++++ .../com/walkguide/entity/GeofenceConfig.java | 47 ++++ .../entity/GuardianNotification.java | 54 +++++ .../walkguide/entity/HardwareShortcut.java | 48 ++++ .../com/walkguide/entity/LocationHistory.java | 39 ++++ .../com/walkguide/entity/ObstacleLog.java | 44 ++++ .../com/walkguide/entity/PairingRelation.java | 43 ++++ .../com/walkguide/entity/RefreshToken.java | 35 +++ .../java/com/walkguide/entity/SosEvent.java | 45 ++++ .../main/java/com/walkguide/entity/User.java | 50 ++-- .../com/walkguide/entity/UserSettings.java | 50 ++++ .../walkguide/entity/VoiceCommandConfig.java | 45 ++++ .../com/walkguide/enums/ActivityLogType.java | 12 + .../walkguide/enums/HardwareShortcutKey.java | 9 + .../com/walkguide/enums/NotificationType.java | 5 + .../com/walkguide/enums/PairingStatus.java | 5 + .../java/com/walkguide/enums/SosStatus.java | 5 + .../com/walkguide/enums/VoiceCommandKey.java | 18 ++ .../exception/GlobalExceptionHandler.java | 44 ++-- .../walkguide/exception/PairingException.java | 5 + .../exception/ResourceNotFoundException.java | 7 + .../repository/ActivityLogRepository.java | 17 ++ .../repository/AiConfigRepository.java | 9 + .../repository/GeofenceConfigRepository.java | 9 + .../GuardianNotificationRepository.java | 13 ++ .../HardwareShortcutRepository.java | 10 + .../repository/LocationHistoryRepository.java | 12 + .../repository/ObstacleLogRepository.java | 10 + .../repository/PairingRelationRepository.java | 17 ++ .../repository/RefreshTokenRepository.java | 10 + .../repository/SosEventRepository.java | 10 + .../walkguide/repository/UserRepository.java | 12 +- .../repository/UserSettingsRepository.java | 9 + .../VoiceCommandConfigRepository.java | 13 ++ .../com/walkguide/security/JwtAuthFilter.java | 39 ++-- .../java/com/walkguide/security/JwtUtil.java | 92 +++++--- .../walkguide/security/SecurityHelper.java | 27 +++ .../walkguide/service/ActivityLogService.java | 57 +++++ .../walkguide/service/AiConfigService.java | 63 +++++ .../com/walkguide/service/AuthService.java | 171 +++++++++++--- .../com/walkguide/service/FcmService.java | 50 ++++ .../walkguide/service/GeofenceService.java | 49 ++++ .../service/GuardianDashboardService.java | 55 +++++ .../service/HardwareShortcutService.java | 46 ++++ .../walkguide/service/LocationService.java | 119 ++++++++++ .../service/NotificationService.java | 96 ++++++++ .../walkguide/service/ObstacleLogService.java | 58 +++++ .../com/walkguide/service/PairingService.java | 217 ++++++++++++++++++ .../com/walkguide/service/SosService.java | 118 ++++++++++ .../service/UserSettingsService.java | 42 ++++ .../service/VoiceCommandService.java | 82 +++++++ .../db/migration/V10__create_sos_events.sql | 14 ++ .../migration/V11__create_user_settings.sql | 12 + .../db/migration/V12__create_ai_configs.sql | 13 ++ .../V13__create_voice_command_configs.sql | 12 + .../V14__create_hardware_shortcuts.sql | 13 ++ .../V15__create_geofence_configs.sql | 11 + .../migration/V16__create_refresh_tokens.sql | 10 + .../migration/V4__alter_users_add_columns.sql | 11 + .../V5__create_pairing_relations.sql | 18 ++ .../db/migration/V6__create_activity_logs.sql | 12 + .../db/migration/V7__create_obstacle_logs.sql | 14 ++ .../migration/V8__create_location_history.sql | 13 ++ .../V9__create_guardian_notifications.sql | 16 ++ 103 files changed, 3107 insertions(+), 304 deletions(-) rename walkguide-backend/demo/src/main/java/com/walkguide/{DemoApplication.java => WalkGuideApplication.java} (53%) create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/controller/PairingController.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/request/AiConfigUpdateRequest.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/request/FcmTokenRequest.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/request/GeofenceConfigRequest.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/request/HardwareShortcutUpdateRequest.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/request/InviteUserRequest.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/request/LocationUpdateRequest.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/request/LoginRequest.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/request/ObstacleLogRequest.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/request/PairingResponseRequest.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/request/RefreshTokenRequest.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/request/RegisterRequest.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/request/SendNotificationRequest.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/request/SosRequest.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/request/UserSettingsUpdateRequest.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/request/VoiceCommandUpdateRequest.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/response/ActivityLogResponse.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/response/AiConfigResponse.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/response/AuthDataResponse.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/response/DashboardResponse.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/response/GeofenceResponse.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/response/HardwareShortcutResponse.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/response/LocationResponse.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/response/NotificationResponse.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/response/ObstacleLogResponse.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/response/PairingStatusResponse.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/response/SosEventResponse.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/response/UserSettingsResponse.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/dto/response/VoiceCommandResponse.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/entity/ActivityLog.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/entity/AiConfig.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/entity/GeofenceConfig.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/entity/GuardianNotification.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/entity/HardwareShortcut.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/entity/LocationHistory.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/entity/ObstacleLog.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/entity/PairingRelation.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/entity/RefreshToken.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/entity/SosEvent.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/entity/UserSettings.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/entity/VoiceCommandConfig.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/enums/ActivityLogType.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/enums/HardwareShortcutKey.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/enums/NotificationType.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/enums/PairingStatus.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/enums/SosStatus.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/enums/VoiceCommandKey.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/exception/PairingException.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/exception/ResourceNotFoundException.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/repository/ActivityLogRepository.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/repository/AiConfigRepository.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/repository/GeofenceConfigRepository.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/repository/GuardianNotificationRepository.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/repository/HardwareShortcutRepository.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/repository/LocationHistoryRepository.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/repository/ObstacleLogRepository.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/repository/PairingRelationRepository.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/repository/RefreshTokenRepository.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/repository/SosEventRepository.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/repository/UserSettingsRepository.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/repository/VoiceCommandConfigRepository.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/security/SecurityHelper.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/service/ActivityLogService.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/service/AiConfigService.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/service/FcmService.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/service/GeofenceService.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/service/GuardianDashboardService.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/service/HardwareShortcutService.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/service/LocationService.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/service/NotificationService.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/service/ObstacleLogService.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/service/PairingService.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/service/SosService.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/service/UserSettingsService.java create mode 100644 walkguide-backend/demo/src/main/java/com/walkguide/service/VoiceCommandService.java create mode 100644 walkguide-backend/demo/src/main/resources/db/migration/V10__create_sos_events.sql create mode 100644 walkguide-backend/demo/src/main/resources/db/migration/V11__create_user_settings.sql create mode 100644 walkguide-backend/demo/src/main/resources/db/migration/V12__create_ai_configs.sql create mode 100644 walkguide-backend/demo/src/main/resources/db/migration/V13__create_voice_command_configs.sql create mode 100644 walkguide-backend/demo/src/main/resources/db/migration/V14__create_hardware_shortcuts.sql create mode 100644 walkguide-backend/demo/src/main/resources/db/migration/V15__create_geofence_configs.sql create mode 100644 walkguide-backend/demo/src/main/resources/db/migration/V16__create_refresh_tokens.sql create mode 100644 walkguide-backend/demo/src/main/resources/db/migration/V4__alter_users_add_columns.sql create mode 100644 walkguide-backend/demo/src/main/resources/db/migration/V5__create_pairing_relations.sql create mode 100644 walkguide-backend/demo/src/main/resources/db/migration/V6__create_activity_logs.sql create mode 100644 walkguide-backend/demo/src/main/resources/db/migration/V7__create_obstacle_logs.sql create mode 100644 walkguide-backend/demo/src/main/resources/db/migration/V8__create_location_history.sql create mode 100644 walkguide-backend/demo/src/main/resources/db/migration/V9__create_guardian_notifications.sql diff --git a/walkguide-backend/demo/pom.xml b/walkguide-backend/demo/pom.xml index 82bb89f..a381d17 100644 --- a/walkguide-backend/demo/pom.xml +++ b/walkguide-backend/demo/pom.xml @@ -65,7 +65,7 @@ org.projectlombok lombok - true + 1.18.36 @@ -137,6 +137,7 @@ org.projectlombok lombok + 1.18.36 diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/DemoApplication.java b/walkguide-backend/demo/src/main/java/com/walkguide/WalkGuideApplication.java similarity index 53% rename from walkguide-backend/demo/src/main/java/com/walkguide/DemoApplication.java rename to walkguide-backend/demo/src/main/java/com/walkguide/WalkGuideApplication.java index 2a650c0..960f41c 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/DemoApplication.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/WalkGuideApplication.java @@ -4,10 +4,8 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class DemoApplication { - - public static void main(String[] args) { - SpringApplication.run(DemoApplication.class, args); - } - +public class WalkGuideApplication { + public static void main(String[] args) { + SpringApplication.run(WalkGuideApplication.class, args); + } } diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/config/DataSeeder.java b/walkguide-backend/demo/src/main/java/com/walkguide/config/DataSeeder.java index 2fc4f89..e1f54ab 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/config/DataSeeder.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/config/DataSeeder.java @@ -18,29 +18,22 @@ public class DataSeeder implements CommandLineRunner { public void run(String... args) throws Exception { if (userRepository.count() == 0) { - // 1. Buat Guardian (tanpa connected_to dulu) User guardian = User.builder() .email("guardian@walkguide.com") .password(passwordEncoder.encode("guardian123")) .role("ROLE_GUARDIAN") .build(); - guardian = userRepository.save(guardian); + userRepository.save(guardian); - // 2. Buat User Tunanetra, langsung sambungkan ke Guardian User user = User.builder() .email("user@walkguide.com") .password(passwordEncoder.encode("user123")) .role("ROLE_USER") - .connectedTo(guardian) .build(); - user = userRepository.save(user); - - // 3. Update Guardian -> sambungkan balik ke User yang dijaganya - guardian.setConnectedTo(user); - userRepository.save(guardian); + userRepository.save(user); System.out.println("DataSeeder: Guardian (" + guardian.getId() - + ") <-> User (" + user.getId() + ") berhasil dihubungkan!"); + + ") dan User (" + user.getId() + ") berhasil dibuat!"); } else { System.out.println("DataSeeder: Database sudah ada data, skip seeding."); } diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/config/OpenApiConfig.java b/walkguide-backend/demo/src/main/java/com/walkguide/config/OpenApiConfig.java index 6f0afc9..d012cfb 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/config/OpenApiConfig.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/config/OpenApiConfig.java @@ -2,6 +2,9 @@ package com.walkguide.config; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.Components; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -12,8 +15,16 @@ public class OpenApiConfig { public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info() - .title("Walk Guide API") - .version("1.0") - .description("API Documentation for Walk Guide Application (Final Exam)")); + .title("WalkGuide API") + .version("1.0") + .description("API Documentation for WalkGuide Application - Final Exam")) + .addSecurityItem(new SecurityRequirement().addList("Bearer Auth")) + .components(new Components() + .addSecuritySchemes("Bearer Auth", + new SecurityScheme() + .name("Bearer Auth") + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"))); } -} \ No newline at end of file +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/config/SecurityConfig.java b/walkguide-backend/demo/src/main/java/com/walkguide/config/SecurityConfig.java index 1ece305..2f25ba7 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/config/SecurityConfig.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/config/SecurityConfig.java @@ -1,5 +1,7 @@ package com.walkguide.config; +import com.walkguide.security.JwtAuthFilter; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -8,43 +10,64 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import com.walkguide.security.JwtAuthFilter; +import java.util.List; @Configuration @EnableWebSecurity +@RequiredArgsConstructor public class SecurityConfig { - // MESIN HASHING PASSWORD: Biar password yang disimpan di database gak bisa dibaca langsung, kita enkripsi dulu pake BCrypt + private final JwtAuthFilter jwtAuthFilter; + @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - // Aturan jalan masuk ke API kita: - // Jangan lupa inject filter-nya di atas (tambahin parameter JwtAuthFilter jwtAuthFilter) @Bean - public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthFilter) throws Exception { + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .cors(cors -> cors.configurationSource(request -> { - var corsConfig = new org.springframework.web.cors.CorsConfiguration(); - corsConfig.setAllowedOriginPatterns(java.util.List.of("http://localhost:*", "http://127.0.0.1:*")); - corsConfig.setAllowedMethods(java.util.List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); - corsConfig.setAllowedHeaders(java.util.List.of("*")); - corsConfig.setAllowCredentials(true); - return corsConfig; - })) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(csrf -> csrf.disable()) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth - .requestMatchers("/api/auth/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll() // Login & Swagger bebas - .requestMatchers("/api/guardian/**").hasRole("GUARDIAN") // Khusus Guardian - .requestMatchers("/api/user/**").hasRole("USER") // Khusus Tunanetra + // Public routes + .requestMatchers( + "/api/v1/auth/**", + "/swagger-ui/**", + "/swagger-ui.html", + "/v3/api-docs/**", + "/ws/**" + ).permitAll() + // Guardian only + .requestMatchers("/api/v1/guardian/**").hasRole("GUARDIAN") + // User only + .requestMatchers("/api/v1/user/**").hasRole("USER") + // Both roles (authenticated) + .requestMatchers("/api/v1/shared/**").authenticated() .anyRequest().authenticated() ) - // TARUH SATPAM DI SINI - .addFilterBefore(jwtAuthFilter, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.class); - + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); + return http.build(); } -} \ No newline at end of file + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + // Allow semua origin untuk testing dengan HP berbeda di jaringan yang sama + config.setAllowedOriginPatterns(List.of("*")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); + config.setAllowedHeaders(List.of("*")); + config.setAllowCredentials(false); // false agar bisa pakai wildcard origin + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/controller/AuthController.java b/walkguide-backend/demo/src/main/java/com/walkguide/controller/AuthController.java index 3b9f2a2..1fbadd8 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/controller/AuthController.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/controller/AuthController.java @@ -1,36 +1,57 @@ package com.walkguide.controller; -import java.util.Map; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - import com.walkguide.dto.ApiResponse; -import com.walkguide.dto.AuthRequest; +import com.walkguide.dto.request.*; +import com.walkguide.dto.response.AuthDataResponse; +import com.walkguide.security.SecurityHelper; import com.walkguide.service.AuthService; - import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/api/auth") +@RequestMapping("/api/v1/auth") @RequiredArgsConstructor public class AuthController { private final AuthService authService; - @PostMapping("/login") - public ResponseEntity>> login(@Valid @RequestBody AuthRequest request) { - - // Panggil service buat cek password dan bikin token - Map tokenData = authService.login(request); - - // Bungkus pakai ApiResponse biar sesuai standar Dosen lu! - ApiResponse> response = new ApiResponse<>(true, tokenData, "Login berhasil"); - - return ResponseEntity.ok(response); + /** Health-check — digunakan Flutter ServerConnectScreen */ + @GetMapping("/ping") + public ResponseEntity> ping() { + return ResponseEntity.ok(ApiResponse.ok("pong", "Server aktif")); } -} \ No newline at end of file + + @PostMapping("/register") + public ResponseEntity> register( + @Valid @RequestBody RegisterRequest req) { + return ResponseEntity.ok(ApiResponse.ok(authService.register(req), "Registrasi berhasil")); + } + + @PostMapping("/login") + public ResponseEntity> login( + @Valid @RequestBody LoginRequest req) { + return ResponseEntity.ok(ApiResponse.ok(authService.login(req), "Login berhasil")); + } + + @PostMapping("/refresh") + public ResponseEntity> refresh( + @RequestBody RefreshTokenRequest req) { + return ResponseEntity.ok(ApiResponse.ok( + authService.refreshToken(req.getRefreshToken()), "Token diperbarui")); + } + + @PostMapping("/logout") + public ResponseEntity> logout() { + authService.logout(SecurityHelper.getCurrentUserId()); + return ResponseEntity.ok(ApiResponse.ok(null, "Logout berhasil")); + } + + @PutMapping("/fcm-token") + public ResponseEntity> updateFcmToken( + @RequestBody FcmTokenRequest req) { + authService.updateFcmToken(SecurityHelper.getCurrentUserId(), req.getFcmToken()); + return ResponseEntity.ok(ApiResponse.ok(null, "FCM token diperbarui")); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/controller/GuardianController.java b/walkguide-backend/demo/src/main/java/com/walkguide/controller/GuardianController.java index 23b0ce4..ac978ab 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/controller/GuardianController.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/controller/GuardianController.java @@ -1,77 +1,168 @@ package com.walkguide.controller; import com.walkguide.dto.ApiResponse; -import com.walkguide.entity.User; -import com.walkguide.repository.UserRepository; -import com.walkguide.security.JwtUtil; -import com.walkguide.service.MockDataService; -import jakarta.servlet.http.HttpServletRequest; +import com.walkguide.dto.request.*; +import com.walkguide.dto.response.*; +import com.walkguide.security.SecurityHelper; +import com.walkguide.service.*; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.Map; +import org.springframework.web.bind.annotation.*; @RestController -@RequestMapping("/api/guardian") +@RequestMapping("/api/v1/guardian") @RequiredArgsConstructor public class GuardianController { - private final MockDataService mockDataService; - private final UserRepository userRepository; - private final JwtUtil jwtUtil; + private final GuardianDashboardService dashboardService; + private final LocationService locationService; + private final ActivityLogService activityLogService; + private final ObstacleLogService obstacleLogService; + private final NotificationService notificationService; + private final SosService sosService; + private final AiConfigService aiConfigService; + private final VoiceCommandService voiceCommandService; + private final HardwareShortcutService hardwareShortcutService; + private final GeofenceService geofenceService; + private final UserSettingsService userSettingsService; - // 4. Ambil Status User yang dimonitor - @GetMapping("/user-status") - public ResponseEntity>> getUserStatus() { - return ResponseEntity.ok(new ApiResponse<>(true, - mockDataService.getUserStatus(), - "Data status user berhasil diambil")); + @GetMapping("/dashboard") + public ResponseEntity> dashboard() { + return ResponseEntity.ok(ApiResponse.ok( + dashboardService.getDashboard(SecurityHelper.getCurrentUserId()), + "Dashboard Guardian")); } - // 5. Setting Hardware Shortcut - @PutMapping("/settings/shortcuts") - public ResponseEntity>> updateShortcuts( - @RequestBody Map request) { - Map updated = mockDataService.updateShortcuts(request); - return ResponseEntity.ok(new ApiResponse<>(true, updated, "Shortcut berhasil diperbarui")); + @GetMapping("/user-location") + public ResponseEntity> userLocation() { + return ResponseEntity.ok(ApiResponse.ok( + locationService.getLastLocationForGuardian(SecurityHelper.getCurrentUserId()).orElse(null), + "Lokasi terakhir user")); } - // 6. Setting Sensitivitas AI - @PutMapping("/settings/ai") - public ResponseEntity>> updateAiSettings( - @RequestBody Map request) { - Map updated = mockDataService.updateAiSettings(request); - return ResponseEntity.ok(new ApiResponse<>(true, updated, "Setting AI berhasil diperbarui")); + @GetMapping("/location-history") + public ResponseEntity>> locationHistory( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "50") int size) { + // Guardian lihat location history user yang dipair + Long guardianId = SecurityHelper.getCurrentUserId(); + // Perlu ambil userId dulu — delegasikan ke service + return ResponseEntity.ok(ApiResponse.ok( + locationService.getLocationHistory(guardianId, + PageRequest.of(page, size, Sort.by("createdAt").descending())), + "Riwayat lokasi")); } - // 7. Lihat User yang terhubung ke Guardian ini - @GetMapping("/my-user") - public ResponseEntity>> getMyConnectedUser( - HttpServletRequest request) { - String token = request.getHeader("Authorization").substring(7); - String email = jwtUtil.extractUsername(token); + @GetMapping("/activity-logs") + public ResponseEntity>> activityLogs( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { + return ResponseEntity.ok(ApiResponse.ok( + activityLogService.getLogsForGuardian(SecurityHelper.getCurrentUserId(), + PageRequest.of(page, size)), "Log aktivitas user")); + } - User guardian = userRepository.findByEmail(email) - .orElseThrow(() -> new RuntimeException("Guardian tidak ditemukan")); + @GetMapping("/obstacle-logs") + public ResponseEntity>> obstacleLogs( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { + return ResponseEntity.ok(ApiResponse.ok( + obstacleLogService.getObstacleLogs(SecurityHelper.getCurrentUserId(), + PageRequest.of(page, size)), "Log obstacle user")); + } - User connectedUser = guardian.getConnectedTo(); - if (connectedUser == null) { - return ResponseEntity.ok(new ApiResponse<>(true, - Map.of("message", "Belum ada user yang terhubung"), - "Tidak ada koneksi")); - } + @PostMapping("/notifications/send") + public ResponseEntity> sendNotif( + @RequestBody SendNotificationRequest req) { + return ResponseEntity.ok(ApiResponse.ok( + notificationService.sendNotification(SecurityHelper.getCurrentUserId(), req), + "Notifikasi terkirim")); + } - Map data = Map.of( - "userId", connectedUser.getId(), - "userEmail", connectedUser.getEmail(), - "connectedSince", connectedUser.getCreatedAt().toString() - ); - return ResponseEntity.ok(new ApiResponse<>(true, data, - "Data user yang dipantau berhasil diambil")); + @GetMapping("/sos-events") + public ResponseEntity>> sosEvents( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { + return ResponseEntity.ok(ApiResponse.ok( + sosService.getSosEventsForGuardian(SecurityHelper.getCurrentUserId(), + PageRequest.of(page, size)), "SOS events")); + } + + @PutMapping("/sos/{id}/acknowledge") + public ResponseEntity> acknowledgeSos(@PathVariable Long id) { + return ResponseEntity.ok(ApiResponse.ok( + sosService.acknowledgeSos(SecurityHelper.getCurrentUserId(), id), + "SOS diakui")); + } + + @GetMapping("/ai-config") + public ResponseEntity> getAiConfig() { + // Guardian lihat config user yang dipair + return ResponseEntity.ok(ApiResponse.ok( + aiConfigService.getConfig(SecurityHelper.getCurrentUserId()), + "AI config")); + } + + @PutMapping("/ai-config") + public ResponseEntity> updateAiConfig( + @RequestBody AiConfigUpdateRequest req) { + return ResponseEntity.ok(ApiResponse.ok( + aiConfigService.updateConfigByGuardian(SecurityHelper.getCurrentUserId(), req), + "AI config diperbarui")); + } + + @GetMapping("/voice-commands") + public ResponseEntity> getVoiceCommands() { + return ResponseEntity.ok(ApiResponse.ok( + voiceCommandService.getAll(SecurityHelper.getCurrentUserId()), + "Voice commands")); + } + + @PutMapping("/voice-commands") + public ResponseEntity> updateVoiceCommand( + @RequestBody VoiceCommandUpdateRequest req) { + return ResponseEntity.ok(ApiResponse.ok( + voiceCommandService.updateByGuardian(SecurityHelper.getCurrentUserId(), req), + "Voice command diperbarui")); + } + + @GetMapping("/shortcuts") + public ResponseEntity> getShortcuts() { + return ResponseEntity.ok(ApiResponse.ok( + hardwareShortcutService.getAll(SecurityHelper.getCurrentUserId()), + "Hardware shortcuts")); + } + + @GetMapping("/geofence") + public ResponseEntity> getGeofence() { + return ResponseEntity.ok(ApiResponse.ok( + geofenceService.getConfig(SecurityHelper.getCurrentUserId()), + "Geofence config")); + } + + @PutMapping("/geofence") + public ResponseEntity> updateGeofence( + @RequestBody GeofenceConfigRequest req) { + return ResponseEntity.ok(ApiResponse.ok( + geofenceService.updateConfig(SecurityHelper.getCurrentUserId(), req), + "Geofence diperbarui")); + } + + @GetMapping("/user-settings") + public ResponseEntity> getUserSettings() { + return ResponseEntity.ok(ApiResponse.ok( + userSettingsService.getSettings(SecurityHelper.getCurrentUserId()), + "User settings")); + } + + @PutMapping("/user-settings") + public ResponseEntity> updateUserSettings( + @RequestBody UserSettingsUpdateRequest req) { + return ResponseEntity.ok(ApiResponse.ok( + userSettingsService.updateSettings(SecurityHelper.getCurrentUserId(), req), + "User settings diperbarui")); } } diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/controller/PairingController.java b/walkguide-backend/demo/src/main/java/com/walkguide/controller/PairingController.java new file mode 100644 index 0000000..3acc44c --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/controller/PairingController.java @@ -0,0 +1,51 @@ +package com.walkguide.controller; + +import com.walkguide.dto.ApiResponse; +import com.walkguide.dto.request.*; +import com.walkguide.dto.response.PairingStatusResponse; +import com.walkguide.security.SecurityHelper; +import com.walkguide.service.PairingService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/shared/pairing") +@RequiredArgsConstructor +public class PairingController { + + private final PairingService pairingService; + + @PostMapping("/invite") + public ResponseEntity> invite( + @RequestBody InviteUserRequest req) { + Long guardianId = SecurityHelper.getCurrentUserId(); + return ResponseEntity.ok(ApiResponse.ok( + pairingService.inviteUser(guardianId, req.getUniqueUserId()), + "Undangan dikirim ke user")); + } + + @PostMapping("/respond") + public ResponseEntity> respond( + @RequestBody PairingResponseRequest req) { + Long userId = SecurityHelper.getCurrentUserId(); + return ResponseEntity.ok(ApiResponse.ok( + pairingService.respondToPairing(userId, req.getPairingId(), req.isAccept()), + req.isAccept() ? "Pairing diterima" : "Pairing ditolak")); + } + + @DeleteMapping("/unpair") + public ResponseEntity> unpair() { + pairingService.unpair(SecurityHelper.getCurrentUserId()); + return ResponseEntity.ok(ApiResponse.ok(null, "Pairing diakhiri")); + } + + @GetMapping("/status") + public ResponseEntity> status(Authentication auth) { + Long userId = SecurityHelper.getCurrentUserId(); + String role = auth.getAuthorities().iterator().next().getAuthority(); + return ResponseEntity.ok(ApiResponse.ok( + pairingService.getStatus(userId, role), "Status pairing")); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/controller/UserController.java b/walkguide-backend/demo/src/main/java/com/walkguide/controller/UserController.java index 34e60fc..65f3d72 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/controller/UserController.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/controller/UserController.java @@ -1,60 +1,171 @@ package com.walkguide.controller; import com.walkguide.dto.ApiResponse; -import com.walkguide.entity.User; +import com.walkguide.dto.request.*; +import com.walkguide.dto.response.*; +import com.walkguide.enums.ActivityLogType; import com.walkguide.repository.UserRepository; -import com.walkguide.security.JwtUtil; -import jakarta.servlet.http.HttpServletRequest; +import com.walkguide.security.SecurityHelper; +import com.walkguide.service.*; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; -import java.util.Map; +import java.util.List; @RestController -@RequestMapping("/api/user") +@RequestMapping("/api/v1/user") @RequiredArgsConstructor public class UserController { + private final LocationService locationService; + private final ObstacleLogService obstacleLogService; + private final SosService sosService; + private final ActivityLogService activityLogService; + private final NotificationService notificationService; + private final UserSettingsService userSettingsService; + private final AiConfigService aiConfigService; + private final VoiceCommandService voiceCommandService; + private final HardwareShortcutService hardwareShortcutService; private final UserRepository userRepository; - private final JwtUtil jwtUtil; - // Sinyal Darurat (Voice Command) - @PostMapping("/emergency") - public ResponseEntity> triggerEmergency( - @RequestBody Map request) { - String triggerType = (String) request.get("triggerType"); - return ResponseEntity.ok(new ApiResponse<>(true, - "Darurat Terkirim", - "Guardian telah diberi peringatan via: " + triggerType)); + @GetMapping("/profile") + public ResponseEntity> getProfile() { + Long userId = SecurityHelper.getCurrentUserId(); + var user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("User tidak ditemukan")); + var profile = java.util.Map.of( + "id", user.getId(), + "email", user.getEmail(), + "displayName", user.getDisplayName() != null ? user.getDisplayName() : "", + "role", user.getRole(), + "uniqueUserId", user.getUniqueUserId() != null ? user.getUniqueUserId() : "" + ); + return ResponseEntity.ok(ApiResponse.ok(profile, "Profil user")); } - // Lihat Guardian yang terhubung ke User ini - @GetMapping("/my-guardian") - public ResponseEntity>> getMyGuardian( - HttpServletRequest request) { - String token = request.getHeader("Authorization").substring(7); - String email = jwtUtil.extractUsername(token); + @GetMapping("/settings") + public ResponseEntity> getSettings() { + return ResponseEntity.ok(ApiResponse.ok( + userSettingsService.getSettings(SecurityHelper.getCurrentUserId()), + "Settings user")); + } - User user = userRepository.findByEmail(email) - .orElseThrow(() -> new RuntimeException("User tidak ditemukan")); + @PutMapping("/settings") + public ResponseEntity> updateSettings( + @RequestBody UserSettingsUpdateRequest req) { + return ResponseEntity.ok(ApiResponse.ok( + userSettingsService.updateSettings(SecurityHelper.getCurrentUserId(), req), + "Settings diperbarui")); + } - User guardian = user.getConnectedTo(); - if (guardian == null) { - return ResponseEntity.ok(new ApiResponse<>(true, - Map.of("message", "Belum ada guardian yang terhubung"), - "Tidak ada koneksi")); - } + @GetMapping("/voice-commands") + public ResponseEntity>> getVoiceCommands() { + return ResponseEntity.ok(ApiResponse.ok( + voiceCommandService.getAll(SecurityHelper.getCurrentUserId()), + "Voice commands")); + } - Map data = Map.of( - "guardianId", guardian.getId(), - "guardianEmail", guardian.getEmail() - ); - return ResponseEntity.ok(new ApiResponse<>(true, data, - "Data guardian berhasil diambil")); + @GetMapping("/shortcuts") + public ResponseEntity>> getShortcuts() { + return ResponseEntity.ok(ApiResponse.ok( + hardwareShortcutService.getAll(SecurityHelper.getCurrentUserId()), + "Hardware shortcuts")); + } + + @PutMapping("/shortcuts") + public ResponseEntity> updateShortcut( + @RequestBody HardwareShortcutUpdateRequest req) { + return ResponseEntity.ok(ApiResponse.ok( + hardwareShortcutService.update(SecurityHelper.getCurrentUserId(), req), + "Shortcut diperbarui")); + } + + @GetMapping("/ai-config") + public ResponseEntity> getAiConfig() { + return ResponseEntity.ok(ApiResponse.ok( + aiConfigService.getConfig(SecurityHelper.getCurrentUserId()), + "AI config")); + } + + @PostMapping("/location") + public ResponseEntity> updateLocation( + @RequestBody LocationUpdateRequest req) { + return ResponseEntity.ok(ApiResponse.ok( + locationService.updateLocation(SecurityHelper.getCurrentUserId(), req), + "Lokasi diperbarui")); + } + + @PostMapping("/obstacle") + public ResponseEntity> logObstacle( + @RequestBody ObstacleLogRequest req) { + return ResponseEntity.ok(ApiResponse.ok( + obstacleLogService.saveObstacle(SecurityHelper.getCurrentUserId(), req), + "Obstacle dicatat")); + } + + @PostMapping("/sos") + public ResponseEntity> triggerSos( + @RequestBody SosRequest req) { + return ResponseEntity.ok(ApiResponse.ok( + sosService.triggerSos(SecurityHelper.getCurrentUserId(), req), + "SOS dikirim! Guardian sudah diberitahu.")); + } + + @GetMapping("/activity-logs") + public ResponseEntity>> getActivityLogs( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { + return ResponseEntity.ok(ApiResponse.ok( + activityLogService.getLogs(SecurityHelper.getCurrentUserId(), + PageRequest.of(page, size)), "Log aktivitas")); + } + + @GetMapping("/notifications") + public ResponseEntity>> getNotifications( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { + return ResponseEntity.ok(ApiResponse.ok( + notificationService.getNotifications(SecurityHelper.getCurrentUserId(), + PageRequest.of(page, size)), "Notifikasi")); + } + + @GetMapping("/notifications/unread-count") + public ResponseEntity> getUnreadCount() { + return ResponseEntity.ok(ApiResponse.ok( + notificationService.getUnreadCount(SecurityHelper.getCurrentUserId()), + "Jumlah notifikasi belum dibaca")); + } + + @PutMapping("/notifications/mark-all-read") + public ResponseEntity> markAllRead() { + notificationService.markAllRead(SecurityHelper.getCurrentUserId()); + return ResponseEntity.ok(ApiResponse.ok(null, "Semua notifikasi ditandai sudah dibaca")); + } + + @PutMapping("/notifications/{id}/read") + public ResponseEntity> markOneRead(@PathVariable Long id) { + notificationService.markOneRead(id); + return ResponseEntity.ok(ApiResponse.ok(null, "Notifikasi ditandai sudah dibaca")); + } + + @PostMapping("/walkguide/start") + public ResponseEntity> walkGuideStart() { + Long userId = SecurityHelper.getCurrentUserId(); + userRepository.findById(userId).ifPresent(u -> + activityLogService.createLog(u, ActivityLogType.WALKGUIDE_START, + "WalkGuide dimulai", null)); + return ResponseEntity.ok(ApiResponse.ok(null, "WalkGuide dimulai")); + } + + @PostMapping("/walkguide/stop") + public ResponseEntity> walkGuideStop() { + Long userId = SecurityHelper.getCurrentUserId(); + userRepository.findById(userId).ifPresent(u -> + activityLogService.createLog(u, ActivityLogType.WALKGUIDE_STOP, + "WalkGuide dihentikan", null)); + return ResponseEntity.ok(ApiResponse.ok(null, "WalkGuide dihentikan")); } } diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/ApiResponse.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/ApiResponse.java index 61a0198..f8563ef 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/dto/ApiResponse.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/ApiResponse.java @@ -9,7 +9,6 @@ public class ApiResponse { private String errorCode; private String timestamp; - // Constructor buat Sukses public ApiResponse(boolean success, T data, String message) { this.success = success; this.data = data; @@ -17,7 +16,6 @@ public class ApiResponse { this.timestamp = Instant.now().toString(); } - // Constructor buat Error public ApiResponse(boolean success, String errorCode, String message) { this.success = success; this.errorCode = errorCode; @@ -25,19 +23,22 @@ public class ApiResponse { this.timestamp = Instant.now().toString(); } - // Getter Setter manual + public static ApiResponse ok(T data, String message) { + return new ApiResponse<>(true, data, message); + } + + public static ApiResponse error(String errorCode, String message) { + return new ApiResponse<>(false, errorCode, message); + } + public boolean isSuccess() { return success; } - public void setSuccess(boolean success) { this.success = success; } - public T getData() { return data; } - public void setData(T data) { this.data = data; } - public String getMessage() { return message; } - public void setMessage(String message) { this.message = message; } - public String getErrorCode() { return errorCode; } - public void setErrorCode(String errorCode) { this.errorCode = errorCode; } - public String getTimestamp() { return timestamp; } + public void setSuccess(boolean success) { this.success = success; } + public void setData(T data) { this.data = data; } + public void setMessage(String message) { this.message = message; } + public void setErrorCode(String errorCode) { this.errorCode = errorCode; } public void setTimestamp(String timestamp) { this.timestamp = timestamp; } -} \ No newline at end of file +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/AiConfigUpdateRequest.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/AiConfigUpdateRequest.java new file mode 100644 index 0000000..21e3137 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/AiConfigUpdateRequest.java @@ -0,0 +1,11 @@ +package com.walkguide.dto.request; +import lombok.Data; + +@Data +public class AiConfigUpdateRequest { + private Double confidenceThreshold; + private Double alertDistanceClose; + private Double alertDistanceMedium; + private Integer maxInferenceFps; + private String enabledLabels; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/FcmTokenRequest.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/FcmTokenRequest.java new file mode 100644 index 0000000..e9d5680 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/FcmTokenRequest.java @@ -0,0 +1,7 @@ +package com.walkguide.dto.request; +import lombok.Data; + +@Data +public class FcmTokenRequest { + private String fcmToken; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/GeofenceConfigRequest.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/GeofenceConfigRequest.java new file mode 100644 index 0000000..567c2c6 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/GeofenceConfigRequest.java @@ -0,0 +1,10 @@ +package com.walkguide.dto.request; +import lombok.Data; + +@Data +public class GeofenceConfigRequest { + private Double centerLat; + private Double centerLng; + private Double radiusMeters; + private Boolean enabled; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/HardwareShortcutUpdateRequest.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/HardwareShortcutUpdateRequest.java new file mode 100644 index 0000000..1464f51 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/HardwareShortcutUpdateRequest.java @@ -0,0 +1,10 @@ +package com.walkguide.dto.request; +import lombok.Data; + +@Data +public class HardwareShortcutUpdateRequest { + private String shortcutKey; // enum HardwareShortcutKey as string + private String buttonName; + private Integer buttonCode; + private Boolean enabled; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/InviteUserRequest.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/InviteUserRequest.java new file mode 100644 index 0000000..599d14d --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/InviteUserRequest.java @@ -0,0 +1,11 @@ +package com.walkguide.dto.request; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class InviteUserRequest { + @NotBlank(message = "User ID tidak boleh kosong") + @Size(min = 12, max = 12, message = "User ID harus tepat 12 karakter") + private String uniqueUserId; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/LocationUpdateRequest.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/LocationUpdateRequest.java new file mode 100644 index 0000000..db61c41 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/LocationUpdateRequest.java @@ -0,0 +1,11 @@ +package com.walkguide.dto.request; +import lombok.Data; + +@Data +public class LocationUpdateRequest { + private Double lat; + private Double lng; + private Double accuracy; + private Double speed; + private Double heading; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/LoginRequest.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/LoginRequest.java new file mode 100644 index 0000000..da84925 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/LoginRequest.java @@ -0,0 +1,15 @@ +package com.walkguide.dto.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class LoginRequest { + @NotBlank(message = "Email tidak boleh kosong") + @Email(message = "Format email tidak valid") + private String email; + + @NotBlank(message = "Password tidak boleh kosong") + private String password; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/ObstacleLogRequest.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/ObstacleLogRequest.java new file mode 100644 index 0000000..cb9071d --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/ObstacleLogRequest.java @@ -0,0 +1,12 @@ +package com.walkguide.dto.request; +import lombok.Data; + +@Data +public class ObstacleLogRequest { + private String label; + private Double confidence; + private String direction; + private String estimatedDist; + private Double lat; + private Double lng; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/PairingResponseRequest.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/PairingResponseRequest.java new file mode 100644 index 0000000..217b577 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/PairingResponseRequest.java @@ -0,0 +1,8 @@ +package com.walkguide.dto.request; +import lombok.Data; + +@Data +public class PairingResponseRequest { + private Long pairingId; + private boolean accept; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/RefreshTokenRequest.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/RefreshTokenRequest.java new file mode 100644 index 0000000..4901e85 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/RefreshTokenRequest.java @@ -0,0 +1,6 @@ +package com.walkguide.dto.request; +import lombok.Data; +@Data +public class RefreshTokenRequest { + private String refreshToken; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/RegisterRequest.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/RegisterRequest.java new file mode 100644 index 0000000..10f88bb --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/RegisterRequest.java @@ -0,0 +1,23 @@ +package com.walkguide.dto.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class RegisterRequest { + @NotBlank(message = "Email tidak boleh kosong") + @Email(message = "Format email tidak valid") + private String email; + + @NotBlank(message = "Password tidak boleh kosong") + @Size(min = 6, message = "Password minimal 6 karakter") + private String password; + + @NotBlank(message = "Nama tidak boleh kosong") + private String displayName; + + @NotBlank(message = "Role tidak boleh kosong") + private String role; // GUARDIAN atau USER +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/SendNotificationRequest.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/SendNotificationRequest.java new file mode 100644 index 0000000..fb697c9 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/SendNotificationRequest.java @@ -0,0 +1,10 @@ +package com.walkguide.dto.request; +import lombok.Data; + +@Data +public class SendNotificationRequest { + private String notifType; // TEXT atau VOICE_NOTE + private String content; + private String voiceNoteUrl; + private Integer voiceNoteDuration; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/SosRequest.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/SosRequest.java new file mode 100644 index 0000000..db23998 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/SosRequest.java @@ -0,0 +1,9 @@ +package com.walkguide.dto.request; +import lombok.Data; + +@Data +public class SosRequest { + private String triggerType; // VOICE_COMMAND, BUTTON, MANUAL + private Double lat; + private Double lng; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/UserSettingsUpdateRequest.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/UserSettingsUpdateRequest.java new file mode 100644 index 0000000..62a44b9 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/UserSettingsUpdateRequest.java @@ -0,0 +1,11 @@ +package com.walkguide.dto.request; +import lombok.Data; + +@Data +public class UserSettingsUpdateRequest { + private String ttsLanguage; + private Double ttsPitch; + private Double ttsSpeed; + private Boolean warnNoGuardian; + private Boolean hapticEnabled; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/VoiceCommandUpdateRequest.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/VoiceCommandUpdateRequest.java new file mode 100644 index 0000000..2d5cd2c --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/request/VoiceCommandUpdateRequest.java @@ -0,0 +1,9 @@ +package com.walkguide.dto.request; +import lombok.Data; + +@Data +public class VoiceCommandUpdateRequest { + private String commandKey; // enum VoiceCommandKey as string + private String triggerPhrase; + private Boolean enabled; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/ActivityLogResponse.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/ActivityLogResponse.java new file mode 100644 index 0000000..124665a --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/ActivityLogResponse.java @@ -0,0 +1,14 @@ +package com.walkguide.dto.response; +import lombok.Builder; +import lombok.Data; +import java.time.LocalDateTime; + +@Data +@Builder +public class ActivityLogResponse { + private Long id; + private String logType; + private String description; + private String metadata; + private LocalDateTime createdAt; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/AiConfigResponse.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/AiConfigResponse.java new file mode 100644 index 0000000..c6b96e1 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/AiConfigResponse.java @@ -0,0 +1,14 @@ +package com.walkguide.dto.response; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class AiConfigResponse { + private Long id; + private Double confidenceThreshold; + private Double alertDistanceClose; + private Double alertDistanceMedium; + private Integer maxInferenceFps; + private String enabledLabels; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/AuthDataResponse.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/AuthDataResponse.java new file mode 100644 index 0000000..90d0f92 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/AuthDataResponse.java @@ -0,0 +1,14 @@ +package com.walkguide.dto.response; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class AuthDataResponse { + private String accessToken; + private String refreshToken; + private String role; + private Long userId; + private String displayName; + private String uniqueUserId; // null untuk GUARDIAN +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/DashboardResponse.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/DashboardResponse.java new file mode 100644 index 0000000..900af51 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/DashboardResponse.java @@ -0,0 +1,24 @@ +package com.walkguide.dto.response; +import lombok.Builder; +import lombok.Data; +import java.util.List; + +@Data +@Builder +public class DashboardResponse { + // Info user yang dipair + private Long pairedUserId; + private String pairedUserName; + private String pairedUserEmail; + private String uniqueUserId; + + // Lokasi terakhir + private LocationResponse lastLocation; + + // Status + private long unreadSosCount; + private long unreadNotifCount; + + // Recent activity (5 terbaru) + private List recentActivity; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/GeofenceResponse.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/GeofenceResponse.java new file mode 100644 index 0000000..fb8f57d --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/GeofenceResponse.java @@ -0,0 +1,13 @@ +package com.walkguide.dto.response; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class GeofenceResponse { + private Long id; + private Double centerLat; + private Double centerLng; + private Double radiusMeters; + private Boolean enabled; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/HardwareShortcutResponse.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/HardwareShortcutResponse.java new file mode 100644 index 0000000..9a54b30 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/HardwareShortcutResponse.java @@ -0,0 +1,13 @@ +package com.walkguide.dto.response; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class HardwareShortcutResponse { + private Long id; + private String shortcutKey; + private String buttonName; + private Integer buttonCode; + private Boolean enabled; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/LocationResponse.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/LocationResponse.java new file mode 100644 index 0000000..7498a6b --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/LocationResponse.java @@ -0,0 +1,16 @@ +package com.walkguide.dto.response; +import lombok.Builder; +import lombok.Data; +import java.time.LocalDateTime; + +@Data +@Builder +public class LocationResponse { + private Long id; + private Double lat; + private Double lng; + private Double accuracy; + private Double speed; + private Double heading; + private LocalDateTime createdAt; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/NotificationResponse.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/NotificationResponse.java new file mode 100644 index 0000000..71caef1 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/NotificationResponse.java @@ -0,0 +1,17 @@ +package com.walkguide.dto.response; +import lombok.Builder; +import lombok.Data; +import java.time.LocalDateTime; + +@Data +@Builder +public class NotificationResponse { + private Long id; + private String notifType; + private String content; + private String voiceNoteUrl; + private Integer voiceNoteDuration; + private Boolean isRead; + private LocalDateTime readAt; + private LocalDateTime createdAt; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/ObstacleLogResponse.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/ObstacleLogResponse.java new file mode 100644 index 0000000..7d708d4 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/ObstacleLogResponse.java @@ -0,0 +1,17 @@ +package com.walkguide.dto.response; +import lombok.Builder; +import lombok.Data; +import java.time.LocalDateTime; + +@Data +@Builder +public class ObstacleLogResponse { + private Long id; + private String label; + private Double confidence; + private String direction; + private String estimatedDist; + private Double lat; + private Double lng; + private LocalDateTime createdAt; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/PairingStatusResponse.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/PairingStatusResponse.java new file mode 100644 index 0000000..5ccd9c9 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/PairingStatusResponse.java @@ -0,0 +1,16 @@ +package com.walkguide.dto.response; +import lombok.Builder; +import lombok.Data; +import java.time.LocalDateTime; + +@Data +@Builder +public class PairingStatusResponse { + private Long pairingId; + private String status; // PENDING, ACTIVE, REJECTED, NONE + private String pairedWithName; // nama Guardian (untuk User) atau nama User (untuk Guardian) + private String pairedWithEmail; + private String uniqueUserId; // ID user yang di-pair + private LocalDateTime invitedAt; + private LocalDateTime respondedAt; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/SosEventResponse.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/SosEventResponse.java new file mode 100644 index 0000000..b88d176 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/SosEventResponse.java @@ -0,0 +1,16 @@ +package com.walkguide.dto.response; +import lombok.Builder; +import lombok.Data; +import java.time.LocalDateTime; + +@Data +@Builder +public class SosEventResponse { + private Long id; + private String triggerType; + private Double lat; + private Double lng; + private String status; + private LocalDateTime acknowledgedAt; + private LocalDateTime createdAt; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/UserSettingsResponse.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/UserSettingsResponse.java new file mode 100644 index 0000000..66ec695 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/UserSettingsResponse.java @@ -0,0 +1,14 @@ +package com.walkguide.dto.response; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class UserSettingsResponse { + private Long id; + private String ttsLanguage; + private Double ttsPitch; + private Double ttsSpeed; + private Boolean warnNoGuardian; + private Boolean hapticEnabled; +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/VoiceCommandResponse.java b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/VoiceCommandResponse.java new file mode 100644 index 0000000..59f586c --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/dto/response/VoiceCommandResponse.java @@ -0,0 +1,13 @@ +package com.walkguide.dto.response; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class VoiceCommandResponse { + private Long id; + private String commandKey; + private String triggerPhrase; + private Boolean enabled; + private String description; // human-readable description of what this command does +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/entity/ActivityLog.java b/walkguide-backend/demo/src/main/java/com/walkguide/entity/ActivityLog.java new file mode 100644 index 0000000..07fcddf --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/entity/ActivityLog.java @@ -0,0 +1,42 @@ +package com.walkguide.entity; + +import com.walkguide.enums.ActivityLogType; +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "activity_logs") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ActivityLog { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Enumerated(EnumType.STRING) + @Column(name = "log_type", nullable = false) + private ActivityLogType logType; + + @Column(columnDefinition = "TEXT") + private String description; + + // JSON string untuk metadata extra (lat/lng, obstacle label, dll) + @Column(columnDefinition = "TEXT") + private String metadata; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/entity/AiConfig.java b/walkguide-backend/demo/src/main/java/com/walkguide/entity/AiConfig.java new file mode 100644 index 0000000..92e4aa1 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/entity/AiConfig.java @@ -0,0 +1,53 @@ +package com.walkguide.entity; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "ai_configs") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiConfig { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "guardian_id") + private Long guardianId; + + @Column(name = "user_id", nullable = false, unique = true) + private Long userId; + + @Column(name = "confidence_threshold", nullable = false) + @Builder.Default + private Double confidenceThreshold = 0.5; + + @Column(name = "alert_distance_close", nullable = false) + @Builder.Default + private Double alertDistanceClose = 1.5; + + @Column(name = "alert_distance_medium", nullable = false) + @Builder.Default + private Double alertDistanceMedium = 3.0; + + @Column(name = "max_inference_fps", nullable = false) + @Builder.Default + private Integer maxInferenceFps = 5; + + @Column(name = "enabled_labels", nullable = false, columnDefinition = "TEXT") + @Builder.Default + private String enabledLabels = "ALL"; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @PrePersist + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/entity/GeofenceConfig.java b/walkguide-backend/demo/src/main/java/com/walkguide/entity/GeofenceConfig.java new file mode 100644 index 0000000..3f657b3 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/entity/GeofenceConfig.java @@ -0,0 +1,47 @@ +package com.walkguide.entity; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "geofence_configs") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GeofenceConfig { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "guardian_id") + private Long guardianId; + + @Column(name = "user_id", nullable = false, unique = true) + private Long userId; + + @Column(name = "center_lat") + private Double centerLat; + + @Column(name = "center_lng") + private Double centerLng; + + @Column(name = "radius_meters", nullable = false) + @Builder.Default + private Double radiusMeters = 500.0; + + @Column(nullable = false) + @Builder.Default + private Boolean enabled = false; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @PrePersist + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/entity/GuardianNotification.java b/walkguide-backend/demo/src/main/java/com/walkguide/entity/GuardianNotification.java new file mode 100644 index 0000000..a060a60 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/entity/GuardianNotification.java @@ -0,0 +1,54 @@ +package com.walkguide.entity; + +import com.walkguide.enums.NotificationType; +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "guardian_notifications") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GuardianNotification { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "guardian_id", nullable = false) + private Long guardianId; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Enumerated(EnumType.STRING) + @Column(name = "notif_type", nullable = false) + @Builder.Default + private NotificationType notifType = NotificationType.TEXT; + + @Column(columnDefinition = "TEXT") + private String content; + + @Column(name = "voice_note_url", length = 500) + private String voiceNoteUrl; + + @Column(name = "voice_note_duration") + private Integer voiceNoteDuration; + + @Column(name = "is_read", nullable = false) + @Builder.Default + private Boolean isRead = false; + + @Column(name = "read_at") + private LocalDateTime readAt; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/entity/HardwareShortcut.java b/walkguide-backend/demo/src/main/java/com/walkguide/entity/HardwareShortcut.java new file mode 100644 index 0000000..722c000 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/entity/HardwareShortcut.java @@ -0,0 +1,48 @@ +package com.walkguide.entity; + +import com.walkguide.enums.HardwareShortcutKey; +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "hardware_shortcuts") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class HardwareShortcut { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "guardian_id") + private Long guardianId; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Enumerated(EnumType.STRING) + @Column(name = "shortcut_key", nullable = false) + private HardwareShortcutKey shortcutKey; + + @Column(name = "button_name", length = 100) + private String buttonName; + + @Column(name = "button_code") + private Integer buttonCode; + + @Column(nullable = false) + @Builder.Default + private Boolean enabled = true; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @PrePersist + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/entity/LocationHistory.java b/walkguide-backend/demo/src/main/java/com/walkguide/entity/LocationHistory.java new file mode 100644 index 0000000..79d971d --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/entity/LocationHistory.java @@ -0,0 +1,39 @@ +package com.walkguide.entity; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "location_history") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LocationHistory { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(nullable = false) + private Double lat; + + @Column(nullable = false) + private Double lng; + + private Double accuracy; + private Double speed; // m/s + private Double heading; // derajat 0-360 + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/entity/ObstacleLog.java b/walkguide-backend/demo/src/main/java/com/walkguide/entity/ObstacleLog.java new file mode 100644 index 0000000..12ae01b --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/entity/ObstacleLog.java @@ -0,0 +1,44 @@ +package com.walkguide.entity; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "obstacle_logs") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ObstacleLog { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(nullable = false) + private String label; // 'person', 'car', 'motorcycle', dll + + @Column(nullable = false) + private Double confidence; + + @Column(nullable = false) + private String direction; // LEFT, CENTER, RIGHT + + @Column(name = "estimated_dist", nullable = false) + private String estimatedDist; // Very Close, Close, Medium, Far + + private Double lat; + private Double lng; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/entity/PairingRelation.java b/walkguide-backend/demo/src/main/java/com/walkguide/entity/PairingRelation.java new file mode 100644 index 0000000..181213f --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/entity/PairingRelation.java @@ -0,0 +1,43 @@ +package com.walkguide.entity; + +import com.walkguide.enums.PairingStatus; +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "pairing_relations") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PairingRelation { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "guardian_id", nullable = false) + private User guardian; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + @Builder.Default + private PairingStatus status = PairingStatus.PENDING; + + @Column(name = "invited_at", nullable = false, updatable = false) + private LocalDateTime invitedAt; + + @Column(name = "responded_at") + private LocalDateTime respondedAt; + + @PrePersist + protected void onCreate() { + invitedAt = LocalDateTime.now(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/entity/RefreshToken.java b/walkguide-backend/demo/src/main/java/com/walkguide/entity/RefreshToken.java new file mode 100644 index 0000000..25bd2c6 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/entity/RefreshToken.java @@ -0,0 +1,35 @@ +package com.walkguide.entity; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "refresh_tokens") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RefreshToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(nullable = false, unique = true, length = 500) + private String token; + + @Column(name = "expires_at", nullable = false) + private LocalDateTime expiresAt; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/entity/SosEvent.java b/walkguide-backend/demo/src/main/java/com/walkguide/entity/SosEvent.java new file mode 100644 index 0000000..2525dac --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/entity/SosEvent.java @@ -0,0 +1,45 @@ +package com.walkguide.entity; + +import com.walkguide.enums.SosStatus; +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "sos_events") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SosEvent { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(name = "trigger_type", nullable = false) + @Builder.Default + private String triggerType = "MANUAL"; // VOICE_COMMAND, BUTTON, MANUAL + + private Double lat; + private Double lng; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + @Builder.Default + private SosStatus status = SosStatus.TRIGGERED; + + @Column(name = "acknowledged_at") + private LocalDateTime acknowledgedAt; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/entity/User.java b/walkguide-backend/demo/src/main/java/com/walkguide/entity/User.java index 64d8160..50668c5 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/entity/User.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/entity/User.java @@ -1,22 +1,8 @@ package com.walkguide.entity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.PrePersist; -import jakarta.persistence.PreUpdate; -import jakarta.persistence.Table; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.Instant; +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; @Entity @Table(name = "users") @@ -37,27 +23,33 @@ public class User { private String password; @Column(nullable = false) - private String role; + private String role; // ROLE_GUARDIAN atau ROLE_USER - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "connected_to") - private User connectedTo; + // 12-char alphanumeric ID khusus untuk ROLE_USER (seperti Discord ID) + @Column(name = "unique_user_id", unique = true, length = 12) + private String uniqueUserId; + + @Column(name = "display_name", length = 100) + private String displayName; + + // Firebase Cloud Messaging token untuk push notification + @Column(name = "fcm_token", length = 500) + private String fcmToken; @Column(name = "created_at", nullable = false, updatable = false) - private Instant createdAt; + private LocalDateTime createdAt; @Column(name = "updated_at", nullable = false) - private Instant updatedAt; + private LocalDateTime updatedAt; @PrePersist - public void onCreate() { - Instant now = Instant.now(); - this.createdAt = now; - this.updatedAt = now; + protected void onCreate() { + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); } @PreUpdate - public void onUpdate() { - this.updatedAt = Instant.now(); + protected void onUpdate() { + updatedAt = LocalDateTime.now(); } } diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/entity/UserSettings.java b/walkguide-backend/demo/src/main/java/com/walkguide/entity/UserSettings.java new file mode 100644 index 0000000..b2efb31 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/entity/UserSettings.java @@ -0,0 +1,50 @@ +package com.walkguide.entity; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "user_settings") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserSettings { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_id", nullable = false, unique = true) + private Long userId; + + @Column(name = "tts_language", nullable = false) + @Builder.Default + private String ttsLanguage = "id-ID"; + + @Column(name = "tts_pitch", nullable = false) + @Builder.Default + private Double ttsPitch = 1.0; + + @Column(name = "tts_speed", nullable = false) + @Builder.Default + private Double ttsSpeed = 0.9; + + @Column(name = "warn_no_guardian", nullable = false) + @Builder.Default + private Boolean warnNoGuardian = true; + + @Column(name = "haptic_enabled", nullable = false) + @Builder.Default + private Boolean hapticEnabled = true; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @PrePersist + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/entity/VoiceCommandConfig.java b/walkguide-backend/demo/src/main/java/com/walkguide/entity/VoiceCommandConfig.java new file mode 100644 index 0000000..008a5e0 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/entity/VoiceCommandConfig.java @@ -0,0 +1,45 @@ +package com.walkguide.entity; + +import com.walkguide.enums.VoiceCommandKey; +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "voice_command_configs") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class VoiceCommandConfig { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "guardian_id") + private Long guardianId; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Enumerated(EnumType.STRING) + @Column(name = "command_key", nullable = false) + private VoiceCommandKey commandKey; + + @Column(name = "trigger_phrase", nullable = false, length = 200) + private String triggerPhrase; + + @Column(nullable = false) + @Builder.Default + private Boolean enabled = true; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @PrePersist + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/enums/ActivityLogType.java b/walkguide-backend/demo/src/main/java/com/walkguide/enums/ActivityLogType.java new file mode 100644 index 0000000..59c01ab --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/enums/ActivityLogType.java @@ -0,0 +1,12 @@ +package com.walkguide.enums; + +public enum ActivityLogType { + LOGIN, LOGOUT, APP_OPEN, APP_CLOSE, + WALKGUIDE_START, WALKGUIDE_STOP, + OBSTACLE_DETECTED, + CALL_INITIATED, CALL_ENDED, + SOS_TRIGGERED, SOS_ACKNOWLEDGED, + LOCATION_UPDATE, + GEOFENCE_EXIT, GEOFENCE_ENTER, + PAIRING_INVITE_SENT, PAIRING_ACCEPTED, PAIRING_REJECTED, PAIRING_DISSOLVED +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/enums/HardwareShortcutKey.java b/walkguide-backend/demo/src/main/java/com/walkguide/enums/HardwareShortcutKey.java new file mode 100644 index 0000000..06e6374 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/enums/HardwareShortcutKey.java @@ -0,0 +1,9 @@ +package com.walkguide.enums; + +public enum HardwareShortcutKey { + CALL_GUARDIAN, + START_WALKGUIDE, + SEND_SOS, + STOP_WALKGUIDE, + OPEN_NOTIFICATION +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/enums/NotificationType.java b/walkguide-backend/demo/src/main/java/com/walkguide/enums/NotificationType.java new file mode 100644 index 0000000..c3e50f4 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/enums/NotificationType.java @@ -0,0 +1,5 @@ +package com.walkguide.enums; + +public enum NotificationType { + TEXT, VOICE_NOTE +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/enums/PairingStatus.java b/walkguide-backend/demo/src/main/java/com/walkguide/enums/PairingStatus.java new file mode 100644 index 0000000..0b368dc --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/enums/PairingStatus.java @@ -0,0 +1,5 @@ +package com.walkguide.enums; + +public enum PairingStatus { + PENDING, ACTIVE, REJECTED +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/enums/SosStatus.java b/walkguide-backend/demo/src/main/java/com/walkguide/enums/SosStatus.java new file mode 100644 index 0000000..b851900 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/enums/SosStatus.java @@ -0,0 +1,5 @@ +package com.walkguide.enums; + +public enum SosStatus { + TRIGGERED, ACKNOWLEDGED, RESOLVED +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/enums/VoiceCommandKey.java b/walkguide-backend/demo/src/main/java/com/walkguide/enums/VoiceCommandKey.java new file mode 100644 index 0000000..4c12458 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/enums/VoiceCommandKey.java @@ -0,0 +1,18 @@ +package com.walkguide.enums; + +public enum VoiceCommandKey { + OPEN_WALKGUIDE, + START_WALKGUIDE, + STOP_WALKGUIDE, + CALL_GUARDIAN, + OPEN_NOTIFICATION, + READ_ALL_NOTIF, + OPEN_SOS, + SEND_SOS, + WHERE_AM_I, + OPEN_ACTIVITY, + OPEN_NAVIGATION, + OPEN_SETTINGS, + REPEAT_LAST, + STOP_TTS +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/exception/GlobalExceptionHandler.java b/walkguide-backend/demo/src/main/java/com/walkguide/exception/GlobalExceptionHandler.java index f4562a7..cb10c92 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/exception/GlobalExceptionHandler.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/exception/GlobalExceptionHandler.java @@ -1,27 +1,43 @@ package com.walkguide.exception; +import com.walkguide.dto.ApiResponse; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; -import com.walkguide.dto.ApiResponse; - @ControllerAdvice public class GlobalExceptionHandler { - // Nangkep error kalau login gagal / user gak ketemu - @ExceptionHandler(RuntimeException.class) - public ResponseEntity> handleRuntimeException(RuntimeException ex) { - ApiResponse response = new ApiResponse<>(false, "AUTH_ERROR", ex.getMessage()); - return ResponseEntity.status(401).body(response); + @ExceptionHandler(ResourceNotFoundException.class) + public ResponseEntity> handleNotFound(ResourceNotFoundException ex) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(ApiResponse.error("NOT_FOUND", ex.getMessage())); } - // Nangkep error dari Validasi (misal email format salah) - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handleValidationException(MethodArgumentNotValidException ex) { - String errorMsg = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage(); - ApiResponse response = new ApiResponse<>(false, "VALIDATION_ERROR", errorMsg); - return ResponseEntity.status(400).body(response); + @ExceptionHandler(PairingException.class) + public ResponseEntity> handlePairing(PairingException ex) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(ApiResponse.error("PAIRING_ERROR", ex.getMessage())); } -} \ No newline at end of file + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidation(MethodArgumentNotValidException ex) { + String msg = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage(); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(ApiResponse.error("VALIDATION_ERROR", msg)); + } + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity> handleRuntime(RuntimeException ex) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(ApiResponse.error("AUTH_ERROR", ex.getMessage())); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleGeneric(Exception ex) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error("INTERNAL_ERROR", "Terjadi kesalahan internal")); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/exception/PairingException.java b/walkguide-backend/demo/src/main/java/com/walkguide/exception/PairingException.java new file mode 100644 index 0000000..a5caf08 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/exception/PairingException.java @@ -0,0 +1,5 @@ +package com.walkguide.exception; + +public class PairingException extends RuntimeException { + public PairingException(String message) { super(message); } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/exception/ResourceNotFoundException.java b/walkguide-backend/demo/src/main/java/com/walkguide/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..4f1c5e8 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/exception/ResourceNotFoundException.java @@ -0,0 +1,7 @@ +package com.walkguide.exception; + +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException(String message) { + super(message); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/repository/ActivityLogRepository.java b/walkguide-backend/demo/src/main/java/com/walkguide/repository/ActivityLogRepository.java new file mode 100644 index 0000000..1b67b68 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/repository/ActivityLogRepository.java @@ -0,0 +1,17 @@ +package com.walkguide.repository; + +import com.walkguide.entity.ActivityLog; +import com.walkguide.enums.ActivityLogType; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public interface ActivityLogRepository extends JpaRepository { + Page findByUser_IdOrderByCreatedAtDesc(Long userId, Pageable pageable); + List findByUser_IdAndCreatedAtBetween(Long userId, LocalDateTime from, LocalDateTime to); + List findByUser_IdAndLogTypeOrderByCreatedAtDesc(Long userId, ActivityLogType logType, Pageable pageable); +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/repository/AiConfigRepository.java b/walkguide-backend/demo/src/main/java/com/walkguide/repository/AiConfigRepository.java new file mode 100644 index 0000000..f0b1532 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/repository/AiConfigRepository.java @@ -0,0 +1,9 @@ +package com.walkguide.repository; +import com.walkguide.entity.AiConfig; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.Optional; +@Repository +public interface AiConfigRepository extends JpaRepository { + Optional findByUserId(Long userId); +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/repository/GeofenceConfigRepository.java b/walkguide-backend/demo/src/main/java/com/walkguide/repository/GeofenceConfigRepository.java new file mode 100644 index 0000000..b381b5d --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/repository/GeofenceConfigRepository.java @@ -0,0 +1,9 @@ +package com.walkguide.repository; +import com.walkguide.entity.GeofenceConfig; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.Optional; +@Repository +public interface GeofenceConfigRepository extends JpaRepository { + Optional findByUserId(Long userId); +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/repository/GuardianNotificationRepository.java b/walkguide-backend/demo/src/main/java/com/walkguide/repository/GuardianNotificationRepository.java new file mode 100644 index 0000000..5acdb49 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/repository/GuardianNotificationRepository.java @@ -0,0 +1,13 @@ +package com.walkguide.repository; +import com.walkguide.entity.GuardianNotification; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.List; +@Repository +public interface GuardianNotificationRepository extends JpaRepository { + Page findByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable); + long countByUserIdAndIsReadFalse(Long userId); + List findByUserIdAndIsReadFalseOrderByCreatedAtAsc(Long userId); +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/repository/HardwareShortcutRepository.java b/walkguide-backend/demo/src/main/java/com/walkguide/repository/HardwareShortcutRepository.java new file mode 100644 index 0000000..3313caf --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/repository/HardwareShortcutRepository.java @@ -0,0 +1,10 @@ +package com.walkguide.repository; +import com.walkguide.entity.HardwareShortcut; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.List; +@Repository +public interface HardwareShortcutRepository extends JpaRepository { + List findByUserId(Long userId); + void deleteByUserId(Long userId); +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/repository/LocationHistoryRepository.java b/walkguide-backend/demo/src/main/java/com/walkguide/repository/LocationHistoryRepository.java new file mode 100644 index 0000000..c7ec7e9 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/repository/LocationHistoryRepository.java @@ -0,0 +1,12 @@ +package com.walkguide.repository; +import com.walkguide.entity.LocationHistory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.Optional; +@Repository +public interface LocationHistoryRepository extends JpaRepository { + Optional findTopByUserIdOrderByCreatedAtDesc(Long userId); + Page findByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable); +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/repository/ObstacleLogRepository.java b/walkguide-backend/demo/src/main/java/com/walkguide/repository/ObstacleLogRepository.java new file mode 100644 index 0000000..c8c9b74 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/repository/ObstacleLogRepository.java @@ -0,0 +1,10 @@ +package com.walkguide.repository; +import com.walkguide.entity.ObstacleLog; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +@Repository +public interface ObstacleLogRepository extends JpaRepository { + Page findByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable); +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/repository/PairingRelationRepository.java b/walkguide-backend/demo/src/main/java/com/walkguide/repository/PairingRelationRepository.java new file mode 100644 index 0000000..5024ca7 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/repository/PairingRelationRepository.java @@ -0,0 +1,17 @@ +package com.walkguide.repository; + +import com.walkguide.entity.PairingRelation; +import com.walkguide.enums.PairingStatus; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.Optional; + +@Repository +public interface PairingRelationRepository extends JpaRepository { + Optional findByGuardian_IdAndStatus(Long guardianId, PairingStatus status); + Optional findByUser_IdAndStatus(Long userId, PairingStatus status); + Optional findByGuardian_Id(Long guardianId); + Optional findByUser_Id(Long userId); + boolean existsByGuardian_IdAndStatus(Long guardianId, PairingStatus status); + boolean existsByUser_IdAndStatus(Long userId, PairingStatus status); +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/repository/RefreshTokenRepository.java b/walkguide-backend/demo/src/main/java/com/walkguide/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..5bebe82 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/repository/RefreshTokenRepository.java @@ -0,0 +1,10 @@ +package com.walkguide.repository; +import com.walkguide.entity.RefreshToken; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.Optional; +@Repository +public interface RefreshTokenRepository extends JpaRepository { + Optional findByToken(String token); + void deleteByUserId(Long userId); +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/repository/SosEventRepository.java b/walkguide-backend/demo/src/main/java/com/walkguide/repository/SosEventRepository.java new file mode 100644 index 0000000..ffd5080 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/repository/SosEventRepository.java @@ -0,0 +1,10 @@ +package com.walkguide.repository; +import com.walkguide.entity.SosEvent; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +@Repository +public interface SosEventRepository extends JpaRepository { + Page findByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable); +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/repository/UserRepository.java b/walkguide-backend/demo/src/main/java/com/walkguide/repository/UserRepository.java index ce8d655..7c64264 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/repository/UserRepository.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/repository/UserRepository.java @@ -2,20 +2,12 @@ package com.walkguide.repository; import com.walkguide.entity.User; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; - import java.util.Optional; @Repository public interface UserRepository extends JpaRepository { - Optional findByEmail(String email); - - @Query("SELECT u FROM User u WHERE u.connectedTo.id = :guardianId AND u.role = 'ROLE_USER'") - Optional findUserByGuardianId(@Param("guardianId") Long guardianId); - - @Query("SELECT u FROM User u WHERE u.connectedTo.id = :userId AND u.role = 'ROLE_GUARDIAN'") - Optional findGuardianByUserId(@Param("userId") Long userId); + Optional findByUniqueUserId(String uniqueUserId); + boolean existsByEmail(String email); } diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/repository/UserSettingsRepository.java b/walkguide-backend/demo/src/main/java/com/walkguide/repository/UserSettingsRepository.java new file mode 100644 index 0000000..f72ebca --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/repository/UserSettingsRepository.java @@ -0,0 +1,9 @@ +package com.walkguide.repository; +import com.walkguide.entity.UserSettings; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.Optional; +@Repository +public interface UserSettingsRepository extends JpaRepository { + Optional findByUserId(Long userId); +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/repository/VoiceCommandConfigRepository.java b/walkguide-backend/demo/src/main/java/com/walkguide/repository/VoiceCommandConfigRepository.java new file mode 100644 index 0000000..907150c --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/repository/VoiceCommandConfigRepository.java @@ -0,0 +1,13 @@ +package com.walkguide.repository; +import com.walkguide.entity.VoiceCommandConfig; +import com.walkguide.enums.VoiceCommandKey; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Optional; +@Repository +public interface VoiceCommandConfigRepository extends JpaRepository { + List findByUserId(Long userId); + Optional findByUserIdAndCommandKey(Long userId, VoiceCommandKey commandKey); + void deleteByUserId(Long userId); +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/security/JwtAuthFilter.java b/walkguide-backend/demo/src/main/java/com/walkguide/security/JwtAuthFilter.java index 64dd69c..26c0510 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/security/JwtAuthFilter.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/security/JwtAuthFilter.java @@ -21,37 +21,40 @@ public class JwtAuthFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { - + final String authHeader = request.getHeader("Authorization"); - // Kalau gak ada token, lewatin aja (biar dicegat sama SecurityConfig) if (authHeader == null || !authHeader.startsWith("Bearer ")) { filterChain.doFilter(request, response); return; } - // Potong tulisan "Bearer " final String jwt = authHeader.substring(7); - - try { - // Ambil email & role dari token lu - String email = jwtUtil.extractUsername(jwt); - String role = jwtUtil.extractRole(jwt); // Pastiin JwtUtil lu punya fungsi extractRole! - // Daftarin user ini ke sistem keamanan Spring - if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( - email, null, Collections.singletonList(new SimpleGrantedAuthority(role)) - ); - SecurityContextHolder.getContext().setAuthentication(authToken); + try { + if (jwtUtil.isTokenValid(jwt)) { + String email = jwtUtil.extractUsername(jwt); + String role = jwtUtil.extractRole(jwt); + Long userId = jwtUtil.extractUserId(jwt); + + if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UsernamePasswordAuthenticationToken authToken = + new UsernamePasswordAuthenticationToken( + email, + userId, // credentials slot dipakai untuk simpan userId + Collections.singletonList(new SimpleGrantedAuthority(role)) + ); + SecurityContextHolder.getContext().setAuthentication(authToken); + } } } catch (Exception e) { - // Token kadaluarsa / rusak - System.out.println("JWT Error: " + e.getMessage()); + logger.warn("JWT processing error: " + e.getMessage()); } filterChain.doFilter(request, response); } -} \ No newline at end of file +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/security/JwtUtil.java b/walkguide-backend/demo/src/main/java/com/walkguide/security/JwtUtil.java index f7dc924..031b6ea 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/security/JwtUtil.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/security/JwtUtil.java @@ -1,39 +1,76 @@ package com.walkguide.security; -import java.security.Key; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; - -import org.springframework.stereotype.Component; - import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; @Component public class JwtUtil { - // Kunci rahasia buat enkripsi & dekripsi token (Minimal 256-bit) - private static final String SECRET_KEY = "404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970"; + @Value("${jwt.secret}") + private String secretKey; + + // Access token berlaku 1 jam + private static final long ACCESS_TOKEN_VALIDITY_MS = 1000L * 60 * 60; + + public String generateAccessToken(String email, String role, Long userId) { + Map claims = new HashMap<>(); + claims.put("role", role); + claims.put("userId", userId); + return Jwts.builder() + .setClaims(claims) + .setSubject(email) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_VALIDITY_MS)) + .signWith(getSignInKey(), SignatureAlgorithm.HS256) + .compact(); + } + + // Refresh token: UUID random, expiry dikelola di DB + public String generateRefreshToken() { + return UUID.randomUUID().toString() + "-" + UUID.randomUUID().toString(); + } - // Fungsi tambahan buat ngebongkar Email (Username) public String extractUsername(String token) { return extractClaim(token, Claims::getSubject); } - // Fungsi tambahan buat ngebongkar Role public String extractRole(String token) { - Claims claims = extractAllClaims(token); - return claims.get("role", String.class); + return extractAllClaims(token).get("role", String.class); } - public T extractClaim(String token, Function claimsResolver) { - final Claims claims = extractAllClaims(token); - return claimsResolver.apply(claims); + public Long extractUserId(String token) { + Object userId = extractAllClaims(token).get("userId"); + if (userId instanceof Integer) return ((Integer) userId).longValue(); + if (userId instanceof Long) return (Long) userId; + return null; + } + + public boolean isTokenValid(String token) { + try { + return !extractExpiration(token).before(new Date()); + } catch (Exception e) { + return false; + } + } + + private Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + public T extractClaim(String token, Function resolver) { + return resolver.apply(extractAllClaims(token)); } private Claims extractAllClaims(String token) { @@ -45,24 +82,7 @@ public class JwtUtil { } private Key getSignInKey() { - byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY); + byte[] keyBytes = Decoders.BASE64.decode(secretKey); return Keys.hmacShaKeyFor(keyBytes); } - - // Fungsi lama lu buat bikin token - public String generateToken(String email, String role) { - Map claims = new HashMap<>(); - claims.put("role", role); - return createToken(claims, email); - } - - private String createToken(Map claims, String subject) { - return Jwts.builder() - .setClaims(claims) - .setSubject(subject) - .setIssuedAt(new Date(System.currentTimeMillis())) - .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // Aktif 10 jam - .signWith(getSignInKey(), SignatureAlgorithm.HS256) - .compact(); - } -} \ No newline at end of file +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/security/SecurityHelper.java b/walkguide-backend/demo/src/main/java/com/walkguide/security/SecurityHelper.java new file mode 100644 index 0000000..00e4c5e --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/security/SecurityHelper.java @@ -0,0 +1,27 @@ +package com.walkguide.security; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; + +/** + * Helper untuk ambil userId dari SecurityContext di mana pun di dalam request. + * userId disimpan di credentials slot oleh JwtAuthFilter. + */ +public class SecurityHelper { + + public static Long getCurrentUserId() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth instanceof UsernamePasswordAuthenticationToken token) { + Object credentials = token.getCredentials(); + if (credentials instanceof Long) return (Long) credentials; + if (credentials instanceof Integer) return ((Integer) credentials).longValue(); + } + return null; + } + + public static String getCurrentUserEmail() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + return auth != null ? auth.getName() : null; + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/service/ActivityLogService.java b/walkguide-backend/demo/src/main/java/com/walkguide/service/ActivityLogService.java new file mode 100644 index 0000000..a61319f --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/service/ActivityLogService.java @@ -0,0 +1,57 @@ +package com.walkguide.service; + +import com.walkguide.dto.response.ActivityLogResponse; +import com.walkguide.entity.ActivityLog; +import com.walkguide.entity.User; +import com.walkguide.enums.ActivityLogType; +import com.walkguide.exception.ResourceNotFoundException; +import com.walkguide.repository.ActivityLogRepository; +import com.walkguide.repository.PairingRelationRepository; +import com.walkguide.enums.PairingStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ActivityLogService { + + private final ActivityLogRepository activityLogRepository; + private final PairingRelationRepository pairingRelationRepository; + + public void createLog(User user, ActivityLogType type, String description, String metadata) { + ActivityLog log = ActivityLog.builder() + .user(user) + .logType(type) + .description(description) + .metadata(metadata) + .build(); + activityLogRepository.save(log); + } + + public Page getLogs(Long userId, Pageable pageable) { + return activityLogRepository + .findByUser_IdOrderByCreatedAtDesc(userId, pageable) + .map(this::toResponse); + } + + public Page getLogsForGuardian(Long guardianId, Pageable pageable) { + var pairing = pairingRelationRepository + .findByGuardian_IdAndStatus(guardianId, PairingStatus.ACTIVE) + .orElseThrow(() -> new ResourceNotFoundException("Belum ada user yang dipair")); + return activityLogRepository + .findByUser_IdOrderByCreatedAtDesc(pairing.getUser().getId(), pageable) + .map(this::toResponse); + } + + private ActivityLogResponse toResponse(ActivityLog log) { + return ActivityLogResponse.builder() + .id(log.getId()) + .logType(log.getLogType().name()) + .description(log.getDescription()) + .metadata(log.getMetadata()) + .createdAt(log.getCreatedAt()) + .build(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/service/AiConfigService.java b/walkguide-backend/demo/src/main/java/com/walkguide/service/AiConfigService.java new file mode 100644 index 0000000..b3c8545 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/service/AiConfigService.java @@ -0,0 +1,63 @@ +package com.walkguide.service; + +import com.walkguide.dto.request.AiConfigUpdateRequest; +import com.walkguide.dto.response.AiConfigResponse; +import com.walkguide.entity.AiConfig; +import com.walkguide.enums.PairingStatus; +import com.walkguide.exception.PairingException; +import com.walkguide.exception.ResourceNotFoundException; +import com.walkguide.repository.AiConfigRepository; +import com.walkguide.repository.PairingRelationRepository; +import com.walkguide.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AiConfigService { + + private final AiConfigRepository aiConfigRepository; + private final PairingRelationRepository pairingRelationRepository; + private final FcmService fcmService; + + public AiConfigResponse getConfig(Long userId) { + AiConfig cfg = aiConfigRepository.findByUserId(userId) + .orElseGet(() -> aiConfigRepository.save(AiConfig.builder().userId(userId).build())); + return toResponse(cfg); + } + + public AiConfigResponse updateConfigByGuardian(Long guardianId, AiConfigUpdateRequest req) { + var pairing = pairingRelationRepository + .findByGuardian_IdAndStatus(guardianId, PairingStatus.ACTIVE) + .orElseThrow(() -> new PairingException("Tidak ada user yang dipair")); + Long userId = pairing.getUser().getId(); + + AiConfig cfg = aiConfigRepository.findByUserId(userId) + .orElseGet(() -> aiConfigRepository.save(AiConfig.builder().userId(userId).guardianId(guardianId).build())); + + if (req.getConfidenceThreshold() != null) cfg.setConfidenceThreshold(req.getConfidenceThreshold()); + if (req.getAlertDistanceClose() != null) cfg.setAlertDistanceClose(req.getAlertDistanceClose()); + if (req.getAlertDistanceMedium() != null) cfg.setAlertDistanceMedium(req.getAlertDistanceMedium()); + if (req.getMaxInferenceFps() != null) cfg.setMaxInferenceFps(req.getMaxInferenceFps()); + if (req.getEnabledLabels() != null) cfg.setEnabledLabels(req.getEnabledLabels()); + + cfg = aiConfigRepository.save(cfg); + + // Beritahu user settings telah diupdate + String userFcm = pairing.getUser().getFcmToken(); + fcmService.sendToToken(userFcm, "Pengaturan AI Diperbarui", + "Guardian mengubah konfigurasi deteksi AI", + java.util.Map.of("type", "SETTINGS_UPDATED", "settingType", "AI_CONFIG")); + + return toResponse(cfg); + } + + private AiConfigResponse toResponse(AiConfig c) { + return AiConfigResponse.builder() + .id(c.getId()).confidenceThreshold(c.getConfidenceThreshold()) + .alertDistanceClose(c.getAlertDistanceClose()) + .alertDistanceMedium(c.getAlertDistanceMedium()) + .maxInferenceFps(c.getMaxInferenceFps()) + .enabledLabels(c.getEnabledLabels()).build(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/service/AuthService.java b/walkguide-backend/demo/src/main/java/com/walkguide/service/AuthService.java index bb8ef9b..6e99612 100644 --- a/walkguide-backend/demo/src/main/java/com/walkguide/service/AuthService.java +++ b/walkguide-backend/demo/src/main/java/com/walkguide/service/AuthService.java @@ -1,48 +1,165 @@ package com.walkguide.service; -import java.util.HashMap; -import java.util.Map; - -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import com.walkguide.dto.request.LoginRequest; +import com.walkguide.dto.request.RegisterRequest; +import com.walkguide.dto.response.AuthDataResponse; +import com.walkguide.entity.RefreshToken; +import com.walkguide.entity.User; +import com.walkguide.entity.UserSettings; +import com.walkguide.enums.ActivityLogType; +import com.walkguide.repository.RefreshTokenRepository; +import com.walkguide.repository.UserRepository; +import com.walkguide.repository.UserSettingsRepository; +import com.walkguide.security.JwtUtil; +import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import com.walkguide.dto.AuthRequest; -import com.walkguide.entity.User; -import com.walkguide.repository.UserRepository; -import com.walkguide.security.JwtUtil; - -import lombok.RequiredArgsConstructor; +import java.security.SecureRandom; +import java.time.LocalDateTime; @Service @RequiredArgsConstructor public class AuthService { private final UserRepository userRepository; + private final RefreshTokenRepository refreshTokenRepository; + private final UserSettingsRepository userSettingsRepository; + private final ActivityLogService activityLogService; private final JwtUtil jwtUtil; - - // Wajib panggil BCrypt biar bisa baca password enkripsi dari database - private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + private final PasswordEncoder passwordEncoder; - public Map login(AuthRequest request) { - // 1. Cari user di database - User user = userRepository.findByEmail(request.getEmail()) + private static final String CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + private static final int ID_LENGTH = 12; + + @Transactional + public AuthDataResponse register(RegisterRequest req) { + if (userRepository.existsByEmail(req.getEmail())) { + throw new RuntimeException("Email sudah terdaftar"); + } + + String role = "GUARDIAN".equalsIgnoreCase(req.getRole()) + ? "ROLE_GUARDIAN" : "ROLE_USER"; + + String uniqueUserId = null; + if ("ROLE_USER".equals(role)) { + uniqueUserId = generateUniqueUserId(); + } + + User user = User.builder() + .email(req.getEmail()) + .password(passwordEncoder.encode(req.getPassword())) + .role(role) + .displayName(req.getDisplayName()) + .uniqueUserId(uniqueUserId) + .build(); + + user = userRepository.save(user); + + // Buat default settings untuk user baru + if ("ROLE_USER".equals(role)) { + userSettingsRepository.save(UserSettings.builder() + .userId(user.getId()) + .build()); + } + + return buildAuthResponse(user); + } + + @Transactional + public AuthDataResponse login(LoginRequest req) { + User user = userRepository.findByEmail(req.getEmail()) .orElseThrow(() -> new RuntimeException("Email tidak terdaftar")); - // 2. Cocokin password (yang diketik VS yang dienkripsi di database) - if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { + if (!passwordEncoder.matches(req.getPassword(), user.getPassword())) { throw new RuntimeException("Password salah"); } - // 3. Kalau bener, bikin Token - // Asumsi JwtUtil lu nerima parameter (email, role). Sesuaikan kalau beda! - String token = jwtUtil.generateToken(user.getEmail(), user.getRole()); + // Hapus refresh token lama + refreshTokenRepository.deleteByUserId(user.getId()); - // 4. Balikin ke Controller dalam bentuk Map - Map data = new HashMap<>(); - data.put("token", token); - data.put("role", user.getRole()); + activityLogService.createLog(user, ActivityLogType.LOGIN, "User login", null); - return data; + return buildAuthResponse(user); } -} \ No newline at end of file + + @Transactional + public AuthDataResponse refreshToken(String token) { + RefreshToken refreshToken = refreshTokenRepository.findByToken(token) + .orElseThrow(() -> new RuntimeException("Refresh token tidak valid")); + + if (refreshToken.getExpiresAt().isBefore(LocalDateTime.now())) { + refreshTokenRepository.delete(refreshToken); + throw new RuntimeException("Refresh token sudah kadaluarsa, silakan login ulang"); + } + + User user = userRepository.findById(refreshToken.getUserId()) + .orElseThrow(() -> new RuntimeException("User tidak ditemukan")); + + String newAccessToken = jwtUtil.generateAccessToken( + user.getEmail(), user.getRole(), user.getId()); + + return AuthDataResponse.builder() + .accessToken(newAccessToken) + .refreshToken(token) // refresh token tetap sama + .role(user.getRole()) + .userId(user.getId()) + .displayName(user.getDisplayName()) + .uniqueUserId(user.getUniqueUserId()) + .build(); + } + + @Transactional + public void logout(Long userId) { + refreshTokenRepository.deleteByUserId(userId); + userRepository.findById(userId).ifPresent(user -> + activityLogService.createLog(user, ActivityLogType.LOGOUT, "User logout", null)); + } + + @Transactional + public void updateFcmToken(Long userId, String fcmToken) { + userRepository.findById(userId).ifPresent(user -> { + user.setFcmToken(fcmToken); + userRepository.save(user); + }); + } + + // ========== PRIVATE HELPERS ========== + + private AuthDataResponse buildAuthResponse(User user) { + String accessToken = jwtUtil.generateAccessToken( + user.getEmail(), user.getRole(), user.getId()); + String refreshTokenStr = jwtUtil.generateRefreshToken(); + + // Simpan refresh token ke DB (berlaku 30 hari) + refreshTokenRepository.save(RefreshToken.builder() + .userId(user.getId()) + .token(refreshTokenStr) + .expiresAt(LocalDateTime.now().plusDays(30)) + .build()); + + return AuthDataResponse.builder() + .accessToken(accessToken) + .refreshToken(refreshTokenStr) + .role(user.getRole()) + .userId(user.getId()) + .displayName(user.getDisplayName()) + .uniqueUserId(user.getUniqueUserId()) + .build(); + } + + private String generateUniqueUserId() { + SecureRandom random = new SecureRandom(); + StringBuilder sb = new StringBuilder(ID_LENGTH); + String candidate; + do { + sb.setLength(0); + for (int i = 0; i < ID_LENGTH; i++) { + sb.append(CHARS.charAt(random.nextInt(CHARS.length()))); + } + candidate = sb.toString(); + } while (userRepository.findByUniqueUserId(candidate).isPresent()); + return candidate; + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/service/FcmService.java b/walkguide-backend/demo/src/main/java/com/walkguide/service/FcmService.java new file mode 100644 index 0000000..466a707 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/service/FcmService.java @@ -0,0 +1,50 @@ +package com.walkguide.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Map; + +/** + * FCM Service untuk push notification. + * Saat ini dalam mode LOG-ONLY agar tidak butuh Firebase credentials dulu. + * Untuk enable FCM nyata: uncomment bagian Firebase dan tambah dependency Firebase Admin SDK. + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class FcmService { + + public void sendToToken(String fcmToken, String title, String body, Map data) { + if (fcmToken == null || fcmToken.isBlank()) { + log.warn("[FCM] Token kosong, skip notifikasi: {} - {}", title, body); + return; + } + // LOG ONLY untuk sekarang + log.info("[FCM] TO={} | TITLE={} | BODY={} | DATA={}", fcmToken, title, body, data); + + // TODO: uncomment ini setelah tambah Firebase Admin SDK ke pom.xml + // dan taruh google-services-admin.json di src/main/resources/firebase/ + // + // try { + // Message message = Message.builder() + // .setToken(fcmToken) + // .setNotification(Notification.builder().setTitle(title).setBody(body).build()) + // .putAllData(data != null ? data : Map.of()) + // .setAndroidConfig(AndroidConfig.builder() + // .setPriority(AndroidConfig.Priority.HIGH) + // .build()) + // .build(); + // String response = FirebaseMessaging.getInstance().send(message); + // log.info("[FCM] Sent successfully: {}", response); + // } catch (FirebaseMessagingException e) { + // log.error("[FCM] Failed to send: {}", e.getMessage()); + // } + } + + public void sendHighPriority(String fcmToken, String title, String body, Map data) { + // SOS dan incoming call pakai ini - sama untuk sekarang + sendToToken(fcmToken, title, body, data); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/service/GeofenceService.java b/walkguide-backend/demo/src/main/java/com/walkguide/service/GeofenceService.java new file mode 100644 index 0000000..bd07591 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/service/GeofenceService.java @@ -0,0 +1,49 @@ +package com.walkguide.service; + +import com.walkguide.dto.request.GeofenceConfigRequest; +import com.walkguide.dto.response.GeofenceResponse; +import com.walkguide.entity.GeofenceConfig; +import com.walkguide.enums.PairingStatus; +import com.walkguide.exception.PairingException; +import com.walkguide.repository.GeofenceConfigRepository; +import com.walkguide.repository.PairingRelationRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class GeofenceService { + + private final GeofenceConfigRepository geofenceConfigRepository; + private final PairingRelationRepository pairingRelationRepository; + + public GeofenceResponse getConfig(Long userId) { + return geofenceConfigRepository.findByUserId(userId) + .map(this::toResponse) + .orElse(GeofenceResponse.builder().enabled(false).radiusMeters(500.0).build()); + } + + public GeofenceResponse updateConfig(Long guardianId, GeofenceConfigRequest req) { + var pairing = pairingRelationRepository + .findByGuardian_IdAndStatus(guardianId, PairingStatus.ACTIVE) + .orElseThrow(() -> new PairingException("Tidak ada user yang dipair")); + Long userId = pairing.getUser().getId(); + + GeofenceConfig cfg = geofenceConfigRepository.findByUserId(userId) + .orElseGet(() -> GeofenceConfig.builder() + .userId(userId).guardianId(guardianId).build()); + + if (req.getCenterLat() != null) cfg.setCenterLat(req.getCenterLat()); + if (req.getCenterLng() != null) cfg.setCenterLng(req.getCenterLng()); + if (req.getRadiusMeters() != null) cfg.setRadiusMeters(req.getRadiusMeters()); + if (req.getEnabled() != null) cfg.setEnabled(req.getEnabled()); + + return toResponse(geofenceConfigRepository.save(cfg)); + } + + private GeofenceResponse toResponse(GeofenceConfig c) { + return GeofenceResponse.builder().id(c.getId()) + .centerLat(c.getCenterLat()).centerLng(c.getCenterLng()) + .radiusMeters(c.getRadiusMeters()).enabled(c.getEnabled()).build(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/service/GuardianDashboardService.java b/walkguide-backend/demo/src/main/java/com/walkguide/service/GuardianDashboardService.java new file mode 100644 index 0000000..4344538 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/service/GuardianDashboardService.java @@ -0,0 +1,55 @@ +package com.walkguide.service; + +import com.walkguide.dto.response.DashboardResponse; +import com.walkguide.enums.PairingStatus; +import com.walkguide.exception.ResourceNotFoundException; +import com.walkguide.repository.*; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class GuardianDashboardService { + + private final PairingRelationRepository pairingRelationRepository; + private final LocationService locationService; + private final ActivityLogService activityLogService; + private final SosEventRepository sosEventRepository; + private final GuardianNotificationRepository notifRepository; + + public DashboardResponse getDashboard(Long guardianId) { + var pairing = pairingRelationRepository + .findByGuardian_IdAndStatus(guardianId, PairingStatus.ACTIVE) + .orElse(null); + + if (pairing == null) { + return DashboardResponse.builder() + .recentActivity(java.util.List.of()) + .unreadSosCount(0).unreadNotifCount(0).build(); + } + + var user = pairing.getUser(); + Long userId = user.getId(); + + var lastLocation = locationService.getLastLocation(userId).orElse(null); + var recentActivity = activityLogService.getLogs(userId, PageRequest.of(0, 5)) + .getContent(); + + // Count unresolved SOS + long unreadSos = sosEventRepository.findByUserIdOrderByCreatedAtDesc(userId, PageRequest.of(0, 100)) + .stream().filter(s -> s.getStatus().name().equals("TRIGGERED")).count(); + long unreadNotif = notifRepository.countByUserIdAndIsReadFalse(userId); + + return DashboardResponse.builder() + .pairedUserId(userId) + .pairedUserName(user.getDisplayName()) + .pairedUserEmail(user.getEmail()) + .uniqueUserId(user.getUniqueUserId()) + .lastLocation(lastLocation) + .unreadSosCount(unreadSos) + .unreadNotifCount(unreadNotif) + .recentActivity(recentActivity) + .build(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/service/HardwareShortcutService.java b/walkguide-backend/demo/src/main/java/com/walkguide/service/HardwareShortcutService.java new file mode 100644 index 0000000..30c624c --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/service/HardwareShortcutService.java @@ -0,0 +1,46 @@ +package com.walkguide.service; + +import com.walkguide.dto.request.HardwareShortcutUpdateRequest; +import com.walkguide.dto.response.HardwareShortcutResponse; +import com.walkguide.enums.HardwareShortcutKey; +import com.walkguide.exception.ResourceNotFoundException; +import com.walkguide.repository.HardwareShortcutRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class HardwareShortcutService { + + private final HardwareShortcutRepository hardwareShortcutRepository; + + public List getAll(Long userId) { + return hardwareShortcutRepository.findByUserId(userId).stream() + .map(s -> HardwareShortcutResponse.builder() + .id(s.getId()).shortcutKey(s.getShortcutKey().name()) + .buttonName(s.getButtonName()).buttonCode(s.getButtonCode()) + .enabled(s.getEnabled()).build()) + .collect(Collectors.toList()); + } + + // Bisa dipanggil dari HP User langsung (capture button) atau dari Guardian + public HardwareShortcutResponse update(Long userId, HardwareShortcutUpdateRequest req) { + HardwareShortcutKey key = HardwareShortcutKey.valueOf(req.getShortcutKey()); + var shortcut = hardwareShortcutRepository.findByUserId(userId).stream() + .filter(s -> s.getShortcutKey() == key) + .findFirst() + .orElseThrow(() -> new ResourceNotFoundException("Shortcut tidak ditemukan")); + + if (req.getButtonName() != null) shortcut.setButtonName(req.getButtonName()); + if (req.getButtonCode() != null) shortcut.setButtonCode(req.getButtonCode()); + if (req.getEnabled() != null) shortcut.setEnabled(req.getEnabled()); + + shortcut = hardwareShortcutRepository.save(shortcut); + return HardwareShortcutResponse.builder() + .id(shortcut.getId()).shortcutKey(shortcut.getShortcutKey().name()) + .buttonName(shortcut.getButtonName()).buttonCode(shortcut.getButtonCode()) + .enabled(shortcut.getEnabled()).build(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/service/LocationService.java b/walkguide-backend/demo/src/main/java/com/walkguide/service/LocationService.java new file mode 100644 index 0000000..6bf3e6c --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/service/LocationService.java @@ -0,0 +1,119 @@ +package com.walkguide.service; + +import com.walkguide.dto.request.LocationUpdateRequest; +import com.walkguide.dto.response.LocationResponse; +import com.walkguide.entity.LocationHistory; +import com.walkguide.entity.User; +import com.walkguide.enums.ActivityLogType; +import com.walkguide.exception.ResourceNotFoundException; +import com.walkguide.repository.*; +import com.walkguide.enums.PairingStatus; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Slf4j +public class LocationService { + + private final LocationHistoryRepository locationHistoryRepository; + private final GeofenceConfigRepository geofenceConfigRepository; + private final PairingRelationRepository pairingRelationRepository; + private final UserRepository userRepository; + private final ActivityLogService activityLogService; + private final FcmService fcmService; + + public LocationResponse updateLocation(Long userId, LocationUpdateRequest req) { + LocationHistory loc = LocationHistory.builder() + .userId(userId) + .lat(req.getLat()) + .lng(req.getLng()) + .accuracy(req.getAccuracy()) + .speed(req.getSpeed()) + .heading(req.getHeading()) + .build(); + loc = locationHistoryRepository.save(loc); + + // Cek geofence + checkGeofence(userId, req.getLat(), req.getLng()); + + return toResponse(loc); + } + + public Optional getLastLocation(Long userId) { + return locationHistoryRepository + .findTopByUserIdOrderByCreatedAtDesc(userId) + .map(this::toResponse); + } + + // Untuk Guardian: ambil last location user yang dipair + public Optional getLastLocationForGuardian(Long guardianId) { + return pairingRelationRepository + .findByGuardian_IdAndStatus(guardianId, PairingStatus.ACTIVE) + .flatMap(p -> locationHistoryRepository + .findTopByUserIdOrderByCreatedAtDesc(p.getUser().getId())) + .map(this::toResponse); + } + + public Page getLocationHistory(Long userId, Pageable pageable) { + return locationHistoryRepository + .findByUserIdOrderByCreatedAtDesc(userId, pageable) + .map(this::toResponse); + } + + // ========== GEOFENCE ========== + + private void checkGeofence(Long userId, double lat, double lng) { + geofenceConfigRepository.findByUserId(userId).ifPresent(cfg -> { + if (!cfg.getEnabled() || cfg.getCenterLat() == null) return; + + double dist = haversineMeters(cfg.getCenterLat(), cfg.getCenterLng(), lat, lng); + + if (dist > cfg.getRadiusMeters()) { + // User keluar geofence — beri tahu Guardian + pairingRelationRepository.findByUser_IdAndStatus(userId, PairingStatus.ACTIVE) + .ifPresent(pairing -> { + User guardian = pairing.getGuardian(); + User user = pairing.getUser(); + fcmService.sendHighPriority( + guardian.getFcmToken(), + "⚠️ User Keluar Area Aman", + user.getDisplayName() + " meninggalkan area geofence! " + + String.format("%.0fm dari pusat area", dist), + Map.of("type", "GEOFENCE_EXIT", + "userId", String.valueOf(userId), + "lat", String.valueOf(lat), + "lng", String.valueOf(lng)) + ); + activityLogService.createLog(user, ActivityLogType.GEOFENCE_EXIT, + String.format("User keluar area geofence (%.0fm dari pusat)", dist), null); + log.info("[GEOFENCE] User {} keluar area ({:.0f}m)", userId, dist); + }); + } + }); + } + + /** Haversine formula — jarak antara 2 koordinat dalam meter */ + public static double haversineMeters(double lat1, double lng1, double lat2, double lng2) { + final double R = 6371000; // radius bumi dalam meter + double dLat = Math.toRadians(lat2 - lat1); + double dLng = Math.toRadians(lng2 - lng1); + double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) + * Math.sin(dLng / 2) * Math.sin(dLng / 2); + return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + } + + private LocationResponse toResponse(LocationHistory l) { + return LocationResponse.builder() + .id(l.getId()).lat(l.getLat()).lng(l.getLng()) + .accuracy(l.getAccuracy()).speed(l.getSpeed()).heading(l.getHeading()) + .createdAt(l.getCreatedAt()).build(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/service/NotificationService.java b/walkguide-backend/demo/src/main/java/com/walkguide/service/NotificationService.java new file mode 100644 index 0000000..d3f63ee --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/service/NotificationService.java @@ -0,0 +1,96 @@ +package com.walkguide.service; + +import com.walkguide.dto.request.SendNotificationRequest; +import com.walkguide.dto.response.NotificationResponse; +import com.walkguide.entity.GuardianNotification; +import com.walkguide.enums.NotificationType; +import com.walkguide.enums.PairingStatus; +import com.walkguide.exception.PairingException; +import com.walkguide.exception.ResourceNotFoundException; +import com.walkguide.repository.GuardianNotificationRepository; +import com.walkguide.repository.PairingRelationRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class NotificationService { + + private final GuardianNotificationRepository notifRepository; + private final PairingRelationRepository pairingRelationRepository; + private final FcmService fcmService; + + @Transactional + public NotificationResponse sendNotification(Long guardianId, SendNotificationRequest req) { + var pairing = pairingRelationRepository + .findByGuardian_IdAndStatus(guardianId, PairingStatus.ACTIVE) + .orElseThrow(() -> new PairingException("Tidak ada user yang dipair")); + + Long userId = pairing.getUser().getId(); + NotificationType type = "VOICE_NOTE".equalsIgnoreCase(req.getNotifType()) + ? NotificationType.VOICE_NOTE : NotificationType.TEXT; + + GuardianNotification notif = GuardianNotification.builder() + .guardianId(guardianId) + .userId(userId) + .notifType(type) + .content(req.getContent()) + .voiceNoteUrl(req.getVoiceNoteUrl()) + .voiceNoteDuration(req.getVoiceNoteDuration()) + .build(); + notif = notifRepository.save(notif); + + // FCM ke user + String fcmToken = pairing.getUser().getFcmToken(); + String fcmBody = type == NotificationType.TEXT + ? req.getContent() : "Voice note from Guardian"; + fcmService.sendToToken(fcmToken, + "Pesan dari Guardian", + fcmBody, + Map.of("type", "NOTIFICATION", + "notifId", String.valueOf(notif.getId()), + "notifType", type.name(), + "voiceNoteUrl", req.getVoiceNoteUrl() != null ? req.getVoiceNoteUrl() : "")); + + return toResponse(notif); + } + + public Page getNotifications(Long userId, Pageable pageable) { + return notifRepository.findByUserIdOrderByCreatedAtDesc(userId, pageable) + .map(this::toResponse); + } + + public long getUnreadCount(Long userId) { + return notifRepository.countByUserIdAndIsReadFalse(userId); + } + + @Transactional + public void markAllRead(Long userId) { + var unread = notifRepository.findByUserIdAndIsReadFalseOrderByCreatedAtAsc(userId); + LocalDateTime now = LocalDateTime.now(); + unread.forEach(n -> { n.setIsRead(true); n.setReadAt(now); }); + notifRepository.saveAll(unread); + } + + @Transactional + public void markOneRead(Long notifId) { + notifRepository.findById(notifId).ifPresent(n -> { + n.setIsRead(true); + n.setReadAt(LocalDateTime.now()); + notifRepository.save(n); + }); + } + + private NotificationResponse toResponse(GuardianNotification n) { + return NotificationResponse.builder() + .id(n.getId()).notifType(n.getNotifType().name()).content(n.getContent()) + .voiceNoteUrl(n.getVoiceNoteUrl()).voiceNoteDuration(n.getVoiceNoteDuration()) + .isRead(n.getIsRead()).readAt(n.getReadAt()).createdAt(n.getCreatedAt()).build(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/service/ObstacleLogService.java b/walkguide-backend/demo/src/main/java/com/walkguide/service/ObstacleLogService.java new file mode 100644 index 0000000..a99ffad --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/service/ObstacleLogService.java @@ -0,0 +1,58 @@ +package com.walkguide.service; + +import com.walkguide.dto.request.ObstacleLogRequest; +import com.walkguide.dto.response.ObstacleLogResponse; +import com.walkguide.entity.ObstacleLog; +import com.walkguide.entity.User; +import com.walkguide.enums.ActivityLogType; +import com.walkguide.repository.ObstacleLogRepository; +import com.walkguide.repository.UserRepository; +import com.walkguide.exception.ResourceNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ObstacleLogService { + + private final ObstacleLogRepository obstacleLogRepository; + private final UserRepository userRepository; + private final ActivityLogService activityLogService; + + public ObstacleLogResponse saveObstacle(Long userId, ObstacleLogRequest req) { + ObstacleLog log = ObstacleLog.builder() + .userId(userId) + .label(req.getLabel()) + .confidence(req.getConfidence()) + .direction(req.getDirection()) + .estimatedDist(req.getEstimatedDist()) + .lat(req.getLat()) + .lng(req.getLng()) + .build(); + log = obstacleLogRepository.save(log); + + // Log ke activity_logs juga + User user = userRepository.findById(userId) + .orElseThrow(() -> new ResourceNotFoundException("User tidak ditemukan")); + String meta = String.format("{\"label\":\"%s\",\"direction\":\"%s\",\"dist\":\"%s\",\"confidence\":%.2f}", + req.getLabel(), req.getDirection(), req.getEstimatedDist(), req.getConfidence()); + activityLogService.createLog(user, ActivityLogType.OBSTACLE_DETECTED, + "Obstacle terdeteksi: " + req.getLabel() + " (" + req.getDirection() + ")", meta); + + return toResponse(log); + } + + public Page getObstacleLogs(Long userId, Pageable pageable) { + return obstacleLogRepository.findByUserIdOrderByCreatedAtDesc(userId, pageable) + .map(this::toResponse); + } + + private ObstacleLogResponse toResponse(ObstacleLog l) { + return ObstacleLogResponse.builder() + .id(l.getId()).label(l.getLabel()).confidence(l.getConfidence()) + .direction(l.getDirection()).estimatedDist(l.getEstimatedDist()) + .lat(l.getLat()).lng(l.getLng()).createdAt(l.getCreatedAt()).build(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/service/PairingService.java b/walkguide-backend/demo/src/main/java/com/walkguide/service/PairingService.java new file mode 100644 index 0000000..d4733a5 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/service/PairingService.java @@ -0,0 +1,217 @@ +package com.walkguide.service; + +import com.walkguide.dto.response.PairingStatusResponse; +import com.walkguide.entity.*; +import com.walkguide.enums.*; +import com.walkguide.exception.PairingException; +import com.walkguide.exception.ResourceNotFoundException; +import com.walkguide.repository.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class PairingService { + + private final PairingRelationRepository pairingRelationRepository; + private final UserRepository userRepository; + private final VoiceCommandConfigRepository voiceCommandConfigRepository; + private final HardwareShortcutRepository hardwareShortcutRepository; + private final AiConfigRepository aiConfigRepository; + private final ActivityLogService activityLogService; + private final FcmService fcmService; + + @Transactional + public PairingStatusResponse inviteUser(Long guardianId, String uniqueUserId) { + // Guardian tidak boleh punya pairing ACTIVE atau PENDING + if (pairingRelationRepository.existsByGuardian_IdAndStatus(guardianId, PairingStatus.ACTIVE)) { + throw new PairingException("Kamu sudah memiliki user yang dipair. Unpair dulu sebelum invite user baru."); + } + if (pairingRelationRepository.existsByGuardian_IdAndStatus(guardianId, PairingStatus.PENDING)) { + throw new PairingException("Kamu sudah punya invite yang menunggu. Tunggu user menerima atau tolak dulu."); + } + + User guardian = userRepository.findById(guardianId) + .orElseThrow(() -> new ResourceNotFoundException("Guardian tidak ditemukan")); + User user = userRepository.findByUniqueUserId(uniqueUserId) + .orElseThrow(() -> new ResourceNotFoundException("User dengan ID '" + uniqueUserId + "' tidak ditemukan")); + + if (!"ROLE_USER".equals(user.getRole())) { + throw new PairingException("ID tersebut bukan milik User. Pastikan kamu memasukkan ID yang benar."); + } + if (pairingRelationRepository.existsByUser_IdAndStatus(user.getId(), PairingStatus.ACTIVE)) { + throw new PairingException("User ini sudah dipair dengan Guardian lain."); + } + + PairingRelation pairing = PairingRelation.builder() + .guardian(guardian) + .user(user) + .status(PairingStatus.PENDING) + .build(); + pairing = pairingRelationRepository.save(pairing); + + // Kirim FCM ke user + fcmService.sendToToken(user.getFcmToken(), + "Pairing Request", + "Guardian " + guardian.getDisplayName() + " mengundang kamu untuk terhubung", + Map.of("type", "PAIRING_INVITE", "guardianName", guardian.getDisplayName())); + + activityLogService.createLog(guardian, ActivityLogType.PAIRING_INVITE_SENT, + "Guardian mengirim invite ke " + user.getDisplayName(), null); + + return buildStatus(pairing, guardian, user, "GUARDIAN"); + } + + @Transactional + public PairingStatusResponse respondToPairing(Long userId, Long pairingId, boolean accept) { + PairingRelation pairing = pairingRelationRepository.findById(pairingId) + .orElseThrow(() -> new ResourceNotFoundException("Pairing tidak ditemukan")); + + if (!pairing.getUser().getId().equals(userId)) { + throw new PairingException("Kamu tidak berhak merespons pairing ini"); + } + if (pairing.getStatus() != PairingStatus.PENDING) { + throw new PairingException("Pairing ini sudah direspons sebelumnya"); + } + + User user = pairing.getUser(); + User guardian = pairing.getGuardian(); + + pairing.setStatus(accept ? PairingStatus.ACTIVE : PairingStatus.REJECTED); + pairing.setRespondedAt(LocalDateTime.now()); + pairingRelationRepository.save(pairing); + + if (accept) { + seedDefaults(guardian.getId(), user.getId()); + activityLogService.createLog(user, ActivityLogType.PAIRING_ACCEPTED, + "User menerima pairing dengan Guardian " + guardian.getDisplayName(), null); + fcmService.sendToToken(guardian.getFcmToken(), + "Pairing Berhasil!", + user.getDisplayName() + " menerima undangan pairing kamu", + Map.of("type", "PAIRING_RESPONSE", "accepted", "true")); + } else { + activityLogService.createLog(user, ActivityLogType.PAIRING_REJECTED, + "User menolak pairing dengan Guardian " + guardian.getDisplayName(), null); + fcmService.sendToToken(guardian.getFcmToken(), + "Pairing Ditolak", + user.getDisplayName() + " menolak undangan pairing kamu", + Map.of("type", "PAIRING_RESPONSE", "accepted", "false")); + } + + return buildStatus(pairing, guardian, user, "USER"); + } + + @Transactional + public void unpair(Long requesterId) { + var pairing = pairingRelationRepository.findByGuardian_Id(requesterId) + .or(() -> pairingRelationRepository.findByUser_Id(requesterId)) + .orElseThrow(() -> new ResourceNotFoundException("Tidak ada pairing yang aktif")); + + User guardian = pairing.getGuardian(); + User user = pairing.getUser(); + + // Hapus semua konfigurasi yang terkait dengan pasangan ini + voiceCommandConfigRepository.deleteByUserId(user.getId()); + hardwareShortcutRepository.deleteByUserId(user.getId()); + aiConfigRepository.findByUserId(user.getId()).ifPresent(aiConfigRepository::delete); + + pairingRelationRepository.delete(pairing); + + // Beritahu pihak lain + boolean requesterIsGuardian = guardian.getId().equals(requesterId); + String otherFcmToken = requesterIsGuardian ? user.getFcmToken() : guardian.getFcmToken(); + String requesterName = requesterIsGuardian ? guardian.getDisplayName() : user.getDisplayName(); + + fcmService.sendToToken(otherFcmToken, + "Pairing Diakhiri", + requesterName + " mengakhiri koneksi pairing", + Map.of("type", "PAIRING_DISSOLVED")); + + activityLogService.createLog(guardian, ActivityLogType.PAIRING_DISSOLVED, + "Pairing antara " + guardian.getDisplayName() + " dan " + user.getDisplayName() + " diakhiri", null); + } + + public PairingStatusResponse getStatus(Long userId, String role) { + if ("ROLE_GUARDIAN".equals(role)) { + return pairingRelationRepository.findByGuardian_Id(userId) + .map(p -> buildStatus(p, p.getGuardian(), p.getUser(), "GUARDIAN")) + .orElse(PairingStatusResponse.builder().status("NONE").build()); + } else { + return pairingRelationRepository.findByUser_Id(userId) + .map(p -> buildStatus(p, p.getGuardian(), p.getUser(), "USER")) + .orElse(PairingStatusResponse.builder().status("NONE").build()); + } + } + + // ========== PRIVATE ========== + + private void seedDefaults(Long guardianId, Long userId) { + // Voice commands default + List defaults = List.of( + vc(guardianId, userId, VoiceCommandKey.OPEN_WALKGUIDE, "Open Walkguide"), + vc(guardianId, userId, VoiceCommandKey.START_WALKGUIDE, "Start Walkguide"), + vc(guardianId, userId, VoiceCommandKey.STOP_WALKGUIDE, "Stop Walkguide"), + vc(guardianId, userId, VoiceCommandKey.CALL_GUARDIAN, "Call Guardian"), + vc(guardianId, userId, VoiceCommandKey.OPEN_NOTIFICATION, "Open Notifications"), + vc(guardianId, userId, VoiceCommandKey.READ_ALL_NOTIF, "Read All My Notifications"), + vc(guardianId, userId, VoiceCommandKey.OPEN_SOS, "Open SOS"), + vc(guardianId, userId, VoiceCommandKey.SEND_SOS, "Send SOS"), + vc(guardianId, userId, VoiceCommandKey.WHERE_AM_I, "Where Am I"), + vc(guardianId, userId, VoiceCommandKey.OPEN_ACTIVITY, "Open Activity Log"), + vc(guardianId, userId, VoiceCommandKey.OPEN_NAVIGATION, "Open Navigation"), + vc(guardianId, userId, VoiceCommandKey.OPEN_SETTINGS, "Open Settings"), + vc(guardianId, userId, VoiceCommandKey.REPEAT_LAST, "Repeat"), + vc(guardianId, userId, VoiceCommandKey.STOP_TTS, "Stop") + ); + voiceCommandConfigRepository.saveAll(defaults); + + // Hardware shortcuts default (Vol Up = Call Guardian, Vol Down = Start WalkGuide) + List shortcuts = List.of( + hs(guardianId, userId, HardwareShortcutKey.CALL_GUARDIAN, "Volume Up", 24), + hs(guardianId, userId, HardwareShortcutKey.START_WALKGUIDE, "Volume Down", 25), + hs(guardianId, userId, HardwareShortcutKey.SEND_SOS, null, null), + hs(guardianId, userId, HardwareShortcutKey.STOP_WALKGUIDE, null, null), + hs(guardianId, userId, HardwareShortcutKey.OPEN_NOTIFICATION, null, null) + ); + hardwareShortcutRepository.saveAll(shortcuts); + + // AI config default + aiConfigRepository.save(AiConfig.builder() + .guardianId(guardianId) + .userId(userId) + .build()); + } + + private VoiceCommandConfig vc(Long gId, Long uId, VoiceCommandKey key, String phrase) { + return VoiceCommandConfig.builder() + .guardianId(gId).userId(uId).commandKey(key).triggerPhrase(phrase).build(); + } + + private HardwareShortcut hs(Long gId, Long uId, HardwareShortcutKey key, String name, Integer code) { + return HardwareShortcut.builder() + .guardianId(gId).userId(uId).shortcutKey(key) + .buttonName(name).buttonCode(code) + .enabled(name != null).build(); + } + + private PairingStatusResponse buildStatus(PairingRelation p, User guardian, User user, String viewerRole) { + String pairedWithName = "GUARDIAN".equals(viewerRole) + ? user.getDisplayName() : guardian.getDisplayName(); + String pairedWithEmail = "GUARDIAN".equals(viewerRole) + ? user.getEmail() : guardian.getEmail(); + return PairingStatusResponse.builder() + .pairingId(p.getId()) + .status(p.getStatus().name()) + .pairedWithName(pairedWithName) + .pairedWithEmail(pairedWithEmail) + .uniqueUserId(user.getUniqueUserId()) + .invitedAt(p.getInvitedAt()) + .respondedAt(p.getRespondedAt()) + .build(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/service/SosService.java b/walkguide-backend/demo/src/main/java/com/walkguide/service/SosService.java new file mode 100644 index 0000000..d466469 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/service/SosService.java @@ -0,0 +1,118 @@ +package com.walkguide.service; + +import com.walkguide.dto.request.SosRequest; +import com.walkguide.dto.response.SosEventResponse; +import com.walkguide.entity.SosEvent; +import com.walkguide.entity.User; +import com.walkguide.enums.ActivityLogType; +import com.walkguide.enums.PairingStatus; +import com.walkguide.enums.SosStatus; +import com.walkguide.exception.ResourceNotFoundException; +import com.walkguide.repository.*; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class SosService { + + private final SosEventRepository sosEventRepository; + private final PairingRelationRepository pairingRelationRepository; + private final UserRepository userRepository; + private final ActivityLogService activityLogService; + private final FcmService fcmService; + + @Transactional + public SosEventResponse triggerSos(Long userId, SosRequest req) { + SosEvent sos = SosEvent.builder() + .userId(userId) + .triggerType(req.getTriggerType() != null ? req.getTriggerType() : "MANUAL") + .lat(req.getLat()) + .lng(req.getLng()) + .status(SosStatus.TRIGGERED) + .build(); + sos = sosEventRepository.save(sos); + final SosEvent savedSos = sos; + + User user = userRepository.findById(userId) + .orElseThrow(() -> new ResourceNotFoundException("User tidak ditemukan")); + + activityLogService.createLog(user, ActivityLogType.SOS_TRIGGERED, + "SOS dikirim via " + sos.getTriggerType(), null); + + // Kirim ke Guardian + pairingRelationRepository.findByUser_IdAndStatus(userId, PairingStatus.ACTIVE) + .ifPresent(pairing -> { + String guardianFcm = pairing.getGuardian().getFcmToken(); + String locStr = req.getLat() != null + ? String.format("Lat:%.4f,Lng:%.4f", req.getLat(), req.getLng()) + : "Lokasi tidak tersedia"; + fcmService.sendHighPriority(guardianFcm, + "🚨 SOS ALERT dari " + user.getDisplayName(), + user.getDisplayName() + " butuh bantuan! " + locStr, + Map.of("type", "SOS_ALERT", + "sosId", String.valueOf(savedSos.getId()), + "userId", String.valueOf(userId), + "lat", String.valueOf(req.getLat() != null ? req.getLat() : 0), + "lng", String.valueOf(req.getLng() != null ? req.getLng() : 0))); + }); + + return toResponse(sos); + } + + @Transactional + public SosEventResponse acknowledgeSos(Long guardianId, Long sosId) { + SosEvent sos = sosEventRepository.findById(sosId) + .orElseThrow(() -> new ResourceNotFoundException("SOS event tidak ditemukan")); + + sos.setStatus(SosStatus.ACKNOWLEDGED); + sos.setAcknowledgedAt(LocalDateTime.now()); + sosEventRepository.save(sos); + + activityLogService.createLog( + userRepository.findById(sos.getUserId()) + .orElseThrow(() -> new ResourceNotFoundException("User tidak ditemukan")), + ActivityLogType.SOS_ACKNOWLEDGED, + "Guardian mengakui SOS", null); + + // Beritahu user + pairingRelationRepository.findByUser_IdAndStatus(sos.getUserId(), PairingStatus.ACTIVE) + .ifPresent(pairing -> { + String userFcm = pairing.getUser().getFcmToken(); + fcmService.sendToToken(userFcm, + "Guardian Merespons SOS", + "Guardian kamu sudah melihat SOS kamu dan sedang menuju lokasimu", + Map.of("type", "SOS_ACKNOWLEDGED")); + }); + + return toResponse(sos); + } + + public Page getSosEvents(Long userId, Pageable pageable) { + return sosEventRepository.findByUserIdOrderByCreatedAtDesc(userId, pageable) + .map(this::toResponse); + } + + // Guardian get SOS for their paired user + public Page getSosEventsForGuardian(Long guardianId, Pageable pageable) { + var pairing = pairingRelationRepository + .findByGuardian_IdAndStatus(guardianId, PairingStatus.ACTIVE) + .orElseThrow(() -> new ResourceNotFoundException("Tidak ada user yang dipair")); + return sosEventRepository + .findByUserIdOrderByCreatedAtDesc(pairing.getUser().getId(), pageable) + .map(this::toResponse); + } + + private SosEventResponse toResponse(SosEvent s) { + return SosEventResponse.builder() + .id(s.getId()).triggerType(s.getTriggerType()) + .lat(s.getLat()).lng(s.getLng()).status(s.getStatus().name()) + .acknowledgedAt(s.getAcknowledgedAt()).createdAt(s.getCreatedAt()).build(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/service/UserSettingsService.java b/walkguide-backend/demo/src/main/java/com/walkguide/service/UserSettingsService.java new file mode 100644 index 0000000..c0c218e --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/service/UserSettingsService.java @@ -0,0 +1,42 @@ +package com.walkguide.service; + +import com.walkguide.dto.request.UserSettingsUpdateRequest; +import com.walkguide.dto.response.UserSettingsResponse; +import com.walkguide.entity.UserSettings; +import com.walkguide.repository.UserSettingsRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserSettingsService { + + private final UserSettingsRepository userSettingsRepository; + + public UserSettingsResponse getSettings(Long userId) { + UserSettings s = userSettingsRepository.findByUserId(userId) + .orElseGet(() -> userSettingsRepository.save( + UserSettings.builder().userId(userId).build())); + return toResponse(s); + } + + public UserSettingsResponse updateSettings(Long userId, UserSettingsUpdateRequest req) { + UserSettings s = userSettingsRepository.findByUserId(userId) + .orElseGet(() -> UserSettings.builder().userId(userId).build()); + + if (req.getTtsLanguage() != null) s.setTtsLanguage(req.getTtsLanguage()); + if (req.getTtsPitch() != null) s.setTtsPitch(req.getTtsPitch()); + if (req.getTtsSpeed() != null) s.setTtsSpeed(req.getTtsSpeed()); + if (req.getWarnNoGuardian() != null) s.setWarnNoGuardian(req.getWarnNoGuardian()); + if (req.getHapticEnabled() != null) s.setHapticEnabled(req.getHapticEnabled()); + + return toResponse(userSettingsRepository.save(s)); + } + + private UserSettingsResponse toResponse(UserSettings s) { + return UserSettingsResponse.builder().id(s.getId()) + .ttsLanguage(s.getTtsLanguage()).ttsPitch(s.getTtsPitch()) + .ttsSpeed(s.getTtsSpeed()).warnNoGuardian(s.getWarnNoGuardian()) + .hapticEnabled(s.getHapticEnabled()).build(); + } +} diff --git a/walkguide-backend/demo/src/main/java/com/walkguide/service/VoiceCommandService.java b/walkguide-backend/demo/src/main/java/com/walkguide/service/VoiceCommandService.java new file mode 100644 index 0000000..7dd53a5 --- /dev/null +++ b/walkguide-backend/demo/src/main/java/com/walkguide/service/VoiceCommandService.java @@ -0,0 +1,82 @@ +package com.walkguide.service; + +import com.walkguide.dto.request.VoiceCommandUpdateRequest; +import com.walkguide.dto.response.VoiceCommandResponse; +import com.walkguide.enums.PairingStatus; +import com.walkguide.enums.VoiceCommandKey; +import com.walkguide.exception.PairingException; +import com.walkguide.exception.ResourceNotFoundException; +import com.walkguide.repository.PairingRelationRepository; +import com.walkguide.repository.VoiceCommandConfigRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class VoiceCommandService { + + private static final Map DESCRIPTIONS = Map.ofEntries( + Map.entry(VoiceCommandKey.OPEN_WALKGUIDE, "Buka menu WalkGuide"), + Map.entry(VoiceCommandKey.START_WALKGUIDE, "Mulai deteksi WalkGuide"), + Map.entry(VoiceCommandKey.STOP_WALKGUIDE, "Hentikan WalkGuide"), + Map.entry(VoiceCommandKey.CALL_GUARDIAN, "Telepon Guardian"), + Map.entry(VoiceCommandKey.OPEN_NOTIFICATION, "Buka menu Notifikasi"), + Map.entry(VoiceCommandKey.READ_ALL_NOTIF, "Baca semua notifikasi dengan TTS"), + Map.entry(VoiceCommandKey.OPEN_SOS, "Buka menu SOS"), + Map.entry(VoiceCommandKey.SEND_SOS, "Kirim sinyal darurat SOS"), + Map.entry(VoiceCommandKey.WHERE_AM_I, "Umumkan lokasi saat ini via TTS"), + Map.entry(VoiceCommandKey.OPEN_ACTIVITY, "Buka log aktivitas"), + Map.entry(VoiceCommandKey.OPEN_NAVIGATION, "Buka mode navigasi"), + Map.entry(VoiceCommandKey.OPEN_SETTINGS, "Buka pengaturan"), + Map.entry(VoiceCommandKey.REPEAT_LAST, "Ulangi TTS terakhir"), + Map.entry(VoiceCommandKey.STOP_TTS, "Hentikan TTS yang sedang berjalan") + ); + + private final VoiceCommandConfigRepository voiceCommandConfigRepository; + private final PairingRelationRepository pairingRelationRepository; + private final FcmService fcmService; + + public List getAll(Long userId) { + return voiceCommandConfigRepository.findByUserId(userId).stream() + .map(vc -> VoiceCommandResponse.builder() + .id(vc.getId()) + .commandKey(vc.getCommandKey().name()) + .triggerPhrase(vc.getTriggerPhrase()) + .enabled(vc.getEnabled()) + .description(DESCRIPTIONS.getOrDefault(vc.getCommandKey(), "")) + .build()) + .collect(Collectors.toList()); + } + + public VoiceCommandResponse updateByGuardian(Long guardianId, VoiceCommandUpdateRequest req) { + var pairing = pairingRelationRepository + .findByGuardian_IdAndStatus(guardianId, PairingStatus.ACTIVE) + .orElseThrow(() -> new PairingException("Tidak ada user yang dipair")); + Long userId = pairing.getUser().getId(); + + VoiceCommandKey key = VoiceCommandKey.valueOf(req.getCommandKey()); + var vc = voiceCommandConfigRepository.findByUserIdAndCommandKey(userId, key) + .orElseThrow(() -> new ResourceNotFoundException("Voice command tidak ditemukan")); + + if (req.getTriggerPhrase() != null && !req.getTriggerPhrase().isBlank()) + vc.setTriggerPhrase(req.getTriggerPhrase()); + if (req.getEnabled() != null) + vc.setEnabled(req.getEnabled()); + + vc = voiceCommandConfigRepository.save(vc); + + fcmService.sendToToken(pairing.getUser().getFcmToken(), + "Voice Command Diperbarui", + "Guardian mengubah perintah suara kamu", + Map.of("type", "SETTINGS_UPDATED", "settingType", "VOICE_COMMAND")); + + return VoiceCommandResponse.builder() + .id(vc.getId()).commandKey(vc.getCommandKey().name()) + .triggerPhrase(vc.getTriggerPhrase()).enabled(vc.getEnabled()) + .description(DESCRIPTIONS.getOrDefault(vc.getCommandKey(), "")).build(); + } +} diff --git a/walkguide-backend/demo/src/main/resources/db/migration/V10__create_sos_events.sql b/walkguide-backend/demo/src/main/resources/db/migration/V10__create_sos_events.sql new file mode 100644 index 0000000..abe7f02 --- /dev/null +++ b/walkguide-backend/demo/src/main/resources/db/migration/V10__create_sos_events.sql @@ -0,0 +1,14 @@ +-- V10: SOS events +CREATE TABLE IF NOT EXISTS sos_events ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + trigger_type VARCHAR(50) NOT NULL DEFAULT 'MANUAL', + lat DOUBLE PRECISION, + lng DOUBLE PRECISION, + status VARCHAR(20) NOT NULL DEFAULT 'TRIGGERED', + acknowledged_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_sos_user_id ON sos_events(user_id); +CREATE INDEX IF NOT EXISTS idx_sos_status ON sos_events(status); +CREATE INDEX IF NOT EXISTS idx_sos_created_at ON sos_events(created_at DESC); diff --git a/walkguide-backend/demo/src/main/resources/db/migration/V11__create_user_settings.sql b/walkguide-backend/demo/src/main/resources/db/migration/V11__create_user_settings.sql new file mode 100644 index 0000000..c7543bc --- /dev/null +++ b/walkguide-backend/demo/src/main/resources/db/migration/V11__create_user_settings.sql @@ -0,0 +1,12 @@ +-- V11: User settings - preferensi personal user (TTS, haptic, dll) +CREATE TABLE IF NOT EXISTS user_settings ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE, + tts_language VARCHAR(10) NOT NULL DEFAULT 'id-ID', + tts_pitch DOUBLE PRECISION NOT NULL DEFAULT 1.0, + tts_speed DOUBLE PRECISION NOT NULL DEFAULT 0.9, + warn_no_guardian BOOLEAN NOT NULL DEFAULT TRUE, + haptic_enabled BOOLEAN NOT NULL DEFAULT TRUE, + updated_at TIMESTAMP NOT NULL DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_user_settings_user_id ON user_settings(user_id); diff --git a/walkguide-backend/demo/src/main/resources/db/migration/V12__create_ai_configs.sql b/walkguide-backend/demo/src/main/resources/db/migration/V12__create_ai_configs.sql new file mode 100644 index 0000000..3cbccdd --- /dev/null +++ b/walkguide-backend/demo/src/main/resources/db/migration/V12__create_ai_configs.sql @@ -0,0 +1,13 @@ +-- V12: AI configs - konfigurasi YOLO detection untuk setiap user (diatur Guardian) +CREATE TABLE IF NOT EXISTS ai_configs ( + id BIGSERIAL PRIMARY KEY, + guardian_id BIGINT REFERENCES users(id) ON DELETE SET NULL, + user_id BIGINT NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE, + confidence_threshold DOUBLE PRECISION NOT NULL DEFAULT 0.5, + alert_distance_close DOUBLE PRECISION NOT NULL DEFAULT 1.5, + alert_distance_medium DOUBLE PRECISION NOT NULL DEFAULT 3.0, + max_inference_fps INT NOT NULL DEFAULT 5, + enabled_labels TEXT NOT NULL DEFAULT 'ALL', + updated_at TIMESTAMP NOT NULL DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_ai_config_user_id ON ai_configs(user_id); diff --git a/walkguide-backend/demo/src/main/resources/db/migration/V13__create_voice_command_configs.sql b/walkguide-backend/demo/src/main/resources/db/migration/V13__create_voice_command_configs.sql new file mode 100644 index 0000000..75ae055 --- /dev/null +++ b/walkguide-backend/demo/src/main/resources/db/migration/V13__create_voice_command_configs.sql @@ -0,0 +1,12 @@ +-- V13: Voice command configs - phrase yang diucapkan user untuk trigger aksi +CREATE TABLE IF NOT EXISTS voice_command_configs ( + id BIGSERIAL PRIMARY KEY, + guardian_id BIGINT REFERENCES users(id) ON DELETE SET NULL, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + command_key VARCHAR(50) NOT NULL, + trigger_phrase VARCHAR(200) NOT NULL, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + CONSTRAINT uq_voice_cmd_user_key UNIQUE (user_id, command_key) +); +CREATE INDEX IF NOT EXISTS idx_voice_cmd_user_id ON voice_command_configs(user_id); diff --git a/walkguide-backend/demo/src/main/resources/db/migration/V14__create_hardware_shortcuts.sql b/walkguide-backend/demo/src/main/resources/db/migration/V14__create_hardware_shortcuts.sql new file mode 100644 index 0000000..567776d --- /dev/null +++ b/walkguide-backend/demo/src/main/resources/db/migration/V14__create_hardware_shortcuts.sql @@ -0,0 +1,13 @@ +-- V14: Hardware shortcuts - tombol fisik HP yang diassign ke aksi tertentu +CREATE TABLE IF NOT EXISTS hardware_shortcuts ( + id BIGSERIAL PRIMARY KEY, + guardian_id BIGINT REFERENCES users(id) ON DELETE SET NULL, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + shortcut_key VARCHAR(50) NOT NULL, + button_name VARCHAR(100), + button_code INT, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), + CONSTRAINT uq_shortcut_user_key UNIQUE (user_id, shortcut_key) +); +CREATE INDEX IF NOT EXISTS idx_shortcut_user_id ON hardware_shortcuts(user_id); diff --git a/walkguide-backend/demo/src/main/resources/db/migration/V15__create_geofence_configs.sql b/walkguide-backend/demo/src/main/resources/db/migration/V15__create_geofence_configs.sql new file mode 100644 index 0000000..3913fcc --- /dev/null +++ b/walkguide-backend/demo/src/main/resources/db/migration/V15__create_geofence_configs.sql @@ -0,0 +1,11 @@ +-- V15: Geofence configs - area aman yang dipantau Guardian +CREATE TABLE IF NOT EXISTS geofence_configs ( + id BIGSERIAL PRIMARY KEY, + guardian_id BIGINT REFERENCES users(id) ON DELETE SET NULL, + user_id BIGINT NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE, + center_lat DOUBLE PRECISION, + center_lng DOUBLE PRECISION, + radius_meters DOUBLE PRECISION NOT NULL DEFAULT 500.0, + enabled BOOLEAN NOT NULL DEFAULT FALSE, + updated_at TIMESTAMP NOT NULL DEFAULT NOW() +); diff --git a/walkguide-backend/demo/src/main/resources/db/migration/V16__create_refresh_tokens.sql b/walkguide-backend/demo/src/main/resources/db/migration/V16__create_refresh_tokens.sql new file mode 100644 index 0000000..73f963b --- /dev/null +++ b/walkguide-backend/demo/src/main/resources/db/migration/V16__create_refresh_tokens.sql @@ -0,0 +1,10 @@ +-- V16: Refresh tokens - untuk perpanjang session tanpa login ulang +CREATE TABLE IF NOT EXISTS refresh_tokens ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token VARCHAR(500) NOT NULL UNIQUE, + expires_at TIMESTAMP NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_refresh_token_user_id ON refresh_tokens(user_id); +CREATE INDEX IF NOT EXISTS idx_refresh_token_token ON refresh_tokens(token); diff --git a/walkguide-backend/demo/src/main/resources/db/migration/V4__alter_users_add_columns.sql b/walkguide-backend/demo/src/main/resources/db/migration/V4__alter_users_add_columns.sql new file mode 100644 index 0000000..2d7753b --- /dev/null +++ b/walkguide-backend/demo/src/main/resources/db/migration/V4__alter_users_add_columns.sql @@ -0,0 +1,11 @@ +-- V4: Tambah kolom baru ke tabel users untuk fitur lengkap WalkGuide +-- Kolom unique_user_id: hanya untuk ROLE_USER, 12 karakter alphanumeric (seperti Discord ID) +-- Kolom display_name: nama tampilan user +-- Kolom fcm_token: Firebase Cloud Messaging token untuk push notification + +ALTER TABLE users + ADD COLUMN IF NOT EXISTS unique_user_id VARCHAR(12) UNIQUE, + ADD COLUMN IF NOT EXISTS display_name VARCHAR(100), + ADD COLUMN IF NOT EXISTS fcm_token VARCHAR(500); + +CREATE INDEX IF NOT EXISTS idx_users_unique_user_id ON users(unique_user_id); diff --git a/walkguide-backend/demo/src/main/resources/db/migration/V5__create_pairing_relations.sql b/walkguide-backend/demo/src/main/resources/db/migration/V5__create_pairing_relations.sql new file mode 100644 index 0000000..b51d89e --- /dev/null +++ b/walkguide-backend/demo/src/main/resources/db/migration/V5__create_pairing_relations.sql @@ -0,0 +1,18 @@ +-- V5: Tabel pairing_relations +-- Menyimpan hubungan Guardian <-> User (1:1) +-- Status: PENDING (menunggu User accept), ACTIVE (sudah pair), REJECTED (ditolak/unpair) + +CREATE TABLE IF NOT EXISTS pairing_relations ( + id BIGSERIAL PRIMARY KEY, + guardian_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + status VARCHAR(20) NOT NULL DEFAULT 'PENDING', + invited_at TIMESTAMP NOT NULL DEFAULT NOW(), + responded_at TIMESTAMP, + CONSTRAINT uq_guardian_active UNIQUE (guardian_id), + CONSTRAINT uq_user_active UNIQUE (user_id) +); + +CREATE INDEX IF NOT EXISTS idx_pairing_guardian ON pairing_relations(guardian_id); +CREATE INDEX IF NOT EXISTS idx_pairing_user ON pairing_relations(user_id); +CREATE INDEX IF NOT EXISTS idx_pairing_status ON pairing_relations(status); diff --git a/walkguide-backend/demo/src/main/resources/db/migration/V6__create_activity_logs.sql b/walkguide-backend/demo/src/main/resources/db/migration/V6__create_activity_logs.sql new file mode 100644 index 0000000..e2ea668 --- /dev/null +++ b/walkguide-backend/demo/src/main/resources/db/migration/V6__create_activity_logs.sql @@ -0,0 +1,12 @@ +-- V6: Activity logs - semua aktivitas user dicatat di sini +CREATE TABLE IF NOT EXISTS activity_logs ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + log_type VARCHAR(50) NOT NULL, + description TEXT, + metadata TEXT, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_activity_user_id ON activity_logs(user_id); +CREATE INDEX IF NOT EXISTS idx_activity_created_at ON activity_logs(created_at DESC); +CREATE INDEX IF NOT EXISTS idx_activity_log_type ON activity_logs(log_type); diff --git a/walkguide-backend/demo/src/main/resources/db/migration/V7__create_obstacle_logs.sql b/walkguide-backend/demo/src/main/resources/db/migration/V7__create_obstacle_logs.sql new file mode 100644 index 0000000..c7da7c8 --- /dev/null +++ b/walkguide-backend/demo/src/main/resources/db/migration/V7__create_obstacle_logs.sql @@ -0,0 +1,14 @@ +-- V7: Obstacle logs - setiap deteksi YOLO yang signifikan dicatat +CREATE TABLE IF NOT EXISTS obstacle_logs ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + label VARCHAR(100) NOT NULL, + confidence DOUBLE PRECISION NOT NULL, + direction VARCHAR(20) NOT NULL, + estimated_dist VARCHAR(50) NOT NULL, + lat DOUBLE PRECISION, + lng DOUBLE PRECISION, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_obstacle_user_id ON obstacle_logs(user_id); +CREATE INDEX IF NOT EXISTS idx_obstacle_created_at ON obstacle_logs(created_at DESC); diff --git a/walkguide-backend/demo/src/main/resources/db/migration/V8__create_location_history.sql b/walkguide-backend/demo/src/main/resources/db/migration/V8__create_location_history.sql new file mode 100644 index 0000000..131d4e5 --- /dev/null +++ b/walkguide-backend/demo/src/main/resources/db/migration/V8__create_location_history.sql @@ -0,0 +1,13 @@ +-- V8: Location history - GPS user dicatat setiap interval +CREATE TABLE IF NOT EXISTS location_history ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + lat DOUBLE PRECISION NOT NULL, + lng DOUBLE PRECISION NOT NULL, + accuracy DOUBLE PRECISION, + speed DOUBLE PRECISION, + heading DOUBLE PRECISION, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_location_user_id ON location_history(user_id); +CREATE INDEX IF NOT EXISTS idx_location_created_at ON location_history(created_at DESC); diff --git a/walkguide-backend/demo/src/main/resources/db/migration/V9__create_guardian_notifications.sql b/walkguide-backend/demo/src/main/resources/db/migration/V9__create_guardian_notifications.sql new file mode 100644 index 0000000..6d9d907 --- /dev/null +++ b/walkguide-backend/demo/src/main/resources/db/migration/V9__create_guardian_notifications.sql @@ -0,0 +1,16 @@ +-- V9: Guardian notifications - pesan dari Guardian ke User (text atau voice note) +CREATE TABLE IF NOT EXISTS guardian_notifications ( + id BIGSERIAL PRIMARY KEY, + guardian_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + notif_type VARCHAR(20) NOT NULL DEFAULT 'TEXT', + content TEXT, + voice_note_url VARCHAR(500), + voice_note_duration INT, + is_read BOOLEAN NOT NULL DEFAULT FALSE, + read_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_notif_user_id ON guardian_notifications(user_id); +CREATE INDEX IF NOT EXISTS idx_notif_is_read ON guardian_notifications(is_read); +CREATE INDEX IF NOT EXISTS idx_notif_created_at ON guardian_notifications(created_at DESC);