comment security and config
This commit is contained in:
@@ -1,3 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* Logs write operations (POST, PUT, DELETE) to the activity log.
|
||||||
|
* Skips GET requests and internal endpoints like health checks.
|
||||||
|
* Builds a human-readable description for each action.
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.config;
|
package com.petshop.backend.config;
|
||||||
|
|
||||||
import com.petshop.backend.entity.User;
|
import com.petshop.backend.entity.User;
|
||||||
@@ -77,11 +85,24 @@ public class ActivityLoggingFilter extends OncePerRequestFilter {
|
|||||||
activityLogService.record(userId, activity);
|
activityLogService.record(userId, activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns a request method + URL + status into a human-readable description
|
||||||
|
* like "Created a new product" or "Failed login attempt".
|
||||||
|
* Parses the URL as /api/v1/{resource}/{id?}/{sub?}/{subsub?} and matches
|
||||||
|
* against known patterns.
|
||||||
|
* @param method HTTP method (GET, POST, etc.)
|
||||||
|
* @param rawUri the request URI, possibly with query params
|
||||||
|
* @param status the HTTP response status code
|
||||||
|
* @return a readable description, or null if we don't recognize the pattern
|
||||||
|
*/
|
||||||
private String describe(String method, String rawUri, int status) {
|
private String describe(String method, String rawUri, int status) {
|
||||||
|
// Strip query params so we only deal with the path
|
||||||
String uri = rawUri.contains("?") ? rawUri.substring(0, rawUri.indexOf('?')) : rawUri;
|
String uri = rawUri.contains("?") ? rawUri.substring(0, rawUri.indexOf('?')) : rawUri;
|
||||||
|
// Split into segments: [api, v1, resource, id/action, sub, ...]
|
||||||
String[] parts = uri.replaceFirst("^/+", "").split("/");
|
String[] parts = uri.replaceFirst("^/+", "").split("/");
|
||||||
if (parts.length < 3) return null;
|
if (parts.length < 3) return null;
|
||||||
|
|
||||||
|
// parts[2] is the resource name (e.g. "products", "auth", "cart")
|
||||||
String r = parts[2];
|
String r = parts[2];
|
||||||
String seg3 = parts.length > 3 ? parts[3] : null;
|
String seg3 = parts.length > 3 ? parts[3] : null;
|
||||||
String seg4 = parts.length > 4 ? parts[4] : null;
|
String seg4 = parts.length > 4 ? parts[4] : null;
|
||||||
@@ -89,6 +110,8 @@ public class ActivityLoggingFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
boolean seg3IsId = seg3 != null && seg3.matches("\\d+");
|
boolean seg3IsId = seg3 != null && seg3.matches("\\d+");
|
||||||
boolean seg4IsId = seg4 != null && seg4.matches("\\d+");
|
boolean seg4IsId = seg4 != null && seg4.matches("\\d+");
|
||||||
|
// Normalize: if seg3 is a numeric ID, treat seg4 as the sub-action
|
||||||
|
// otherwise seg3 itself is the sub-action (like "login", "add", etc.)
|
||||||
String id = seg3IsId ? seg3 : null;
|
String id = seg3IsId ? seg3 : null;
|
||||||
String sub = seg3IsId ? seg4 : seg3;
|
String sub = seg3IsId ? seg4 : seg3;
|
||||||
String subsub = seg3IsId ? seg5 : seg4;
|
String subsub = seg3IsId ? seg5 : seg4;
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
/*
|
||||||
|
* Registers the activity logging filter with the servlet container.
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.config;
|
package com.petshop.backend.config;
|
||||||
|
|
||||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
/*
|
||||||
|
* Runs once on startup to mark past appointments as completed.
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.config;
|
package com.petshop.backend.config;
|
||||||
|
|
||||||
import com.petshop.backend.service.AppointmentService;
|
import com.petshop.backend.service.AppointmentService;
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Business settings loaded from application.yml (store hours,
|
||||||
|
* slot intervals, image size limits, discount/loyalty config).
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.config;
|
package com.petshop.backend.config;
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Sets up Caffeine caching for the app. Currently used to cache
|
||||||
|
* user auth lookups so the filter doesn't query the DB every time.
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.config;
|
package com.petshop.backend.config;
|
||||||
|
|
||||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* Creates default admin, staff, and customer accounts on startup
|
||||||
|
* if they don't already exist. Also fills in missing fields on
|
||||||
|
* existing accounts to keep them consistent.
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.config;
|
package com.petshop.backend.config;
|
||||||
|
|
||||||
import com.petshop.backend.entity.User;
|
import com.petshop.backend.entity.User;
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Runs Flyway migrations before the app starts. Retries up to
|
||||||
|
* 15 times so the app can wait for the database container to be ready.
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.config;
|
package com.petshop.backend.config;
|
||||||
|
|
||||||
import org.flywaydb.core.Flyway;
|
import org.flywaydb.core.Flyway;
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Adds extra pet and product seed data in the local dev profile
|
||||||
|
* if the database only has the base seed rows.
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.config;
|
package com.petshop.backend.config;
|
||||||
|
|
||||||
import com.petshop.backend.repository.PetRepository;
|
import com.petshop.backend.repository.PetRepository;
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Tells Tomcat to accept backslashes in URLs so the Android
|
||||||
|
* and desktop clients don't get rejected for path issues.
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.config;
|
package com.petshop.backend.config;
|
||||||
|
|
||||||
import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory;
|
import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory;
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Strips trailing slashes and normalizes paths so /api/pets/
|
||||||
|
* and /api/pets both hit the same controller.
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.config;
|
package com.petshop.backend.config;
|
||||||
|
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
@@ -63,6 +70,12 @@ public class TrailingSlashNormalizationFilter extends OncePerRequestFilter {
|
|||||||
filterChain.doFilter(wrapper, response);
|
filterChain.doFilter(wrapper, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up a URL path by fixing backslashes, collapsing double slashes,
|
||||||
|
* lowercasing API/WS paths, and stripping trailing slashes.
|
||||||
|
* @param value the raw path string
|
||||||
|
* @return the normalized path, or null if input was null
|
||||||
|
*/
|
||||||
private String normalizePath(String value) {
|
private String normalizePath(String value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -74,6 +87,7 @@ public class TrailingSlashNormalizationFilter extends OncePerRequestFilter {
|
|||||||
if (shouldLowercase(normalized)) {
|
if (shouldLowercase(normalized)) {
|
||||||
normalized = normalized.toLowerCase(java.util.Locale.ROOT);
|
normalized = normalized.toLowerCase(java.util.Locale.ROOT);
|
||||||
}
|
}
|
||||||
|
// Strip trailing slashes but keep the root "/" intact
|
||||||
int end = normalized.length();
|
int end = normalized.length();
|
||||||
while (end > 1 && normalized.charAt(end - 1) == '/') {
|
while (end > 1 && normalized.charAt(end - 1) == '/') {
|
||||||
end--;
|
end--;
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
/*
|
||||||
|
* Intercepts WebSocket messages to authenticate and authorize users.
|
||||||
|
* Validates the token on CONNECT, then checks permissions on
|
||||||
|
* SUBSCRIBE and SEND for chat topics.
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.config;
|
package com.petshop.backend.config;
|
||||||
|
|
||||||
import com.petshop.backend.entity.User;
|
import com.petshop.backend.entity.User;
|
||||||
@@ -33,6 +41,14 @@ public class WebSocketAuthChannelInterceptor implements ChannelInterceptor {
|
|||||||
this.chatService = chatService;
|
this.chatService = chatService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intercepts every STOMP message before it's sent. On CONNECT it validates the
|
||||||
|
* JWT and stores the authenticated user. On SUBSCRIBE/SEND it checks that the
|
||||||
|
* user has access to the requested chat destination.
|
||||||
|
* @param message the STOMP message being sent
|
||||||
|
* @param channel the channel the message is going through
|
||||||
|
* @return the original message if everything checks out
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||||
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
|
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
|
||||||
@@ -95,6 +111,14 @@ public class WebSocketAuthChannelInterceptor implements ChannelInterceptor {
|
|||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to figure out who the user is from multiple sources: the principal on the
|
||||||
|
* message, the session attributes (fallback for some STOMP clients), or by
|
||||||
|
* re-parsing the token from headers as a last resort.
|
||||||
|
* @param principal the principal attached to the message, may be null
|
||||||
|
* @param accessor the STOMP header accessor for fallback lookups
|
||||||
|
* @return the User entity, or null if unauthenticated
|
||||||
|
*/
|
||||||
private User resolveUser(Principal principal, StompHeaderAccessor accessor) {
|
private User resolveUser(Principal principal, StompHeaderAccessor accessor) {
|
||||||
Principal currentPrincipal = principal;
|
Principal currentPrincipal = principal;
|
||||||
if (currentPrincipal == null && accessor.getSessionAttributes() != null) {
|
if (currentPrincipal == null && accessor.getSessionAttributes() != null) {
|
||||||
@@ -136,6 +160,13 @@ public class WebSocketAuthChannelInterceptor implements ChannelInterceptor {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the user is allowed to subscribe to this destination.
|
||||||
|
* Customers can only subscribe to their own conversations, not the
|
||||||
|
* staff-wide conversation feed.
|
||||||
|
* @param destination the STOMP destination being subscribed to
|
||||||
|
* @param user the user trying to subscribe
|
||||||
|
*/
|
||||||
private void authorizeSubscription(String destination, User user) {
|
private void authorizeSubscription(String destination, User user) {
|
||||||
destination = normalizeDestination(destination);
|
destination = normalizeDestination(destination);
|
||||||
if (destination == null || destination.startsWith("/user/queue/")) {
|
if (destination == null || destination.startsWith("/user/queue/")) {
|
||||||
@@ -157,6 +188,12 @@ public class WebSocketAuthChannelInterceptor implements ChannelInterceptor {
|
|||||||
throw new IllegalArgumentException("Not authorized to subscribe to destination");
|
throw new IllegalArgumentException("Not authorized to subscribe to destination");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the user is allowed to send a message to this destination.
|
||||||
|
* Only allows sending to conversation message endpoints that the user has access to.
|
||||||
|
* @param destination the STOMP destination being sent to
|
||||||
|
* @param user the user trying to send
|
||||||
|
*/
|
||||||
private void authorizeSend(String destination, User user) {
|
private void authorizeSend(String destination, User user) {
|
||||||
destination = normalizeDestination(destination);
|
destination = normalizeDestination(destination);
|
||||||
Long conversationId = extractConversationId(destination, "/app/chat/conversations/");
|
Long conversationId = extractConversationId(destination, "/app/chat/conversations/");
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* WebSocket configuration for the live chat feature.
|
||||||
|
* Registers STOMP endpoints and the auth interceptor.
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.config;
|
package com.petshop.backend.config;
|
||||||
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Represents the currently logged-in user in the security context.
|
||||||
|
* Holds the user ID, username, role, and token version.
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.security;
|
package com.petshop.backend.security;
|
||||||
|
|
||||||
import com.petshop.backend.entity.User;
|
import com.petshop.backend.entity.User;
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Filter that runs on every request to validate the JWT token
|
||||||
|
* and set up the security context if the token is valid.
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.security;
|
package com.petshop.backend.security;
|
||||||
|
|
||||||
import com.petshop.backend.entity.User;
|
import com.petshop.backend.entity.User;
|
||||||
@@ -30,12 +37,20 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
this.apiErrorResponder = apiErrorResponder;
|
this.apiErrorResponder = apiErrorResponder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the JWT from the Authorization header, validates it, and sets up
|
||||||
|
* the Spring Security context so downstream filters/controllers know who the user is.
|
||||||
|
* @param request the incoming HTTP request
|
||||||
|
* @param response the HTTP response (used to write 401 errors)
|
||||||
|
* @param filterChain the remaining filters to invoke
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(
|
protected void doFilterInternal(
|
||||||
@NonNull HttpServletRequest request,
|
@NonNull HttpServletRequest request,
|
||||||
@NonNull HttpServletResponse response,
|
@NonNull HttpServletResponse response,
|
||||||
@NonNull FilterChain filterChain
|
@NonNull FilterChain filterChain
|
||||||
) throws ServletException, IOException {
|
) throws ServletException, IOException {
|
||||||
|
// Skip requests that don't have a Bearer token
|
||||||
final String authHeader = request.getHeader("Authorization");
|
final String authHeader = request.getHeader("Authorization");
|
||||||
final String jwt;
|
final String jwt;
|
||||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||||
@@ -43,6 +58,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Strip "Bearer " prefix to get the raw token
|
||||||
jwt = authHeader.substring(7);
|
jwt = authHeader.substring(7);
|
||||||
Long userId;
|
Long userId;
|
||||||
String username;
|
String username;
|
||||||
@@ -58,6 +74,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only set up auth context if there isn't one already for this request
|
||||||
if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||||
if (jwtUtil.extractExpiration(jwt).before(new Date())) {
|
if (jwtUtil.extractExpiration(jwt).before(new Date())) {
|
||||||
writeUnauthorized(request, response, "Invalid or expired token", null);
|
writeUnauthorized(request, response, "Invalid or expired token", null);
|
||||||
@@ -69,6 +86,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
writeUnauthorized(request, response, "User account is inactive", null);
|
writeUnauthorized(request, response, "User account is inactive", null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Token version mismatch means the user logged out or changed password
|
||||||
if (!authData.tokenVersion().equals(jwtTokenVersion)) {
|
if (!authData.tokenVersion().equals(jwtTokenVersion)) {
|
||||||
writeUnauthorized(request, response, "Invalid or expired token", null);
|
writeUnauthorized(request, response, "Invalid or expired token", null);
|
||||||
return;
|
return;
|
||||||
@@ -82,6 +100,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build the authentication object and store it in the security context
|
||||||
AppPrincipal principal = new AppPrincipal(userId, username, role, jwtTokenVersion);
|
AppPrincipal principal = new AppPrincipal(userId, username, role, jwtTokenVersion);
|
||||||
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
|
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
|
||||||
principal,
|
principal,
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* JWT token utility for generating, parsing, and validating tokens.
|
||||||
|
* Tokens store userId, username, role, and a version for invalidation.
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.security;
|
package com.petshop.backend.security;
|
||||||
|
|
||||||
import com.petshop.backend.entity.User;
|
import com.petshop.backend.entity.User;
|
||||||
@@ -50,6 +57,12 @@ public class JwtUtil {
|
|||||||
return extractAllClaims(token).get("role", String.class);
|
return extractAllClaims(token).get("role", String.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the token version from the JWT, used to invalidate old tokens
|
||||||
|
* when the user logs out or changes their password.
|
||||||
|
* @param token the JWT string
|
||||||
|
* @return the token version number, or null if not present
|
||||||
|
*/
|
||||||
public Integer extractTokenVersion(String token) {
|
public Integer extractTokenVersion(String token) {
|
||||||
Number tokenVersion = extractAllClaims(token).get("tokenVersion", Number.class);
|
Number tokenVersion = extractAllClaims(token).get("tokenVersion", Number.class);
|
||||||
return tokenVersion == null ? null : tokenVersion.intValue();
|
return tokenVersion == null ? null : tokenVersion.intValue();
|
||||||
@@ -76,6 +89,11 @@ public class JwtUtil {
|
|||||||
return extractExpiration(token).before(new Date());
|
return extractExpiration(token).before(new Date());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new JWT containing the user's id, username, role, and token version.
|
||||||
|
* @param user the user to generate a token for
|
||||||
|
* @return the signed JWT string
|
||||||
|
*/
|
||||||
public String generateToken(User user) {
|
public String generateToken(User user) {
|
||||||
Map<String, Object> claims = new HashMap<>();
|
Map<String, Object> claims = new HashMap<>();
|
||||||
claims.put("username", user.getUsername());
|
claims.put("username", user.getUsername());
|
||||||
@@ -94,6 +112,13 @@ public class JwtUtil {
|
|||||||
.compact();
|
.compact();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that the token belongs to this user, has the right role and version,
|
||||||
|
* and hasn't expired yet.
|
||||||
|
* @param token the JWT string
|
||||||
|
* @param user the user to validate against
|
||||||
|
* @return true if the token is valid for this user
|
||||||
|
*/
|
||||||
public Boolean validateToken(String token, User user) {
|
public Boolean validateToken(String token, User user) {
|
||||||
Long userId = extractUserId(token);
|
Long userId = extractUserId(token);
|
||||||
String role = extractRole(token);
|
String role = extractRole(token);
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Filter that applies rate limiting to auth endpoints.
|
||||||
|
* Each rule defines max requests and window size in minutes.
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.security;
|
package com.petshop.backend.security;
|
||||||
|
|
||||||
import com.petshop.backend.exception.ApiErrorResponder;
|
import com.petshop.backend.exception.ApiErrorResponder;
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Sliding-window rate limiter that tracks request timestamps per key.
|
||||||
|
* Used to limit login/register attempts by IP.
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.security;
|
package com.petshop.backend.security;
|
||||||
|
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
@@ -15,12 +22,22 @@ public class RateLimiterService {
|
|||||||
|
|
||||||
private final Map<String, Deque<Instant>> buckets = new ConcurrentHashMap<>();
|
private final Map<String, Deque<Instant>> buckets = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a request identified by key is within the rate limit.
|
||||||
|
* Uses a sliding window: keeps a queue of timestamps per key, drops any
|
||||||
|
* that fall outside the window, then checks if there's room for one more.
|
||||||
|
* @param key identifier for the rate limit bucket (e.g. IP address)
|
||||||
|
* @param maxRequests max number of requests allowed in the window
|
||||||
|
* @param window the time window to count requests in
|
||||||
|
* @return true if the request is allowed, false if rate limited
|
||||||
|
*/
|
||||||
public boolean isAllowed(String key, int maxRequests, Duration window) {
|
public boolean isAllowed(String key, int maxRequests, Duration window) {
|
||||||
Instant now = Instant.now();
|
Instant now = Instant.now();
|
||||||
Instant windowStart = now.minus(window);
|
Instant windowStart = now.minus(window);
|
||||||
|
|
||||||
Deque<Instant> timestamps = buckets.computeIfAbsent(key, k -> new ArrayDeque<>());
|
Deque<Instant> timestamps = buckets.computeIfAbsent(key, k -> new ArrayDeque<>());
|
||||||
synchronized (timestamps) {
|
synchronized (timestamps) {
|
||||||
|
// Remove timestamps that are older than the window
|
||||||
while (!timestamps.isEmpty() && timestamps.peekFirst().isBefore(windowStart)) {
|
while (!timestamps.isEmpty() && timestamps.peekFirst().isBefore(windowStart)) {
|
||||||
timestamps.pollFirst();
|
timestamps.pollFirst();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Returns a JSON error when a logged-in user tries to access
|
||||||
|
* something they don't have permission for.
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.security;
|
package com.petshop.backend.security;
|
||||||
|
|
||||||
import com.petshop.backend.exception.ApiErrorResponder;
|
import com.petshop.backend.exception.ApiErrorResponder;
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
/*
|
||||||
|
* Returns a JSON error when an unauthenticated request hits a protected endpoint.
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.security;
|
package com.petshop.backend.security;
|
||||||
|
|
||||||
import com.petshop.backend.exception.ApiErrorResponder;
|
import com.petshop.backend.exception.ApiErrorResponder;
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Security setup for the app. Defines which endpoints are public,
|
||||||
|
* which need login, and configures the filter chain.
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.security;
|
package com.petshop.backend.security;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* Caches user auth data (active status, token version) so the
|
||||||
|
* JWT filter doesn't hit the database on every request.
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.security;
|
package com.petshop.backend.security;
|
||||||
|
|
||||||
import com.petshop.backend.repository.UserRepository;
|
import com.petshop.backend.repository.UserRepository;
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
/*
|
||||||
|
* Loads user details from the database for Spring Security login.
|
||||||
|
*
|
||||||
|
* Author: Harkamal
|
||||||
|
* Date: April 2026
|
||||||
|
*/
|
||||||
package com.petshop.backend.security;
|
package com.petshop.backend.security;
|
||||||
|
|
||||||
import com.petshop.backend.entity.User;
|
import com.petshop.backend.entity.User;
|
||||||
|
|||||||
Reference in New Issue
Block a user