Feat: Creating basic files to get the example of API

Files added:
- controllers where the endpoints are defined
- services where the business logic is defined
- database.dbml where the database schema is defined
- pom.xml updated with required dependencies
This commit is contained in:
2026-01-18 16:21:48 -05:00
parent ada8520e77
commit fd29446433
24 changed files with 1521 additions and 65 deletions

26
.gitignore vendored
View File

@@ -31,3 +31,29 @@ build/
### VS Code ###
.vscode/
### Java ###
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*

40
database.dbml Normal file
View File

@@ -0,0 +1,40 @@
// Database schema for Secure Software Design API
// Use https://dbml.dbdiagram.io to visualize
Project secure_software_design {
database_type: 'PostgreSQL'
Note: 'Secure API with JWT Authentication'
}
Table users {
id bigint [pk, increment]
username varchar(50) [not null, unique, note: 'User login name (3-50 characters)']
email varchar(255) [not null, unique, note: 'User email address']
password_hash varchar(255) [not null, note: 'BCrypt hashed password']
role varchar(50) [not null, note: 'User role (e.g., USER, ADMIN)']
enabled boolean [not null, default: true, note: 'Account active status']
indexes {
username [unique]
email [unique]
}
}
Table tokens {
id bigint [pk, increment]
token varchar(500) [not null, unique, note: 'JWT token string']
username varchar(50) [not null, note: 'Username associated with token']
created_at timestamp [not null, note: 'Token creation timestamp']
expires_at timestamp [not null, note: 'Token expiration timestamp']
revoked boolean [not null, default: false, note: 'Token revocation status']
indexes {
token [unique]
username
(username, revoked)
}
}
// Relationships
// Token belongs to user
Ref: tokens.username > users.username

119
pom.xml
View File

@@ -1,63 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.api</groupId>
<artifactId>application</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>application</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.1</version>
<relativePath/>
</parent>
<groupId>com.api</groupId>
<artifactId>application</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>application</name>
<description>Secure database access via HTTPS</description>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<!-- Web / REST API -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Database access with SQL injection protection -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Security: authentication and authorization -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Input validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- PostgreSQL database driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -3,11 +3,16 @@ package com.api.main;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/*
* Main entry point for the Secure Software Design API application.
* This class bootstraps the Spring Boot application and initializes
* all necessary configurations, security filters, and components.
* Uses @SpringBootApplication for auto-configuration and component scanning.
*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@@ -0,0 +1,36 @@
package com.api.main.config;
import java.util.HashMap;
import java.util.Map;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
/*
* Global exception handler for the application.
* Catches and processes validation exceptions across all controllers.
* Returns structured error responses with field-level validation messages.
* Ensures consistent error format for API clients.
* Uses @ControllerAdvice to apply globally to all request mappings.
*/
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> fieldErrors = new HashMap<>();
ex.getBindingResult()
.getFieldErrors()
.forEach(error -> fieldErrors.put(error.getField(), error.getDefaultMessage()));
Map<String, Object> response = new HashMap<>();
response.put("status", "error");
response.put("message", "Validation failed");
response.put("errors", fieldErrors);
return ResponseEntity.badRequest().body(response);
}
}

View File

