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:
26
.gitignore
vendored
26
.gitignore
vendored
@@ -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
40
database.dbml
Normal 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
119
pom.xml
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
99
src/main/java/com/api/main/config/SecurityConfig.java
Normal file
99
src/main/java/com/api/main/config/SecurityConfig.java
Normal 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();
|
||||
}
|
||||
}
|
||||
52
src/main/java/com/api/main/constants/Constants.java
Normal file
52
src/main/java/com/api/main/constants/Constants.java
Normal 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";
|
||||
}
|
||||
42
src/main/java/com/api/main/controllers/AuthController.java
Normal file
42
src/main/java/com/api/main/controllers/AuthController.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/main/java/com/api/main/controllers/Health.java
Normal file
22
src/main/java/com/api/main/controllers/Health.java
Normal 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));
|
||||
}
|
||||
}
|
||||
92
src/main/java/com/api/main/controllers/UserController.java
Normal file
92
src/main/java/com/api/main/controllers/UserController.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/main/java/com/api/main/dto/CreateUserRequest.java
Normal file
79
src/main/java/com/api/main/dto/CreateUserRequest.java
Normal 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;
|
||||
}
|
||||
}
|
||||
41
src/main/java/com/api/main/dto/ErrorResponse.java
Normal file
41
src/main/java/com/api/main/dto/ErrorResponse.java
Normal 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;
|
||||
}
|
||||
}
|
||||
26
src/main/java/com/api/main/dto/HealthResponse.java
Normal file
26
src/main/java/com/api/main/dto/HealthResponse.java
Normal 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;
|
||||
}
|
||||
}
|
||||
41
src/main/java/com/api/main/dto/LoginRequest.java
Normal file
41
src/main/java/com/api/main/dto/LoginRequest.java
Normal 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;
|
||||
}
|
||||
}
|
||||
51
src/main/java/com/api/main/dto/LoginResponse.java
Normal file
51
src/main/java/com/api/main/dto/LoginResponse.java
Normal 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;
|
||||
}
|
||||
}
|
||||
59
src/main/java/com/api/main/dto/UserResponse.java
Normal file
59
src/main/java/com/api/main/dto/UserResponse.java
Normal 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;
|
||||
}
|
||||
}
|
||||
158
src/main/java/com/api/main/entity/Token.java
Normal file
158
src/main/java/com/api/main/entity/Token.java
Normal 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;
|
||||
}
|
||||
}
|
||||
244
src/main/java/com/api/main/entity/User.java
Normal file
244
src/main/java/com/api/main/entity/User.java
Normal 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
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
58
src/main/java/com/api/main/repositories/TokenRepository.java
Normal file
58
src/main/java/com/api/main/repositories/TokenRepository.java
Normal 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();
|
||||
}
|
||||
27
src/main/java/com/api/main/repositories/UserRepository.java
Normal file
27
src/main/java/com/api/main/repositories/UserRepository.java
Normal 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);
|
||||
}
|
||||
@@ -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())));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
130
src/main/java/com/api/main/services/AuthService.java
Normal file
130
src/main/java/com/api/main/services/AuthService.java
Normal 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();
|
||||
}
|
||||
}
|
||||
BIN
src/main/resources/keystore.p12
Normal file
BIN
src/main/resources/keystore.p12
Normal file
Binary file not shown.
Reference in New Issue
Block a user