diff --git a/medplat-android/.gradle/7.2/fileChanges/last-build.bin b/medplat-android/.gradle/7.2/fileChanges/last-build.bin
new file mode 100644
index 000000000..f76dd238a
Binary files /dev/null and b/medplat-android/.gradle/7.2/fileChanges/last-build.bin differ
diff --git a/medplat-android/.gradle/7.2/fileHashes/fileHashes.lock b/medplat-android/.gradle/7.2/fileHashes/fileHashes.lock
new file mode 100644
index 000000000..eeea1d5d2
Binary files /dev/null and b/medplat-android/.gradle/7.2/fileHashes/fileHashes.lock differ
diff --git a/medplat-android/.gradle/7.2/gc.properties b/medplat-android/.gradle/7.2/gc.properties
new file mode 100644
index 000000000..e69de29bb
diff --git a/medplat-android/.gradle/checksums/checksums.lock b/medplat-android/.gradle/checksums/checksums.lock
new file mode 100644
index 000000000..f8abfb771
Binary files /dev/null and b/medplat-android/.gradle/checksums/checksums.lock differ
diff --git a/medplat-android/.gradle/vcs-1/gc.properties b/medplat-android/.gradle/vcs-1/gc.properties
new file mode 100644
index 000000000..e69de29bb
diff --git a/medplat-web/pom.xml b/medplat-web/pom.xml
index 9a95bf8aa..c52a55d80 100644
--- a/medplat-web/pom.xml
+++ b/medplat-web/pom.xml
@@ -268,6 +268,12 @@
xstream
1.4.20
+
+
+ com.bucket4j
+ bucket4j-core
+ 8.1.1
+
diff --git a/medplat-web/src/main/java/com/argusoft/medplat/config/RateLimitConfig.java b/medplat-web/src/main/java/com/argusoft/medplat/config/RateLimitConfig.java
new file mode 100644
index 000000000..881804dfc
--- /dev/null
+++ b/medplat-web/src/main/java/com/argusoft/medplat/config/RateLimitConfig.java
@@ -0,0 +1,19 @@
+package com.argusoft.medplat.config;
+
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class RateLimitConfig {
+
+ @Bean
+ public FilterRegistrationBean rateLimitingFilter() {
+ FilterRegistrationBean registration = new FilterRegistrationBean<>();
+ registration.setFilter(new RateLimitingFilter());
+ registration.addUrlPatterns("/api/*");
+ registration.setName("rateLimitingFilter");
+ registration.setOrder(1);
+ return registration;
+ }
+}
\ No newline at end of file
diff --git a/medplat-web/src/main/java/com/argusoft/medplat/config/RateLimitDao.java b/medplat-web/src/main/java/com/argusoft/medplat/config/RateLimitDao.java
new file mode 100644
index 000000000..87a82f823
--- /dev/null
+++ b/medplat-web/src/main/java/com/argusoft/medplat/config/RateLimitDao.java
@@ -0,0 +1,60 @@
+package com.argusoft.medplat.config;
+
+import org.hibernate.SessionFactory;
+import org.hibernate.query.NativeQuery;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Date;
+import java.util.List;
+
+@Repository
+@Transactional
+public class RateLimitDao {
+
+ @Autowired
+ private SessionFactory sessionFactory;
+
+ // Read config value from DB (e.g. RATE_LIMIT_GLOBAL)
+ public String getConfigValue(String key, String defaultValue) {
+ try {
+ String sql = "SELECT config_value FROM rate_limit_config WHERE config_key = :key";
+ NativeQuery query = sessionFactory.getCurrentSession().createNativeQuery(sql);
+ query.setParameter("key", key);
+ String result = query.uniqueResult();
+ return result != null ? result : defaultValue;
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+
+ // Read sensitive endpoints list from DB
+ public List getSensitiveEndpoints() {
+ try {
+ String sql = "SELECT endpoint_pattern FROM rate_limit_sensitive_endpoints";
+ NativeQuery query = sessionFactory.getCurrentSession().createNativeQuery(sql);
+ return query.list();
+ } catch (Exception e) {
+ return List.of("/login", "/users", "/oauth", "/password");
+ }
+ }
+
+ // Save violation log to DB
+ public void saveViolationLog(String ipAddress, String endpoint, boolean isSensitive) {
+ try {
+ String sql = "INSERT INTO rate_limit_violation_log " +
+ "(ip_address, endpoint, is_sensitive, violated_at) " +
+ "VALUES (:ip, :endpoint, :isSensitive, :violatedAt)";
+ NativeQuery> query = sessionFactory.getCurrentSession().createNativeQuery(sql);
+ query.setParameter("ip", ipAddress);
+ query.setParameter("endpoint", endpoint);
+ query.setParameter("isSensitive", isSensitive);
+ query.setParameter("violatedAt", new Date());
+ query.executeUpdate();
+ } catch (Exception e) {
+ // fallback log to console if DB fails
+ System.err.println("[RATE LIMIT LOG FAILED] " + e.getMessage());
+ }
+ }
+}
\ No newline at end of file
diff --git a/medplat-web/src/main/java/com/argusoft/medplat/config/RateLimitViolationLog.java b/medplat-web/src/main/java/com/argusoft/medplat/config/RateLimitViolationLog.java
new file mode 100644
index 000000000..3af105735
--- /dev/null
+++ b/medplat-web/src/main/java/com/argusoft/medplat/config/RateLimitViolationLog.java
@@ -0,0 +1,46 @@
+package com.argusoft.medplat.config;
+
+import javax.persistence.*;
+import java.util.Date;
+
+@Entity
+@Table(name = "rate_limit_violation_log")
+public class RateLimitViolationLog {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Integer id;
+
+ @Column(name = "ip_address")
+ private String ipAddress;
+
+ @Column(name = "endpoint")
+ private String endpoint;
+
+ @Column(name = "is_sensitive")
+ private Boolean isSensitive;
+
+ @Column(name = "violated_at")
+ @Temporal(TemporalType.TIMESTAMP)
+ private Date violatedAt;
+
+ public RateLimitViolationLog() {}
+
+ public RateLimitViolationLog(String ipAddress, String endpoint, Boolean isSensitive) {
+ this.ipAddress = ipAddress;
+ this.endpoint = endpoint;
+ this.isSensitive = isSensitive;
+ this.violatedAt = new Date();
+ }
+
+ public Integer getId() { return id; }
+ public void setId(Integer id) { this.id = id; }
+ public String getIpAddress() { return ipAddress; }
+ public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; }
+ public String getEndpoint() { return endpoint; }
+ public void setEndpoint(String endpoint) { this.endpoint = endpoint; }
+ public Boolean getIsSensitive() { return isSensitive; }
+ public void setIsSensitive(Boolean isSensitive) { this.isSensitive = isSensitive; }
+ public Date getViolatedAt() { return violatedAt; }
+ public void setViolatedAt(Date violatedAt) { this.violatedAt = violatedAt; }
+}
\ No newline at end of file
diff --git a/medplat-web/src/main/java/com/argusoft/medplat/config/RateLimitingFilter.java b/medplat-web/src/main/java/com/argusoft/medplat/config/RateLimitingFilter.java
new file mode 100644
index 000000000..444235d68
--- /dev/null
+++ b/medplat-web/src/main/java/com/argusoft/medplat/config/RateLimitingFilter.java
@@ -0,0 +1,97 @@
+package com.argusoft.medplat.config;
+
+import io.github.bucket4j.Bandwidth;
+import io.github.bucket4j.Bucket;
+import io.github.bucket4j.Refill;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+@Order(1)
+public class RateLimitingFilter implements Filter {
+
+ private static final Logger log = LoggerFactory.getLogger(RateLimitingFilter.class);
+
+ @Autowired
+ private RateLimitDao rateLimitDao;
+
+ private final Map buckets = new ConcurrentHashMap<>();
+
+ private Bucket createBucket(int limit) {
+ return Bucket.builder()
+ .addLimit(Bandwidth.classic(limit,
+ Refill.intervally(limit, Duration.ofMinutes(1))))
+ .build();
+ }
+
+ private boolean isSensitivePath(String path) {
+ try {
+ List sensitiveEndpoints = rateLimitDao.getSensitiveEndpoints();
+ return sensitiveEndpoints.stream().anyMatch(path::contains);
+ } catch (Exception e) {
+ // fallback if DB not available
+ return path.contains("/login") || path.contains("/users") ||
+ path.contains("/oauth") || path.contains("/password");
+ }
+ }
+
+ private int getLimit(String key, String defaultValue) {
+ try {
+ String value = rateLimitDao.getConfigValue(key, defaultValue);
+ return Integer.parseInt(value);
+ } catch (Exception e) {
+ return Integer.parseInt(defaultValue);
+ }
+ }
+
+ @Override
+ public void doFilter(ServletRequest req, ServletResponse res,
+ FilterChain chain) throws IOException, ServletException {
+
+ HttpServletRequest request = (HttpServletRequest) req;
+ HttpServletResponse response = (HttpServletResponse) res;
+
+ String ip = request.getRemoteAddr();
+ String path = request.getRequestURI();
+ boolean sensitive = isSensitivePath(path);
+
+ int limit = sensitive
+ ? getLimit("RATE_LIMIT_SENSITIVE", "10")
+ : getLimit("RATE_LIMIT_GLOBAL", "100");
+
+ String bucketKey = ip + ":" + (sensitive ? "sensitive" : "global");
+ Bucket bucket = buckets.computeIfAbsent(bucketKey, k -> createBucket(limit));
+
+ if (bucket.tryConsume(1)) {
+ chain.doFilter(req, res);
+ } else {
+ // Log violation to DB
+ log.warn("[RATE LIMIT VIOLATED] IP={} | Path={} | Sensitive={}", ip, path, sensitive);
+ rateLimitDao.saveViolationLog(ip, path, sensitive);
+
+ response.setStatus(429);
+ response.setContentType("application/json");
+ response.getWriter().write(
+ "{\"status\": 429, " +
+ "\"error\": \"Too Many Requests\", " +
+ "\"message\": \"Rate limit exceeded. Please try again later.\"}"
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/medplat-web/src/main/resources/db/migration/V21__rate_limit_config_and_logs.sql b/medplat-web/src/main/resources/db/migration/V21__rate_limit_config_and_logs.sql
new file mode 100644
index 000000000..a12c00c56
--- /dev/null
+++ b/medplat-web/src/main/resources/db/migration/V21__rate_limit_config_and_logs.sql
@@ -0,0 +1,33 @@
+-- Table to store rate limit configurations
+CREATE TABLE IF NOT EXISTS rate_limit_config (
+ id SERIAL PRIMARY KEY,
+ config_key VARCHAR(100) NOT NULL UNIQUE,
+ config_value VARCHAR(100) NOT NULL,
+ description VARCHAR(255)
+);
+
+-- Default values
+INSERT INTO rate_limit_config (config_key, config_value, description)
+VALUES
+ ('RATE_LIMIT_GLOBAL', '100', 'Max requests per minute per IP for all endpoints'),
+ ('RATE_LIMIT_SENSITIVE', '10', 'Max requests per minute per IP for sensitive endpoints')
+ON CONFLICT (config_key) DO NOTHING;
+
+-- Sensitive endpoints stored in DB
+CREATE TABLE IF NOT EXISTS rate_limit_sensitive_endpoints (
+ id SERIAL PRIMARY KEY,
+ endpoint_pattern VARCHAR(255) NOT NULL UNIQUE
+);
+
+INSERT INTO rate_limit_sensitive_endpoints (endpoint_pattern)
+VALUES ('/login'), ('/users'), ('/oauth'), ('/password')
+ON CONFLICT (endpoint_pattern) DO NOTHING;
+
+-- Table to log rate limit violations
+CREATE TABLE IF NOT EXISTS rate_limit_violation_log (
+ id SERIAL PRIMARY KEY,
+ ip_address VARCHAR(100),
+ endpoint VARCHAR(255),
+ is_sensitive BOOLEAN,
+ violated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
diff --git a/medplat-web/src/main/resources/root-config.properties b/medplat-web/src/main/resources/root-config.properties
index baf0bf7e2..ac07e4ae9 100644
--- a/medplat-web/src/main/resources/root-config.properties
+++ b/medplat-web/src/main/resources/root-config.properties
@@ -1 +1,3 @@
-appName=medplat
\ No newline at end of file
+appName=medplat
+RATE_LIMIT_GLOBAL=100
+RATE_LIMIT_SENSITIVE=10
\ No newline at end of file