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