@@ -0,0 +1,99 @@
package com.api.main.config;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
/*
* Main security configuration for the application.
* Configures Spring Security with stateless authentication.
* Defines authorization rules for endpoints:
* - Public: /auth/login, /health
* - Protected: All other endpoints require authentication
* Uses BCrypt for password hashing with secure work factor.
* Disables CSRF as the API is stateless (token-based).
* Enforces HTTPS when SSL is enabled.
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private final UserDetailsService userDetailsService;
@Value("${server.ssl.enabled:true}")
private boolean sslEnabled;
public SecurityConfig(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(authenticationProvider());
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(
auth ->
auth.requestMatchers("/auth/login", "/health")
.permitAll()
.anyRequest()
.authenticated())
.sessionManagement(
session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(
ex ->
ex.authenticationEntryPoint(
(request, response, authException) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response
.getWriter()
.write("{\"status\":\"error\",\"message\":\"Unauthorized\"}");
})
.accessDeniedHandler(
(request, response, accessDeniedException) -> {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json");
response
.getWriter()
.write("{\"status\":\"error\",\"message\":\"Access denied\"}");
}))
.authenticationProvider(authenticationProvider())
.httpBasic(basic -> basic.realmName("Secure API"));
if (sslEnabled) {
http.requiresChannel(channel -> channel.anyRequest().requiresSecure());
}
return http.build();
}
}

View File

@@ -0,0 +1,52 @@
package com.api.main.constants;
/*
* Centralized constants for the application.
* Contains reusable string literals to avoid hardcoding.
* Includes JPQL queries, error messages, and HTTP-related constants.
* Promotes consistency and maintainability across the codebase.
* All constants are static and final for immutability.
*/
public class Constants {
/*
* JPQL queries use named parameters (:username) instead of string concatenation.
* Hibernate binds these parameters safely, preventing SQL injection attacks.
* User input is never directly interpolated into the query string.
*/
public static final String ADDING_TOKEN_QUERY =
"UPDATE Token t SET t.revoked = true WHERE t.username = :username AND t.revoked = false";
public static final String DELETING_TOKEN_QUERY =
"DELETE FROM Token t WHERE t.expiresAt < CURRENT_TIMESTAMP";
public static final String USER_NOT_FOUND_MESSAGE = "User not found: ";
public static final String ROLE = "ROLE_";
public static final String AUTHORIZATION = "Authorization";
public static final String BEARER_PREFIX = "Bearer ";
public static final String JWT_TOKEN_INVALID_MESSAGE = "JWT token validation failed: ";
public static final String UNAUTHORIZED_MESSAGE = "Unauthorized access attempt";
public static final String STATUS = "status";
public static final String SUCCESS = "success";
public static final String MESSAGE = "message";
public static final String LOGGED_OUT_SUCCESSFULLY = "Logged out successfully";
public static final String ERROR = "error";
public static final String LOGOUT_FAILED = "Logout failed";
public static final String UP = "UP";
public static final String INVALID_CREDENTIALS = "Invalid credentials";
public static final String INTERNAL_SERVER_ERROR = "Internal server error";
}

View File

@@ -0,0 +1,42 @@
package com.api.main.controllers;
import com.api.main.constants.Constants;
import com.api.main.dto.ErrorResponse;
import com.api.main.dto.LoginRequest;
import com.api.main.dto.LoginResponse;
import com.api.main.services.AuthService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.web.bind.annotation.*;
/*
* REST controller for authentication operations.
* Handles user login and token generation.
* Validates credentials against stored user data.
* Returns secure tokens for subsequent API requests.
*/
@RestController
@RequestMapping("/auth")
public class AuthController {
private final AuthService authService;
public AuthController(AuthService authService) {
this.authService = authService;
}
@PostMapping("/login")
public ResponseEntity<?> login(@Valid @RequestBody LoginRequest request) {
try {
LoginResponse response = authService.authenticate(request);
return ResponseEntity.ok(response);
} catch (BadCredentialsException e) {
return ResponseEntity.status(401)
.body(new ErrorResponse(Constants.ERROR, Constants.INVALID_CREDENTIALS));
} catch (Exception e) {
return ResponseEntity.status(500)
.body(new ErrorResponse(Constants.ERROR, Constants.INTERNAL_SERVER_ERROR));
}
}
}

View File

@@ -0,0 +1,22 @@
package com.api.main.controllers;
import com.api.main.constants.Constants;
import com.api.main.dto.HealthResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/*
* REST controller for application health monitoring.
* Provides a public endpoint for checking API availability.
* Used by load balancers and monitoring systems.
* Does not require authentication for accessibility.
*/
@RestController
public class Health {
@GetMapping("/health")
public ResponseEntity<HealthResponse> healthCheck() {
return ResponseEntity.ok(new HealthResponse(Constants.UP));
}
}

View File

