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;
|
||||
|
||||
import com.petshop.backend.entity.User;
|
||||
@@ -77,11 +85,24 @@ public class ActivityLoggingFilter extends OncePerRequestFilter {
|
||||
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) {
|
||||
// Strip query params so we only deal with the path
|
||||
String uri = rawUri.contains("?") ? rawUri.substring(0, rawUri.indexOf('?')) : rawUri;
|
||||
// Split into segments: [api, v1, resource, id/action, sub, ...]
|
||||
String[] parts = uri.replaceFirst("^/+", "").split("/");
|
||||
if (parts.length < 3) return null;
|
||||
|
||||
// parts[2] is the resource name (e.g. "products", "auth", "cart")
|
||||
String r = parts[2];
|
||||
String seg3 = parts.length > 3 ? parts[3] : 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 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 sub = seg3IsId ? seg4 : seg3;
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
@@ -63,6 +70,12 @@ public class TrailingSlashNormalizationFilter extends OncePerRequestFilter {
|
||||
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) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
@@ -74,6 +87,7 @@ public class TrailingSlashNormalizationFilter extends OncePerRequestFilter {
|
||||
if (shouldLowercase(normalized)) {
|
||||
normalized = normalized.toLowerCase(java.util.Locale.ROOT);
|
||||
}
|
||||
// Strip trailing slashes but keep the root "/" intact
|
||||
int end = normalized.length();
|
||||
while (end > 1 && normalized.charAt(end - 1) == '/') {
|
||||
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;
|
||||
|
||||
import com.petshop.backend.entity.User;
|
||||
@@ -33,6 +41,14 @@ public class WebSocketAuthChannelInterceptor implements ChannelInterceptor {
|
||||
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
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
|
||||
@@ -95,6 +111,14 @@ public class WebSocketAuthChannelInterceptor implements ChannelInterceptor {
|
||||
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) {
|
||||
Principal currentPrincipal = principal;
|
||||
if (currentPrincipal == null && accessor.getSessionAttributes() != null) {
|
||||
@@ -136,6 +160,13 @@ public class WebSocketAuthChannelInterceptor implements ChannelInterceptor {
|
||||
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) {
|
||||
destination = normalizeDestination(destination);
|
||||
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");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
destination = normalizeDestination(destination);
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
import com.petshop.backend.entity.User;
|
||||
@@ -30,12 +37,20 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
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
|
||||
protected void doFilterInternal(
|
||||
@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull FilterChain filterChain
|
||||
) throws ServletException, IOException {
|
||||
// Skip requests that don't have a Bearer token
|
||||
final String authHeader = request.getHeader("Authorization");
|
||||
final String jwt;
|
||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||
@@ -43,6 +58,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
return;
|
||||
}
|
||||
|
||||
// Strip "Bearer " prefix to get the raw token
|
||||
jwt = authHeader.substring(7);
|
||||
Long userId;
|
||||
String username;
|
||||
@@ -58,6 +74,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only set up auth context if there isn't one already for this request
|
||||
if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||
if (jwtUtil.extractExpiration(jwt).before(new Date())) {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
// Token version mismatch means the user logged out or changed password
|
||||
if (!authData.tokenVersion().equals(jwtTokenVersion)) {
|
||||
writeUnauthorized(request, response, "Invalid or expired token", null);
|
||||
return;
|
||||
@@ -82,6 +100,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build the authentication object and store it in the security context
|
||||
AppPrincipal principal = new AppPrincipal(userId, username, role, jwtTokenVersion);
|
||||
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
|
||||
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;
|
||||
|
||||
import com.petshop.backend.entity.User;
|
||||
@@ -50,6 +57,12 @@ public class JwtUtil {
|
||||
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) {
|
||||
Number tokenVersion = extractAllClaims(token).get("tokenVersion", Number.class);
|
||||
return tokenVersion == null ? null : tokenVersion.intValue();
|
||||
@@ -76,6 +89,11 @@ public class JwtUtil {
|
||||
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) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("username", user.getUsername());
|
||||
@@ -94,6 +112,13 @@ public class JwtUtil {
|
||||
.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) {
|
||||
Long userId = extractUserId(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;
|
||||
|
||||
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;
|
||||
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
@@ -15,12 +22,22 @@ public class RateLimiterService {
|
||||
|
||||
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) {
|
||||
Instant now = Instant.now();
|
||||
Instant windowStart = now.minus(window);
|
||||
|
||||
Deque<Instant> timestamps = buckets.computeIfAbsent(key, k -> new ArrayDeque<>());
|
||||
synchronized (timestamps) {
|
||||
// Remove timestamps that are older than the window
|
||||
while (!timestamps.isEmpty() && timestamps.peekFirst().isBefore(windowStart)) {
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
import com.petshop.backend.entity.User;
|
||||
|
||||
Reference in New Issue
Block a user