diff --git a/pom.xml b/pom.xml
index 171ab162..11ad9f37 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.iemr.common-APIcommon-api
- 3.6.1
+ 3.6.0warCommon-API
diff --git a/src/main/environment/common_ci.properties b/src/main/environment/common_ci.properties
index a6451109..0184b32f 100644
--- a/src/main/environment/common_ci.properties
+++ b/src/main/environment/common_ci.properties
@@ -192,4 +192,15 @@ cors.allowed-origins=@env.CORS_ALLOWED_ORIGINS@
video-call-url=@env.VIDEO_CALL_URL@
jibri.output.path=@env.JIBRI_OUTPUT_PATH@
video.recording.path=@env.VIDEO_RECORDING_PATH@
+
+platform.feedback.ratelimit.enabled=@env.PLATFORM_FEEDBACK_RATELIMIT_ENABLED@
+platform.feedback.ratelimit.pepper=@env.PLATFORM_FEEDBACK_RATELIMIT_PEPPER@
+platform.feedback.ratelimit.trust-forwarded-for=@env.PLATFORM_FEEDBACK_RATELIMIT_TRUST_FORWARDED_FOR@
+platform.feedback.ratelimit.forwarded-for-header=@env.PLATFORM_FEEDBACK_RATELIMIT_FORWARDED_FOR_HEADER@
+platform.feedback.ratelimit.minute-limit=@env.PLATFORM_FEEDBACK_RATELIMIT_MINUTE_LIMIT@
+platform.feedback.ratelimit.day-limit=@env.PLATFORM_FEEDBACK_RATELIMIT_DAY_LIMIT@
+platform.feedback.ratelimit.user-day-limit=@env.PLATFORM_FEEDBACK_RATELIMIT_USER_DAY_LIMIT@
+platform.feedback.ratelimit.fail-window-minutes=@env.PLATFORM_FEEDBACK_RATELIMIT_FAIL_WINDOW_MINUTES@
+platform.feedback.ratelimit.backoff-minutes=@env.PLATFORM_FEEDBACK_RATELIMIT_BACKOFF_MINUTES@
generateBeneficiaryIDs-api-url=@env.GEN_BENEFICIARY_IDS_API_URL@
+
diff --git a/src/main/environment/common_docker.properties b/src/main/environment/common_docker.properties
index 1035af57..a81ea62e 100644
--- a/src/main/environment/common_docker.properties
+++ b/src/main/environment/common_docker.properties
@@ -195,4 +195,15 @@ firebase.credential-file=${FIREBASE_CREDENTIAL}
video-call-url=${VIDEO_CALL_URL}
jibri.output.path={JIBRI_OUTPUT_PATH}
video.recording.path={VIDEO_RECORDING_PATH}
+
+# Platform Feedback module
+platform.feedback.ratelimit.enabled=${PLATFORM_FEEDBACK_RATELIMIT_ENABLED}
+platform.feedback.ratelimit.pepper=${PLATFORM_FEEDBACK_RATELIMIT_PEPPER}
+platform.feedback.ratelimit.trust-forwarded-for=${PLATFORM_FEEDBACK_RATELIMIT_TRUST_FORWARDED_FOR}
+platform.feedback.ratelimit.forwarded-for-header=${PLATFORM_FEEDBACK_RATELIMIT_FORWARDED_FOR_HEADER}
+platform.feedback.ratelimit.minute-limit=${PLATFORM_FEEDBACK_RATELIMIT_MINUTE_LIMIT}
+platform.feedback.ratelimit.day-limit=${PLATFORM_FEEDBACK_RATELIMIT_DAY_LIMIT}
+platform.feedback.ratelimit.user-day-limit=${PLATFORM_FEEDBACK_RATELIMIT_USER_DAY_LIMIT}
+platform.feedback.ratelimit.fail-window-minutes=${PLATFORM_FEEDBACK_RATELIMIT_FAIL_WINDOW_MINUTES}
+platform.feedback.ratelimit.backoff-minutes=${PLATFORM_FEEDBACK_RATELIMIT_BACKOFF_MINUTES}
generateBeneficiaryIDs-api-url={GEN_BENEFICIARY_IDS_API_URL}
diff --git a/src/main/environment/common_example.properties b/src/main/environment/common_example.properties
index bea76523..aca73ddb 100644
--- a/src/main/environment/common_example.properties
+++ b/src/main/environment/common_example.properties
@@ -208,5 +208,21 @@ captcha.enable-captcha=true
cors.allowed-origins=http://localhost:*
+# ---Platform Feedback module ---
+# Rate limiter OFF locally (no Redis required)
+platform.feedback.ratelimit.enabled=true
+platform.feedback.ratelimit.pepper=dev-pepper-123 # dummy
+
+# trust forwarded-for locally is harmless (localhost only)
+platform.feedback.ratelimit.trust-forwarded-for=false
+platform.feedback.ratelimit.forwarded-for-header=X-Forwarded-For
+
+# Optional overrides (not needed if disabled)
+platform.feedback.ratelimit.minute-limit=10
+platform.feedback.ratelimit.day-limit=100
+platform.feedback.ratelimit.user-day-limit=50
+platform.feedback.ratelimit.fail-window-minutes=5
+platform.feedback.ratelimit.backoff-minutes=15
+
### generate Beneficiary IDs URL
generateBeneficiaryIDs-api-url=/generateBeneficiaryController/generateBeneficiaryIDs
diff --git a/src/main/java/com/iemr/common/config/RedisConfig.java b/src/main/java/com/iemr/common/config/RedisConfig.java
index e812b3f9..796ad557 100644
--- a/src/main/java/com/iemr/common/config/RedisConfig.java
+++ b/src/main/java/com/iemr/common/config/RedisConfig.java
@@ -33,6 +33,7 @@
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.iemr.common.data.users.User;
+import org.springframework.data.redis.core.StringRedisTemplate;
@Configuration
public class RedisConfig {
@@ -57,4 +58,10 @@ public RedisTemplate redisTemplate(RedisConnectionFactory factor
return template;
}
+ // new bean for rate limiting & counters
+ @Bean
+ public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
+ return new StringRedisTemplate(factory);
+ }
+
}
diff --git a/src/main/java/com/iemr/common/controller/platform_feedback/PlatformFeedbackController.java b/src/main/java/com/iemr/common/controller/platform_feedback/PlatformFeedbackController.java
new file mode 100644
index 00000000..fd9aad90
--- /dev/null
+++ b/src/main/java/com/iemr/common/controller/platform_feedback/PlatformFeedbackController.java
@@ -0,0 +1,74 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.common.controller;
+
+import com.iemr.common.dto.*;
+import com.iemr.common.service.PlatformFeedbackService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.media.Content;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.HttpStatus;
+import org.springframework.validation.annotation.Validated;
+import jakarta.validation.Valid;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Tag(name = "Platform Feedback", description = "Feedback ingestion and category listing for platform-wide feedback")
+@RestController
+@RequestMapping("/platform-feedback")
+@Validated
+public class PlatformFeedbackController {
+
+ private final PlatformFeedbackService service;
+
+ public PlatformFeedbackController(PlatformFeedbackService service) {
+ this.service = service;
+ }
+
+ @Operation(summary = "Submit feedback (public endpoint)",
+ description = "Accepts feedback (anonymous or identified). Accepts categoryId or categorySlug; slug is preferred.")
+ @ApiResponse(responseCode = "201", description = "Feedback accepted")
+ @ApiResponse(responseCode = "400", description = "Validation or business error", content = @Content)
+ @PostMapping
+ public ResponseEntity submit(
+ @io.swagger.v3.oas.annotations.parameters.RequestBody(description = "Feedback payload")
+ @Valid @RequestBody FeedbackRequest req) {
+ FeedbackResponse resp = service.submitFeedback(req);
+ return ResponseEntity.status(HttpStatus.CREATED).body(resp);
+ }
+
+ @Operation(summary = "List active categories",
+ description = "Returns active categories. Optionally filter by serviceLine (frontend convenience).")
+ @ApiResponse(responseCode = "200", description = "List of categories")
+ @GetMapping("/categories")
+ public ResponseEntity> list(
+ @Parameter(description = "Optional serviceLine to prefer scopes (1097|104|AAM|MMU|TM|ECD)")
+ @RequestParam(required = false) String serviceLine) {
+ if (serviceLine == null) serviceLine = "GLOBAL";
+ List list = service.listCategories(serviceLine);
+ return ResponseEntity.ok(list);
+ }
+}
diff --git a/src/main/java/com/iemr/common/data/platform_feedback/Feedback.java b/src/main/java/com/iemr/common/data/platform_feedback/Feedback.java
new file mode 100644
index 00000000..b499ef3a
--- /dev/null
+++ b/src/main/java/com/iemr/common/data/platform_feedback/Feedback.java
@@ -0,0 +1,171 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.common.model;
+
+import jakarta.persistence.*;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.Size;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+@Entity
+@Table(name = "m_platform_feedback")
+public class Feedback {
+
+ @Id
+ @Column(name = "FeedbackID", length = 36, updatable = false, nullable = false)
+ private String feedbackId;
+
+ @Column(name = "CreatedAt", nullable = false, insertable = false, updatable = false)
+ private LocalDateTime createdAt;
+
+ @Column(name = "UpdatedAt", nullable = false, insertable = false, updatable = false)
+ private LocalDateTime updatedAt;
+
+ @Min(1)
+ @Max(5)
+ @Column(name = "Rating", nullable = false)
+ private int rating;
+
+ @Size(max = 2000)
+ @Column(name = "Comment", columnDefinition = "TEXT", nullable = true)
+ private String comment;
+
+ @Column(name = "ServiceLine", nullable = false, length = 10)
+ private String serviceLine;
+
+ @Column(name = "IsAnonymous", nullable = false)
+ private boolean isAnonymous = true;
+
+ @ManyToOne(fetch = FetchType.LAZY, optional = false)
+ @JoinColumn(name = "CategoryID", referencedColumnName = "CategoryID", nullable = false)
+ private FeedbackCategory category;
+
+ @Column(name = "UserID", nullable = true)
+ private Integer userId;
+
+ public Feedback() {
+ this.feedbackId = UUID.randomUUID().toString();
+ }
+
+ public Feedback(int rating, String comment, String serviceLine, boolean isAnonymous, FeedbackCategory category, Integer userId) {
+ this(); // ensures feedbackId
+ this.setRating(rating);
+ this.setComment(comment);
+ this.setServiceLine(serviceLine);
+ this.isAnonymous = isAnonymous;
+ this.category = category;
+ this.userId = userId;
+ }
+
+ public String getFeedbackId() {
+ return feedbackId;
+ }
+
+ // Don't usually set feedbackId externally, but keep setter for testing/migration if needed
+ public void setFeedbackId(String feedbackId) {
+ this.feedbackId = feedbackId;
+ }
+
+ public LocalDateTime getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(LocalDateTime createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ public LocalDateTime getUpdatedAt() {
+ return updatedAt;
+ }
+
+ public void setUpdatedAt(LocalDateTime updatedAt) {
+ this.updatedAt = updatedAt;
+ }
+
+ public int getRating() {
+ return rating;
+ }
+
+ public void setRating(int rating) {
+ if (rating < 1 || rating > 5) {
+ throw new IllegalArgumentException("Rating must be between 1 and 5.");
+ }
+ this.rating = rating;
+ }
+
+ public String getComment() {
+ return comment;
+ }
+
+ public void setComment(String comment) {
+ if (comment != null && comment.length() > 2000) {
+ throw new IllegalArgumentException("Comment cannot exceed 2000 characters");
+ }
+ this.comment = (comment == null || comment.trim().isEmpty()) ? null : comment.trim();
+ }
+
+ public String getServiceLine() {
+ return serviceLine;
+ }
+
+ public void setServiceLine(String serviceLine) {
+ if (serviceLine == null || serviceLine.trim().isEmpty()) {
+ throw new IllegalArgumentException("ServiceLine must not be null or empty.");
+ }
+ this.serviceLine = serviceLine;
+ }
+
+ public boolean isAnonymous() {
+ return isAnonymous;
+ }
+
+ public void setAnonymous(boolean anonymous) {
+ isAnonymous = anonymous;
+ if (anonymous) {
+ this.userId = null;
+ }
+ }
+
+ public FeedbackCategory getCategory() {
+ return category;
+ }
+
+ public void setCategory(FeedbackCategory category) {
+ if (category == null) {
+ throw new IllegalArgumentException("Category must not be null.");
+ }
+ this.category = category;
+ }
+
+ public Integer getUserId() {
+ return userId;
+ }
+
+ public void setUserId(Integer userId) {
+ this.userId = userId;
+ if (userId != null) {
+ this.isAnonymous = false;
+ }
+ }
+}
diff --git a/src/main/java/com/iemr/common/data/platform_feedback/FeedbackCategory.java b/src/main/java/com/iemr/common/data/platform_feedback/FeedbackCategory.java
new file mode 100644
index 00000000..ee9ffffe
--- /dev/null
+++ b/src/main/java/com/iemr/common/data/platform_feedback/FeedbackCategory.java
@@ -0,0 +1,153 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.common.model;
+
+import jakarta.persistence.*;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+/**
+ * FeedbackCategory maps to the m_feedback_category table.
+ */
+@Entity
+@Table(name = "m_feedback_category", uniqueConstraints = {
+ @UniqueConstraint(name = "uq_category_slug", columnNames = "Slug")
+})
+public class FeedbackCategory {
+
+ @Id
+ @Column(name = "CategoryID", length = 36, updatable = false, nullable = false)
+ private String categoryId;
+
+ @NotBlank
+ @Pattern(regexp = "^[a-z0-9]+(?:-[a-z0-9]+)*$", message = "Slug must be lowercase alphanumeric with optional single dashes")
+ @Size(max = 64)
+ @Column(name = "Slug", nullable = false, length = 64)
+ private String slug;
+
+ @NotBlank
+ @Size(max = 128)
+ @Column(name = "Label", nullable = false, length = 128)
+ private String label;
+
+ @NotBlank
+ @Size(max = 20)
+ @Column(name = "Scope", nullable = false, length = 20)
+ private String scope;
+
+ @Column(name = "Active", nullable = false)
+ private boolean active = true;
+
+
+ @Column(name = "CreatedAt", nullable = false, insertable = false, updatable = false)
+ private LocalDateTime createdAt;
+
+ @Column(name = "UpdatedAt", nullable = false, insertable = false, updatable = false)
+ private LocalDateTime updatedAt;
+
+ // ===== Constructors =====
+ public FeedbackCategory() {
+ // nothing — categoryId will be generated at persist if not provided
+ }
+
+ public FeedbackCategory(String slug, String label, String scope, boolean active) {
+ this.slug = slug;
+ this.label = label;
+ this.scope = scope;
+ this.active = active;
+ }
+
+ // ===== JPA lifecycle hooks =====
+ /**
+ * Ensure CategoryID exists for new rows created via JPA. If DB seeding provides IDs,
+ * those will be used instead (we only set if null).
+ */
+ @PrePersist
+ protected void ensureId() {
+ if (this.categoryId == null || this.categoryId.trim().isEmpty()) {
+ this.categoryId = UUID.randomUUID().toString();
+ }
+ }
+
+ // ===== Getters & Setters =====
+ public String getCategoryId() {
+ return categoryId;
+ }
+
+ // categoryId is generated at persist; setter kept for migration/tests
+ public void setCategoryId(String categoryId) {
+ this.categoryId = categoryId;
+ }
+
+ public String getSlug() {
+ return slug;
+ }
+
+ public void setSlug(String slug) {
+ this.slug = slug;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public void setLabel(String label) {
+ this.label = label;
+ }
+
+ public String getScope() {
+ return scope;
+ }
+
+ public void setScope(String scope) {
+ this.scope = scope;
+ }
+
+ public boolean isActive() {
+ return active;
+ }
+
+ public void setActive(boolean active) {
+ this.active = active;
+ }
+
+ public LocalDateTime getCreatedAt() {
+ return createdAt;
+ }
+
+ // CreatedAt normally set by DB; setter available for tests/migrations
+ public void setCreatedAt(LocalDateTime createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ public LocalDateTime getUpdatedAt() {
+ return updatedAt;
+ }
+
+ // UpdatedAt normally managed by DB
+ public void setUpdatedAt(LocalDateTime updatedAt) {
+ this.updatedAt = updatedAt;
+ }
+}
diff --git a/src/main/java/com/iemr/common/dto/platform_feedback/CategoryResponse.java b/src/main/java/com/iemr/common/dto/platform_feedback/CategoryResponse.java
new file mode 100644
index 00000000..df1c7184
--- /dev/null
+++ b/src/main/java/com/iemr/common/dto/platform_feedback/CategoryResponse.java
@@ -0,0 +1,24 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.common.dto;
+
+public record CategoryResponse(String categoryId, String slug, String label, String scope, boolean active) {}
\ No newline at end of file
diff --git a/src/main/java/com/iemr/common/dto/platform_feedback/FeedbackRequest.java b/src/main/java/com/iemr/common/dto/platform_feedback/FeedbackRequest.java
new file mode 100644
index 00000000..7002ece9
--- /dev/null
+++ b/src/main/java/com/iemr/common/dto/platform_feedback/FeedbackRequest.java
@@ -0,0 +1,38 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.common.dto;
+
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+import java.time.LocalDateTime;
+
+public record FeedbackRequest(
+ @Min(1) @Max(5) int rating,
+ String categoryId,
+ String categorySlug,
+ @Size(max = 2000) String comment,
+ boolean isAnonymous,
+ @Pattern(regexp = "^(1097|104|AAM|MMU|TM|ECD)$") String serviceLine,
+ Integer userId
+) {}
\ No newline at end of file
diff --git a/src/main/java/com/iemr/common/dto/platform_feedback/FeedbackResponse.java b/src/main/java/com/iemr/common/dto/platform_feedback/FeedbackResponse.java
new file mode 100644
index 00000000..6b348f6e
--- /dev/null
+++ b/src/main/java/com/iemr/common/dto/platform_feedback/FeedbackResponse.java
@@ -0,0 +1,26 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.common.dto;
+import java.time.LocalDateTime;
+
+
+public record FeedbackResponse(String id, LocalDateTime createdAt) {}
\ No newline at end of file
diff --git a/src/main/java/com/iemr/common/exception/BadRequestException.java b/src/main/java/com/iemr/common/exception/BadRequestException.java
new file mode 100644
index 00000000..65d146a6
--- /dev/null
+++ b/src/main/java/com/iemr/common/exception/BadRequestException.java
@@ -0,0 +1,31 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.common.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(HttpStatus.BAD_REQUEST)
+public class BadRequestException extends RuntimeException {
+ public BadRequestException(String message) { super(message); }
+ public BadRequestException(String message, Throwable cause) { super(message, cause); }
+}
diff --git a/src/main/java/com/iemr/common/filter/PlatformFeedbackRateLimitFilter.java b/src/main/java/com/iemr/common/filter/PlatformFeedbackRateLimitFilter.java
new file mode 100644
index 00000000..d6dddf55
--- /dev/null
+++ b/src/main/java/com/iemr/common/filter/PlatformFeedbackRateLimitFilter.java
@@ -0,0 +1,216 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.common.filter;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.Base64;
+import java.util.concurrent.TimeUnit;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+
+
+@Component
+@ConditionalOnProperty(prefix = "platform.feedback.ratelimit", name = "enabled", havingValue = "true", matchIfMissing = false)
+@Order(Ordered.HIGHEST_PRECEDENCE + 10) // run early (adjust order as needed)
+public class PlatformFeedbackRateLimitFilter extends OncePerRequestFilter {
+
+ private final StringRedisTemplate redis;
+ private final String pepper;
+ private final boolean trustForwardedFor;
+ private final String forwardedForHeader;
+
+ // Limits & TTLs (tweak if needed)
+ private static final int MINUTE_LIMIT = 10;
+ private static final int DAY_LIMIT = 100;
+ private static final int USER_DAY_LIMIT = 50; // for identified users
+ private static final Duration MINUTE_WINDOW = Duration.ofMinutes(1);
+ private static final Duration DAY_WINDOW = Duration.ofHours(48); // keep key TTL ~48h
+ private static final Duration FAIL_COUNT_WINDOW = Duration.ofMinutes(5);
+ private static final int FAILS_TO_BACKOFF = 3;
+ private static final Duration BACKOFF_WINDOW = Duration.ofMinutes(15);
+
+ public PlatformFeedbackRateLimitFilter(StringRedisTemplate redis,
+ org.springframework.core.env.Environment env) {
+ this.redis = redis;
+ this.pepper = env.getProperty("platform.feedback.pepper", "");
+ this.trustForwardedFor = Boolean.parseBoolean(env.getProperty("platform.feedback.trust-forwarded-for", "true"));
+ this.forwardedForHeader = env.getProperty("platform.feedback.forwarded-for-header", "X-Forwarded-For");
+ }
+
+ @Override
+ protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
+ // Only filter specific endpoints (POST to platform-feedback). Keep it narrow.
+ String path = request.getRequestURI();
+ String method = request.getMethod();
+ // adjust path as needed (supports /common-api/platform-feedback and subpaths)
+ return !("POST".equalsIgnoreCase(method) && path != null && path.matches("^/platform-feedback(?:/.*)?$"));
+ }
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException {
+ // compute day key
+ String clientIp = extractClientIp(request);
+ if (clientIp == null || clientIp.isBlank()) {
+ // If we can't identify an IP, be conservative and allow but log (or optionally block)
+ filterChain.doFilter(request, response);
+ return;
+ }
+
+ String today = LocalDate.now(ZoneId.of("Asia/Kolkata")).toString().replaceAll("-", ""); // yyyyMMdd
+ String ipHash = sha256Base64(clientIp + pepper + today); // base64 shorter storage; not reversible without pepper
+ String minKey = "rl:fb:min:" + ipHash + ":" + (System.currentTimeMillis() / 60000L); // minute-slotted
+ String dayKey = "rl:fb:day:" + today + ":" + ipHash;
+ String failKey = "rl:fb:fail:" + ipHash;
+ String backoffKey = "rl:fb:backoff:" + ipHash;
+
+ // If under backoff -> respond 429 with Retry-After = TTL
+ Long backoffTtl = getTtlSeconds(backoffKey);
+ if (backoffTtl != null && backoffTtl > 0) {
+ sendTooMany(response, backoffTtl);
+ return;
+ }
+
+ // Minute window check (INCR + TTL if first)
+ long minuteCount = incrementWithExpire(minKey, 1, MINUTE_WINDOW.getSeconds());
+ if (minuteCount > MINUTE_LIMIT) {
+ handleFailureAndMaybeBackoff(failKey, backoffKey, response, minKey, dayKey);
+ return;
+ }
+
+ // Day window check
+ long dayCount = incrementWithExpire(dayKey, 1, DAY_WINDOW.getSeconds());
+ if (dayCount > DAY_LIMIT) {
+ handleFailureAndMaybeBackoff(failKey, backoffKey, response, minKey, dayKey);
+ return;
+ }
+
+ // Optional: per-user daily cap if we can extract an authenticated user id from header/jwt
+ Integer userId = extractUserIdFromRequest(request); // implement extraction as per your JWT scheme
+ if (userId != null) {
+ String userDayKey = "rl:fb:user:" + today + ":" + userId;
+ long ucount = incrementWithExpire(userDayKey, 1, DAY_WINDOW.getSeconds());
+ if (ucount > USER_DAY_LIMIT) {
+ handleFailureAndMaybeBackoff(failKey, backoffKey, response, minKey, userDayKey);
+ return;
+ }
+ }
+
+ // All checks passed — proceed to controller
+ filterChain.doFilter(request, response);
+ }
+
+ // increments key by delta; sets TTL when key is new (INCR returns 1)
+ private long incrementWithExpire(String key, long delta, long ttlSeconds) {
+ Long value = redis.opsForValue().increment(key, delta);
+ if (value != null && value == 1L) {
+ redis.expire(key, ttlSeconds, TimeUnit.SECONDS);
+ }
+ return value == null ? 0L : value;
+ }
+
+ private void handleFailureAndMaybeBackoff(String failKey, String backoffKey, HttpServletResponse response, String trigKey, String dayKey) throws IOException {
+ // increment fail counter and possibly set backoff
+ Long fails = redis.opsForValue().increment(failKey, 1);
+ if (fails != null && fails == 1L) {
+ redis.expire(failKey, FAIL_COUNT_WINDOW.getSeconds(), TimeUnit.SECONDS);
+ }
+ if (fails != null && fails >= FAILS_TO_BACKOFF) {
+ // set backoff flag
+ redis.opsForValue().set(backoffKey, "1", BACKOFF_WINDOW.getSeconds(), TimeUnit.SECONDS);
+ sendTooMany(response, BACKOFF_WINDOW.getSeconds());
+ return;
+ }
+
+ // otherwise respond with Retry-After for the triggering key TTL (minute/day)
+ Long retryAfter = getTtlSeconds(trigKey);
+ if (retryAfter == null || retryAfter <= 0) retryAfter = 60L;
+ sendTooMany(response, retryAfter);
+ }
+
+ private void sendTooMany(HttpServletResponse response, long retryAfterSeconds) throws IOException {
+ response.setStatus(429);
+ response.setHeader("Retry-After", String.valueOf(retryAfterSeconds));
+ response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+ String body = String.format("{\"code\":\"RATE_LIMITED\",\"message\":\"Too many requests\",\"retryAfter\":%d}", retryAfterSeconds);
+ response.getWriter().write(body);
+ }
+
+ private Long getTtlSeconds(String key) {
+ Long ttl = redis.getExpire(key, TimeUnit.SECONDS);
+ return ttl == null || ttl < 0 ? null : ttl;
+ }
+
+ private String extractClientIp(HttpServletRequest request) {
+ if (trustForwardedFor) {
+ String header = request.getHeader(forwardedForHeader);
+ if (StringUtils.hasText(header)) {
+ // X-Forwarded-For may contain comma-separated list; take the first (client) entry
+ String[] parts = header.split(",");
+ if (parts.length > 0) {
+ String ip = parts[0].trim();
+ if (StringUtils.hasText(ip)) return ip;
+ }
+ }
+ }
+ return request.getRemoteAddr();
+ }
+
+ private Integer extractUserIdFromRequest(HttpServletRequest request) {
+ // implement based on how you propagate JWT or user info.
+ // Example: if your gateway injects header X-User-Id for authenticated requests:
+ String s = request.getHeader("X-User-Id");
+ if (StringUtils.hasText(s)) {
+ try { return Integer.valueOf(s); } catch (NumberFormatException ignored) {}
+ }
+ // If JWT parsing required, do it here, but keep this filter light — prefer upstream auth filter to populate a header.
+ return null;
+ }
+
+ private static String sha256Base64(String input) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hashed = digest.digest(input.getBytes(StandardCharsets.UTF_8));
+ // base64 url-safe or normal base64 — either is fine; base64 is shorter than hex
+ return Base64.getUrlEncoder().withoutPadding().encodeToString(hashed);
+ } catch (Exception ex) {
+ throw new RuntimeException("sha256 failure", ex);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/iemr/common/repository/platform_feedback/PlatformFeedbackCategoryRepository.java b/src/main/java/com/iemr/common/repository/platform_feedback/PlatformFeedbackCategoryRepository.java
new file mode 100644
index 00000000..083ca5f7
--- /dev/null
+++ b/src/main/java/com/iemr/common/repository/platform_feedback/PlatformFeedbackCategoryRepository.java
@@ -0,0 +1,34 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.common.repository.platform_feedback;
+
+import com.iemr.common.model.FeedbackCategory;
+import java.util.List;
+import java.util.Optional;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface PlatformFeedbackCategoryRepository extends JpaRepository {
+ Optional findBySlugIgnoreCase(String slug);
+ List findByActiveTrueOrderByLabelAsc();
+}
diff --git a/src/main/java/com/iemr/common/repository/platform_feedback/PlatformFeedbackRepository.java b/src/main/java/com/iemr/common/repository/platform_feedback/PlatformFeedbackRepository.java
new file mode 100644
index 00000000..fd00bdbc
--- /dev/null
+++ b/src/main/java/com/iemr/common/repository/platform_feedback/PlatformFeedbackRepository.java
@@ -0,0 +1,30 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.common.repository.platform_feedback;
+
+import com.iemr.common.model.Feedback;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface PlatformFeedbackRepository extends JpaRepository {
+}
diff --git a/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryServiceImpl.java b/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryServiceImpl.java
index 2e6ac1d4..a79d3683 100644
--- a/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryServiceImpl.java
+++ b/src/main/java/com/iemr/common/service/beneficiary/IdentityBeneficiaryServiceImpl.java
@@ -42,6 +42,7 @@
import com.iemr.common.utils.mapper.InputMapper;
import com.iemr.common.utils.mapper.OutputMapper;
import com.iemr.common.utils.response.OutputResponse;
+import org.springframework.beans.factory.annotation.Value;
@Service
public class IdentityBeneficiaryServiceImpl implements IdentityBeneficiaryService {
@@ -51,6 +52,7 @@ public class IdentityBeneficiaryServiceImpl implements IdentityBeneficiaryServic
Logger logger = LoggerFactory.getLogger(this.getClass().getName());
private static HttpUtils httpUtils = new HttpUtils();
private InputMapper inputMapper = new InputMapper();
+
@Value("${identity-api-url}")
private String identityBaseURL;
@Value("${identity-1097-api-url}")
diff --git a/src/main/java/com/iemr/common/service/platform_feedback/PlatformFeedbackService.java b/src/main/java/com/iemr/common/service/platform_feedback/PlatformFeedbackService.java
new file mode 100644
index 00000000..5149e3ed
--- /dev/null
+++ b/src/main/java/com/iemr/common/service/platform_feedback/PlatformFeedbackService.java
@@ -0,0 +1,113 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
+package com.iemr.common.service;
+
+import com.iemr.common.dto.*;
+import com.iemr.common.model.Feedback;
+import com.iemr.common.model.FeedbackCategory;
+import com.iemr.common.repository.platform_feedback.PlatformFeedbackCategoryRepository;
+import com.iemr.common.repository.platform_feedback.PlatformFeedbackRepository;
+import com.iemr.common.exception.BadRequestException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.UUID;
+
+@Service
+public class PlatformFeedbackService {
+
+ private final PlatformFeedbackRepository feedbackRepo;
+ private final PlatformFeedbackCategoryRepository categoryRepo;
+
+ public PlatformFeedbackService(PlatformFeedbackRepository feedbackRepo,
+ PlatformFeedbackCategoryRepository categoryRepo) {
+ this.feedbackRepo = feedbackRepo;
+ this.categoryRepo = categoryRepo;
+ }
+
+ @Transactional
+ public FeedbackResponse submitFeedback(FeedbackRequest req) {
+ // basic validations
+ if (req.rating() < 1 || req.rating() > 5) {
+ throw new BadRequestException("rating must be between 1 and 5");
+ }
+ if (!req.isAnonymous() && req.userId() == null) {
+ throw new BadRequestException("userId required when isAnonymous=false");
+ }
+
+ FeedbackCategory category = resolveCategory(req.categoryId(), req.categorySlug(), req.serviceLine());
+
+ Feedback fb = new Feedback();
+ fb.setFeedbackId(UUID.randomUUID().toString());
+ fb.setCreatedAt(LocalDateTime.now());
+ fb.setUpdatedAt(LocalDateTime.now());
+ fb.setRating(req.rating());
+ fb.setComment(req.comment() == null ? "" : req.comment());
+ fb.setServiceLine(req.serviceLine());
+ fb.setAnonymous(req.isAnonymous());
+ fb.setCategory(category);
+ fb.setUserId(req.userId());
+
+ feedbackRepo.save(fb);
+ return new FeedbackResponse(fb.getFeedbackId(), fb.getCreatedAt());
+ }
+
+ private FeedbackCategory resolveCategory(String categoryId, String categorySlug, String serviceLine) {
+ if (categoryId != null && categorySlug != null) {
+ FeedbackCategory byId = categoryRepo.findById(categoryId)
+ .orElseThrow(() -> new BadRequestException("invalid categoryId"));
+ if (!byId.getSlug().equalsIgnoreCase(categorySlug)) {
+ throw new BadRequestException("categoryId and categorySlug mismatch");
+ }
+ if (!byId.isActive()) throw new BadRequestException("category inactive");
+ // optional: check scope matches serviceLine or GLOBAL
+ return byId;
+ }
+
+ if (categoryId != null) {
+ FeedbackCategory byId = categoryRepo.findById(categoryId)
+ .orElseThrow(() -> new BadRequestException("invalid categoryId"));
+ if (!byId.isActive()) throw new BadRequestException("category inactive");
+ return byId;
+ }
+
+ if (categorySlug != null) {
+ FeedbackCategory bySlug = categoryRepo.findBySlugIgnoreCase(categorySlug)
+ .orElseThrow(() -> new BadRequestException("invalid categorySlug"));
+ if (!bySlug.isActive()) throw new BadRequestException("category inactive");
+ return bySlug;
+ }
+
+ throw new BadRequestException("categoryId or categorySlug required");
+ }
+
+ @Transactional(readOnly = true)
+ public List listCategories(String serviceLine) {
+ List all = categoryRepo.findByActiveTrueOrderByLabelAsc();
+ // filter by scope or return all; FE can filter further
+ return all.stream()
+ .filter(c -> "GLOBAL".equals(c.getScope()) || c.getScope().equalsIgnoreCase(serviceLine))
+ .map(c -> new CategoryResponse(c.getCategoryId(), c.getSlug(), c.getLabel(), c.getScope(), c.isActive()))
+ .toList();
+ }
+}
diff --git a/src/main/java/com/iemr/common/utils/FilterConfig.java b/src/main/java/com/iemr/common/utils/FilterConfig.java
index 9f6efb13..42bd04ad 100644
--- a/src/main/java/com/iemr/common/utils/FilterConfig.java
+++ b/src/main/java/com/iemr/common/utils/FilterConfig.java
@@ -1,28 +1,102 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
package com.iemr.common.utils;
+import com.iemr.common.filter.PlatformFeedbackRateLimitFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
-import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.env.Environment;
+import org.springframework.data.redis.core.StringRedisTemplate;
@Configuration
public class FilterConfig {
- @Value("${cors.allowed-origins}")
- private String allowedOrigins;
+ private static final Logger log = LoggerFactory.getLogger(FilterConfig.class);
+
+ @Value("${cors.allowed-origins}")
+ private String allowedOrigins;
+
+ @Bean
+ public FilterRegistrationBean jwtUserIdValidationFilter(
+ JwtAuthenticationUtil jwtAuthenticationUtil) {
+ FilterRegistrationBean registrationBean = new FilterRegistrationBean<>();
+
+ // Pass allowedOrigins explicitly to the filter constructor
+ JwtUserIdValidationFilter filter = new JwtUserIdValidationFilter(jwtAuthenticationUtil, allowedOrigins);
+
+ registrationBean.setFilter(filter);
+ registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
+ registrationBean.addUrlPatterns("/*"); // Apply filter to all API endpoints
+ log.info("Registered JwtUserIdValidationFilter on /* with order {}", Ordered.HIGHEST_PRECEDENCE);
+ return registrationBean;
+ }
+
+ /**
+ * Register the platform feedback rate-limit filter in a non-invasive way.
+ *
+ * - The filter is mapped only to the public feedback endpoints to avoid affecting other routes.
+ * - Order is intentionally set after the Jwt filter so authentication runs first.
+ */
+ @Bean
+ public FilterRegistrationBean platformFeedbackRateLimitFilter(
+ StringRedisTemplate stringRedisTemplate,
+ Environment env) {
+
+ // Read flag from environment (property file or env var)
+ boolean enabled = Boolean.parseBoolean(env.getProperty("platform.feedback.ratelimit.enabled", "false"));
+
+ // Allow optional override for order if needed
+ int defaultOrder = Ordered.HIGHEST_PRECEDENCE + 10;
+ int order = defaultOrder;
+ String orderStr = env.getProperty("platform.feedback.ratelimit.order");
+ if (orderStr != null) {
+ try {
+ order = Integer.parseInt(orderStr);
+ } catch (NumberFormatException e) {
+ log.warn("Invalid platform.feedback.ratelimit.order value '{}', using default {}", orderStr, defaultOrder);
+ }
+ }
+
+ PlatformFeedbackRateLimitFilter filter = new PlatformFeedbackRateLimitFilter(stringRedisTemplate, env);
+
+ FilterRegistrationBean reg = new FilterRegistrationBean<>(filter);
+
+ reg.addUrlPatterns(
+ "/platform-feedback/*",
+ "/platform-feedback"
+ );
- @Bean
- public FilterRegistrationBean jwtUserIdValidationFilter(
- JwtAuthenticationUtil jwtAuthenticationUtil) {
- FilterRegistrationBean registrationBean = new FilterRegistrationBean<>();
+ reg.setOrder(order);
+ // Do not remove the bean from context when disabled; keep registration but disable execution
+ reg.setEnabled(enabled);
- // Pass allowedOrigins explicitly to the filter constructor
- JwtUserIdValidationFilter filter = new JwtUserIdValidationFilter(jwtAuthenticationUtil, allowedOrigins);
+ log.info("Registered PlatformFeedbackRateLimitFilter (enabled={}, order={}) mapped to {}",
+ enabled, order, reg.getUrlPatterns());
- registrationBean.setFilter(filter);
- registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
- registrationBean.addUrlPatterns("/*"); // Apply filter to all API endpoints
- return registrationBean;
- }
-}
+ return reg;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java
index 412352fc..81d79221 100644
--- a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java
+++ b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java
@@ -1,3 +1,24 @@
+/*
+ * AMRIT – Accessible Medical Records via Integrated Technology
+ * Integrated EHR (Electronic Health Records) Solution
+ *
+ * Copyright (C) "Piramal Swasthya Management and Research Institute"
+ *
+ * This file is part of AMRIT.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see https://www.gnu.org/licenses/.
+ */
package com.iemr.common.utils;
import java.io.IOException;
@@ -99,6 +120,13 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo
logger.info("JwtUserIdValidationFilter invoked for path: " + path);
+ // NEW: if this is a platform-feedback endpoint, treat it as public (skip auth)
+ // and also ensure we don't clear any user cookies for these requests.
+ if (isPlatformFeedbackPath(path, contextPath)) {
+ logger.debug("Platform-feedback path detected - skipping authentication and leaving cookies intact: {}", path);
+ filterChain.doFilter(servletRequest, servletResponse);
+ return;
+ }
// Log cookies for debugging
Cookie[] cookies = request.getCookies();
if (cookies != null) {
@@ -166,6 +194,19 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo
}
}
+ /**
+ * New helper: identifies platform-feedback endpoints so we can treat them
+ * specially (public + preserve cookies).
+ */
+ private boolean isPlatformFeedbackPath(String path, String contextPath) {
+ if (path == null) return false;
+ String normalized = path.toLowerCase();
+ String base = (contextPath == null ? "" : contextPath).toLowerCase();
+ // match /platform-feedback and anything under it
+ return normalized.startsWith(base + "/platform-feedback");
+ }
+
+
private boolean isOriginAllowed(String origin) {
if (origin == null || allowedOrigins == null || allowedOrigins.trim().isEmpty()) {
logger.warn("No allowed origins configured or origin is null");
diff --git a/src/main/java/com/iemr/common/utils/RestTemplateUtil.java b/src/main/java/com/iemr/common/utils/RestTemplateUtil.java
index 447ba80f..c8299fe7 100644
--- a/src/main/java/com/iemr/common/utils/RestTemplateUtil.java
+++ b/src/main/java/com/iemr/common/utils/RestTemplateUtil.java
@@ -10,14 +10,17 @@
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
+import com.iemr.common.constant.Constants;
+
import jakarta.servlet.http.HttpServletRequest;
public class RestTemplateUtil {
private final static Logger logger = LoggerFactory.getLogger(RestTemplateUtil.class);
-
+
public static HttpEntity