@@ -0,0 +1,92 @@
package com.api.main.controllers;
import com.api.main.constants.Constants;
import com.api.main.dto.CreateUserRequest;
import com.api.main.dto.ErrorResponse;
import com.api.main.dto.UserResponse;
import com.api.main.entity.User;
import com.api.main.services.AuthService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import java.util.Collections;
import java.util.Map;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
/*
* REST controller for user management operations.
* Provides endpoints for user profile access, creation, and logout.
* Admin-only endpoints are protected with @PreAuthorize.
* All endpoints require valid authentication.
* Supports token invalidation for secure logout.
*/
@RestController
@RequestMapping("/users")
public class UserController {
private final AuthService authService;
public UserController(AuthService authService) {
this.authService = authService;
}
@GetMapping("/me")
public ResponseEntity<?> getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return ResponseEntity.status(401)
.body(new ErrorResponse(Constants.ERROR, Constants.UNAUTHORIZED_MESSAGE));
}
try {
String username = authentication.getName();
UserResponse userResponse = authService.getCurrentUser(username);
return ResponseEntity.ok(userResponse);
} catch (RuntimeException e) {
return ResponseEntity.status(401)
.body(new ErrorResponse(Constants.ERROR, Constants.UNAUTHORIZED_MESSAGE));
}
}
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/create")
public ResponseEntity<?> createUser(@Valid @RequestBody CreateUserRequest request) {
try {
User user =
authService.registerUser(
request.getUsername(), request.getEmail(), request.getPassword(), request.getRole());
UserResponse response =
new UserResponse(
user.getId(),
user.getUsername(),
user.getEmail(),
Collections.singletonList(user.getRole()));
return ResponseEntity.status(201).body(response);
} catch (RuntimeException e) {
return ResponseEntity.status(400).body(new ErrorResponse(Constants.ERROR, e.getMessage()));
}
}
@PostMapping("/logout")
public ResponseEntity<?> logout(HttpServletRequest request) {
try {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
authService.logout(token);
}
return ResponseEntity.ok(
Map.of(
Constants.STATUS,
Constants.SUCCESS,
Constants.MESSAGE,
Constants.LOGGED_OUT_SUCCESSFULLY));
} catch (RuntimeException e) {
return ResponseEntity.status(500)
.body(new ErrorResponse(Constants.ERROR, Constants.LOGOUT_FAILED));
}
}
}

View File

