update backend
This commit is contained in:
parent
c8d9eefa74
commit
feb8e78b0c
@ -65,7 +65,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<optional>true</optional>
|
<version>1.18.36</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- JWT -->
|
<!-- JWT -->
|
||||||
@ -137,6 +137,7 @@
|
|||||||
<path>
|
<path>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.36</version>
|
||||||
</path>
|
</path>
|
||||||
</annotationProcessorPaths>
|
</annotationProcessorPaths>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|||||||
@ -4,10 +4,8 @@ import org.springframework.boot.SpringApplication;
|
|||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class DemoApplication {
|
public class WalkGuideApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(DemoApplication.class, args);
|
SpringApplication.run(WalkGuideApplication.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -18,29 +18,22 @@ public class DataSeeder implements CommandLineRunner {
|
|||||||
public void run(String... args) throws Exception {
|
public void run(String... args) throws Exception {
|
||||||
if (userRepository.count() == 0) {
|
if (userRepository.count() == 0) {
|
||||||
|
|
||||||
// 1. Buat Guardian (tanpa connected_to dulu)
|
|
||||||
User guardian = User.builder()
|
User guardian = User.builder()
|
||||||
.email("guardian@walkguide.com")
|
.email("guardian@walkguide.com")
|
||||||
.password(passwordEncoder.encode("guardian123"))
|
.password(passwordEncoder.encode("guardian123"))
|
||||||
.role("ROLE_GUARDIAN")
|
.role("ROLE_GUARDIAN")
|
||||||
.build();
|
.build();
|
||||||
guardian = userRepository.save(guardian);
|
userRepository.save(guardian);
|
||||||
|
|
||||||
// 2. Buat User Tunanetra, langsung sambungkan ke Guardian
|
|
||||||
User user = User.builder()
|
User user = User.builder()
|
||||||
.email("user@walkguide.com")
|
.email("user@walkguide.com")
|
||||||
.password(passwordEncoder.encode("user123"))
|
.password(passwordEncoder.encode("user123"))
|
||||||
.role("ROLE_USER")
|
.role("ROLE_USER")
|
||||||
.connectedTo(guardian)
|
|
||||||
.build();
|
.build();
|
||||||
user = userRepository.save(user);
|
userRepository.save(user);
|
||||||
|
|
||||||
// 3. Update Guardian -> sambungkan balik ke User yang dijaganya
|
|
||||||
guardian.setConnectedTo(user);
|
|
||||||
userRepository.save(guardian);
|
|
||||||
|
|
||||||
System.out.println("DataSeeder: Guardian (" + guardian.getId()
|
System.out.println("DataSeeder: Guardian (" + guardian.getId()
|
||||||
+ ") <-> User (" + user.getId() + ") berhasil dihubungkan!");
|
+ ") dan User (" + user.getId() + ") berhasil dibuat!");
|
||||||
} else {
|
} else {
|
||||||
System.out.println("DataSeeder: Database sudah ada data, skip seeding.");
|
System.out.println("DataSeeder: Database sudah ada data, skip seeding.");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,9 @@ package com.walkguide.config;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.models.OpenAPI;
|
import io.swagger.v3.oas.models.OpenAPI;
|
||||||
import io.swagger.v3.oas.models.info.Info;
|
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.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
@ -14,6 +17,14 @@ public class OpenApiConfig {
|
|||||||
.info(new Info()
|
.info(new Info()
|
||||||
.title("WalkGuide API")
|
.title("WalkGuide API")
|
||||||
.version("1.0")
|
.version("1.0")
|
||||||
.description("API Documentation for Walk Guide Application (Final Exam)"));
|
.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")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
package com.walkguide.config;
|
package com.walkguide.config;
|
||||||
|
|
||||||
|
import com.walkguide.security.JwtAuthFilter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
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.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
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
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class SecurityConfig {
|
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
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return new BCryptPasswordEncoder();
|
return new BCryptPasswordEncoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aturan jalan masuk ke API kita:
|
|
||||||
// Jangan lupa inject filter-nya di atas (tambahin parameter JwtAuthFilter jwtAuthFilter)
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthFilter) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
http
|
http
|
||||||
.cors(cors -> cors.configurationSource(request -> {
|
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||||
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;
|
|
||||||
}))
|
|
||||||
.csrf(csrf -> csrf.disable())
|
.csrf(csrf -> csrf.disable())
|
||||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(session ->
|
||||||
|
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
.requestMatchers("/api/auth/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll() // Login & Swagger bebas
|
// Public routes
|
||||||
.requestMatchers("/api/guardian/**").hasRole("GUARDIAN") // Khusus Guardian
|
.requestMatchers(
|
||||||
.requestMatchers("/api/user/**").hasRole("USER") // Khusus Tunanetra
|
"/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()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
// TARUH SATPAM DI SINI
|
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
.addFilterBefore(jwtAuthFilter, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.class);
|
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,36 +1,57 @@
|
|||||||
package com.walkguide.controller;
|
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.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 com.walkguide.service.AuthService;
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/auth")
|
@RequestMapping("/api/v1/auth")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AuthController {
|
public class AuthController {
|
||||||
|
|
||||||
private final AuthService authService;
|
private final AuthService authService;
|
||||||
|
|
||||||
|
/** Health-check — digunakan Flutter ServerConnectScreen */
|
||||||
|
@GetMapping("/ping")
|
||||||
|
public ResponseEntity<ApiResponse<String>> ping() {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok("pong", "Server aktif"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/register")
|
||||||
|
public ResponseEntity<ApiResponse<AuthDataResponse>> register(
|
||||||
|
@Valid @RequestBody RegisterRequest req) {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(authService.register(req), "Registrasi berhasil"));
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public ResponseEntity<ApiResponse<Map<String, String>>> login(@Valid @RequestBody AuthRequest request) {
|
public ResponseEntity<ApiResponse<AuthDataResponse>> login(
|
||||||
|
@Valid @RequestBody LoginRequest req) {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(authService.login(req), "Login berhasil"));
|
||||||
|
}
|
||||||
|
|
||||||
// Panggil service buat cek password dan bikin token
|
@PostMapping("/refresh")
|
||||||
Map<String, String> tokenData = authService.login(request);
|
public ResponseEntity<ApiResponse<AuthDataResponse>> refresh(
|
||||||
|
@RequestBody RefreshTokenRequest req) {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
authService.refreshToken(req.getRefreshToken()), "Token diperbarui"));
|
||||||
|
}
|
||||||
|
|
||||||
// Bungkus pakai ApiResponse biar sesuai standar Dosen lu!
|
@PostMapping("/logout")
|
||||||
ApiResponse<Map<String, String>> response = new ApiResponse<>(true, tokenData, "Login berhasil");
|
public ResponseEntity<ApiResponse<Void>> logout() {
|
||||||
|
authService.logout(SecurityHelper.getCurrentUserId());
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(null, "Logout berhasil"));
|
||||||
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
@PutMapping("/fcm-token")
|
||||||
|
public ResponseEntity<ApiResponse<Void>> updateFcmToken(
|
||||||
|
@RequestBody FcmTokenRequest req) {
|
||||||
|
authService.updateFcmToken(SecurityHelper.getCurrentUserId(), req.getFcmToken());
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(null, "FCM token diperbarui"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,77 +1,168 @@
|
|||||||
package com.walkguide.controller;
|
package com.walkguide.controller;
|
||||||
|
|
||||||
import com.walkguide.dto.ApiResponse;
|
import com.walkguide.dto.ApiResponse;
|
||||||
import com.walkguide.entity.User;
|
import com.walkguide.dto.request.*;
|
||||||
import com.walkguide.repository.UserRepository;
|
import com.walkguide.dto.response.*;
|
||||||
import com.walkguide.security.JwtUtil;
|
import com.walkguide.security.SecurityHelper;
|
||||||
import com.walkguide.service.MockDataService;
|
import com.walkguide.service.*;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
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.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
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;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/guardian")
|
@RequestMapping("/api/v1/guardian")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class GuardianController {
|
public class GuardianController {
|
||||||
|
|
||||||
private final MockDataService mockDataService;
|
private final GuardianDashboardService dashboardService;
|
||||||
private final UserRepository userRepository;
|
private final LocationService locationService;
|
||||||
private final JwtUtil jwtUtil;
|
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("/dashboard")
|
||||||
@GetMapping("/user-status")
|
public ResponseEntity<ApiResponse<DashboardResponse>> dashboard() {
|
||||||
public ResponseEntity<ApiResponse<Map<String, Object>>> getUserStatus() {
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
return ResponseEntity.ok(new ApiResponse<>(true,
|
dashboardService.getDashboard(SecurityHelper.getCurrentUserId()),
|
||||||
mockDataService.getUserStatus(),
|
"Dashboard Guardian"));
|
||||||
"Data status user berhasil diambil"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Setting Hardware Shortcut
|
@GetMapping("/user-location")
|
||||||
@PutMapping("/settings/shortcuts")
|
public ResponseEntity<ApiResponse<LocationResponse>> userLocation() {
|
||||||
public ResponseEntity<ApiResponse<Map<String, Object>>> updateShortcuts(
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
@RequestBody Map<String, Object> request) {
|
locationService.getLastLocationForGuardian(SecurityHelper.getCurrentUserId()).orElse(null),
|
||||||
Map<String, Object> updated = mockDataService.updateShortcuts(request);
|
"Lokasi terakhir user"));
|
||||||
return ResponseEntity.ok(new ApiResponse<>(true, updated, "Shortcut berhasil diperbarui"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Setting Sensitivitas AI
|
@GetMapping("/location-history")
|
||||||
@PutMapping("/settings/ai")
|
public ResponseEntity<ApiResponse<Page<LocationResponse>>> locationHistory(
|
||||||
public ResponseEntity<ApiResponse<Map<String, Object>>> updateAiSettings(
|
@RequestParam(defaultValue = "0") int page,
|
||||||
@RequestBody Map<String, Object> request) {
|
@RequestParam(defaultValue = "50") int size) {
|
||||||
Map<String, Object> updated = mockDataService.updateAiSettings(request);
|
// Guardian lihat location history user yang dipair
|
||||||
return ResponseEntity.ok(new ApiResponse<>(true, updated, "Setting AI berhasil diperbarui"));
|
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("/activity-logs")
|
||||||
@GetMapping("/my-user")
|
public ResponseEntity<ApiResponse<Page<ActivityLogResponse>>> activityLogs(
|
||||||
public ResponseEntity<ApiResponse<Map<String, Object>>> getMyConnectedUser(
|
@RequestParam(defaultValue = "0") int page,
|
||||||
HttpServletRequest request) {
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
String token = request.getHeader("Authorization").substring(7);
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
String email = jwtUtil.extractUsername(token);
|
activityLogService.getLogsForGuardian(SecurityHelper.getCurrentUserId(),
|
||||||
|
PageRequest.of(page, size)), "Log aktivitas user"));
|
||||||
User guardian = userRepository.findByEmail(email)
|
|
||||||
.orElseThrow(() -> new RuntimeException("Guardian tidak ditemukan"));
|
|
||||||
|
|
||||||
User connectedUser = guardian.getConnectedTo();
|
|
||||||
if (connectedUser == null) {
|
|
||||||
return ResponseEntity.ok(new ApiResponse<>(true,
|
|
||||||
Map.of("message", "Belum ada user yang terhubung"),
|
|
||||||
"Tidak ada koneksi"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Object> data = Map.of(
|
@GetMapping("/obstacle-logs")
|
||||||
"userId", connectedUser.getId(),
|
public ResponseEntity<ApiResponse<Page<ObstacleLogResponse>>> obstacleLogs(
|
||||||
"userEmail", connectedUser.getEmail(),
|
@RequestParam(defaultValue = "0") int page,
|
||||||
"connectedSince", connectedUser.getCreatedAt().toString()
|
@RequestParam(defaultValue = "20") int size) {
|
||||||
);
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
return ResponseEntity.ok(new ApiResponse<>(true, data,
|
obstacleLogService.getObstacleLogs(SecurityHelper.getCurrentUserId(),
|
||||||
"Data user yang dipantau berhasil diambil"));
|
PageRequest.of(page, size)), "Log obstacle user"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/notifications/send")
|
||||||
|
public ResponseEntity<ApiResponse<NotificationResponse>> sendNotif(
|
||||||
|
@RequestBody SendNotificationRequest req) {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
notificationService.sendNotification(SecurityHelper.getCurrentUserId(), req),
|
||||||
|
"Notifikasi terkirim"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/sos-events")
|
||||||
|
public ResponseEntity<ApiResponse<Page<SosEventResponse>>> 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<ApiResponse<SosEventResponse>> acknowledgeSos(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
sosService.acknowledgeSos(SecurityHelper.getCurrentUserId(), id),
|
||||||
|
"SOS diakui"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/ai-config")
|
||||||
|
public ResponseEntity<ApiResponse<AiConfigResponse>> getAiConfig() {
|
||||||
|
// Guardian lihat config user yang dipair
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
aiConfigService.getConfig(SecurityHelper.getCurrentUserId()),
|
||||||
|
"AI config"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/ai-config")
|
||||||
|
public ResponseEntity<ApiResponse<AiConfigResponse>> updateAiConfig(
|
||||||
|
@RequestBody AiConfigUpdateRequest req) {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
aiConfigService.updateConfigByGuardian(SecurityHelper.getCurrentUserId(), req),
|
||||||
|
"AI config diperbarui"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/voice-commands")
|
||||||
|
public ResponseEntity<ApiResponse<?>> getVoiceCommands() {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
voiceCommandService.getAll(SecurityHelper.getCurrentUserId()),
|
||||||
|
"Voice commands"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/voice-commands")
|
||||||
|
public ResponseEntity<ApiResponse<VoiceCommandResponse>> updateVoiceCommand(
|
||||||
|
@RequestBody VoiceCommandUpdateRequest req) {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
voiceCommandService.updateByGuardian(SecurityHelper.getCurrentUserId(), req),
|
||||||
|
"Voice command diperbarui"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/shortcuts")
|
||||||
|
public ResponseEntity<ApiResponse<?>> getShortcuts() {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
hardwareShortcutService.getAll(SecurityHelper.getCurrentUserId()),
|
||||||
|
"Hardware shortcuts"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/geofence")
|
||||||
|
public ResponseEntity<ApiResponse<GeofenceResponse>> getGeofence() {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
geofenceService.getConfig(SecurityHelper.getCurrentUserId()),
|
||||||
|
"Geofence config"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/geofence")
|
||||||
|
public ResponseEntity<ApiResponse<GeofenceResponse>> updateGeofence(
|
||||||
|
@RequestBody GeofenceConfigRequest req) {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
geofenceService.updateConfig(SecurityHelper.getCurrentUserId(), req),
|
||||||
|
"Geofence diperbarui"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/user-settings")
|
||||||
|
public ResponseEntity<ApiResponse<UserSettingsResponse>> getUserSettings() {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
userSettingsService.getSettings(SecurityHelper.getCurrentUserId()),
|
||||||
|
"User settings"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/user-settings")
|
||||||
|
public ResponseEntity<ApiResponse<UserSettingsResponse>> updateUserSettings(
|
||||||
|
@RequestBody UserSettingsUpdateRequest req) {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
userSettingsService.updateSettings(SecurityHelper.getCurrentUserId(), req),
|
||||||
|
"User settings diperbarui"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<ApiResponse<PairingStatusResponse>> 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<ApiResponse<PairingStatusResponse>> 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<ApiResponse<Void>> unpair() {
|
||||||
|
pairingService.unpair(SecurityHelper.getCurrentUserId());
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(null, "Pairing diakhiri"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/status")
|
||||||
|
public ResponseEntity<ApiResponse<PairingStatusResponse>> 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,60 +1,171 @@
|
|||||||
package com.walkguide.controller;
|
package com.walkguide.controller;
|
||||||
|
|
||||||
import com.walkguide.dto.ApiResponse;
|
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.repository.UserRepository;
|
||||||
import com.walkguide.security.JwtUtil;
|
import com.walkguide.security.SecurityHelper;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import com.walkguide.service.*;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
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 java.util.Map;
|
import java.util.List;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/user")
|
@RequestMapping("/api/v1/user")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class UserController {
|
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 UserRepository userRepository;
|
||||||
private final JwtUtil jwtUtil;
|
|
||||||
|
|
||||||
// Sinyal Darurat (Voice Command)
|
@GetMapping("/profile")
|
||||||
@PostMapping("/emergency")
|
public ResponseEntity<ApiResponse<?>> getProfile() {
|
||||||
public ResponseEntity<ApiResponse<String>> triggerEmergency(
|
Long userId = SecurityHelper.getCurrentUserId();
|
||||||
@RequestBody Map<String, Object> request) {
|
var user = userRepository.findById(userId)
|
||||||
String triggerType = (String) request.get("triggerType");
|
|
||||||
return ResponseEntity.ok(new ApiResponse<>(true,
|
|
||||||
"Darurat Terkirim",
|
|
||||||
"Guardian telah diberi peringatan via: " + triggerType));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lihat Guardian yang terhubung ke User ini
|
|
||||||
@GetMapping("/my-guardian")
|
|
||||||
public ResponseEntity<ApiResponse<Map<String, Object>>> getMyGuardian(
|
|
||||||
HttpServletRequest request) {
|
|
||||||
String token = request.getHeader("Authorization").substring(7);
|
|
||||||
String email = jwtUtil.extractUsername(token);
|
|
||||||
|
|
||||||
User user = userRepository.findByEmail(email)
|
|
||||||
.orElseThrow(() -> new RuntimeException("User tidak ditemukan"));
|
.orElseThrow(() -> new RuntimeException("User tidak ditemukan"));
|
||||||
|
var profile = java.util.Map.of(
|
||||||
User guardian = user.getConnectedTo();
|
"id", user.getId(),
|
||||||
if (guardian == null) {
|
"email", user.getEmail(),
|
||||||
return ResponseEntity.ok(new ApiResponse<>(true,
|
"displayName", user.getDisplayName() != null ? user.getDisplayName() : "",
|
||||||
Map.of("message", "Belum ada guardian yang terhubung"),
|
"role", user.getRole(),
|
||||||
"Tidak ada koneksi"));
|
"uniqueUserId", user.getUniqueUserId() != null ? user.getUniqueUserId() : ""
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Object> data = Map.of(
|
|
||||||
"guardianId", guardian.getId(),
|
|
||||||
"guardianEmail", guardian.getEmail()
|
|
||||||
);
|
);
|
||||||
return ResponseEntity.ok(new ApiResponse<>(true, data,
|
return ResponseEntity.ok(ApiResponse.ok(profile, "Profil user"));
|
||||||
"Data guardian berhasil diambil"));
|
}
|
||||||
|
|
||||||
|
@GetMapping("/settings")
|
||||||
|
public ResponseEntity<ApiResponse<UserSettingsResponse>> getSettings() {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
userSettingsService.getSettings(SecurityHelper.getCurrentUserId()),
|
||||||
|
"Settings user"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/settings")
|
||||||
|
public ResponseEntity<ApiResponse<UserSettingsResponse>> updateSettings(
|
||||||
|
@RequestBody UserSettingsUpdateRequest req) {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
userSettingsService.updateSettings(SecurityHelper.getCurrentUserId(), req),
|
||||||
|
"Settings diperbarui"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/voice-commands")
|
||||||
|
public ResponseEntity<ApiResponse<List<VoiceCommandResponse>>> getVoiceCommands() {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
voiceCommandService.getAll(SecurityHelper.getCurrentUserId()),
|
||||||
|
"Voice commands"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/shortcuts")
|
||||||
|
public ResponseEntity<ApiResponse<List<HardwareShortcutResponse>>> getShortcuts() {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
hardwareShortcutService.getAll(SecurityHelper.getCurrentUserId()),
|
||||||
|
"Hardware shortcuts"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/shortcuts")
|
||||||
|
public ResponseEntity<ApiResponse<HardwareShortcutResponse>> updateShortcut(
|
||||||
|
@RequestBody HardwareShortcutUpdateRequest req) {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
hardwareShortcutService.update(SecurityHelper.getCurrentUserId(), req),
|
||||||
|
"Shortcut diperbarui"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/ai-config")
|
||||||
|
public ResponseEntity<ApiResponse<AiConfigResponse>> getAiConfig() {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
aiConfigService.getConfig(SecurityHelper.getCurrentUserId()),
|
||||||
|
"AI config"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/location")
|
||||||
|
public ResponseEntity<ApiResponse<LocationResponse>> updateLocation(
|
||||||
|
@RequestBody LocationUpdateRequest req) {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
locationService.updateLocation(SecurityHelper.getCurrentUserId(), req),
|
||||||
|
"Lokasi diperbarui"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/obstacle")
|
||||||
|
public ResponseEntity<ApiResponse<ObstacleLogResponse>> logObstacle(
|
||||||
|
@RequestBody ObstacleLogRequest req) {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
obstacleLogService.saveObstacle(SecurityHelper.getCurrentUserId(), req),
|
||||||
|
"Obstacle dicatat"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/sos")
|
||||||
|
public ResponseEntity<ApiResponse<SosEventResponse>> triggerSos(
|
||||||
|
@RequestBody SosRequest req) {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
sosService.triggerSos(SecurityHelper.getCurrentUserId(), req),
|
||||||
|
"SOS dikirim! Guardian sudah diberitahu."));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/activity-logs")
|
||||||
|
public ResponseEntity<ApiResponse<Page<ActivityLogResponse>>> 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<ApiResponse<Page<NotificationResponse>>> 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<ApiResponse<Long>> getUnreadCount() {
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(
|
||||||
|
notificationService.getUnreadCount(SecurityHelper.getCurrentUserId()),
|
||||||
|
"Jumlah notifikasi belum dibaca"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/notifications/mark-all-read")
|
||||||
|
public ResponseEntity<ApiResponse<Void>> markAllRead() {
|
||||||
|
notificationService.markAllRead(SecurityHelper.getCurrentUserId());
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(null, "Semua notifikasi ditandai sudah dibaca"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/notifications/{id}/read")
|
||||||
|
public ResponseEntity<ApiResponse<Void>> markOneRead(@PathVariable Long id) {
|
||||||
|
notificationService.markOneRead(id);
|
||||||
|
return ResponseEntity.ok(ApiResponse.ok(null, "Notifikasi ditandai sudah dibaca"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/walkguide/start")
|
||||||
|
public ResponseEntity<ApiResponse<Void>> 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<ApiResponse<Void>> 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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,6 @@ public class ApiResponse<T> {
|
|||||||
private String errorCode;
|
private String errorCode;
|
||||||
private String timestamp;
|
private String timestamp;
|
||||||
|
|
||||||
// Constructor buat Sukses
|
|
||||||
public ApiResponse(boolean success, T data, String message) {
|
public ApiResponse(boolean success, T data, String message) {
|
||||||
this.success = success;
|
this.success = success;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
@ -17,7 +16,6 @@ public class ApiResponse<T> {
|
|||||||
this.timestamp = Instant.now().toString();
|
this.timestamp = Instant.now().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constructor buat Error
|
|
||||||
public ApiResponse(boolean success, String errorCode, String message) {
|
public ApiResponse(boolean success, String errorCode, String message) {
|
||||||
this.success = success;
|
this.success = success;
|
||||||
this.errorCode = errorCode;
|
this.errorCode = errorCode;
|
||||||
@ -25,19 +23,22 @@ public class ApiResponse<T> {
|
|||||||
this.timestamp = Instant.now().toString();
|
this.timestamp = Instant.now().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getter Setter manual
|
public static <T> ApiResponse<T> ok(T data, String message) {
|
||||||
|
return new ApiResponse<>(true, data, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> error(String errorCode, String message) {
|
||||||
|
return new ApiResponse<>(false, errorCode, message);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isSuccess() { return success; }
|
public boolean isSuccess() { return success; }
|
||||||
public void setSuccess(boolean success) { this.success = success; }
|
|
||||||
|
|
||||||
public T getData() { return data; }
|
public T getData() { return data; }
|
||||||
public void setData(T data) { this.data = data; }
|
|
||||||
|
|
||||||
public String getMessage() { return message; }
|
public String getMessage() { return message; }
|
||||||
public void setMessage(String message) { this.message = message; }
|
|
||||||
|
|
||||||
public String getErrorCode() { return errorCode; }
|
public String getErrorCode() { return errorCode; }
|
||||||
public void setErrorCode(String errorCode) { this.errorCode = errorCode; }
|
|
||||||
|
|
||||||
public String getTimestamp() { return timestamp; }
|
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; }
|
public void setTimestamp(String timestamp) { this.timestamp = timestamp; }
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.walkguide.dto.request;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class FcmTokenRequest {
|
||||||
|
private String fcmToken;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package com.walkguide.dto.request;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class PairingResponseRequest {
|
||||||
|
private Long pairingId;
|
||||||
|
private boolean accept;
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package com.walkguide.dto.request;
|
||||||
|
import lombok.Data;
|
||||||
|
@Data
|
||||||
|
public class RefreshTokenRequest {
|
||||||
|
private String refreshToken;
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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<ActivityLogResponse> recentActivity;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,22 +1,8 @@
|
|||||||
package com.walkguide.entity;
|
package com.walkguide.entity;
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.*;
|
||||||
import jakarta.persistence.Entity;
|
import lombok.*;
|
||||||
import jakarta.persistence.FetchType;
|
import java.time.LocalDateTime;
|
||||||
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;
|
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "users")
|
@Table(name = "users")
|
||||||
@ -37,27 +23,33 @@ public class User {
|
|||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private String role;
|
private String role; // ROLE_GUARDIAN atau ROLE_USER
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
// 12-char alphanumeric ID khusus untuk ROLE_USER (seperti Discord ID)
|
||||||
@JoinColumn(name = "connected_to")
|
@Column(name = "unique_user_id", unique = true, length = 12)
|
||||||
private User connectedTo;
|
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)
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
private Instant createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
@Column(name = "updated_at", nullable = false)
|
@Column(name = "updated_at", nullable = false)
|
||||||
private Instant updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
@PrePersist
|
@PrePersist
|
||||||
public void onCreate() {
|
protected void onCreate() {
|
||||||
Instant now = Instant.now();
|
createdAt = LocalDateTime.now();
|
||||||
this.createdAt = now;
|
updatedAt = LocalDateTime.now();
|
||||||
this.updatedAt = now;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreUpdate
|
@PreUpdate
|
||||||
public void onUpdate() {
|
protected void onUpdate() {
|
||||||
this.updatedAt = Instant.now();
|
updatedAt = LocalDateTime.now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package com.walkguide.enums;
|
||||||
|
|
||||||
|
public enum HardwareShortcutKey {
|
||||||
|
CALL_GUARDIAN,
|
||||||
|
START_WALKGUIDE,
|
||||||
|
SEND_SOS,
|
||||||
|
STOP_WALKGUIDE,
|
||||||
|
OPEN_NOTIFICATION
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.walkguide.enums;
|
||||||
|
|
||||||
|
public enum NotificationType {
|
||||||
|
TEXT, VOICE_NOTE
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.walkguide.enums;
|
||||||
|
|
||||||
|
public enum PairingStatus {
|
||||||
|
PENDING, ACTIVE, REJECTED
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.walkguide.enums;
|
||||||
|
|
||||||
|
public enum SosStatus {
|
||||||
|
TRIGGERED, ACKNOWLEDGED, RESOLVED
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -1,27 +1,43 @@
|
|||||||
package com.walkguide.exception;
|
package com.walkguide.exception;
|
||||||
|
|
||||||
|
import com.walkguide.dto.ApiResponse;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
|
||||||
import com.walkguide.dto.ApiResponse;
|
|
||||||
|
|
||||||
@ControllerAdvice
|
@ControllerAdvice
|
||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
// Nangkep error kalau login gagal / user gak ketemu
|
@ExceptionHandler(ResourceNotFoundException.class)
|
||||||
@ExceptionHandler(RuntimeException.class)
|
public ResponseEntity<ApiResponse<Object>> handleNotFound(ResourceNotFoundException ex) {
|
||||||
public ResponseEntity<ApiResponse<Object>> handleRuntimeException(RuntimeException ex) {
|
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||||
ApiResponse<Object> response = new ApiResponse<>(false, "AUTH_ERROR", ex.getMessage());
|
.body(ApiResponse.error("NOT_FOUND", ex.getMessage()));
|
||||||
return ResponseEntity.status(401).body(response);
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(PairingException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Object>> handlePairing(PairingException ex) {
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(ApiResponse.error("PAIRING_ERROR", ex.getMessage()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nangkep error dari Validasi (misal email format salah)
|
|
||||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
public ResponseEntity<ApiResponse<Object>> handleValidationException(MethodArgumentNotValidException ex) {
|
public ResponseEntity<ApiResponse<Object>> handleValidation(MethodArgumentNotValidException ex) {
|
||||||
String errorMsg = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
|
String msg = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
|
||||||
ApiResponse<Object> response = new ApiResponse<>(false, "VALIDATION_ERROR", errorMsg);
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
||||||
return ResponseEntity.status(400).body(response);
|
.body(ApiResponse.error("VALIDATION_ERROR", msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(RuntimeException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Object>> handleRuntime(RuntimeException ex) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||||
|
.body(ApiResponse.error("AUTH_ERROR", ex.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<ApiResponse<Object>> handleGeneric(Exception ex) {
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body(ApiResponse.error("INTERNAL_ERROR", "Terjadi kesalahan internal"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package com.walkguide.exception;
|
||||||
|
|
||||||
|
public class PairingException extends RuntimeException {
|
||||||
|
public PairingException(String message) { super(message); }
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.walkguide.exception;
|
||||||
|
|
||||||
|
public class ResourceNotFoundException extends RuntimeException {
|
||||||
|
public ResourceNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<ActivityLog, Long> {
|
||||||
|
Page<ActivityLog> findByUser_IdOrderByCreatedAtDesc(Long userId, Pageable pageable);
|
||||||
|
List<ActivityLog> findByUser_IdAndCreatedAtBetween(Long userId, LocalDateTime from, LocalDateTime to);
|
||||||
|
List<ActivityLog> findByUser_IdAndLogTypeOrderByCreatedAtDesc(Long userId, ActivityLogType logType, Pageable pageable);
|
||||||
|
}
|
||||||
@ -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<AiConfig, Long> {
|
||||||
|
Optional<AiConfig> findByUserId(Long userId);
|
||||||
|
}
|
||||||
@ -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<GeofenceConfig, Long> {
|
||||||
|
Optional<GeofenceConfig> findByUserId(Long userId);
|
||||||
|
}
|
||||||
@ -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<GuardianNotification, Long> {
|
||||||
|
Page<GuardianNotification> findByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable);
|
||||||
|
long countByUserIdAndIsReadFalse(Long userId);
|
||||||
|
List<GuardianNotification> findByUserIdAndIsReadFalseOrderByCreatedAtAsc(Long userId);
|
||||||
|
}
|
||||||
@ -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<HardwareShortcut, Long> {
|
||||||
|
List<HardwareShortcut> findByUserId(Long userId);
|
||||||
|
void deleteByUserId(Long userId);
|
||||||
|
}
|
||||||
@ -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<LocationHistory, Long> {
|
||||||
|
Optional<LocationHistory> findTopByUserIdOrderByCreatedAtDesc(Long userId);
|
||||||
|
Page<LocationHistory> findByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable);
|
||||||
|
}
|
||||||
@ -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<ObstacleLog, Long> {
|
||||||
|
Page<ObstacleLog> findByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable);
|
||||||
|
}
|
||||||
@ -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<PairingRelation, Long> {
|
||||||
|
Optional<PairingRelation> findByGuardian_IdAndStatus(Long guardianId, PairingStatus status);
|
||||||
|
Optional<PairingRelation> findByUser_IdAndStatus(Long userId, PairingStatus status);
|
||||||
|
Optional<PairingRelation> findByGuardian_Id(Long guardianId);
|
||||||
|
Optional<PairingRelation> findByUser_Id(Long userId);
|
||||||
|
boolean existsByGuardian_IdAndStatus(Long guardianId, PairingStatus status);
|
||||||
|
boolean existsByUser_IdAndStatus(Long userId, PairingStatus status);
|
||||||
|
}
|
||||||
@ -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<RefreshToken, Long> {
|
||||||
|
Optional<RefreshToken> findByToken(String token);
|
||||||
|
void deleteByUserId(Long userId);
|
||||||
|
}
|
||||||
@ -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<SosEvent, Long> {
|
||||||
|
Page<SosEvent> findByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable);
|
||||||
|
}
|
||||||
@ -2,20 +2,12 @@ package com.walkguide.repository;
|
|||||||
|
|
||||||
import com.walkguide.entity.User;
|
import com.walkguide.entity.User;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
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 org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface UserRepository extends JpaRepository<User, Long> {
|
public interface UserRepository extends JpaRepository<User, Long> {
|
||||||
|
|
||||||
Optional<User> findByEmail(String email);
|
Optional<User> findByEmail(String email);
|
||||||
|
Optional<User> findByUniqueUserId(String uniqueUserId);
|
||||||
@Query("SELECT u FROM User u WHERE u.connectedTo.id = :guardianId AND u.role = 'ROLE_USER'")
|
boolean existsByEmail(String email);
|
||||||
Optional<User> findUserByGuardianId(@Param("guardianId") Long guardianId);
|
|
||||||
|
|
||||||
@Query("SELECT u FROM User u WHERE u.connectedTo.id = :userId AND u.role = 'ROLE_GUARDIAN'")
|
|
||||||
Optional<User> findGuardianByUserId(@Param("userId") Long userId);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<UserSettings, Long> {
|
||||||
|
Optional<UserSettings> findByUserId(Long userId);
|
||||||
|
}
|
||||||
@ -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<VoiceCommandConfig, Long> {
|
||||||
|
List<VoiceCommandConfig> findByUserId(Long userId);
|
||||||
|
Optional<VoiceCommandConfig> findByUserIdAndCommandKey(Long userId, VoiceCommandKey commandKey);
|
||||||
|
void deleteByUserId(Long userId);
|
||||||
|
}
|
||||||
@ -21,35 +21,38 @@ public class JwtAuthFilter extends OncePerRequestFilter {
|
|||||||
private final JwtUtil jwtUtil;
|
private final JwtUtil jwtUtil;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
protected void doFilterInternal(HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
FilterChain filterChain)
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
|
|
||||||
final String authHeader = request.getHeader("Authorization");
|
final String authHeader = request.getHeader("Authorization");
|
||||||
|
|
||||||
// Kalau gak ada token, lewatin aja (biar dicegat sama SecurityConfig)
|
|
||||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Potong tulisan "Bearer "
|
|
||||||
final String jwt = authHeader.substring(7);
|
final String jwt = authHeader.substring(7);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ambil email & role dari token lu
|
if (jwtUtil.isTokenValid(jwt)) {
|
||||||
String email = jwtUtil.extractUsername(jwt);
|
String email = jwtUtil.extractUsername(jwt);
|
||||||
String role = jwtUtil.extractRole(jwt); // Pastiin JwtUtil lu punya fungsi extractRole!
|
String role = jwtUtil.extractRole(jwt);
|
||||||
|
Long userId = jwtUtil.extractUserId(jwt);
|
||||||
|
|
||||||
// Daftarin user ini ke sistem keamanan Spring
|
|
||||||
if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||||
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
|
UsernamePasswordAuthenticationToken authToken =
|
||||||
email, null, Collections.singletonList(new SimpleGrantedAuthority(role))
|
new UsernamePasswordAuthenticationToken(
|
||||||
|
email,
|
||||||
|
userId, // credentials slot dipakai untuk simpan userId
|
||||||
|
Collections.singletonList(new SimpleGrantedAuthority(role))
|
||||||
);
|
);
|
||||||
SecurityContextHolder.getContext().setAuthentication(authToken);
|
SecurityContextHolder.getContext().setAuthentication(authToken);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Token kadaluarsa / rusak
|
logger.warn("JWT processing error: " + e.getMessage());
|
||||||
System.out.println("JWT Error: " + e.getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
|
|||||||
@ -1,39 +1,76 @@
|
|||||||
package com.walkguide.security;
|
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.Claims;
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
import io.jsonwebtoken.SignatureAlgorithm;
|
import io.jsonwebtoken.SignatureAlgorithm;
|
||||||
import io.jsonwebtoken.io.Decoders;
|
import io.jsonwebtoken.io.Decoders;
|
||||||
import io.jsonwebtoken.security.Keys;
|
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
|
@Component
|
||||||
public class JwtUtil {
|
public class JwtUtil {
|
||||||
|
|
||||||
// Kunci rahasia buat enkripsi & dekripsi token (Minimal 256-bit)
|
@Value("${jwt.secret}")
|
||||||
private static final String SECRET_KEY = "404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970";
|
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<String, Object> 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) {
|
public String extractUsername(String token) {
|
||||||
return extractClaim(token, Claims::getSubject);
|
return extractClaim(token, Claims::getSubject);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fungsi tambahan buat ngebongkar Role
|
|
||||||
public String extractRole(String token) {
|
public String extractRole(String token) {
|
||||||
Claims claims = extractAllClaims(token);
|
return extractAllClaims(token).get("role", String.class);
|
||||||
return claims.get("role", String.class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
|
public Long extractUserId(String token) {
|
||||||
final Claims claims = extractAllClaims(token);
|
Object userId = extractAllClaims(token).get("userId");
|
||||||
return claimsResolver.apply(claims);
|
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> T extractClaim(String token, Function<Claims, T> resolver) {
|
||||||
|
return resolver.apply(extractAllClaims(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Claims extractAllClaims(String token) {
|
private Claims extractAllClaims(String token) {
|
||||||
@ -45,24 +82,7 @@ public class JwtUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Key getSignInKey() {
|
private Key getSignInKey() {
|
||||||
byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY);
|
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
|
||||||
return Keys.hmacShaKeyFor(keyBytes);
|
return Keys.hmacShaKeyFor(keyBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fungsi lama lu buat bikin token
|
|
||||||
public String generateToken(String email, String role) {
|
|
||||||
Map<String, Object> claims = new HashMap<>();
|
|
||||||
claims.put("role", role);
|
|
||||||
return createToken(claims, email);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String createToken(Map<String, Object> 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<ActivityLogResponse> getLogs(Long userId, Pageable pageable) {
|
||||||
|
return activityLogRepository
|
||||||
|
.findByUser_IdOrderByCreatedAtDesc(userId, pageable)
|
||||||
|
.map(this::toResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page<ActivityLogResponse> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,48 +1,165 @@
|
|||||||
package com.walkguide.service;
|
package com.walkguide.service;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import com.walkguide.dto.request.LoginRequest;
|
||||||
import java.util.Map;
|
import com.walkguide.dto.request.RegisterRequest;
|
||||||
|
import com.walkguide.dto.response.AuthDataResponse;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
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.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import com.walkguide.dto.AuthRequest;
|
import java.security.SecureRandom;
|
||||||
import com.walkguide.entity.User;
|
import java.time.LocalDateTime;
|
||||||
import com.walkguide.repository.UserRepository;
|
|
||||||
import com.walkguide.security.JwtUtil;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AuthService {
|
public class AuthService {
|
||||||
|
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
private final RefreshTokenRepository refreshTokenRepository;
|
||||||
|
private final UserSettingsRepository userSettingsRepository;
|
||||||
|
private final ActivityLogService activityLogService;
|
||||||
private final JwtUtil jwtUtil;
|
private final JwtUtil jwtUtil;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
// Wajib panggil BCrypt biar bisa baca password enkripsi dari database
|
private static final String CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
|
private static final int ID_LENGTH = 12;
|
||||||
|
|
||||||
public Map<String, String> login(AuthRequest request) {
|
@Transactional
|
||||||
// 1. Cari user di database
|
public AuthDataResponse register(RegisterRequest req) {
|
||||||
User user = userRepository.findByEmail(request.getEmail())
|
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"));
|
.orElseThrow(() -> new RuntimeException("Email tidak terdaftar"));
|
||||||
|
|
||||||
// 2. Cocokin password (yang diketik VS yang dienkripsi di database)
|
if (!passwordEncoder.matches(req.getPassword(), user.getPassword())) {
|
||||||
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
|
|
||||||
throw new RuntimeException("Password salah");
|
throw new RuntimeException("Password salah");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Kalau bener, bikin Token
|
// Hapus refresh token lama
|
||||||
// Asumsi JwtUtil lu nerima parameter (email, role). Sesuaikan kalau beda!
|
refreshTokenRepository.deleteByUserId(user.getId());
|
||||||
String token = jwtUtil.generateToken(user.getEmail(), user.getRole());
|
|
||||||
|
|
||||||
// 4. Balikin ke Controller dalam bentuk Map
|
activityLogService.createLog(user, ActivityLogType.LOGIN, "User login", null);
|
||||||
Map<String, String> data = new HashMap<>();
|
|
||||||
data.put("token", token);
|
|
||||||
data.put("role", user.getRole());
|
|
||||||
|
|
||||||
return data;
|
return buildAuthResponse(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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<String, String> 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<String, String> data) {
|
||||||
|
// SOS dan incoming call pakai ini - sama untuk sekarang
|
||||||
|
sendToToken(fcmToken, title, body, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<HardwareShortcutResponse> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<LocationResponse> getLastLocation(Long userId) {
|
||||||
|
return locationHistoryRepository
|
||||||
|
.findTopByUserIdOrderByCreatedAtDesc(userId)
|
||||||
|
.map(this::toResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Untuk Guardian: ambil last location user yang dipair
|
||||||
|
public Optional<LocationResponse> getLastLocationForGuardian(Long guardianId) {
|
||||||
|
return pairingRelationRepository
|
||||||
|
.findByGuardian_IdAndStatus(guardianId, PairingStatus.ACTIVE)
|
||||||
|
.flatMap(p -> locationHistoryRepository
|
||||||
|
.findTopByUserIdOrderByCreatedAtDesc(p.getUser().getId()))
|
||||||
|
.map(this::toResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Page<LocationResponse> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<NotificationResponse> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<ObstacleLogResponse> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<VoiceCommandConfig> 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<HardwareShortcut> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<SosEventResponse> getSosEvents(Long userId, Pageable pageable) {
|
||||||
|
return sosEventRepository.findByUserIdOrderByCreatedAtDesc(userId, pageable)
|
||||||
|
.map(this::toResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guardian get SOS for their paired user
|
||||||
|
public Page<SosEventResponse> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<VoiceCommandKey, String> 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<VoiceCommandResponse> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
@ -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);
|
||||||
@ -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);
|
||||||
@ -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);
|
||||||
@ -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);
|
||||||
@ -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()
|
||||||
|
);
|
||||||
@ -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);
|
||||||
@ -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);
|
||||||
@ -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);
|
||||||
@ -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);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user