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/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..5eb3e59 --- /dev/null +++ b/IOT_service/src/main/java/com/example/IOT_service/config/SecurityConfig.java @@ -0,0 +1,23 @@ +package com.example.IOT_service.config; + +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; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()); + + return http.build(); + } +} \ No newline at end of file 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..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 @@ -1,126 +1,101 @@ 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; -import org.springframework.http.HttpStatus; 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; import java.util.Map; @RestController @RequestMapping("/api/sensordata") -@CrossOrigin(origins = "*") @RequiredArgsConstructor @Slf4j public class SensorDataController { - private final SensorDataService service; + private final SensorDataService sensorDataService; + private final DeviceService deviceService; - // POST endpoint - receives data from ESP32 - @PostMapping - public ResponseEntity receiveSensorData(@RequestBody SensorData data) { - try { - log.info("Received sensor data: RoomTemp={}, Humidity={}, WaterTemp={}, BPM={}, AvgBPM={}, SpO2={}", - data.getRoomTemp(), data.getHumidity(), data.getWaterTempC(), - data.getBpm(), data.getAvgBpm(), data.getSpo2()); - - SensorData saved = service.saveSensorData(data); - - return ResponseEntity.ok(Map.of( - "success", true, - "message", "Data saved successfully", - "id", saved.getId(), - "timestamp", saved.getReceivedAt() - )); - - } 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() - )); - } - } + // 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) { - // GET all data - @GetMapping - public ResponseEntity> getAllData() { - return ResponseEntity.ok(service.getAllData()); - } + log.info("Incoming device upload: deviceId={} bpm={} spo2={}", deviceId, data.getBpm(), data.getSpo2()); + + Device device = deviceService.validateDevice(deviceId, deviceToken); + + data.setUserId(device.getUserId()); + data.setDeviceId(device.getDeviceId()); - // GET latest N records - @GetMapping("/latest/{limit}") - public ResponseEntity> getLatestData(@PathVariable int limit) { - return ResponseEntity.ok(service.getLatestData(limit)); + 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() + )); } - // GET most recent single reading - @GetMapping("/current") - public ResponseEntity getCurrentReading() { - return service.getLatestReading() - .map(data -> ResponseEntity.ok((Object) data)) - .orElse(ResponseEntity.ok(Map.of("message", "No data available"))); + // Optional localhost test endpoint - keep only during testing + @PostMapping + public ResponseEntity receiveLocalTest(@RequestBody SensorData data) { + SensorData saved = sensorDataService.save(data); + return ResponseEntity.ok(Map.of("success", true, "id", saved.getId())); } - // GET data from last N hours - @GetMapping("/recent/{hours}") - public ResponseEntity> getRecentData(@PathVariable int hours) { - return ResponseEntity.ok(service.getRecentData(hours)); + @GetMapping("/patient/{userId}/latest/{limit}") + public ResponseEntity> getLatest(@PathVariable Long userId, + @PathVariable int limit) { + return ResponseEntity.ok(sensorDataService.getLatestForUser(userId, limit)); } - // 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" - )); - } 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())); - } + @GetMapping("/patient/{userId}/current") + public ResponseEntity getCurrent(@PathVariable Long userId) { + return sensorDataService.getLatestOne(userId) + .>map(ResponseEntity::ok) + .orElse(ResponseEntity.ok(Map.of("message", "No data yet"))); } - // Health check @GetMapping("/health") - public ResponseEntity healthCheck() { + public ResponseEntity health() { return ResponseEntity.ok(Map.of( "status", "healthy", - "timestamp", LocalDateTime.now(), - "service", "Sensor Data API" + "timestamp", LocalDateTime.now() )); } - @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 - )); - } - - 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 + @PostMapping("/devices/assign") + public ResponseEntity assignDevice(@RequestBody DeviceAssignmentRequest request) { + Device device = deviceService.assignDeviceToUser(request.getDeviceId(), request.getUserId()); return ResponseEntity.ok(Map.of( - "data", data, - "latestTimestamp", latest.getReceivedAt(), - "ageSeconds", ageSeconds, - "stale", stale + "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/exception/GlobalExceptionHandler.java b/IOT_service/src/main/java/com/example/IOT_service/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..64613d6 --- /dev/null +++ b/IOT_service/src/main/java/com/example/IOT_service/exception/GlobalExceptionHandler.java @@ -0,0 +1,32 @@ +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) { + 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() + )); + } + + @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/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 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..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,6 +18,12 @@ public class SensorData { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(name = "user_id") + private Long userId; + + @Column(name = "device_id") + private String deviceId; + @Column(name = "room_temp") private Double roomTemp; @@ -46,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..e15cdfe --- /dev/null +++ b/IOT_service/src/main/java/com/example/IOT_service/repository/DeviceRepository.java @@ -0,0 +1,11 @@ +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); + Optional findByDeviceId(String deviceId); +} \ 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 a57ae41..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 @@ -4,31 +4,16 @@ import org.springframework.data.jpa.repository.JpaRepository; 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 { - // Get all data ordered by most recent first - List findAllByOrderByReceivedAtDesc(); - - // Get data within time range - List findByReceivedAtBetweenOrderByReceivedAtDesc( - LocalDateTime start, - LocalDateTime end - ); - - // Get latest N records - @Query(value = "SELECT * FROM sensor_data ORDER BY received_at DESC LIMIT ?1", + @Query(value = "SELECT * FROM sensor_data WHERE user_id = ?1 ORDER BY received_at DESC LIMIT ?2", nativeQuery = true) - List findLatestN(int limit); - - // Get the most recent reading - Optional findTopByOrderByReceivedAtDesc(); + List findLatestForUser(Long userId, int limit); - // 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); -} + Optional findTopByUserIdOrderByReceivedAtDesc(Long userId); +} \ 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..ce1d3be --- /dev/null +++ b/IOT_service/src/main/java/com/example/IOT_service/service/DeviceService.java @@ -0,0 +1,34 @@ +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; + +import java.util.List; + +@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")); + } + + 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/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..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 @@ -4,8 +4,7 @@ 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; @@ -15,38 +14,17 @@ public class SensorDataService { private final SensorDataRepository repository; - @Transactional - public SensorData saveSensorData(SensorData data) { + public SensorData save(SensorData data) { return repository.save(data); } - public List getAllData() { - return repository.findAllByOrderByReceivedAtDesc(); - } - - public List getLatestData(int limit) { - return repository.findLatestN(limit); - } - - public Optional getLatestReading() { - return repository.findTopByOrderByReceivedAtDesc(); - } - - public List getDataInRange(LocalDateTime start, LocalDateTime end) { - return repository.findByReceivedAtBetweenOrderByReceivedAtDesc(start, end); - } - - public List getRecentData(int hours) { - LocalDateTime since = LocalDateTime.now().minusHours(hours); - return repository.findRecentData(since); + // Only return this patient's data + public List getLatestForUser(Long userId, int limit) { + return repository.findLatestForUser(userId, limit); } - @Transactional - public void deleteOldData(int daysToKeep) { - LocalDateTime cutoff = LocalDateTime.now().minusDays(daysToKeep); - List oldData = repository.findByReceivedAtBetweenOrderByReceivedAtDesc( - LocalDateTime.MIN, cutoff - ); - repository.deleteAll(oldData); + // Return this patient's latest saved reading only + public Optional getLatestOne(Long userId) { + 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 1509e6f..2afa347 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 - 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: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 spring.jpa.properties.hibernate.format_sql=true +spring.jpa.open-in-view=false -# 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 +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/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 664bd21..5a2707b 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,14 @@ @Configuration public class CorsConfig { + /** + * 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 CorsWebFilter corsWebFilter() { + public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOriginPatterns(List.of( "http://localhost:*", @@ -27,7 +33,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..2256e7c 100644 --- a/apigateway/src/main/java/com/example/apigateway/config/SecurityConfig.java +++ b/apigateway/src/main/java/com/example/apigateway/config/SecurityConfig.java @@ -5,19 +5,28 @@ 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 — 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/apigateway/src/main/resources/application.yml b/apigateway/src/main/resources/application.yml index 17f41bd..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 - uri: lb://iot-service + order: -10 + uri: http://iot-service:8082 predicates: - - Path=/api/iot/** + - 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: 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/docker-compose.yml b/docker-compose.yml index 9074c99..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,6 +96,8 @@ services: condition: service_healthy discoveryserver: condition: service_healthy + mainservice: + condition: service_started env_file: - .env environment: @@ -100,6 +106,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: @@ -137,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 @@ -156,3 +167,7 @@ services: - "3001:3000" depends_on: - prometheus + +volumes: + postgres_data: + mysql_data: \ No newline at end of file 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/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 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/dto/PatientDTO.java b/mainservice/src/main/java/com/example/mainservice/dto/PatientDTO.java index 794f618..df0d701 100644 --- a/mainservice/src/main/java/com/example/mainservice/dto/PatientDTO.java +++ b/mainservice/src/main/java/com/example/mainservice/dto/PatientDTO.java @@ -51,4 +51,6 @@ public class PatientDTO { private String chronicConditions; private String emergencyNotes; + private String deviceId; + } 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 30f96ed..24147d0 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/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 a797768..dce0fde 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) { @@ -49,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()) @@ -68,6 +70,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; @@ -88,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()) @@ -110,6 +120,7 @@ public void deletePatient(Long Id) { patientrepo.deleteById(Id); } + @Transactional public PatientDTO updatePatient(Long Id, PatientDTO dto) { Patient p = patientrepo.findById(Id).orElseThrow(); @@ -174,8 +185,16 @@ public PatientDTO updatePatient(Long Id, PatientDTO dto) { 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) { @@ -193,8 +212,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()); 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); 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 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"] -