@@ -0,0 +1,79 @@
package com.api.main.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
/*
* Data Transfer Object for user registration requests.
* Contains validation constraints for secure user creation:
* - Username: 3-50 characters, must be unique
* - Email: Valid format, must be unique
* - Password: Minimum 8 chars with uppercase, lowercase, number, and special char
* - Role: Required for authorization assignment
* Input validation helps prevent injection attacks and data integrity issues.
*/
public class CreateUserRequest {
@NotBlank(message = "Username is required")
@Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
private String username;
@NotBlank(message = "Email is required")
@Email(message = "Email must be valid")
private String email;
@NotBlank(message = "Password is required")
@Size(min = 8, message = "Password must be at least 8 characters")
@Pattern(
regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$",
message =
"Password must contain at least one uppercase letter, one lowercase letter, one number,"
+ " and one special character (@$!%*?&)")
private String password;
@NotBlank(message = "Role is required")
private String role;
public CreateUserRequest() {}
public CreateUserRequest(String username, String email, String password, String role) {
this.username = username;
this.email = email;
this.password = password;
this.role = role;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
}

View File

@@ -0,0 +1,41 @@
package com.api.main.dto;
/*
* Data Transfer Object for error responses.
* Provides a standardized format for API error messages.
* Contains status and message fields for consistent error handling.
* Used across all endpoints to return meaningful error information.
*/
public class ErrorResponse {
private String status;
private String message;
public ErrorResponse() {}
public ErrorResponse(String message) {
this.status = "error";
this.message = message;
}
public ErrorResponse(String status, String message) {
this.status = status;
this.message = message;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

View File

@@ -0,0 +1,26 @@
package com.api.main.dto;
/*
* Data Transfer Object for health check responses.
* Returns the current status of the API service.
* Used by monitoring systems to verify application availability.
* Typically returns "UP" when the service is running correctly.
*/
public class HealthResponse {
private String status;
public HealthResponse() {}
public HealthResponse(String status) {
this.status = status;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}

View File

@@ -0,0 +1,41 @@
package com.api.main.dto;
import jakarta.validation.constraints.NotBlank;
/*
* Data Transfer Object for authentication requests.
* Contains username and password for user login.
* Both fields are required and validated before processing.
* Passwords are never stored or logged in plain text.
*/
public class LoginRequest {
@NotBlank(message = "Username is required")
private String username;
@NotBlank(message = "Password is required")
private String password;
public LoginRequest() {}
public LoginRequest(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}

View File

@@ -0,0 +1,51 @@
package com.api.main.dto;
/*
* Data Transfer Object for authentication response.
* Contains the token returned after successful login.
* Used to transfer authentication results between layers.
* The token should be stored securely and sent in subsequent requests.
*/
public class LoginResponse {
private String status;
private String message;
private String token;
public LoginResponse() {}
public LoginResponse(String status, String message) {
this.status = status;
this.message = message;
}
public LoginResponse(String status, String message, String token) {
this.status = status;
this.message = message;
this.token = token;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}

View File

@@ -0,0 +1,59 @@
package com.api.main.dto;
import java.util.ArrayList;
import java.util.List;
/*
* Data Transfer Object for user information responses.
* Contains safe user data to expose via the API.
* Excludes sensitive fields like password hash for security.
* Used when returning user profile information to clients.
*/
public class UserResponse {
private Long id;
private String username;
private String email;
private List<String> roles;
public UserResponse() {}
public UserResponse(Long id, String username, String email, List<String> roles) {
this.id = id;
this.username = username;
this.email = email;
this.roles = roles != null ? new ArrayList<>(roles) : null;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public List<String> getRoles() {
return roles != null ? new ArrayList<>(roles) : null;
}
public void setRoles(List<String> roles) {
this.roles = roles != null ? new ArrayList<>(roles) : null;
}
}

View File

@@ -0,0 +1,158 @@
package com.api.main.entity;
import jakarta.persistence.*;
import java.time.Instant;
/*
* JPA Entity representing a JWT token in the database.
* Enables token tracking, validation, and revocation.
* Stores token metadata including creation and expiration timestamps.
* Revoked tokens are invalidated even before expiration.
* Essential for implementing secure logout and token management.
*/
@Entity
@Table(name = "tokens")
public class Token {
/* Primary key for the token entity */
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/* The JWT token string */
@Column(nullable = false, unique = true, length = 500)
private String token;
/* Username associated with the token */
@Column(nullable = false)
private String username;
/* Timestamp when the token was created */
@Column(nullable = false)
private Instant createdAt;
/* Timestamp when the token expires */
@Column(nullable = false)
private Instant expiresAt;
/* Flag indicating if the token has been revoked */
@Column(nullable = false)
private boolean revoked = false;
/* Default constructor */
public Token() {}
/*
* Constructor with parameters
* @param token The JWT token string
* @param username Username associated with the token
* @param createdAt Timestamp when the token was created
* @param expiresAt Timestamp when the token expires
*
*/
public Token(String token, String username, Instant createdAt, Instant expiresAt) {
this.token = token;
this.username = username;
this.createdAt = createdAt;
this.expiresAt = expiresAt;
this.revoked = false;
}
/* Getters and Setters */
/* Get the token ID
* @return id
*
*/
public Long getId() {
return id;
}
/* Set the token ID
* @param id Token ID
*
*/
public void setId(Long id) {
this.id = id;
}
/* Get the JWT token string
* @return token
*
*/
public String getToken() {
return token;
}
/* Set the JWT token string
* @param token The JWT token string
*
*/
public void setToken(String token) {
this.token = token;
}
/* Get the username associated with the token
* @return username
*
*/
public String getUsername() {
return username;
}
/* Set the username associated with the token
* @param username Username associated with the token
*
*/
public void setUsername(String username) {
this.username = username;
}
/* Get the creation timestamp
* @return createdAt
*
*/
public Instant getCreatedAt() {
return createdAt;
}
/* Set the creation timestamp
* @param createdAt Timestamp when the token was created
*
*/
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
/* Get the expiration timestamp
* @return expiresAt
*
*/
public Instant getExpiresAt() {
return expiresAt;
}
/* Set the expiration timestamp
* @param expiresAt Timestamp when the token expires
*
*/
public void setExpiresAt(Instant expiresAt) {
this.expiresAt = expiresAt;
}
/* Check if the token is revoked
* @return revoked
*
*/
public boolean isRevoked() {
return revoked;
}
/* Set the revoked status
* @param revoked Flag indicating if the token has been revoked
*
*/
public void setRevoked(boolean revoked) {
this.revoked = revoked;
}
}

View File

@@ -0,0 +1,244 @@
package com.api.main.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
/*
* JPA Entity representing a user in the system.
* Stores user credentials and profile information securely.
* Password is stored as a BCrypt hash, never in plain text.
* Contains validation constraints for username, email, and password.
* Used for authentication and authorization throughout the application.
*/
@Entity
@Table(name = "users")
public class User {
/*
* Primary key for the user entity
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/*
* Username of the user
* Must be unique and between 3 to 50 characters
* @return username
*/
@NotBlank
@Size(min = 3, max = 50)
@Column(unique = true, nullable = false)
private String username;
/*
* Email address of the user
* Must be unique and a valid email format
* @return email
*/
@NotBlank
@Email
@Column(unique = true, nullable = false)
private String email;
/*
* BCrypt hashed password of the user
* Stored securely, never in plain text
* @return passwordHash
*/
@NotBlank
@Column(nullable = false)
private String passwordHash;
/*
* Role of the user (e.g., ROLE_USER, ROLE_ADMIN)
* Used for authorization purposes
* @return role
*/
@NotBlank
@Column(nullable = false)
private String role;
/*
* Flag indicating if the user account is enabled
* Disabled accounts cannot authenticate
* @return enabled
*/
@Column(nullable = false)
private boolean enabled = true;
/* Default constructor */
public User() {}
/*
* Constructor with parameters
* @param username Username of the user
* @param email Email address of the user
* @param passwordHash BCrypt hashed password
* @param role Role of the user
*
*/
public User(String username, String email, String passwordHash, String role) {
this.username = username;
this.email = email;
this.passwordHash = passwordHash;
this.role = role;
this.enabled = true;
}
/*
* Constructor with all parameters
* @param id User ID
* @param username Username of the user
* @param email Email address of the user
* @param passwordHash BCrypt hashed password
* @param role Role of the user
* @param enabled Account enabled status
*
*/
public User(
Long id, String username, String email, String passwordHash, String role, boolean enabled) {
this.id = id;
this.username = username;
this.email = email;
this.passwordHash = passwordHash;
this.role = role;
this.enabled = enabled;
}
/* Getters and Setters */
/*
* Get the user ID
* @return id
*
*/
public Long getId() {
return id;
}
/*
* Set the user ID
* @param id User ID
*
*/
public void setId(Long id) {
this.id = id;
}
/*
* Get the username
* @return username
*
*/
public String getUsername() {
return username;
}
/*
* Set the username
* @param username Username of the user
*
*/
public void setUsername(String username) {
this.username = username;
}
/*
* Get the email address
* @return email
*
*/
public String getEmail() {
return email;
}
/*
* Set the email address
* @param email Email address of the user
*
*/
public void setEmail(String email) {
this.email = email;
}
/*
* Get the BCrypt hashed password
* @return passwordHash
*
*/
public String getPasswordHash() {
return passwordHash;
}
/*
* Set the BCrypt hashed password
* @param passwordHash BCrypt hashed password
*
*/
public void setPasswordHash(String passwordHash) {
this.passwordHash = passwordHash;
}
/*
* Get the user role
* @return role
*
*/
public String getRole() {
return role;
}
/*
* Set the user role
* @param role Role of the user
*
*/
public void setRole(String role) {
this.role = role;
}
/*
* Check if the user account is enabled
* @return enabled
*
*/
public boolean isEnabled() {
return enabled;
}
/*
* Set the user account enabled status
* @param enabled Account enabled status
*
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/*
* String representation of the User entity
* @return String representation
*
*/
@Override
public String toString() {
return "User{"
+ "id="
+ id
+ ", username='"
+ username
+ '\''
+ ", email='"
+ email
+ '\''
+ ", role='"
+ role
+ '\''
+ ", enabled="
+ enabled
+ '}';
}
}

View File

@@ -0,0 +1,58 @@
package com.api.main.repositories;
import com.api.main.constants.Constants;
import com.api.main.entity.Token;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
/*
* Repository interface for Token entity database operations.
* Manages JWT token persistence and lifecycle.
* Provides methods for token validation and revocation.
* Supports bulk operations for revoking user tokens and cleanup.
* Uses JPQL queries for efficient token management.
*
* SQL Injection Protection:
* All queries use parameterized statements. Derived methods (findByToken) and
* @Query annotations with named parameters (:username) are safely bound by
* Hibernate. No raw SQL or string concatenation is used anywhere.
*/
@Repository
public interface TokenRepository extends JpaRepository<Token, Long> {
/*
* Find a token by its string value.
* @param token The JWT token string
* @return Optional containing the Token if found, else empty
*
*/
Optional<Token> findByToken(String token);
/*
* Find a non-revoked token by its string value.
* @param token The JWT token string
* @return Optional containing the Token if found and not revoked, else empty
*
*/
Optional<Token> findByTokenAndRevokedFalse(String token);
/*
* Revoke all tokens associated with a specific username.
* @param username The username whose tokens are to be revoked
*
*/
@Modifying
@Query(Constants.ADDING_TOKEN_QUERY)
void revokeAllUserTokens(String username);
/*
* Delete all expired tokens from the database.
*
*/
@Modifying
@Query(Constants.DELETING_TOKEN_QUERY)
void deleteExpiredTokens();
}

View File

@@ -0,0 +1,27 @@
package com.api.main.repositories;
import com.api.main.entity.User;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/*
* Repository interface for User entity database operations.
* Extends JpaRepository for standard CRUD operations.
* Provides custom query methods for user lookup and existence checks.
* Used by authentication and user management services.
*
* SQL Injection Protection:
* Spring Data JPA derived query methods (findByUsername, existsByUsername, etc.)
* automatically use parameterized queries. User input is bound as parameters,
* never concatenated into SQL strings, making SQL injection impossible.
*/
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
}

View File

@@ -0,0 +1,59 @@
package com.api.main.security;
import com.api.main.constants.Constants;
import com.api.main.entity.User;
import com.api.main.repositories.UserRepository;
import java.util.Collections;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/*
* Custom implementation of Spring Security's UserDetailsService.
* Loads user-specific data from the database during authentication.
* Converts the application's User entity to Spring Security's UserDetails.
* Maps user roles with the ROLE_ prefix for proper authorization checks.
* Throws UsernameNotFoundException if the user does not exist.
*/
@Service
public class CustomUserDetailsService implements UserDetailsService {
/* Repository for accessing user data from the database */
private final UserRepository userRepository;
/*
* Constructor for CustomUserDetailsService
* @param userRepository Repository to access user data
*
*/
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
/*
* Load user details by username for authentication.
* @param username The username of the user to load
* @return UserDetails object containing user information and authorities
* @throws UsernameNotFoundException if the user is not found
*
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user =
userRepository
.findByUsername(username)
.orElseThrow(
() -> new UsernameNotFoundException(Constants.USER_NOT_FOUND_MESSAGE + username));
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPasswordHash(),
user.isEnabled(),
true,
true,
true,
Collections.singletonList(new SimpleGrantedAuthority(Constants.ROLE + user.getRole())));
}
}

View File

@@ -0,0 +1,72 @@
package com.api.main.security;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/*
* Filter that adds security headers to all HTTP responses.
* Implements defense-in-depth by configuring multiple security headers:
* - X-Frame-Options: Prevents clickjacking attacks
* - X-Content-Type-Options: Prevents MIME type sniffing
* - X-XSS-Protection: Enables browser XSS filtering
* - Content-Security-Policy: Restricts resource loading sources
* - Strict-Transport-Security: Enforces HTTPS connections
* - Permissions-Policy: Disables unnecessary browser features
* - Cache-Control: Prevents caching of sensitive data
* Runs with highest precedence to ensure headers are applied early.
*/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SecurityHeadersFilter implements Filter {
/* Initialization method for the filter */
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (response instanceof HttpServletResponse httpResponse) {
// Prevent clickjacking
httpResponse.setHeader("X-Frame-Options", "DENY");
// Prevent MIME type sniffing
httpResponse.setHeader("X-Content-Type-Options", "nosniff");
// Enable XSS filter in browsers
httpResponse.setHeader("X-XSS-Protection", "1; mode=block");
// Control referrer information
httpResponse.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
// Content Security Policy - restrict resource loading
httpResponse.setHeader(
"Content-Security-Policy",
"default-src 'self'; "
+ "script-src 'self'; "
+ "style-src 'self' 'unsafe-inline'; "
+ "img-src 'self' data:; "
+ "font-src 'self'; "
+ "frame-ancestors 'none'; "
+ "form-action 'self'");
// HTTP Strict Transport Security (HSTS)
httpResponse.setHeader(
"Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload");
// Permissions Policy - disable unnecessary features
httpResponse.setHeader(
"Permissions-Policy", "geolocation=(), microphone=(), camera=(), payment=(), usb=()");
// Prevent caching of sensitive data
httpResponse.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, private");
httpResponse.setHeader("Pragma", "no-cache");
}
chain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,130 @@
package com.api.main.services;
import com.api.main.dto.LoginRequest;
import com.api.main.dto.LoginResponse;
import com.api.main.dto.UserResponse;
import com.api.main.entity.Token;
import com.api.main.entity.User;
import com.api.main.repositories.TokenRepository;
import com.api.main.repositories.UserRepository;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Collections;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/*
* Service layer for authentication and user management operations.
* Handles user login, registration, and logout functionality.
* Generates and validates tokens for authenticated sessions.
* Stores tokens in database for revocation support.
* Uses transactional operations for data consistency.
* Passwords are hashed using BCrypt before storage.
*/
@Service
public class AuthService {
private final UserRepository userRepository;
private final TokenRepository tokenRepository;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
@Value("${token.expiration:86400000}")
private Long tokenExpiration;
public AuthService(
UserRepository userRepository,
TokenRepository tokenRepository,
PasswordEncoder passwordEncoder,
AuthenticationManager authenticationManager) {
this.userRepository = userRepository;
this.tokenRepository = tokenRepository;
this.passwordEncoder = passwordEncoder;
this.authenticationManager = authenticationManager;
}
@Transactional
public LoginResponse authenticate(LoginRequest request) {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));
User user =
userRepository
.findByUsername(request.getUsername())
.orElseThrow(() -> new BadCredentialsException("Invalid credentials"));
String tokenValue = generateToken();
Instant now = Instant.now();
Instant expiresAt = now.plus(tokenExpiration, ChronoUnit.MILLIS);
Token token = new Token(tokenValue, user.getUsername(), now, expiresAt);
tokenRepository.save(token);
return new LoginResponse("success", "Authentication successful", tokenValue);
} catch (AuthenticationException e) {
throw new BadCredentialsException("Invalid username or password");
}
}
private String generateToken() {
byte[] randomBytes = new byte[32];
new java.security.SecureRandom().nextBytes(randomBytes);
return UUID.randomUUID().toString() + "-" + Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
}
public UserResponse getCurrentUser(String username) {
User user =
userRepository
.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
return new UserResponse(
user.getId(),
user.getUsername(),
user.getEmail(),
Collections.singletonList(user.getRole()));
}
@Transactional
public User registerUser(String username, String email, String password, String role) {
if (userRepository.existsByUsername(username)) {
throw new RuntimeException("Username already exists");
}
if (userRepository.existsByEmail(email)) {
throw new RuntimeException("Email already exists");
}
User user = new User();
user.setUsername(username);
user.setEmail(email);
user.setPasswordHash(passwordEncoder.encode(password));
user.setRole(role);
user.setEnabled(true);
return userRepository.save(user);
}
@Transactional
public void logout(String token) {
tokenRepository
.findByToken(token)
.ifPresent(
t -> {
t.setRevoked(true);
tokenRepository.save(t);
});
}
public boolean isTokenValid(String token) {
return tokenRepository.findByTokenAndRevokedFalse(token).isPresent();
}
}

Binary file not shown.