From 33616a9b07b84ea0ba2ecd4088bcff882f94bf88 Mon Sep 17 00:00:00 2001 From: Kavindi487 Date: Mon, 16 Mar 2026 10:52:23 +0530 Subject: [PATCH 01/12] updated --- IOT_service/pom.xml | 41 ++--- .../IOT_service/config/SecurityConfig.java | 36 ++++ .../controller/SensorDataController.java | 168 ++++++++++++------ .../IOT_service/filter/JwtAuthFilter.java | 80 +++++++++ .../IOT_service/model/DeviceRegistry.java | 24 +++ .../example/IOT_service/model/SensorData.java | 8 + .../repository/DeviceRegistryRepository.java | 11 ++ .../repository/SensorDataRepository.java | 31 ++-- .../service/SensorDataService.java | 41 +++-- .../src/main/resources/application.properties | 39 ++-- apigateway/src/main/resources/application.yml | 2 +- docker-compose.yml | 3 + .../controller/AuthController.java | 68 ++++--- .../com/example/mainservice/util/JwtUtil.java | 51 +++--- 14 files changed, 411 insertions(+), 192 deletions(-) create mode 100644 IOT_service/src/main/java/com/example/IOT_service/config/SecurityConfig.java create mode 100644 IOT_service/src/main/java/com/example/IOT_service/filter/JwtAuthFilter.java create mode 100644 IOT_service/src/main/java/com/example/IOT_service/model/DeviceRegistry.java create mode 100644 IOT_service/src/main/java/com/example/IOT_service/repository/DeviceRegistryRepository.java diff --git a/IOT_service/pom.xml b/IOT_service/pom.xml index 42ce3c8..53559d2 100644 --- a/IOT_service/pom.xml +++ b/IOT_service/pom.xml @@ -22,71 +22,56 @@ org.springframework.boot spring-boot-starter-web - - org.springframework.boot spring-boot-starter-data-jpa - - org.springframework.boot spring-boot-starter-validation - - org.springframework.boot spring-boot-starter-actuator - - + - com.h2database - h2 - runtime + org.springframework.boot + spring-boot-starter-security - - + org.postgresql postgresql 42.7.7 runtime - - + org.projectlombok lombok true - + org.springframework.kafka spring-kafka - + org.springframework.cloud spring-cloud-starter-netflix-eureka-client - - + - org.springframework.boot - spring-boot-starter-test - test + io.micrometer + micrometer-registry-prometheus - + org.springframework.boot - spring-boot-starter-actuator - - - io.micrometer - micrometer-registry-prometheus + spring-boot-starter-test + test diff --git a/IOT_service/src/main/java/com/example/IOT_service/config/SecurityConfig.java b/IOT_service/src/main/java/com/example/IOT_service/config/SecurityConfig.java new file mode 100644 index 0000000..f499f63 --- /dev/null +++ b/IOT_service/src/main/java/com/example/IOT_service/config/SecurityConfig.java @@ -0,0 +1,36 @@ +package com.example.IOT_service.config; + +import com.example.IOT_service.filter.JwtAuthFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtAuthFilter jwtAuthFilter; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .cors(cors -> cors.disable()) + .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + // Health check and actuator remain open + .requestMatchers("/api/sensordata/health", "/actuator/**").permitAll() + // Everything else requires a valid JWT (validated via mainservice) + .anyRequest().authenticated() + ) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} diff --git a/IOT_service/src/main/java/com/example/IOT_service/controller/SensorDataController.java b/IOT_service/src/main/java/com/example/IOT_service/controller/SensorDataController.java index 30a1474..697d117 100644 --- a/IOT_service/src/main/java/com/example/IOT_service/controller/SensorDataController.java +++ b/IOT_service/src/main/java/com/example/IOT_service/controller/SensorDataController.java @@ -2,125 +2,183 @@ import com.example.IOT_service.model.SensorData; import com.example.IOT_service.service.SensorDataService; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; import java.time.LocalDateTime; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; @RestController @RequestMapping("/api/sensordata") -@CrossOrigin(origins = "*") @RequiredArgsConstructor @Slf4j public class SensorDataController { private final SensorDataService service; - // POST endpoint - receives data from ESP32 + // SSE emitters keyed by userId — enables real-time push to frontend + private final ConcurrentHashMap emitters = new ConcurrentHashMap<>(); + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); + + // ── POST: ESP32 sends data ──────────────────────────────────────────────── @PostMapping - public ResponseEntity receiveSensorData(@RequestBody SensorData data) { + public ResponseEntity receiveSensorData(@RequestBody SensorData data, + HttpServletRequest request) { try { - log.info("Received sensor data: RoomTemp={}, Humidity={}, WaterTemp={}, BPM={}, AvgBPM={}, SpO2={}", - data.getRoomTemp(), data.getHumidity(), data.getWaterTempC(), - data.getBpm(), data.getAvgBpm(), data.getSpo2()); + Long userId = (Long) request.getAttribute("userId"); + data.setUserId(userId); SensorData saved = service.saveSensorData(data); + log.info("Saved sensor data id={} userId={} deviceId={}", saved.getId(), userId, saved.getDeviceId()); + + // Push to any connected SSE client for this user in real-time + pushToSseClient(userId, saved); return ResponseEntity.ok(Map.of( "success", true, - "message", "Data saved successfully", "id", saved.getId(), "timestamp", saved.getReceivedAt() )); - + } catch (IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(Map.of("success", false, "message", e.getMessage())); } catch (Exception e) { log.error("Error saving sensor data", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(Map.of( - "success", false, - "message", "Error: " + e.getMessage() - )); + .body(Map.of("success", false, "message", e.getMessage())); } } - // GET all data + // ── SSE: frontend subscribes here for real-time data ───────────────────── + // Usage: GET /api/sensordata/stream (with Authorization header) + @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter streamSensorData(HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + + // 5-minute timeout; frontend should reconnect automatically + SseEmitter emitter = new SseEmitter(5 * 60 * 1000L); + + emitters.put(userId, emitter); + + emitter.onCompletion(() -> emitters.remove(userId)); + emitter.onTimeout(() -> { + emitters.remove(userId); + emitter.complete(); + }); + emitter.onError(e -> emitters.remove(userId)); + + // Send current latest reading immediately on connect + service.getLatestReadingForUser(userId).ifPresent(data -> { + try { + emitter.send(SseEmitter.event().name("sensorData").data(data)); + } catch (IOException ignored) {} + }); + + // Heartbeat every 30s to keep the connection alive through proxies + scheduler.scheduleAtFixedRate(() -> { + try { + if (emitters.containsKey(userId)) { + emitter.send(SseEmitter.event().name("heartbeat").data("ping")); + } + } catch (Exception e) { + emitters.remove(userId); + } + }, 30, 30, TimeUnit.SECONDS); + + return emitter; + } + + // ── GET: authenticated user's own data only ─────────────────────────────── @GetMapping - public ResponseEntity> getAllData() { - return ResponseEntity.ok(service.getAllData()); + public ResponseEntity> getMyData(HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + return ResponseEntity.ok(service.getAllDataForUser(userId)); } - // GET latest N records @GetMapping("/latest/{limit}") - public ResponseEntity> getLatestData(@PathVariable int limit) { - return ResponseEntity.ok(service.getLatestData(limit)); + public ResponseEntity> getLatestData(@PathVariable int limit, + HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + return ResponseEntity.ok(service.getLatestDataForUser(userId, limit)); } - // GET most recent single reading @GetMapping("/current") - public ResponseEntity getCurrentReading() { - return service.getLatestReading() + public ResponseEntity getCurrentReading(HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + return service.getLatestReadingForUser(userId) .map(data -> ResponseEntity.ok((Object) data)) - .orElse(ResponseEntity.ok(Map.of("message", "No data available"))); + .orElse(ResponseEntity.ok(Map.of("message", "No data available yet"))); } - // GET data from last N hours @GetMapping("/recent/{hours}") - public ResponseEntity> getRecentData(@PathVariable int hours) { - return ResponseEntity.ok(service.getRecentData(hours)); + public ResponseEntity> getRecentData(@PathVariable int hours, + HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + return ResponseEntity.ok(service.getRecentDataForUser(userId, hours)); + } + + @GetMapping("/latest/{limit}/with-status") + public ResponseEntity getLatestWithStatus(@PathVariable int limit, + HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + List data = service.getLatestDataForUser(userId, limit); + if (data.isEmpty()) { + return ResponseEntity.ok(Map.of("data", List.of(), "stale", true, + "ageSeconds", null, "latestTimestamp", null)); + } + SensorData latest = data.get(0); + long ageSeconds = java.time.Duration.between(latest.getReceivedAt(), LocalDateTime.now()).getSeconds(); + return ResponseEntity.ok(Map.of( + "data", data, + "latestTimestamp", latest.getReceivedAt(), + "ageSeconds", ageSeconds, + "stale", ageSeconds > 30 + )); } - // DELETE old data @DeleteMapping("/cleanup/{days}") public ResponseEntity cleanupOldData(@PathVariable int days) { try { service.deleteOldData(days); - return ResponseEntity.ok(Map.of( - "success", true, - "message", "Deleted data older than " + days + " days" - )); + return ResponseEntity.ok(Map.of("success", true, + "message", "Deleted data older than " + days + " days")); } catch (Exception e) { - log.error("Error cleaning up data", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(Map.of("success", false, "message", e.getMessage())); } } - // Health check + // ── Health check (public, no auth needed) ──────────────────────────────── @GetMapping("/health") public ResponseEntity healthCheck() { return ResponseEntity.ok(Map.of( "status", "healthy", "timestamp", LocalDateTime.now(), - "service", "Sensor Data API" + "service", "IoT Sensor Data API", + "activeStreams", emitters.size() )); } - @GetMapping("/latest/{limit}/with-status") - public ResponseEntity getLatestWithStatus(@PathVariable int limit) { - List data = service.getLatestData(limit); - if (data.isEmpty()) { - return ResponseEntity.ok(Map.of( - "data", List.of(), - "stale", true, - "ageSeconds", null, - "latestTimestamp", null - )); + // ── Internal helper: push new reading to the user's SSE stream ─────────── + private void pushToSseClient(Long userId, SensorData data) { + SseEmitter emitter = emitters.get(userId); + if (emitter == null) return; + try { + emitter.send(SseEmitter.event().name("sensorData").data(data)); + } catch (Exception e) { + emitters.remove(userId); } - - SensorData latest = data.get(0); // because repository returns DESC - long ageSeconds = java.time.Duration.between(latest.getReceivedAt(), LocalDateTime.now()).getSeconds(); - boolean stale = ageSeconds > 30; // choose threshold - - return ResponseEntity.ok(Map.of( - "data", data, - "latestTimestamp", latest.getReceivedAt(), - "ageSeconds", ageSeconds, - "stale", stale - )); } - } diff --git a/IOT_service/src/main/java/com/example/IOT_service/filter/JwtAuthFilter.java b/IOT_service/src/main/java/com/example/IOT_service/filter/JwtAuthFilter.java new file mode 100644 index 0000000..b121fc1 --- /dev/null +++ b/IOT_service/src/main/java/com/example/IOT_service/filter/JwtAuthFilter.java @@ -0,0 +1,80 @@ +package com.example.IOT_service.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Map; + +@Component +@Slf4j +public class JwtAuthFilter extends OncePerRequestFilter { + + // Points to mainservice — uses container name inside Docker, localhost outside + @Value("${auth.service.url:http://mainservice:8080}") + private String authServiceUrl; + + private final RestTemplate restTemplate = new RestTemplate(); + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain chain) + throws ServletException, IOException { + + String path = request.getRequestURI(); + + // Skip auth for health / actuator endpoints + if (path.contains("/health") || path.contains("/actuator")) { + chain.doFilter(request, response); + return; + } + + String authHeader = request.getHeader("Authorization"); + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + sendUnauthorized(response, "Missing Authorization header — attach Bearer token"); + return; + } + + try { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", authHeader); + HttpEntity entity = new HttpEntity<>(headers); + + // Call mainservice /api/auth/validate + ResponseEntity authResponse = restTemplate.exchange( + authServiceUrl + "/api/auth/validate", + HttpMethod.GET, + entity, + Map.class + ); + + if (authResponse.getStatusCode().is2xxSuccessful() && authResponse.getBody() != null) { + Long userId = Long.valueOf(authResponse.getBody().get("userId").toString()); + // Make userId available to every controller via request attribute + request.setAttribute("userId", userId); + chain.doFilter(request, response); + } else { + sendUnauthorized(response, "Token validation failed"); + } + + } catch (Exception e) { + log.error("JWT validation error calling mainservice: {}", e.getMessage()); + sendUnauthorized(response, "Invalid or expired token"); + } + } + + private void sendUnauthorized(HttpServletResponse response, String message) throws IOException { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json"); + response.getWriter().write("{\"error\": \"" + message + "\"}"); + } +} diff --git a/IOT_service/src/main/java/com/example/IOT_service/model/DeviceRegistry.java b/IOT_service/src/main/java/com/example/IOT_service/model/DeviceRegistry.java new file mode 100644 index 0000000..aa2612a --- /dev/null +++ b/IOT_service/src/main/java/com/example/IOT_service/model/DeviceRegistry.java @@ -0,0 +1,24 @@ +package com.example.IOT_service.model; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "device_registry") +@Data +@NoArgsConstructor +public class DeviceRegistry { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + // Must match the hardcoded DEVICE_ID on the ESP32 + @Column(name = "device_id", unique = true, nullable = false) + private String deviceId; + + // References user.id from mainservice (cross-service — store as plain Long) + @Column(name = "user_id", nullable = false) + private Long userId; +} diff --git a/IOT_service/src/main/java/com/example/IOT_service/model/SensorData.java b/IOT_service/src/main/java/com/example/IOT_service/model/SensorData.java index 7cf235a..ebecaa1 100644 --- a/IOT_service/src/main/java/com/example/IOT_service/model/SensorData.java +++ b/IOT_service/src/main/java/com/example/IOT_service/model/SensorData.java @@ -17,6 +17,14 @@ public class SensorData { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + // ── USER SCOPING ───────────────────────────────────────────────────────── + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(name = "device_id") + private String deviceId; + // ───────────────────────────────────────────────────────────────────────── + @Column(name = "room_temp") private Double roomTemp; diff --git a/IOT_service/src/main/java/com/example/IOT_service/repository/DeviceRegistryRepository.java b/IOT_service/src/main/java/com/example/IOT_service/repository/DeviceRegistryRepository.java new file mode 100644 index 0000000..4fb1716 --- /dev/null +++ b/IOT_service/src/main/java/com/example/IOT_service/repository/DeviceRegistryRepository.java @@ -0,0 +1,11 @@ +package com.example.IOT_service.repository; + +import com.example.IOT_service.model.DeviceRegistry; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.Optional; + +@Repository +public interface DeviceRegistryRepository extends JpaRepository { + Optional findByDeviceId(String deviceId); +} diff --git a/IOT_service/src/main/java/com/example/IOT_service/repository/SensorDataRepository.java b/IOT_service/src/main/java/com/example/IOT_service/repository/SensorDataRepository.java index a57ae41..ab4d9ce 100644 --- a/IOT_service/src/main/java/com/example/IOT_service/repository/SensorDataRepository.java +++ b/IOT_service/src/main/java/com/example/IOT_service/repository/SensorDataRepository.java @@ -2,8 +2,10 @@ import com.example.IOT_service.model.SensorData; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; + import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -11,24 +13,23 @@ @Repository public interface SensorDataRepository extends JpaRepository { - // Get all data ordered by most recent first - List findAllByOrderByReceivedAtDesc(); + // All data for a specific user, newest first + List findByUserIdOrderByReceivedAtDesc(Long userId); - // Get data within time range - List findByReceivedAtBetweenOrderByReceivedAtDesc( - LocalDateTime start, - LocalDateTime end - ); + // Most recent single reading for a user + Optional findTopByUserIdOrderByReceivedAtDesc(Long userId); - // Get latest N records - @Query(value = "SELECT * FROM sensor_data ORDER BY received_at DESC LIMIT ?1", + // Latest N records for a user + @Query(value = "SELECT * FROM sensor_data WHERE user_id = ?1 ORDER BY received_at DESC LIMIT ?2", nativeQuery = true) - List findLatestN(int limit); + List findLatestNForUser(Long userId, int limit); - // Get the most recent reading - Optional findTopByOrderByReceivedAtDesc(); + // Records newer than `since` for a user + @Query("SELECT s FROM SensorData s WHERE s.userId = :userId AND s.receivedAt >= :since ORDER BY s.receivedAt DESC") + List findRecentDataForUser(Long userId, LocalDateTime since); - // Get data from last N hours - @Query("SELECT s FROM SensorData s WHERE s.receivedAt >= :since ORDER BY s.receivedAt DESC") - List findRecentData(LocalDateTime since); + // Cleanup + @Modifying + @Query("DELETE FROM SensorData s WHERE s.receivedAt < :cutoff") + void deleteByReceivedAtBefore(LocalDateTime cutoff); } diff --git a/IOT_service/src/main/java/com/example/IOT_service/service/SensorDataService.java b/IOT_service/src/main/java/com/example/IOT_service/service/SensorDataService.java index fb15715..7301c1a 100644 --- a/IOT_service/src/main/java/com/example/IOT_service/service/SensorDataService.java +++ b/IOT_service/src/main/java/com/example/IOT_service/service/SensorDataService.java @@ -1,10 +1,13 @@ package com.example.IOT_service.service; +import com.example.IOT_service.model.DeviceRegistry; import com.example.IOT_service.model.SensorData; +import com.example.IOT_service.repository.DeviceRegistryRepository; import com.example.IOT_service.repository.SensorDataRepository; 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.Optional; @@ -14,39 +17,45 @@ public class SensorDataService { private final SensorDataRepository repository; + private final DeviceRegistryRepository deviceRegistryRepository; @Transactional public SensorData saveSensorData(SensorData data) { + // If ESP32 sent a deviceId, verify it is registered to the authenticated user + if (data.getDeviceId() != null && !data.getDeviceId().isBlank()) { + Optional device = deviceRegistryRepository + .findByDeviceId(data.getDeviceId()); + + if (device.isPresent() && !device.get().getUserId().equals(data.getUserId())) { + throw new IllegalArgumentException( + "Device '" + data.getDeviceId() + "' is not registered to this account"); + } + // If device not found in registry we allow it (unregistered device falls back + // to JWT-based userId only). You can change this to throw if you want strict mode. + } return repository.save(data); } - public List getAllData() { - return repository.findAllByOrderByReceivedAtDesc(); - } - - public List getLatestData(int limit) { - return repository.findLatestN(limit); + public List getAllDataForUser(Long userId) { + return repository.findByUserIdOrderByReceivedAtDesc(userId); } - public Optional getLatestReading() { - return repository.findTopByOrderByReceivedAtDesc(); + public List getLatestDataForUser(Long userId, int limit) { + return repository.findLatestNForUser(userId, limit); } - public List getDataInRange(LocalDateTime start, LocalDateTime end) { - return repository.findByReceivedAtBetweenOrderByReceivedAtDesc(start, end); + public Optional getLatestReadingForUser(Long userId) { + return repository.findTopByUserIdOrderByReceivedAtDesc(userId); } - public List getRecentData(int hours) { + public List getRecentDataForUser(Long userId, int hours) { LocalDateTime since = LocalDateTime.now().minusHours(hours); - return repository.findRecentData(since); + return repository.findRecentDataForUser(userId, since); } @Transactional public void deleteOldData(int daysToKeep) { LocalDateTime cutoff = LocalDateTime.now().minusDays(daysToKeep); - List oldData = repository.findByReceivedAtBetweenOrderByReceivedAtDesc( - LocalDateTime.MIN, cutoff - ); - repository.deleteAll(oldData); + repository.deleteByReceivedAtBefore(cutoff); } } diff --git a/IOT_service/src/main/resources/application.properties b/IOT_service/src/main/resources/application.properties index 1509e6f..267a5c4 100644 --- a/IOT_service/src/main/resources/application.properties +++ b/IOT_service/src/main/resources/application.properties @@ -1,42 +1,33 @@ spring.application.name=IOT_service server.port=8082 -# DB (use container hostnames in Docker; use Azure DB host in Azure) -spring.datasource.url=${DATABASE_URL:jdbc:postgresql://smartcare-postgres.postgres.database.azure.com/sensordb} -spring.datasource.username=${DB_USERNAME:smartcarepgadmin} -spring.datasource.password=${DB_PASSWORD:admin@#123} +# PostgreSQL +spring.datasource.url=${DATABASE_URL:jdbc:postgresql://postgres:5432/sensordb} +spring.datasource.username=${DB_USERNAME:postgres} +spring.datasource.password=${DB_PASSWORD:123} spring.datasource.driver-class-name=org.postgresql.Driver -# JPA/Hibernate +# JPA/Hibernate ? ddl-auto=update will auto-create the new columns/tables spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect spring.jpa.properties.hibernate.format_sql=true -# Logging -logging.level.com.example.sensor=DEBUG -logging.level.org.springframework.web=INFO -logging.level.org.hibernate.SQL=DEBUG - -# CORS (already handled in controller, but backup) -spring.web.cors.allowed-origins=* -spring.web.cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS -spring.web.cors.allowed-headers=* - +# Auth service URL (mainservice) ? used by JwtAuthFilter to validate tokens +# Docker: http://mainservice:8080 | local: http://localhost:8080 +auth.service.url=${AUTH_SERVICE_URL:http://mainservice:8080} -# Kafka (IMPORTANT: docker -> kafka:9092, azure -> event hubs / kafka host) -#spring.kafka.bootstrap-servers=${KAFKA_BOOTSTRAP_SERVERS:kafka:9092} -#spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer -#spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer -# -#kafka.topic.sensor-data=${SENSOR_TOPIC:sensor-data-topic} -# Kafka - DISABLED (not used yet) +# Kafka ? disabled spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration -# Eureka registration +# Eureka eureka.client.service-url.defaultZone=${EUREKA_CLIENT_SERVICEURL_DEFAULTZONE:http://localhost:8761/eureka} # Actuator + Prometheus management.endpoints.web.exposure.include=health,info,prometheus management.endpoint.health.show-details=always -management.metrics.tags.application=${spring.application.name} \ No newline at end of file +management.metrics.tags.application=${spring.application.name} + +# Logging +logging.level.com.example.IOT_service=DEBUG +logging.level.org.springframework.web=INFO diff --git a/apigateway/src/main/resources/application.yml b/apigateway/src/main/resources/application.yml index 17f41bd..9f6bba4 100644 --- a/apigateway/src/main/resources/application.yml +++ b/apigateway/src/main/resources/application.yml @@ -11,7 +11,7 @@ spring: - id: iot-service uri: lb://iot-service predicates: - - Path=/api/iot/** + - Path=/api/sensordata/** filters: - StripPrefix=0 diff --git a/docker-compose.yml b/docker-compose.yml index 9074c99..f26023f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -92,6 +92,8 @@ services: condition: service_healthy discoveryserver: condition: service_healthy + mainservice: # ← ADD: IoT service needs mainservice to be up + condition: service_started env_file: - .env environment: @@ -100,6 +102,7 @@ services: SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD} EUREKA_CLIENT_SERVICEURL_DEFAULTZONE: http://discoveryserver:8761/eureka SPRING_AUTOCONFIGURE_EXCLUDE: org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration + AUTH_SERVICE_URL: http://mainservice:8080 chatbotbackend: build: diff --git a/mainservice/src/main/java/com/example/mainservice/controller/AuthController.java b/mainservice/src/main/java/com/example/mainservice/controller/AuthController.java index 9494127..beba181 100644 --- a/mainservice/src/main/java/com/example/mainservice/controller/AuthController.java +++ b/mainservice/src/main/java/com/example/mainservice/controller/AuthController.java @@ -11,6 +11,8 @@ import com.example.mainservice.dto.VerifyForgotPasswordOtpRequest; import com.example.mainservice.dto.VerifyLoginOtpRequest; import com.example.mainservice.service.AuthService; +import com.example.mainservice.util.JwtUtil; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import lombok.AllArgsConstructor; import lombok.Data; @@ -27,10 +29,10 @@ @RestController @RequestMapping("/api/auth") @RequiredArgsConstructor -//@CrossOrigin(origins = {"http://localhost:5173", "http://localhost:3000"}) public class AuthController { private final AuthService authService; + private final JwtUtil jwtUtil; // ← added @PostMapping("/login") public ResponseEntity login(@Valid @RequestBody LoginRequest loginRequest) { @@ -38,16 +40,12 @@ public ResponseEntity login(@Valid @RequestBody LoginRequest loginRequest) { AuthResponse response = authService.login(loginRequest); return ResponseEntity.ok(response); } catch (org.springframework.security.core.AuthenticationException e) { - // Handle authentication failures (wrong username/password) - // This must be caught before Exception since it extends Exception return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(new ErrorResponse("Invalid email or password")); } catch (RuntimeException e) { - // All business logic errors (wrong password, invalid role, etc.) -> 401 return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(new ErrorResponse(e.getMessage() != null ? e.getMessage() : "Invalid email or password")); } catch (Exception e) { - // Handle other exceptions return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(new ErrorResponse("Invalid email or password")); } @@ -88,8 +86,6 @@ public ResponseEntity signup(@Valid @RequestBody SignupRequest signupRequest) public ResponseEntity forgotPassword(@Valid @RequestBody ForgotPasswordRequest request) { try { ForgotPasswordResponse response = authService.forgotPassword(request); - // Response includes resetToken and resetLink for development - // In production, you may want to hide these and only return the message return ResponseEntity.ok(response); } catch (RuntimeException e) { return ResponseEntity.status(HttpStatus.BAD_REQUEST) @@ -100,7 +96,6 @@ public ResponseEntity forgotPassword(@Valid @RequestBody ForgotPasswordReques } } - // New OTP-based forgot-password flow (preferred) @PostMapping("/forgot-password/request-otp") public ResponseEntity forgotPasswordRequestOtp(@Valid @RequestBody ForgotPasswordRequest request) { try { @@ -168,6 +163,38 @@ public ResponseEntity test() { return ResponseEntity.ok("Backend is running!"); } + // ═══════════════════════════════════════════════════════════════════════ + // NEW endpoint — called by the IoT service to validate a JWT and get userId + // Already permitted in SecurityConfig via /api/auth/** → no changes needed there + // ═══════════════════════════════════════════════════════════════════════ + @GetMapping("/validate") + public ResponseEntity validateToken(HttpServletRequest request) { + String authHeader = request.getHeader("Authorization"); + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(new ErrorResponse("Missing or invalid Authorization header")); + } + try { + String token = authHeader.substring(7); + Long userId = jwtUtil.extractUserId(token); + String username = jwtUtil.extractUsername(token); + String role = jwtUtil.extractRole(token); + + if (userId == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(new ErrorResponse("Token has no userId — please log in again")); + } + return ResponseEntity.ok(Map.of( + "userId", userId, + "username", username, + "role", role + )); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(new ErrorResponse("Invalid or expired token")); + } + } + @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException ex) { Map errors = new HashMap<>(); @@ -176,24 +203,13 @@ public ResponseEntity handleValidationExceptions(MethodArgumentNo String errorMessage = error.getDefaultMessage(); errors.put(fieldName, errorMessage); }); - - String errorMessage = errors.values().stream() - .findFirst() - .orElse("Validation failed"); - - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(new ErrorResponse(errorMessage)); + String errorMessage = errors.values().stream().findFirst().orElse("Validation failed"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse(errorMessage)); } - @Data - @AllArgsConstructor - public static class ErrorResponse { - private String message; - } + @Data @AllArgsConstructor + public static class ErrorResponse { private String message; } - @Data - @AllArgsConstructor - public static class SuccessResponse { - private String message; - } -} \ No newline at end of file + @Data @AllArgsConstructor + public static class SuccessResponse { private String message; } +} diff --git a/mainservice/src/main/java/com/example/mainservice/util/JwtUtil.java b/mainservice/src/main/java/com/example/mainservice/util/JwtUtil.java index dece1fe..ed28a24 100644 --- a/mainservice/src/main/java/com/example/mainservice/util/JwtUtil.java +++ b/mainservice/src/main/java/com/example/mainservice/util/JwtUtil.java @@ -57,12 +57,21 @@ public String generateToken(UserDetails userDetails) { return createToken(claims, userDetails.getUsername()); } + // ── Original (kept for backward compat) ────────────────────────────────── public String generateToken(UserDetails userDetails, String role) { Map claims = new HashMap<>(); claims.put("role", role); return createToken(claims, userDetails.getUsername()); } + // ── NEW: embeds userId so IoT service can identify the user ────────────── + public String generateToken(UserDetails userDetails, String role, Long userId) { + Map claims = new HashMap<>(); + claims.put("role", role); + claims.put("userId", userId); + return createToken(claims, userDetails.getUsername()); + } + private String createToken(Map claims, String subject) { return Jwts.builder() .claims(claims) @@ -78,21 +87,24 @@ public Boolean validateToken(String token, UserDetails userDetails) { return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); } - /** - * Generate a JWT token for password reset - * Token expires in 1 hour (3600000 milliseconds) - * @param username The username - * @param role The user role - * @return JWT token for password reset - */ + // ── NEW: extract userId claim ───────────────────────────────────────────── + public Long extractUserId(String token) { + Claims claims = extractAllClaims(token); + Object userId = claims.get("userId"); + if (userId == null) return null; + return Long.valueOf(userId.toString()); + } + + public String extractRole(String token) { + Claims claims = extractAllClaims(token); + return (String) claims.get("role"); + } + public String generatePasswordResetToken(String username, String role) { Map claims = new HashMap<>(); claims.put("role", role); - claims.put("type", "password_reset"); // Mark as password reset token - - // Password reset tokens expire in 1 hour (3600000 milliseconds) - long resetTokenExpiration = 3600000L; // 1 hour in milliseconds - + claims.put("type", "password_reset"); + long resetTokenExpiration = 3600000L; return Jwts.builder() .claims(claims) .subject(username) @@ -102,21 +114,6 @@ public String generatePasswordResetToken(String username, String role) { .compact(); } - /** - * Extract role from JWT token - * @param token JWT token - * @return Role string - */ - public String extractRole(String token) { - Claims claims = extractAllClaims(token); - return (String) claims.get("role"); - } - - /** - * Validate password reset token (checks expiration and signature) - * @param token JWT token - * @return true if token is valid, false otherwise - */ public Boolean validatePasswordResetToken(String token) { try { return !isTokenExpired(token); From d6ebb3bb5c3d97a511ceb6d0c05299d6704e9355 Mon Sep 17 00:00:00 2001 From: Kavindi487 Date: Mon, 16 Mar 2026 12:37:09 +0530 Subject: [PATCH 02/12] updated --- .../IOT_service/config/SecurityConfig.java | 41 ++-- .../controller/SensorDataController.java | 175 +++--------------- .../IOT_service/filter/JwtAuthFilter.java | 80 -------- .../IOT_service/model/DeviceRegistry.java | 24 --- .../example/IOT_service/model/SensorData.java | 5 +- .../repository/DeviceRegistryRepository.java | 11 -- .../repository/SensorDataRepository.java | 25 +-- .../service/SensorDataService.java | 55 ++---- 8 files changed, 72 insertions(+), 344 deletions(-) delete mode 100644 IOT_service/src/main/java/com/example/IOT_service/filter/JwtAuthFilter.java delete mode 100644 IOT_service/src/main/java/com/example/IOT_service/model/DeviceRegistry.java delete mode 100644 IOT_service/src/main/java/com/example/IOT_service/repository/DeviceRegistryRepository.java diff --git a/IOT_service/src/main/java/com/example/IOT_service/config/SecurityConfig.java b/IOT_service/src/main/java/com/example/IOT_service/config/SecurityConfig.java index f499f63..9ade293 100644 --- a/IOT_service/src/main/java/com/example/IOT_service/config/SecurityConfig.java +++ b/IOT_service/src/main/java/com/example/IOT_service/config/SecurityConfig.java @@ -1,36 +1,43 @@ package com.example.IOT_service.config; -import com.example.IOT_service.filter.JwtAuthFilter; -import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; 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 java.util.List; @Configuration @EnableWebSecurity -@RequiredArgsConstructor public class SecurityConfig { - private final JwtAuthFilter jwtAuthFilter; - @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .csrf(csrf -> csrf.disable()) - .cors(cors -> cors.disable()) - .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(auth -> auth - // Health check and actuator remain open - .requestMatchers("/api/sensordata/health", "/actuator/**").permitAll() - // Everything else requires a valid JWT (validated via mainservice) - .anyRequest().authenticated() - ) - .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); - + .csrf(csrf -> csrf.disable()) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .anyRequest().permitAll() // ← open, no JWT needed + ); return http.build(); } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOriginPatterns(List.of("*")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + config.setAllowedHeaders(List.of("*")); + config.setAllowCredentials(false); + config.setMaxAge(3600L); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } } diff --git a/IOT_service/src/main/java/com/example/IOT_service/controller/SensorDataController.java b/IOT_service/src/main/java/com/example/IOT_service/controller/SensorDataController.java index 697d117..99077c2 100644 --- a/IOT_service/src/main/java/com/example/IOT_service/controller/SensorDataController.java +++ b/IOT_service/src/main/java/com/example/IOT_service/controller/SensorDataController.java @@ -2,23 +2,14 @@ import com.example.IOT_service.model.SensorData; import com.example.IOT_service.service.SensorDataService; -import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import java.io.IOException; import java.time.LocalDateTime; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; @RestController @RequestMapping("/api/sensordata") @@ -28,157 +19,35 @@ public class SensorDataController { private final SensorDataService service; - // SSE emitters keyed by userId — enables real-time push to frontend - private final ConcurrentHashMap emitters = new ConcurrentHashMap<>(); - private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); - - // ── POST: ESP32 sends data ──────────────────────────────────────────────── + // ── ESP32 posts here — no auth needed ───────────────────────────────────── + // ESP32 includes userId in the JSON body itself @PostMapping - public ResponseEntity receiveSensorData(@RequestBody SensorData data, - HttpServletRequest request) { - try { - Long userId = (Long) request.getAttribute("userId"); - data.setUserId(userId); - - SensorData saved = service.saveSensorData(data); - log.info("Saved sensor data id={} userId={} deviceId={}", saved.getId(), userId, saved.getDeviceId()); - - // Push to any connected SSE client for this user in real-time - pushToSseClient(userId, saved); - - return ResponseEntity.ok(Map.of( - "success", true, - "id", saved.getId(), - "timestamp", saved.getReceivedAt() - )); - } catch (IllegalArgumentException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN) - .body(Map.of("success", false, "message", e.getMessage())); - } catch (Exception e) { - log.error("Error saving sensor data", e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(Map.of("success", false, "message", e.getMessage())); - } - } - - // ── SSE: frontend subscribes here for real-time data ───────────────────── - // Usage: GET /api/sensordata/stream (with Authorization header) - @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public SseEmitter streamSensorData(HttpServletRequest request) { - Long userId = (Long) request.getAttribute("userId"); - - // 5-minute timeout; frontend should reconnect automatically - SseEmitter emitter = new SseEmitter(5 * 60 * 1000L); - - emitters.put(userId, emitter); - - emitter.onCompletion(() -> emitters.remove(userId)); - emitter.onTimeout(() -> { - emitters.remove(userId); - emitter.complete(); - }); - emitter.onError(e -> emitters.remove(userId)); - - // Send current latest reading immediately on connect - service.getLatestReadingForUser(userId).ifPresent(data -> { - try { - emitter.send(SseEmitter.event().name("sensorData").data(data)); - } catch (IOException ignored) {} - }); - - // Heartbeat every 30s to keep the connection alive through proxies - scheduler.scheduleAtFixedRate(() -> { - try { - if (emitters.containsKey(userId)) { - emitter.send(SseEmitter.event().name("heartbeat").data("ping")); - } - } catch (Exception e) { - emitters.remove(userId); - } - }, 30, 30, TimeUnit.SECONDS); - - return emitter; - } - - // ── GET: authenticated user's own data only ─────────────────────────────── - @GetMapping - public ResponseEntity> getMyData(HttpServletRequest request) { - Long userId = (Long) request.getAttribute("userId"); - return ResponseEntity.ok(service.getAllDataForUser(userId)); + public ResponseEntity receive(@RequestBody SensorData data) { + SensorData saved = service.save(data); + log.info("Saved: id={} userId={} bpm={} spo2={}", saved.getId(), saved.getUserId(), saved.getBpm(), saved.getSpo2()); + return ResponseEntity.ok(Map.of("success", true, "id", saved.getId())); } - @GetMapping("/latest/{limit}") - public ResponseEntity> getLatestData(@PathVariable int limit, - HttpServletRequest request) { - Long userId = (Long) request.getAttribute("userId"); - return ResponseEntity.ok(service.getLatestDataForUser(userId, limit)); + // ── Frontend fetches latest 50 readings for a patient ───────────────────── + // GET /api/sensordata/patient/1/latest/50 + @GetMapping("/patient/{userId}/latest/{limit}") + public ResponseEntity> getLatest(@PathVariable Long userId, + @PathVariable int limit) { + return ResponseEntity.ok(service.getLatestForUser(userId, limit)); } - @GetMapping("/current") - public ResponseEntity getCurrentReading(HttpServletRequest request) { - Long userId = (Long) request.getAttribute("userId"); - return service.getLatestReadingForUser(userId) - .map(data -> ResponseEntity.ok((Object) data)) - .orElse(ResponseEntity.ok(Map.of("message", "No data available yet"))); + // ── Frontend fetches single current reading ──────────────────────────────── + // GET /api/sensordata/patient/1/current + @GetMapping("/patient/{userId}/current") + public ResponseEntity getCurrent(@PathVariable Long userId) { + return service.getLatestOne(userId) + .map(d -> ResponseEntity.ok((Object) d)) + .orElse(ResponseEntity.ok(Map.of("message", "No data yet"))); } - @GetMapping("/recent/{hours}") - public ResponseEntity> getRecentData(@PathVariable int hours, - HttpServletRequest request) { - Long userId = (Long) request.getAttribute("userId"); - return ResponseEntity.ok(service.getRecentDataForUser(userId, hours)); - } - - @GetMapping("/latest/{limit}/with-status") - public ResponseEntity getLatestWithStatus(@PathVariable int limit, - HttpServletRequest request) { - Long userId = (Long) request.getAttribute("userId"); - List data = service.getLatestDataForUser(userId, limit); - if (data.isEmpty()) { - return ResponseEntity.ok(Map.of("data", List.of(), "stale", true, - "ageSeconds", null, "latestTimestamp", null)); - } - SensorData latest = data.get(0); - long ageSeconds = java.time.Duration.between(latest.getReceivedAt(), LocalDateTime.now()).getSeconds(); - return ResponseEntity.ok(Map.of( - "data", data, - "latestTimestamp", latest.getReceivedAt(), - "ageSeconds", ageSeconds, - "stale", ageSeconds > 30 - )); - } - - @DeleteMapping("/cleanup/{days}") - public ResponseEntity cleanupOldData(@PathVariable int days) { - try { - service.deleteOldData(days); - return ResponseEntity.ok(Map.of("success", true, - "message", "Deleted data older than " + days + " days")); - } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(Map.of("success", false, "message", e.getMessage())); - } - } - - // ── Health check (public, no auth needed) ──────────────────────────────── + // ── Health check ────────────────────────────────────────────────────────── @GetMapping("/health") - public ResponseEntity healthCheck() { - return ResponseEntity.ok(Map.of( - "status", "healthy", - "timestamp", LocalDateTime.now(), - "service", "IoT Sensor Data API", - "activeStreams", emitters.size() - )); - } - - // ── Internal helper: push new reading to the user's SSE stream ─────────── - private void pushToSseClient(Long userId, SensorData data) { - SseEmitter emitter = emitters.get(userId); - if (emitter == null) return; - try { - emitter.send(SseEmitter.event().name("sensorData").data(data)); - } catch (Exception e) { - emitters.remove(userId); - } + public ResponseEntity health() { + return ResponseEntity.ok(Map.of("status", "healthy", "timestamp", LocalDateTime.now())); } } diff --git a/IOT_service/src/main/java/com/example/IOT_service/filter/JwtAuthFilter.java b/IOT_service/src/main/java/com/example/IOT_service/filter/JwtAuthFilter.java deleted file mode 100644 index b121fc1..0000000 --- a/IOT_service/src/main/java/com/example/IOT_service/filter/JwtAuthFilter.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.example.IOT_service.filter; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.*; -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; -import java.util.Map; - -@Component -@Slf4j -public class JwtAuthFilter extends OncePerRequestFilter { - - // Points to mainservice — uses container name inside Docker, localhost outside - @Value("${auth.service.url:http://mainservice:8080}") - private String authServiceUrl; - - private final RestTemplate restTemplate = new RestTemplate(); - - @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain chain) - throws ServletException, IOException { - - String path = request.getRequestURI(); - - // Skip auth for health / actuator endpoints - if (path.contains("/health") || path.contains("/actuator")) { - chain.doFilter(request, response); - return; - } - - String authHeader = request.getHeader("Authorization"); - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - sendUnauthorized(response, "Missing Authorization header — attach Bearer token"); - return; - } - - try { - HttpHeaders headers = new HttpHeaders(); - headers.set("Authorization", authHeader); - HttpEntity entity = new HttpEntity<>(headers); - - // Call mainservice /api/auth/validate - ResponseEntity authResponse = restTemplate.exchange( - authServiceUrl + "/api/auth/validate", - HttpMethod.GET, - entity, - Map.class - ); - - if (authResponse.getStatusCode().is2xxSuccessful() && authResponse.getBody() != null) { - Long userId = Long.valueOf(authResponse.getBody().get("userId").toString()); - // Make userId available to every controller via request attribute - request.setAttribute("userId", userId); - chain.doFilter(request, response); - } else { - sendUnauthorized(response, "Token validation failed"); - } - - } catch (Exception e) { - log.error("JWT validation error calling mainservice: {}", e.getMessage()); - sendUnauthorized(response, "Invalid or expired token"); - } - } - - private void sendUnauthorized(HttpServletResponse response, String message) throws IOException { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json"); - response.getWriter().write("{\"error\": \"" + message + "\"}"); - } -} diff --git a/IOT_service/src/main/java/com/example/IOT_service/model/DeviceRegistry.java b/IOT_service/src/main/java/com/example/IOT_service/model/DeviceRegistry.java deleted file mode 100644 index aa2612a..0000000 --- a/IOT_service/src/main/java/com/example/IOT_service/model/DeviceRegistry.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.example.IOT_service.model; - -import jakarta.persistence.*; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Entity -@Table(name = "device_registry") -@Data -@NoArgsConstructor -public class DeviceRegistry { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - // Must match the hardcoded DEVICE_ID on the ESP32 - @Column(name = "device_id", unique = true, nullable = false) - private String deviceId; - - // References user.id from mainservice (cross-service — store as plain Long) - @Column(name = "user_id", nullable = false) - private Long userId; -} diff --git a/IOT_service/src/main/java/com/example/IOT_service/model/SensorData.java b/IOT_service/src/main/java/com/example/IOT_service/model/SensorData.java index ebecaa1..29e092c 100644 --- a/IOT_service/src/main/java/com/example/IOT_service/model/SensorData.java +++ b/IOT_service/src/main/java/com/example/IOT_service/model/SensorData.java @@ -17,13 +17,12 @@ public class SensorData { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - // ── USER SCOPING ───────────────────────────────────────────────────────── - @Column(name = "user_id", nullable = false) + // userId — set by ESP32 in the JSON body (no auth required) + @Column(name = "user_id") private Long userId; @Column(name = "device_id") private String deviceId; - // ───────────────────────────────────────────────────────────────────────── @Column(name = "room_temp") private Double roomTemp; diff --git a/IOT_service/src/main/java/com/example/IOT_service/repository/DeviceRegistryRepository.java b/IOT_service/src/main/java/com/example/IOT_service/repository/DeviceRegistryRepository.java deleted file mode 100644 index 4fb1716..0000000 --- a/IOT_service/src/main/java/com/example/IOT_service/repository/DeviceRegistryRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.IOT_service.repository; - -import com.example.IOT_service.model.DeviceRegistry; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -import java.util.Optional; - -@Repository -public interface DeviceRegistryRepository extends JpaRepository { - Optional findByDeviceId(String deviceId); -} diff --git a/IOT_service/src/main/java/com/example/IOT_service/repository/SensorDataRepository.java b/IOT_service/src/main/java/com/example/IOT_service/repository/SensorDataRepository.java index ab4d9ce..fd29fcf 100644 --- a/IOT_service/src/main/java/com/example/IOT_service/repository/SensorDataRepository.java +++ b/IOT_service/src/main/java/com/example/IOT_service/repository/SensorDataRepository.java @@ -2,34 +2,23 @@ import com.example.IOT_service.model.SensorData; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @Repository public interface SensorDataRepository extends JpaRepository { - // All data for a specific user, newest first - List findByUserIdOrderByReceivedAtDesc(Long userId); - - // Most recent single reading for a user - Optional findTopByUserIdOrderByReceivedAtDesc(Long userId); - - // Latest N records for a user + // Latest N records for a specific patient @Query(value = "SELECT * FROM sensor_data WHERE user_id = ?1 ORDER BY received_at DESC LIMIT ?2", - nativeQuery = true) - List findLatestNForUser(Long userId, int limit); + nativeQuery = true) + List findLatestForUser(Long userId, int limit); - // Records newer than `since` for a user - @Query("SELECT s FROM SensorData s WHERE s.userId = :userId AND s.receivedAt >= :since ORDER BY s.receivedAt DESC") - List findRecentDataForUser(Long userId, LocalDateTime since); + // Most recent single record for a patient (used as fallback) + Optional findTopByUserIdOrderByReceivedAtDesc(Long userId); - // Cleanup - @Modifying - @Query("DELETE FROM SensorData s WHERE s.receivedAt < :cutoff") - void deleteByReceivedAtBefore(LocalDateTime cutoff); + // Most recent record across ALL patients (for ESP32 that posts without userId) + Optional findTopByOrderByReceivedAtDesc(); } diff --git a/IOT_service/src/main/java/com/example/IOT_service/service/SensorDataService.java b/IOT_service/src/main/java/com/example/IOT_service/service/SensorDataService.java index 7301c1a..db1d440 100644 --- a/IOT_service/src/main/java/com/example/IOT_service/service/SensorDataService.java +++ b/IOT_service/src/main/java/com/example/IOT_service/service/SensorDataService.java @@ -1,14 +1,10 @@ package com.example.IOT_service.service; -import com.example.IOT_service.model.DeviceRegistry; import com.example.IOT_service.model.SensorData; -import com.example.IOT_service.repository.DeviceRegistryRepository; import com.example.IOT_service.repository.SensorDataRepository; 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.Optional; @@ -17,45 +13,28 @@ public class SensorDataService { private final SensorDataRepository repository; - private final DeviceRegistryRepository deviceRegistryRepository; - @Transactional - public SensorData saveSensorData(SensorData data) { - // If ESP32 sent a deviceId, verify it is registered to the authenticated user - if (data.getDeviceId() != null && !data.getDeviceId().isBlank()) { - Optional device = deviceRegistryRepository - .findByDeviceId(data.getDeviceId()); - - if (device.isPresent() && !device.get().getUserId().equals(data.getUserId())) { - throw new IllegalArgumentException( - "Device '" + data.getDeviceId() + "' is not registered to this account"); - } - // If device not found in registry we allow it (unregistered device falls back - // to JWT-based userId only). You can change this to throw if you want strict mode. - } + public SensorData save(SensorData data) { return repository.save(data); } - public List getAllDataForUser(Long userId) { - return repository.findByUserIdOrderByReceivedAtDesc(userId); - } - - public List getLatestDataForUser(Long userId, int limit) { - return repository.findLatestNForUser(userId, limit); - } - - public Optional getLatestReadingForUser(Long userId) { - return repository.findTopByUserIdOrderByReceivedAtDesc(userId); - } - - public List getRecentDataForUser(Long userId, int hours) { - LocalDateTime since = LocalDateTime.now().minusHours(hours); - return repository.findRecentDataForUser(userId, since); + // Get latest N readings for a user, falls back to latest overall if none found + public List getLatestForUser(Long userId, int limit) { + List data = repository.findLatestForUser(userId, limit); + if (!data.isEmpty()) return data; + // Fallback: return latest readings regardless of user + return repository.findAll() + .stream() + .sorted((a, b) -> b.getReceivedAt().compareTo(a.getReceivedAt())) + .limit(limit) + .toList(); } - @Transactional - public void deleteOldData(int daysToKeep) { - LocalDateTime cutoff = LocalDateTime.now().minusDays(daysToKeep); - repository.deleteByReceivedAtBefore(cutoff); + // Single most recent reading for a user — used for "current" display + // Falls back to latest overall reading if nothing found for this user + public Optional getLatestOne(Long userId) { + Optional userReading = repository.findTopByUserIdOrderByReceivedAtDesc(userId); + if (userReading.isPresent()) return userReading; + return repository.findTopByOrderByReceivedAtDesc(); // DB fallback } } From a15363f8d0a247d34562c42aada9a642722c292a Mon Sep 17 00:00:00 2001 From: Kavindi487 Date: Mon, 16 Mar 2026 13:43:39 +0530 Subject: [PATCH 03/12] updated --- apigateway/src/main/resources/application.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apigateway/src/main/resources/application.yml b/apigateway/src/main/resources/application.yml index 9f6bba4..fdb290b 100644 --- a/apigateway/src/main/resources/application.yml +++ b/apigateway/src/main/resources/application.yml @@ -9,7 +9,7 @@ spring: gateway: routes: - id: iot-service - uri: lb://iot-service + uri: lb://IOT_service predicates: - Path=/api/sensordata/** filters: From 184a5b8719d799483fd7895e7c3416b1ff6e2fd2 Mon Sep 17 00:00:00 2001 From: Kavindi487 Date: Wed, 18 Mar 2026 12:44:07 +0530 Subject: [PATCH 04/12] Create Device.java --- .../com/example/IOT_service/model/Device.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 IOT_service/src/main/java/com/example/IOT_service/model/Device.java diff --git a/IOT_service/src/main/java/com/example/IOT_service/model/Device.java b/IOT_service/src/main/java/com/example/IOT_service/model/Device.java new file mode 100644 index 0000000..af61a62 --- /dev/null +++ b/IOT_service/src/main/java/com/example/IOT_service/model/Device.java @@ -0,0 +1,30 @@ +package com.example.IOT_service.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "devices") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Device { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "device_id", unique = true, nullable = false) + private String deviceId; + + @Column(name = "device_token", nullable = false) + private String deviceToken; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Column(name = "status", nullable = false) + private String status; // ACTIVE / INACTIVE +} \ No newline at end of file From c883a75416b3c6a8f2525febac8495859c803de5 Mon Sep 17 00:00:00 2001 From: Kavindi487 Date: Wed, 18 Mar 2026 18:42:10 +0530 Subject: [PATCH 05/12] updated --- .../controller/SensorDataController.java | 62 ++++++++++++++----- .../example/IOT_service/model/SensorData.java | 8 ++- .../repository/DeviceRepository.java | 10 +++ .../repository/SensorDataRepository.java | 9 +-- .../IOT_service/service/DeviceService.java | 19 ++++++ .../service/SensorDataService.java | 20 ++---- .../src/main/resources/application.properties | 6 +- docker-compose.yml | 14 ++++- 8 files changed, 102 insertions(+), 46 deletions(-) create mode 100644 IOT_service/src/main/java/com/example/IOT_service/repository/DeviceRepository.java create mode 100644 IOT_service/src/main/java/com/example/IOT_service/service/DeviceService.java diff --git a/IOT_service/src/main/java/com/example/IOT_service/controller/SensorDataController.java b/IOT_service/src/main/java/com/example/IOT_service/controller/SensorDataController.java index 99077c2..b101a54 100644 --- a/IOT_service/src/main/java/com/example/IOT_service/controller/SensorDataController.java +++ b/IOT_service/src/main/java/com/example/IOT_service/controller/SensorDataController.java @@ -1,6 +1,8 @@ package com.example.IOT_service.controller; +import com.example.IOT_service.model.Device; import com.example.IOT_service.model.SensorData; +import com.example.IOT_service.service.DeviceService; import com.example.IOT_service.service.SensorDataService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -17,37 +19,63 @@ @Slf4j public class SensorDataController { - private final SensorDataService service; + private final SensorDataService sensorDataService; + private final DeviceService deviceService; - // ── ESP32 posts here — no auth needed ───────────────────────────────────── - // ESP32 includes userId in the JSON body itself + // Device upload endpoint for hosted use + @PostMapping("/device/upload") + public ResponseEntity receiveFromDevice( + @RequestHeader("X-Device-Id") String deviceId, + @RequestHeader("X-Device-Token") String deviceToken, + @RequestBody SensorData data) { + + Device device = deviceService.validateDevice(deviceId, deviceToken); + + data.setUserId(device.getUserId()); + data.setDeviceId(device.getDeviceId()); + + SensorData saved = sensorDataService.save(data); + + log.info("Saved: id={} userId={} deviceId={} bpm={} spo2={}", + saved.getId(), + saved.getUserId(), + saved.getDeviceId(), + saved.getBpm(), + saved.getSpo2()); + + return ResponseEntity.ok(Map.of( + "success", true, + "id", saved.getId(), + "userId", saved.getUserId(), + "deviceId", saved.getDeviceId() + )); + } + + // Optional localhost test endpoint - keep only during testing @PostMapping - public ResponseEntity receive(@RequestBody SensorData data) { - SensorData saved = service.save(data); - log.info("Saved: id={} userId={} bpm={} spo2={}", saved.getId(), saved.getUserId(), saved.getBpm(), saved.getSpo2()); + public ResponseEntity receiveLocalTest(@RequestBody SensorData data) { + SensorData saved = sensorDataService.save(data); return ResponseEntity.ok(Map.of("success", true, "id", saved.getId())); } - // ── Frontend fetches latest 50 readings for a patient ───────────────────── - // GET /api/sensordata/patient/1/latest/50 @GetMapping("/patient/{userId}/latest/{limit}") public ResponseEntity> getLatest(@PathVariable Long userId, - @PathVariable int limit) { - return ResponseEntity.ok(service.getLatestForUser(userId, limit)); + @PathVariable int limit) { + return ResponseEntity.ok(sensorDataService.getLatestForUser(userId, limit)); } - // ── Frontend fetches single current reading ──────────────────────────────── - // GET /api/sensordata/patient/1/current @GetMapping("/patient/{userId}/current") public ResponseEntity getCurrent(@PathVariable Long userId) { - return service.getLatestOne(userId) - .map(d -> ResponseEntity.ok((Object) d)) + return sensorDataService.getLatestOne(userId) + .>map(ResponseEntity::ok) .orElse(ResponseEntity.ok(Map.of("message", "No data yet"))); } - // ── Health check ────────────────────────────────────────────────────────── @GetMapping("/health") public ResponseEntity health() { - return ResponseEntity.ok(Map.of("status", "healthy", "timestamp", LocalDateTime.now())); + return ResponseEntity.ok(Map.of( + "status", "healthy", + "timestamp", LocalDateTime.now() + )); } -} +} \ No newline at end of file diff --git a/IOT_service/src/main/java/com/example/IOT_service/model/SensorData.java b/IOT_service/src/main/java/com/example/IOT_service/model/SensorData.java index 29e092c..55d36e8 100644 --- a/IOT_service/src/main/java/com/example/IOT_service/model/SensorData.java +++ b/IOT_service/src/main/java/com/example/IOT_service/model/SensorData.java @@ -4,6 +4,7 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; + import java.time.LocalDateTime; @Entity @@ -17,7 +18,6 @@ public class SensorData { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - // userId — set by ESP32 in the JSON body (no auth required) @Column(name = "user_id") private Long userId; @@ -53,6 +53,8 @@ public class SensorData { @PrePersist protected void onCreate() { - receivedAt = LocalDateTime.now(); + if (receivedAt == null) { + receivedAt = LocalDateTime.now(); + } } -} +} \ No newline at end of file diff --git a/IOT_service/src/main/java/com/example/IOT_service/repository/DeviceRepository.java b/IOT_service/src/main/java/com/example/IOT_service/repository/DeviceRepository.java new file mode 100644 index 0000000..0b1d96c --- /dev/null +++ b/IOT_service/src/main/java/com/example/IOT_service/repository/DeviceRepository.java @@ -0,0 +1,10 @@ +package com.example.IOT_service.repository; + +import com.example.IOT_service.model.Device; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface DeviceRepository extends JpaRepository { + Optional findByDeviceIdAndDeviceTokenAndStatus(String deviceId, String deviceToken, String status); +} \ No newline at end of file diff --git a/IOT_service/src/main/java/com/example/IOT_service/repository/SensorDataRepository.java b/IOT_service/src/main/java/com/example/IOT_service/repository/SensorDataRepository.java index fd29fcf..f66d887 100644 --- a/IOT_service/src/main/java/com/example/IOT_service/repository/SensorDataRepository.java +++ b/IOT_service/src/main/java/com/example/IOT_service/repository/SensorDataRepository.java @@ -11,14 +11,9 @@ @Repository public interface SensorDataRepository extends JpaRepository { - // Latest N records for a specific patient @Query(value = "SELECT * FROM sensor_data WHERE user_id = ?1 ORDER BY received_at DESC LIMIT ?2", - nativeQuery = true) + nativeQuery = true) List findLatestForUser(Long userId, int limit); - // Most recent single record for a patient (used as fallback) Optional findTopByUserIdOrderByReceivedAtDesc(Long userId); - - // Most recent record across ALL patients (for ESP32 that posts without userId) - Optional findTopByOrderByReceivedAtDesc(); -} +} \ No newline at end of file diff --git a/IOT_service/src/main/java/com/example/IOT_service/service/DeviceService.java b/IOT_service/src/main/java/com/example/IOT_service/service/DeviceService.java new file mode 100644 index 0000000..809d669 --- /dev/null +++ b/IOT_service/src/main/java/com/example/IOT_service/service/DeviceService.java @@ -0,0 +1,19 @@ +package com.example.IOT_service.service; + +import com.example.IOT_service.model.Device; +import com.example.IOT_service.repository.DeviceRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DeviceService { + + private final DeviceRepository deviceRepository; + + public Device validateDevice(String deviceId, String deviceToken) { + return deviceRepository + .findByDeviceIdAndDeviceTokenAndStatus(deviceId, deviceToken, "ACTIVE") + .orElseThrow(() -> new RuntimeException("Invalid or inactive device")); + } +} \ No newline at end of file diff --git a/IOT_service/src/main/java/com/example/IOT_service/service/SensorDataService.java b/IOT_service/src/main/java/com/example/IOT_service/service/SensorDataService.java index db1d440..e8cd32c 100644 --- a/IOT_service/src/main/java/com/example/IOT_service/service/SensorDataService.java +++ b/IOT_service/src/main/java/com/example/IOT_service/service/SensorDataService.java @@ -18,23 +18,13 @@ public SensorData save(SensorData data) { return repository.save(data); } - // Get latest N readings for a user, falls back to latest overall if none found + // Only return this patient's data public List getLatestForUser(Long userId, int limit) { - List data = repository.findLatestForUser(userId, limit); - if (!data.isEmpty()) return data; - // Fallback: return latest readings regardless of user - return repository.findAll() - .stream() - .sorted((a, b) -> b.getReceivedAt().compareTo(a.getReceivedAt())) - .limit(limit) - .toList(); + return repository.findLatestForUser(userId, limit); } - // Single most recent reading for a user — used for "current" display - // Falls back to latest overall reading if nothing found for this user + // Return this patient's latest saved reading only public Optional getLatestOne(Long userId) { - Optional userReading = repository.findTopByUserIdOrderByReceivedAtDesc(userId); - if (userReading.isPresent()) return userReading; - return repository.findTopByOrderByReceivedAtDesc(); // DB fallback + return repository.findTopByUserIdOrderByReceivedAtDesc(userId); } -} +} \ No newline at end of file diff --git a/IOT_service/src/main/resources/application.properties b/IOT_service/src/main/resources/application.properties index 267a5c4..b54ae6d 100644 --- a/IOT_service/src/main/resources/application.properties +++ b/IOT_service/src/main/resources/application.properties @@ -2,9 +2,9 @@ spring.application.name=IOT_service server.port=8082 # PostgreSQL -spring.datasource.url=${DATABASE_URL:jdbc:postgresql://postgres:5432/sensordb} -spring.datasource.username=${DB_USERNAME:postgres} -spring.datasource.password=${DB_PASSWORD:123} +SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB} +SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER} +SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD} spring.datasource.driver-class-name=org.postgresql.Driver # JPA/Hibernate ? ddl-auto=update will auto-create the new columns/tables diff --git a/docker-compose.yml b/docker-compose.yml index f26023f..eafc52f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,8 @@ services: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} ports: - "3307:3306" + volumes: + - mysql_data:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-proot"] interval: 10s @@ -34,6 +36,8 @@ services: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} ports: - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] interval: 10s @@ -92,7 +96,7 @@ services: condition: service_healthy discoveryserver: condition: service_healthy - mainservice: # ← ADD: IoT service needs mainservice to be up + mainservice: condition: service_started env_file: - .env @@ -140,6 +144,10 @@ services: depends_on: discoveryserver: condition: service_healthy + mainservice: + condition: service_started + iot-service: + condition: service_started environment: EUREKA_CLIENT_SERVICEURL_DEFAULTZONE: http://discoveryserver:8761/eureka @@ -159,3 +167,7 @@ services: - "3001:3000" depends_on: - prometheus + +volumes: + postgres_data: + mysql_data: \ No newline at end of file From 3e12fee326b25cf2f278438c2854d0bea4007eab Mon Sep 17 00:00:00 2001 From: Kavindi487 Date: Wed, 18 Mar 2026 23:54:58 +0530 Subject: [PATCH 06/12] ..... --- .../IOT_service/config/SecurityConfig.java | 10 ++++++++-- .../src/main/resources/application.properties | 16 ++++++++-------- .../example/apigateway/config/CorsConfig.java | 17 +++++++++++++---- .../apigateway/config/SecurityConfig.java | 12 +++++++++++- apigateway/src/main/resources/application.yml | 2 +- 5 files changed, 41 insertions(+), 16 deletions(-) diff --git a/IOT_service/src/main/java/com/example/IOT_service/config/SecurityConfig.java b/IOT_service/src/main/java/com/example/IOT_service/config/SecurityConfig.java index 9ade293..a548dcf 100644 --- a/IOT_service/src/main/java/com/example/IOT_service/config/SecurityConfig.java +++ b/IOT_service/src/main/java/com/example/IOT_service/config/SecurityConfig.java @@ -23,7 +23,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .cors(cors -> cors.configurationSource(corsConfigurationSource())) .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth - .anyRequest().permitAll() // ← open, no JWT needed + .anyRequest().permitAll() ); return http.build(); } @@ -31,9 +31,14 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); - config.setAllowedOriginPatterns(List.of("*")); + // Use allowedOriginPatterns (not wildcard "*") so credentials can work if needed + config.setAllowedOriginPatterns(List.of( + "http://localhost:*", + "https://*.azurecontainerapps.io" + )); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); config.setAllowedHeaders(List.of("*")); + // allowCredentials=false: IoT device uploads use X-Device-Token header, not cookies config.setAllowCredentials(false); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); @@ -41,3 +46,4 @@ public CorsConfigurationSource corsConfigurationSource() { return source; } } + diff --git a/IOT_service/src/main/resources/application.properties b/IOT_service/src/main/resources/application.properties index b54ae6d..7ff604a 100644 --- a/IOT_service/src/main/resources/application.properties +++ b/IOT_service/src/main/resources/application.properties @@ -1,23 +1,23 @@ spring.application.name=IOT_service server.port=8082 -# PostgreSQL -SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB} -SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER} -SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD} +# PostgreSQL - correct .properties syntax (= not :) +spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://postgres:5432/smartcare_iot} +spring.datasource.username=${SPRING_DATASOURCE_USERNAME:postgres} +spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:postgres} spring.datasource.driver-class-name=org.postgresql.Driver -# JPA/Hibernate ? ddl-auto=update will auto-create the new columns/tables +# JPA/Hibernate spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect spring.jpa.properties.hibernate.format_sql=true +spring.jpa.open-in-view=false -# Auth service URL (mainservice) ? used by JwtAuthFilter to validate tokens -# Docker: http://mainservice:8080 | local: http://localhost:8080 +# Auth service URL auth.service.url=${AUTH_SERVICE_URL:http://mainservice:8080} -# Kafka ? disabled +# Kafka - disabled spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration # Eureka diff --git a/apigateway/src/main/java/com/example/apigateway/config/CorsConfig.java b/apigateway/src/main/java/com/example/apigateway/config/CorsConfig.java index 664bd21..5024bbc 100644 --- a/apigateway/src/main/java/com/example/apigateway/config/CorsConfig.java +++ b/apigateway/src/main/java/com/example/apigateway/config/CorsConfig.java @@ -3,7 +3,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.reactive.CorsWebFilter; +import org.springframework.web.cors.reactive.CorsConfigurationSource; import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; import java.util.Arrays; @@ -12,8 +12,18 @@ @Configuration public class CorsConfig { + /** + * Expose a CorsConfigurationSource bean instead of CorsWebFilter. + * + * Spring Cloud Gateway's built-in CORS support picks this up automatically, + * and Spring Security (when cors() is enabled) uses the same source — + * so the Access-Control-Allow-Origin header is written exactly once. + * + * DO NOT also register a CorsWebFilter bean; that would add a second + * CORS filter and produce duplicate headers, which browsers reject. + */ @Bean - public CorsWebFilter corsWebFilter() { + public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOriginPatterns(List.of( "http://localhost:*", @@ -27,7 +37,6 @@ public CorsWebFilter corsWebFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); - - return new CorsWebFilter(source); + return source; } } \ No newline at end of file diff --git a/apigateway/src/main/java/com/example/apigateway/config/SecurityConfig.java b/apigateway/src/main/java/com/example/apigateway/config/SecurityConfig.java index dc04823..dd10751 100644 --- a/apigateway/src/main/java/com/example/apigateway/config/SecurityConfig.java +++ b/apigateway/src/main/java/com/example/apigateway/config/SecurityConfig.java @@ -5,16 +5,26 @@ import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.web.cors.reactive.CorsConfigurationSource; @Configuration @EnableWebFluxSecurity public class SecurityConfig { + private final CorsConfigurationSource corsConfigurationSource; + + public SecurityConfig(CorsConfigurationSource corsConfigurationSource) { + this.corsConfigurationSource = corsConfigurationSource; + } + @Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http .csrf(ServerHttpSecurity.CsrfSpec::disable) - .cors(ServerHttpSecurity.CorsSpec::disable) + // Wire our CorsConfigurationSource so Security handles CORS once, + // instead of disabling it and letting the downstream service's + // header bleed through (which caused the duplicate-header error). + .cors(cors -> cors.configurationSource(corsConfigurationSource)) .authorizeExchange(exchanges -> exchanges .anyExchange().permitAll() ); diff --git a/apigateway/src/main/resources/application.yml b/apigateway/src/main/resources/application.yml index fdb290b..555b051 100644 --- a/apigateway/src/main/resources/application.yml +++ b/apigateway/src/main/resources/application.yml @@ -9,7 +9,7 @@ spring: gateway: routes: - id: iot-service - uri: lb://IOT_service + uri: http://iot-service:8082 predicates: - Path=/api/sensordata/** filters: From e1c7273c7ea78250d3839b2c5fcff9628ba4a324 Mon Sep 17 00:00:00 2001 From: Kavindi487 Date: Thu, 19 Mar 2026 09:15:18 +0530 Subject: [PATCH 07/12] updated --- IOT_service/Dockerfile | 19 +++++--- .../IOT_service/config/SecurityConfig.java | 3 -- .../exception/GlobalExceptionHandler.java | 29 ++++++++++++ .../src/main/resources/application.properties | 8 ++-- apigateway/Dockerfile | 20 +++++--- .../example/apigateway/config/CorsConfig.java | 14 ++---- .../apigateway/config/SecurityConfig.java | 7 ++- chatbotbackend/Dockerfile | 17 +++++-- discoveryserver/Dockerfile | 19 +++++--- mainservice/Dockerfile | 22 +++++---- .../mainservice/config/SecurityConfig.java | 47 ++++++++++++++----- vitalReports-AI/Dockerfile | 19 +++++--- 12 files changed, 154 insertions(+), 70 deletions(-) create mode 100644 IOT_service/src/main/java/com/example/IOT_service/exception/GlobalExceptionHandler.java diff --git a/IOT_service/Dockerfile b/IOT_service/Dockerfile index 9ec6a1b..b763ccb 100644 --- a/IOT_service/Dockerfile +++ b/IOT_service/Dockerfile @@ -1,11 +1,18 @@ -#FROM maven:3.9-eclipse-temurin-17 AS build -#WORKDIR /app -#COPY . . -#RUN +FROM maven:3.9-eclipse-temurin-17 AS build +WORKDIR /app +COPY pom.xml . +COPY IOT_service/pom.xml IOT_service/ +COPY mainservice/pom.xml mainservice/ +COPY apigateway/pom.xml apigateway/ +COPY chatbotbackend/pom.xml chatbotbackend/ +COPY discoveryserver/pom.xml discoveryserver/ +COPY vitalReports-AI/pom.xml vitalReports-AI/ +RUN mvn dependency:go-offline -pl IOT_service -am -q 2>/dev/null || true +COPY IOT_service/src IOT_service/src +RUN mvn -pl IOT_service -am clean package -DskipTests -q FROM eclipse-temurin:17-jre WORKDIR /app -COPY IOT_service/target/*.jar app.jar +COPY --from=build /app/IOT_service/target/*.jar app.jar EXPOSE 8082 ENTRYPOINT ["java","-jar","app.jar"] - diff --git a/IOT_service/src/main/java/com/example/IOT_service/config/SecurityConfig.java b/IOT_service/src/main/java/com/example/IOT_service/config/SecurityConfig.java index a548dcf..52cbfa4 100644 --- a/IOT_service/src/main/java/com/example/IOT_service/config/SecurityConfig.java +++ b/IOT_service/src/main/java/com/example/IOT_service/config/SecurityConfig.java @@ -31,14 +31,12 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); - // Use allowedOriginPatterns (not wildcard "*") so credentials can work if needed config.setAllowedOriginPatterns(List.of( "http://localhost:*", "https://*.azurecontainerapps.io" )); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); config.setAllowedHeaders(List.of("*")); - // allowCredentials=false: IoT device uploads use X-Device-Token header, not cookies config.setAllowCredentials(false); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); @@ -46,4 +44,3 @@ public CorsConfigurationSource corsConfigurationSource() { return source; } } - diff --git a/IOT_service/src/main/java/com/example/IOT_service/exception/GlobalExceptionHandler.java b/IOT_service/src/main/java/com/example/IOT_service/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..032785f --- /dev/null +++ b/IOT_service/src/main/java/com/example/IOT_service/exception/GlobalExceptionHandler.java @@ -0,0 +1,29 @@ +package com.example.IOT_service.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.time.LocalDateTime; +import java.util.Map; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(RuntimeException.class) + public ResponseEntity handleRuntime(RuntimeException ex) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of( + "error", ex.getMessage(), + "timestamp", LocalDateTime.now().toString() + )); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleAll(Exception ex) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(Map.of( + "error", "Internal server error: " + ex.getMessage(), + "timestamp", LocalDateTime.now().toString() + )); + } +} diff --git a/IOT_service/src/main/resources/application.properties b/IOT_service/src/main/resources/application.properties index 7ff604a..2afa347 100644 --- a/IOT_service/src/main/resources/application.properties +++ b/IOT_service/src/main/resources/application.properties @@ -1,13 +1,13 @@ spring.application.name=IOT_service server.port=8082 -# PostgreSQL - correct .properties syntax (= not :) -spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://postgres:5432/smartcare_iot} +# PostgreSQL - env vars injected by docker-compose +spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://postgres:5432/sensordb} spring.datasource.username=${SPRING_DATASOURCE_USERNAME:postgres} -spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:postgres} +spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:123} spring.datasource.driver-class-name=org.postgresql.Driver -# JPA/Hibernate +# JPA/Hibernate - auto-creates sensor_data and devices tables on first run spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect diff --git a/apigateway/Dockerfile b/apigateway/Dockerfile index 531c1c5..e02c25f 100644 --- a/apigateway/Dockerfile +++ b/apigateway/Dockerfile @@ -1,12 +1,18 @@ -#FROM maven:3.9-eclipse-temurin-17 AS build -#WORKDIR /app -#COPY . . -#RUN mvn -pl apigateway -am clean package -DskipTests - +FROM maven:3.9-eclipse-temurin-17 AS build +WORKDIR /app +COPY pom.xml . +COPY IOT_service/pom.xml IOT_service/ +COPY mainservice/pom.xml mainservice/ +COPY apigateway/pom.xml apigateway/ +COPY chatbotbackend/pom.xml chatbotbackend/ +COPY discoveryserver/pom.xml discoveryserver/ +COPY vitalReports-AI/pom.xml vitalReports-AI/ +RUN mvn dependency:go-offline -pl apigateway -am -q 2>/dev/null || true +COPY apigateway/src apigateway/src +RUN mvn -pl apigateway -am clean package -DskipTests -q FROM eclipse-temurin:17-jre WORKDIR /app -COPY apigateway/target/*.jar app.jar +COPY --from=build /app/apigateway/target/*.jar app.jar EXPOSE 8088 ENTRYPOINT ["java","-jar","app.jar"] - diff --git a/apigateway/src/main/java/com/example/apigateway/config/CorsConfig.java b/apigateway/src/main/java/com/example/apigateway/config/CorsConfig.java index 5024bbc..5a2707b 100644 --- a/apigateway/src/main/java/com/example/apigateway/config/CorsConfig.java +++ b/apigateway/src/main/java/com/example/apigateway/config/CorsConfig.java @@ -13,14 +13,10 @@ public class CorsConfig { /** - * Expose a CorsConfigurationSource bean instead of CorsWebFilter. - * - * Spring Cloud Gateway's built-in CORS support picks this up automatically, - * and Spring Security (when cors() is enabled) uses the same source — - * so the Access-Control-Allow-Origin header is written exactly once. - * - * DO NOT also register a CorsWebFilter bean; that would add a second - * CORS filter and produce duplicate headers, which browsers reject. + * Single CorsConfigurationSource bean — consumed by the gateway's built-in + * CORS handling AND by Spring Security (wired in SecurityConfig). + * This ensures Access-Control-Allow-Origin is written EXACTLY ONCE. + * Never register a CorsWebFilter alongside this; that produces duplicate headers. */ @Bean public CorsConfigurationSource corsConfigurationSource() { @@ -39,4 +35,4 @@ public CorsConfigurationSource corsConfigurationSource() { source.registerCorsConfiguration("/**", config); return source; } -} \ No newline at end of file +} diff --git a/apigateway/src/main/java/com/example/apigateway/config/SecurityConfig.java b/apigateway/src/main/java/com/example/apigateway/config/SecurityConfig.java index dd10751..2256e7c 100644 --- a/apigateway/src/main/java/com/example/apigateway/config/SecurityConfig.java +++ b/apigateway/src/main/java/com/example/apigateway/config/SecurityConfig.java @@ -21,13 +21,12 @@ public SecurityConfig(CorsConfigurationSource corsConfigurationSource) { public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { http .csrf(ServerHttpSecurity.CsrfSpec::disable) - // Wire our CorsConfigurationSource so Security handles CORS once, - // instead of disabling it and letting the downstream service's - // header bleed through (which caused the duplicate-header error). + // Wire our CorsConfigurationSource — Security strips downstream CORS headers + // and re-writes the correct one ONCE. Never use .cors(disable) here. .cors(cors -> cors.configurationSource(corsConfigurationSource)) .authorizeExchange(exchanges -> exchanges .anyExchange().permitAll() ); return http.build(); } -} \ No newline at end of file +} diff --git a/chatbotbackend/Dockerfile b/chatbotbackend/Dockerfile index d3f4ee5..4854a43 100644 --- a/chatbotbackend/Dockerfile +++ b/chatbotbackend/Dockerfile @@ -1,9 +1,18 @@ -#FROM maven:3.9-eclipse-temurin-17 AS build -#WORKDIR /app -#COPY . . +FROM maven:3.9-eclipse-temurin-17 AS build +WORKDIR /app +COPY pom.xml . +COPY IOT_service/pom.xml IOT_service/ +COPY mainservice/pom.xml mainservice/ +COPY apigateway/pom.xml apigateway/ +COPY chatbotbackend/pom.xml chatbotbackend/ +COPY discoveryserver/pom.xml discoveryserver/ +COPY vitalReports-AI/pom.xml vitalReports-AI/ +RUN mvn dependency:go-offline -pl chatbotbackend -am -q 2>/dev/null || true +COPY chatbotbackend/src chatbotbackend/src +RUN mvn -pl chatbotbackend -am clean package -DskipTests -q FROM eclipse-temurin:17-jre WORKDIR /app -COPY chatbotbackend/target/*.jar app.jar +COPY --from=build /app/chatbotbackend/target/*.jar app.jar EXPOSE 8081 ENTRYPOINT ["java","-jar","app.jar"] diff --git a/discoveryserver/Dockerfile b/discoveryserver/Dockerfile index 09a716d..8cd1ca7 100644 --- a/discoveryserver/Dockerfile +++ b/discoveryserver/Dockerfile @@ -1,11 +1,18 @@ -#FROM maven:3.9-eclipse-temurin-17 AS build -#WORKDIR /app -#COPY . . -#RUN mvn -pl discoveryserver -am clean package -DskipTests - +FROM maven:3.9-eclipse-temurin-17 AS build +WORKDIR /app +COPY pom.xml . +COPY IOT_service/pom.xml IOT_service/ +COPY mainservice/pom.xml mainservice/ +COPY apigateway/pom.xml apigateway/ +COPY chatbotbackend/pom.xml chatbotbackend/ +COPY discoveryserver/pom.xml discoveryserver/ +COPY vitalReports-AI/pom.xml vitalReports-AI/ +RUN mvn dependency:go-offline -pl discoveryserver -am -q 2>/dev/null || true +COPY discoveryserver/src discoveryserver/src +RUN mvn -pl discoveryserver -am clean package -DskipTests -q FROM eclipse-temurin:17-jre WORKDIR /app -COPY discoveryserver/target/*.jar app.jar +COPY --from=build /app/discoveryserver/target/*.jar app.jar EXPOSE 8761 ENTRYPOINT ["java","-jar","app.jar"] diff --git a/mainservice/Dockerfile b/mainservice/Dockerfile index d3effaf..aef4575 100644 --- a/mainservice/Dockerfile +++ b/mainservice/Dockerfile @@ -1,18 +1,22 @@ -#FROM maven:3.9-eclipse-temurin-17 AS build -#WORKDIR /app -#COPY . . -#RUN mvn -pl mainservice -am clean package -DskipTests +FROM maven:3.9-eclipse-temurin-17 AS build +WORKDIR /app +COPY pom.xml . +COPY IOT_service/pom.xml IOT_service/ +COPY mainservice/pom.xml mainservice/ +COPY apigateway/pom.xml apigateway/ +COPY chatbotbackend/pom.xml chatbotbackend/ +COPY discoveryserver/pom.xml discoveryserver/ +COPY vitalReports-AI/pom.xml vitalReports-AI/ +RUN mvn dependency:go-offline -pl mainservice -am -q 2>/dev/null || true +COPY mainservice/src mainservice/src +RUN mvn -pl mainservice -am clean package -DskipTests -q FROM eclipse-temurin:17-jre - -# Install Tesseract OCR RUN apt-get update && apt-get install -y \ tesseract-ocr \ tesseract-ocr-eng \ && rm -rf /var/lib/apt/lists/* - WORKDIR /app -COPY mainservice/target/*.jar app.jar +COPY --from=build /app/mainservice/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java","-jar","app.jar"] - diff --git a/mainservice/src/main/java/com/example/mainservice/config/SecurityConfig.java b/mainservice/src/main/java/com/example/mainservice/config/SecurityConfig.java index 35bd4fc..2f816cb 100644 --- a/mainservice/src/main/java/com/example/mainservice/config/SecurityConfig.java +++ b/mainservice/src/main/java/com/example/mainservice/config/SecurityConfig.java @@ -1,7 +1,6 @@ package com.example.mainservice.config; import com.example.mainservice.security.JwtAuthenticationFilter; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; @@ -18,6 +17,10 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + import java.util.List; @Configuration @@ -28,9 +31,6 @@ public class SecurityConfig { private final UserDetailsService userDetailsService; private final JwtAuthenticationFilter jwtAuthenticationFilter; -// @Value("${spring.web.cors.allowed-origins:http://localhost:5173,http://localhost:3000,https://frontend.mangobush-8de88b36.southeastasia.azurecontainerapps.io}") -// private String allowedOrigins; - public SecurityConfig( @Lazy UserDetailsService userDetailsService, @Lazy JwtAuthenticationFilter jwtAuthenticationFilter @@ -49,18 +49,45 @@ public AuthenticationManager authenticationManager(AuthenticationConfiguration c return config.getAuthenticationManager(); } + /** + * Mainservice runs BEHIND the API Gateway in all deployed/docker environments. + * The gateway owns CORS — it strips downstream CORS headers and re-writes the + * correct one exactly once. If mainservice also adds CORS headers the browser + * sees duplicates and blocks the request. + * + * This CorsConfigurationSource intentionally allows all origins with no credentials + * so mainservice emits NO Access-Control-Allow-Origin header of its own — + * leaving that entirely to the gateway. + * + * For direct local dev access (port 8080 without gateway) the permissive config + * below still works correctly. + */ + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOriginPatterns(List.of("*")); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); + config.setAllowedHeaders(List.of("*")); + config.setAllowCredentials(false); + config.setMaxAge(3600L); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } + @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) - .cors(AbstractHttpConfigurer::disable) + // Use our corsConfigurationSource instead of disable() — + // this prevents mainservice from emitting its own CORS headers + // which would duplicate the ones the gateway already sets. + .cors(cors -> cors.configurationSource(corsConfigurationSource())) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth - // OPTIONS always allowed (CORS preflight) .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() - // PUBLIC - both /api/xxx and /xxx (after gateway StripPrefix=1 removes /api) .requestMatchers("/api/auth/**", "/auth/**").permitAll() .requestMatchers("/api/admin/**", "/admin/**").permitAll() .requestMatchers("/api/doctor/**", "/doctor/**").permitAll() @@ -70,22 +97,18 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/error").permitAll() .requestMatchers("/actuator/**").permitAll() - // Public GET endpoints .requestMatchers(HttpMethod.GET, "/api/doctors/**", "/doctors/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/appointment-types/**", "/appointment-types/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/availability/doctor/**", "/availability/doctor/**").permitAll() - // PATIENT .requestMatchers(HttpMethod.POST, "/api/appointments/book", "/appointments/book").hasAnyRole("PATIENT", "ADMIN") .requestMatchers(HttpMethod.GET, "/api/appointments/user/**", "/appointments/user/**").hasAnyRole("PATIENT", "ADMIN") - // ADMIN ONLY .requestMatchers(HttpMethod.POST, "/api/doctors/**", "/doctors/**").hasRole("ADMIN") .requestMatchers(HttpMethod.PUT, "/api/doctors/**", "/doctors/**").hasRole("ADMIN") .requestMatchers(HttpMethod.DELETE, "/api/doctors/**", "/doctors/**").hasRole("ADMIN") .requestMatchers(HttpMethod.POST, "/api/appointment-types/**", "/appointment-types/**").hasRole("ADMIN") - // Chat - authenticated .requestMatchers("/api/chat/**", "/chat/**").authenticated() .anyRequest().authenticated() @@ -94,4 +117,4 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti return http.build(); } -} \ No newline at end of file +} diff --git a/vitalReports-AI/Dockerfile b/vitalReports-AI/Dockerfile index 32ae4da..8c9b99e 100644 --- a/vitalReports-AI/Dockerfile +++ b/vitalReports-AI/Dockerfile @@ -1,11 +1,18 @@ -#FROM maven:3.9-eclipse-temurin-17 AS build -#WORKDIR /app -#COPY . . -#RUN mvn -pl vitalReports-AI -am clean package -DskipTests +FROM maven:3.9-eclipse-temurin-17 AS build +WORKDIR /app +COPY pom.xml . +COPY IOT_service/pom.xml IOT_service/ +COPY mainservice/pom.xml mainservice/ +COPY apigateway/pom.xml apigateway/ +COPY chatbotbackend/pom.xml chatbotbackend/ +COPY discoveryserver/pom.xml discoveryserver/ +COPY vitalReports-AI/pom.xml vitalReports-AI/ +RUN mvn dependency:go-offline -pl vitalReports-AI -am -q 2>/dev/null || true +COPY vitalReports-AI/src vitalReports-AI/src +RUN mvn -pl vitalReports-AI -am clean package -DskipTests -q FROM eclipse-temurin:17-jre WORKDIR /app -COPY vitalReports-AI/target/*.jar app.jar +COPY --from=build /app/vitalReports-AI/target/*.jar app.jar EXPOSE 8083 ENTRYPOINT ["java","-jar","app.jar"] - From db7eb2c278b6d3e0c35c8414a458e48b4eaebe9d Mon Sep 17 00:00:00 2001 From: Kavindi487 Date: Thu, 19 Mar 2026 11:25:42 +0530 Subject: [PATCH 08/12] updated --- .../example/IOT_service/controller/SensorDataController.java | 2 ++ .../IOT_service/exception/GlobalExceptionHandler.java | 5 ++++- apigateway/src/main/resources/application.yml | 4 ++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/IOT_service/src/main/java/com/example/IOT_service/controller/SensorDataController.java b/IOT_service/src/main/java/com/example/IOT_service/controller/SensorDataController.java index b101a54..42bf751 100644 --- a/IOT_service/src/main/java/com/example/IOT_service/controller/SensorDataController.java +++ b/IOT_service/src/main/java/com/example/IOT_service/controller/SensorDataController.java @@ -29,6 +29,8 @@ public ResponseEntity receiveFromDevice( @RequestHeader("X-Device-Token") String deviceToken, @RequestBody SensorData data) { + log.info("Incoming device upload: deviceId={} bpm={} spo2={}", deviceId, data.getBpm(), data.getSpo2()); + Device device = deviceService.validateDevice(deviceId, deviceToken); data.setUserId(device.getUserId()); diff --git a/IOT_service/src/main/java/com/example/IOT_service/exception/GlobalExceptionHandler.java b/IOT_service/src/main/java/com/example/IOT_service/exception/GlobalExceptionHandler.java index 032785f..64613d6 100644 --- a/IOT_service/src/main/java/com/example/IOT_service/exception/GlobalExceptionHandler.java +++ b/IOT_service/src/main/java/com/example/IOT_service/exception/GlobalExceptionHandler.java @@ -13,7 +13,10 @@ public class GlobalExceptionHandler { @ExceptionHandler(RuntimeException.class) public ResponseEntity handleRuntime(RuntimeException ex) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of( + HttpStatus status = "Invalid or inactive device".equals(ex.getMessage()) + ? HttpStatus.UNAUTHORIZED + : HttpStatus.BAD_REQUEST; + return ResponseEntity.status(status).body(Map.of( "error", ex.getMessage(), "timestamp", LocalDateTime.now().toString() )); diff --git a/apigateway/src/main/resources/application.yml b/apigateway/src/main/resources/application.yml index 555b051..7a70166 100644 --- a/apigateway/src/main/resources/application.yml +++ b/apigateway/src/main/resources/application.yml @@ -9,11 +9,13 @@ spring: gateway: routes: - id: iot-service + order: -10 uri: http://iot-service:8082 predicates: - Path=/api/sensordata/** filters: - StripPrefix=0 + - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST - id: chatbotbackend uri: lb://chatbotbackend @@ -30,11 +32,13 @@ spring: - StripPrefix=0 - id: mainservice + order: 10 uri: lb://mainservice predicates: - Path=/api/** filters: - StripPrefix=0 + - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST eureka: client: From 1683110a4fce8d4f919186e5b98d4b4b2d780534 Mon Sep 17 00:00:00 2001 From: Kavindi487 Date: Thu, 19 Mar 2026 13:02:28 +0530 Subject: [PATCH 09/12] updated --- .../IOT_service/config/SecurityConfig.java | 33 +++---------------- 1 file changed, 5 insertions(+), 28 deletions(-) diff --git a/IOT_service/src/main/java/com/example/IOT_service/config/SecurityConfig.java b/IOT_service/src/main/java/com/example/IOT_service/config/SecurityConfig.java index 52cbfa4..5eb3e59 100644 --- a/IOT_service/src/main/java/com/example/IOT_service/config/SecurityConfig.java +++ b/IOT_service/src/main/java/com/example/IOT_service/config/SecurityConfig.java @@ -6,11 +6,6 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; - -import java.util.List; @Configuration @EnableWebSecurity @@ -19,28 +14,10 @@ public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .csrf(csrf -> csrf.disable()) - .cors(cors -> cors.configurationSource(corsConfigurationSource())) - .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(auth -> auth - .anyRequest().permitAll() - ); - return http.build(); - } + .csrf(csrf -> csrf.disable()) + .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()); - @Bean - public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration config = new CorsConfiguration(); - config.setAllowedOriginPatterns(List.of( - "http://localhost:*", - "https://*.azurecontainerapps.io" - )); - config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); - config.setAllowedHeaders(List.of("*")); - config.setAllowCredentials(false); - config.setMaxAge(3600L); - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", config); - return source; + return http.build(); } -} +} \ No newline at end of file From 383dcfc99573a8b7f3beae1d65a0b2adbbb7b7cc Mon Sep 17 00:00:00 2001 From: Kavindi487 Date: Fri, 20 Mar 2026 16:11:05 +0530 Subject: [PATCH 10/12] updated --- .../controller/SensorDataController.java | 18 ++++++++++ .../dto/DeviceAssignmentRequest.java | 9 +++++ .../repository/DeviceRepository.java | 1 + .../IOT_service/service/DeviceService.java | 15 ++++++++ .../example/mainservice/dto/PatientDTO.java | 2 ++ .../service/IotDeviceAssignmentService.java | 34 +++++++++++++++++++ .../mainservice/service/PatientService.java | 8 +++++ .../src/main/resources/application.properties | 2 ++ 8 files changed, 89 insertions(+) create mode 100644 IOT_service/src/main/java/com/example/IOT_service/dto/DeviceAssignmentRequest.java create mode 100644 mainservice/src/main/java/com/example/mainservice/service/IotDeviceAssignmentService.java diff --git a/IOT_service/src/main/java/com/example/IOT_service/controller/SensorDataController.java b/IOT_service/src/main/java/com/example/IOT_service/controller/SensorDataController.java index 42bf751..490b0b0 100644 --- a/IOT_service/src/main/java/com/example/IOT_service/controller/SensorDataController.java +++ b/IOT_service/src/main/java/com/example/IOT_service/controller/SensorDataController.java @@ -8,6 +8,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import com.example.IOT_service.dto.DeviceAssignmentRequest; import java.time.LocalDateTime; import java.util.List; @@ -80,4 +81,21 @@ public ResponseEntity health() { "timestamp", LocalDateTime.now() )); } + + @PostMapping("/devices/assign") + public ResponseEntity assignDevice(@RequestBody DeviceAssignmentRequest request) { + Device device = deviceService.assignDeviceToUser(request.getDeviceId(), request.getUserId()); + + return ResponseEntity.ok(Map.of( + "success", true, + "message", "Device assigned successfully", + "deviceId", device.getDeviceId(), + "userId", device.getUserId() + )); + } + + @GetMapping("/devices") + public ResponseEntity getAllDevices() { + return ResponseEntity.ok(deviceService.getAllDevices()); + } } \ No newline at end of file diff --git a/IOT_service/src/main/java/com/example/IOT_service/dto/DeviceAssignmentRequest.java b/IOT_service/src/main/java/com/example/IOT_service/dto/DeviceAssignmentRequest.java new file mode 100644 index 0000000..2c194ce --- /dev/null +++ b/IOT_service/src/main/java/com/example/IOT_service/dto/DeviceAssignmentRequest.java @@ -0,0 +1,9 @@ +package com.example.IOT_service.dto; + +import lombok.Data; + +@Data +public class DeviceAssignmentRequest { + private String deviceId; + private Long userId; +} \ No newline at end of file diff --git a/IOT_service/src/main/java/com/example/IOT_service/repository/DeviceRepository.java b/IOT_service/src/main/java/com/example/IOT_service/repository/DeviceRepository.java index 0b1d96c..e15cdfe 100644 --- a/IOT_service/src/main/java/com/example/IOT_service/repository/DeviceRepository.java +++ b/IOT_service/src/main/java/com/example/IOT_service/repository/DeviceRepository.java @@ -7,4 +7,5 @@ public interface DeviceRepository extends JpaRepository { Optional findByDeviceIdAndDeviceTokenAndStatus(String deviceId, String deviceToken, String status); + Optional findByDeviceId(String deviceId); } \ No newline at end of file diff --git a/IOT_service/src/main/java/com/example/IOT_service/service/DeviceService.java b/IOT_service/src/main/java/com/example/IOT_service/service/DeviceService.java index 809d669..ce1d3be 100644 --- a/IOT_service/src/main/java/com/example/IOT_service/service/DeviceService.java +++ b/IOT_service/src/main/java/com/example/IOT_service/service/DeviceService.java @@ -5,6 +5,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.List; + @Service @RequiredArgsConstructor public class DeviceService { @@ -16,4 +18,17 @@ public Device validateDevice(String deviceId, String deviceToken) { .findByDeviceIdAndDeviceTokenAndStatus(deviceId, deviceToken, "ACTIVE") .orElseThrow(() -> new RuntimeException("Invalid or inactive device")); } + + public Device assignDeviceToUser(String deviceId, Long userId) { + Device device = deviceRepository.findByDeviceId(deviceId) + .orElseThrow(() -> new RuntimeException("Device not found")); + + device.setUserId(userId); + device.setStatus("ACTIVE"); + return deviceRepository.save(device); + } + + public List getAllDevices() { + return deviceRepository.findAll(); + } } \ No newline at end of file diff --git a/mainservice/src/main/java/com/example/mainservice/dto/PatientDTO.java b/mainservice/src/main/java/com/example/mainservice/dto/PatientDTO.java index a4c8928..9bc78b3 100644 --- a/mainservice/src/main/java/com/example/mainservice/dto/PatientDTO.java +++ b/mainservice/src/main/java/com/example/mainservice/dto/PatientDTO.java @@ -50,4 +50,6 @@ public class PatientDTO { private String pastSurgeries; private String emergencyNotes; + private String deviceId; + } diff --git a/mainservice/src/main/java/com/example/mainservice/service/IotDeviceAssignmentService.java b/mainservice/src/main/java/com/example/mainservice/service/IotDeviceAssignmentService.java new file mode 100644 index 0000000..07103db --- /dev/null +++ b/mainservice/src/main/java/com/example/mainservice/service/IotDeviceAssignmentService.java @@ -0,0 +1,34 @@ +package com.example.mainservice.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class IotDeviceAssignmentService { + + @Value("${iot.service.url:http://iot-service:8082}") + private String iotServiceUrl; + + private final RestTemplate restTemplate = new RestTemplate(); + + public void assignDevice(String deviceId, Long userId) { + String url = iotServiceUrl + "/api/sensordata/devices/assign"; + + Map body = new HashMap<>(); + body.put("deviceId", deviceId); + body.put("userId", userId); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity> request = new HttpEntity<>(body, headers); + restTemplate.postForEntity(url, request, String.class); + } +} \ No newline at end of file diff --git a/mainservice/src/main/java/com/example/mainservice/service/PatientService.java b/mainservice/src/main/java/com/example/mainservice/service/PatientService.java index 5cd2d6a..d4980a8 100644 --- a/mainservice/src/main/java/com/example/mainservice/service/PatientService.java +++ b/mainservice/src/main/java/com/example/mainservice/service/PatientService.java @@ -21,6 +21,7 @@ public class PatientService { private final PasswordEncoder passwordEncoder; private final EmergencyContactRepository emergencyContactRepository; private final LocationService locationService; + private final IotDeviceAssignmentService iotDeviceAssignmentService; public Patient create(PatientDTO patient) { @@ -68,6 +69,13 @@ public Patient create(PatientDTO patient) { // Auto-create emergency contact from guardian info createEmergencyContactFromGuardian(savedPatient); + if (patient.getDeviceId() != null && !patient.getDeviceId().isBlank()) { + try { + iotDeviceAssignmentService.assignDevice(patient.getDeviceId(), savedPatient.getId()); + } catch (Exception e) { + System.err.println("Device assignment failed: " + e.getMessage()); + } + } return savedPatient; diff --git a/mainservice/src/main/resources/application.properties b/mainservice/src/main/resources/application.properties index 06c40c6..ccb09a6 100644 --- a/mainservice/src/main/resources/application.properties +++ b/mainservice/src/main/resources/application.properties @@ -87,3 +87,5 @@ management.metrics.tags.application=${spring.application.name} logging.level.com.example.mainservice=DEBUG logging.level.org.springframework.security=DEBUG logging.level.org.springframework.web=DEBUG + +iot.service.url=${IOT_SERVICE_URL:http://iot-service:8082} \ No newline at end of file From 7952ac9f6042bf16f091cd4fa3beba216d020b49 Mon Sep 17 00:00:00 2001 From: Kavindi487 Date: Fri, 20 Mar 2026 16:36:46 +0530 Subject: [PATCH 11/12] updated --- .../controller/AdminPatientController.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 mainservice/src/main/java/com/example/mainservice/controller/AdminPatientController.java diff --git a/mainservice/src/main/java/com/example/mainservice/controller/AdminPatientController.java b/mainservice/src/main/java/com/example/mainservice/controller/AdminPatientController.java new file mode 100644 index 0000000..0e57cf6 --- /dev/null +++ b/mainservice/src/main/java/com/example/mainservice/controller/AdminPatientController.java @@ -0,0 +1,25 @@ +package com.example.mainservice.controller; + +import com.example.mainservice.dto.PatientDTO; +import com.example.mainservice.service.PatientService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/admin/patient") +@RequiredArgsConstructor +public class AdminPatientController { + + private final PatientService patientService; + + @PostMapping("/create") + public ResponseEntity createPatient(@RequestBody PatientDTO patientDto) { + return ResponseEntity.ok(patientService.create(patientDto)); + } + + @PutMapping("/update/{id}") + public ResponseEntity updatePatient(@PathVariable Long id, @RequestBody PatientDTO patientDto) { + return ResponseEntity.ok(patientService.updatePatient(id, patientDto)); + } +} \ No newline at end of file From a583addd41e1341a521090fd3591b73d11f80b90 Mon Sep 17 00:00:00 2001 From: Kavindi487 Date: Fri, 20 Mar 2026 17:19:13 +0530 Subject: [PATCH 12/12] device_id is used for patient real time data mapping --- .../example/mainservice/entity/Patient.java | 3 + .../mainservice/service/PatientService.java | 96 ++++++++----------- 2 files changed, 41 insertions(+), 58 deletions(-) diff --git a/mainservice/src/main/java/com/example/mainservice/entity/Patient.java b/mainservice/src/main/java/com/example/mainservice/entity/Patient.java index 2ac978d..49972cf 100644 --- a/mainservice/src/main/java/com/example/mainservice/entity/Patient.java +++ b/mainservice/src/main/java/com/example/mainservice/entity/Patient.java @@ -53,6 +53,9 @@ public class Patient { @Column(nullable = false) private String bloodType; + @Column(name = "device_id", unique = true) + private String deviceId; + // Emergency panel fields @Column private String city; diff --git a/mainservice/src/main/java/com/example/mainservice/service/PatientService.java b/mainservice/src/main/java/com/example/mainservice/service/PatientService.java index d4980a8..cc81526 100644 --- a/mainservice/src/main/java/com/example/mainservice/service/PatientService.java +++ b/mainservice/src/main/java/com/example/mainservice/service/PatientService.java @@ -50,6 +50,7 @@ public Patient create(PatientDTO patient) { .username(patient.getUsername()) .password(passwordEncoder.encode(patient.getPassword())) .bloodType(patient.getBloodType()) + .deviceId(patient.getDeviceId()) // NEW: Emergency fields .city(patient.getCity()) @@ -96,6 +97,7 @@ public List getDetails() { .bloodType(p.getBloodType()) .password(p.getPassword()) .username(p.getUsername()) + .deviceId(p.getDeviceId()) // NEW: Emergency fields .city(p.getCity()) .district(p.getDistrict()) @@ -117,70 +119,48 @@ public void deletePatient(Long Id) { patientrepo.deleteById(Id); } + @Transactional public PatientDTO updatePatient(Long Id, PatientDTO dto) { Patient p = patientrepo.findById(Id).orElseThrow(); - if (dto.getName() != null) - p.setName(dto.getName()); - if (dto.getDateOfBirth() != null) - p.setDateOfBirth(dto.getDateOfBirth()); - if (dto.getAddress() != null) { - p.setAddress(dto.getAddress()); - // Recalculate coordinates if address changed - String fullAddress = buildFullAddress(dto); - Double[] coordinates = locationService.getCoordinatesFromAddress(fullAddress); - p.setLatitude(coordinates[0]); - p.setLongitude(coordinates[1]); - } - if (dto.getEmail() != null) - p.setEmail(dto.getEmail()); - if (dto.getNicNo() != null) - p.setNicNo(dto.getNicNo()); - if (dto.getGender() != null) - p.setGender(dto.getGender()); - if (dto.getContactNo() != null) - p.setContactNo(dto.getContactNo()); - if (dto.getGuardiansName() != null) - p.setGuardiansName(dto.getGuardiansName()); - if (dto.getGuardiansContactNo() != null) - p.setGuardiansContactNo(dto.getGuardiansContactNo()); - if (dto.getBloodType() != null) - p.setBloodType(dto.getBloodType()); - if (dto.getPassword() != null) - p.setPassword(dto.getPassword()); - if (dto.getUsername() != null) - p.setUsername(dto.getUsername()); - - // NEW: Update emergency fields - if (dto.getCity() != null) - p.setCity(dto.getCity()); - if (dto.getDistrict() != null) - p.setDistrict(dto.getDistrict()); - if (dto.getPostalCode() != null) - p.setPostalCode(dto.getPostalCode()); - if (dto.getLatitude() != null) - p.setLatitude(dto.getLatitude()); - if (dto.getLongitude() != null) - p.setLongitude(dto.getLongitude()); - if (dto.getGuardianRelationship() != null) - p.setGuardianRelationship(dto.getGuardianRelationship()); - if (dto.getGuardianEmail() != null) - p.setGuardianEmail(dto.getGuardianEmail()); - if (dto.getMedicalConditions() != null) - p.setMedicalConditions(dto.getMedicalConditions()); - if (dto.getAllergies() != null) - p.setAllergies(dto.getAllergies()); - if (dto.getCurrentMedications() != null) - p.setCurrentMedications(dto.getCurrentMedications()); - if (dto.getPastSurgeries() != null) - p.setPastSurgeries(dto.getPastSurgeries()); - if (dto.getEmergencyNotes() != null) - p.setEmergencyNotes(dto.getEmergencyNotes()); + if (dto.getName() != null) p.setName(dto.getName()); + if (dto.getDateOfBirth() != null) p.setDateOfBirth(dto.getDateOfBirth()); + if (dto.getAddress() != null) p.setAddress(dto.getAddress()); + if (dto.getEmail() != null) p.setEmail(dto.getEmail()); + if (dto.getNicNo() != null) p.setNicNo(dto.getNicNo()); + if (dto.getGender() != null) p.setGender(dto.getGender()); + if (dto.getContactNo() != null) p.setContactNo(dto.getContactNo()); + if (dto.getGuardiansName() != null) p.setGuardiansName(dto.getGuardiansName()); + if (dto.getGuardiansContactNo() != null) p.setGuardiansContactNo(dto.getGuardiansContactNo()); + if (dto.getBloodType() != null) p.setBloodType(dto.getBloodType()); + if (dto.getUsername() != null) p.setUsername(dto.getUsername()); + if (dto.getDeviceId() != null) p.setDeviceId(dto.getDeviceId()); + + if (dto.getCity() != null) p.setCity(dto.getCity()); + if (dto.getDistrict() != null) p.setDistrict(dto.getDistrict()); + if (dto.getPostalCode() != null) p.setPostalCode(dto.getPostalCode()); + if (dto.getLatitude() != null) p.setLatitude(dto.getLatitude()); + if (dto.getLongitude() != null) p.setLongitude(dto.getLongitude()); + if (dto.getGuardianRelationship() != null) p.setGuardianRelationship(dto.getGuardianRelationship()); + if (dto.getGuardianEmail() != null) p.setGuardianEmail(dto.getGuardianEmail()); + if (dto.getMedicalConditions() != null) p.setMedicalConditions(dto.getMedicalConditions()); + if (dto.getAllergies() != null) p.setAllergies(dto.getAllergies()); + if (dto.getCurrentMedications() != null) p.setCurrentMedications(dto.getCurrentMedications()); + if (dto.getPastSurgeries() != null) p.setPastSurgeries(dto.getPastSurgeries()); + if (dto.getEmergencyNotes() != null) p.setEmergencyNotes(dto.getEmergencyNotes()); Patient updatedPatient = patientrepo.save(p); - return convertToDTO(updatedPatient); + if (dto.getDeviceId() != null && !dto.getDeviceId().isBlank()) { + try { + iotDeviceAssignmentService.assignDevice(dto.getDeviceId(), updatedPatient.getId()); + } catch (Exception e) { + System.err.println("Device reassignment failed: " + e.getMessage()); + } + } + + return convertToDTO(updatedPatient); } private PatientDTO convertToDTO(Patient patient) { @@ -198,8 +178,8 @@ private PatientDTO convertToDTO(Patient patient) { dto.setBloodType(patient.getBloodType()); dto.setPassword(patient.getPassword()); dto.setUsername(patient.getUsername()); + dto.setDeviceId(patient.getDeviceId()); - // NEW: Emergency fields dto.setCity(patient.getCity()); dto.setDistrict(patient.getDistrict()); dto.setPostalCode(patient.getPostalCode());