diff --git a/auth-service/pom.xml b/auth-service/pom.xml index 295687d..31668e3 100644 --- a/auth-service/pom.xml +++ b/auth-service/pom.xml @@ -1,89 +1,89 @@ - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.5.6 - - - com.techtorque - auth-service - 0.0.1-SNAPSHOT - auth-service - auth-service of techtorque - - - - - - - - - - - - - - - 17 - - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.postgresql - postgresql - runtime - - - org.projectlombok - lombok - true - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.6 + + + com.techtorque + auth-service + 0.0.1-SNAPSHOT + auth-service + auth-service of techtorque + + + + + + + + + + + + + + + 17 + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.postgresql + postgresql + runtime + + + org.projectlombok + lombok + true + - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - - io.jsonwebtoken - jjwt-api - 0.12.5 - - - io.jsonwebtoken - jjwt-impl - 0.12.5 - runtime - - - io.jsonwebtoken - jjwt-jackson - 0.12.5 - runtime - + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + io.jsonwebtoken + jjwt-api + 0.12.6 + + + io.jsonwebtoken + jjwt-impl + 0.12.6 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.12.6 + runtime + - - + + org.springdoc springdoc-openapi-starter-webmvc-ui @@ -92,13 +92,13 @@ - - - - org.springframework.boot - spring-boot-maven-plugin - - - + + + + org.springframework.boot + spring-boot-maven-plugin + + + diff --git a/auth-service/src/main/java/com/techtorque/auth_service/config/AuthEntryPointJwt.java b/auth-service/src/main/java/com/techtorque/auth_service/config/AuthEntryPointJwt.java new file mode 100644 index 0000000..95a2b9e --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/config/AuthEntryPointJwt.java @@ -0,0 +1,44 @@ +package com.techtorque.auth_service.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * JWT Authentication Entry Point + * Handles unauthorized access attempts + */ +@Component +public class AuthEntryPointJwt implements AuthenticationEntryPoint { + + private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class); + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + logger.error("Unauthorized error: {}", authException.getMessage()); + + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + final Map body = new HashMap<>(); + body.put("status", HttpServletResponse.SC_UNAUTHORIZED); + body.put("error", "Unauthorized"); + body.put("message", authException.getMessage()); + body.put("path", request.getServletPath()); + + final ObjectMapper mapper = new ObjectMapper(); + mapper.writeValue(response.getOutputStream(), body); + } +} \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/config/AuthTokenFilter.java b/auth-service/src/main/java/com/techtorque/auth_service/config/AuthTokenFilter.java new file mode 100644 index 0000000..32fc015 --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/config/AuthTokenFilter.java @@ -0,0 +1,72 @@ +package com.techtorque.auth_service.config; + +import com.techtorque.auth_service.service.UserService; +import com.techtorque.auth_service.util.JwtUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +/** + * JWT Authentication Filter + * Processes JWT tokens from HTTP requests and sets up Spring Security context + */ +public class AuthTokenFilter extends OncePerRequestFilter { + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private UserService userDetailsService; + + private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + try { + String jwt = parseJwt(request); + if (jwt != null && jwtUtil.validateJwtToken(jwt)) { + String username = jwtUtil.extractUsername(jwt); + + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, null, + userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception e) { + logger.error("Cannot set user authentication: {}", e); + } + + filterChain.doFilter(request, response); + } + + /** + * Extract JWT token from Authorization header + * @param request HTTP request + * @return JWT token or null if not found + */ + private String parseJwt(HttpServletRequest request) { + String headerAuth = request.getHeader("Authorization"); + + if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { + return headerAuth.substring(7); + } + + return null; + } +} \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/config/DataSeeder.java b/auth-service/src/main/java/com/techtorque/auth_service/config/DataSeeder.java index 3127c65..0339a2a 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/config/DataSeeder.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/config/DataSeeder.java @@ -1,49 +1,116 @@ package com.techtorque.auth_service.config; +import com.techtorque.auth_service.entity.Role; +import com.techtorque.auth_service.entity.RoleName; import com.techtorque.auth_service.entity.User; +import com.techtorque.auth_service.repository.RoleRepository; import com.techtorque.auth_service.repository.UserRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; +import java.util.HashSet; +import java.util.Set; + +/** + * Data seeder to initialize roles and default users with proper security + * Runs at application startup to ensure required data exists + */ @Component public class DataSeeder implements CommandLineRunner { private static final Logger logger = LoggerFactory.getLogger(DataSeeder.class); + @Autowired + private RoleRepository roleRepository; + @Autowired private UserRepository userRepository; + @Autowired + private PasswordEncoder passwordEncoder; + @Override public void run(String... args) throws Exception { + logger.info("Starting data seeding..."); + + // First, create roles if they don't exist + seedRoles(); + + // Then, seed users with proper roles seedUsers(); + + logger.info("Data seeding completed successfully!"); + } + + /** + * Create all required roles in the system + */ + private void seedRoles() { + createRoleIfNotExists(RoleName.ADMIN); + createRoleIfNotExists(RoleName.EMPLOYEE); + createRoleIfNotExists(RoleName.CUSTOMER); } + /** + * Create role if it doesn't exist + * @param roleName Role name to create + */ + private void createRoleIfNotExists(RoleName roleName) { + if (!roleRepository.existsByName(roleName)) { + Role role = new Role(); // Use default constructor + role.setName(roleName); // Set the role name + roleRepository.save(role); + logger.info("Created role: {}", roleName); + } + } + + /** + * Create default users with proper password encoding and role assignments + */ private void seedUsers() { - logger.info("Starting data seeding..."); - // Check if users already exist to avoid duplicates if (userRepository.count() > 0) { - logger.info("Users already exist in database. Skipping seeding."); + logger.info("Users already exist in database. Skipping user seeding."); return; } - // Create default test users - createUserIfNotExists("user", "password", "user@techtorque.com"); - createUserIfNotExists("admin", "admin123", "admin@techtorque.com"); - createUserIfNotExists("testuser", "test123", "test@techtorque.com"); - createUserIfNotExists("demo", "demo123", "demo@techtorque.com"); + // Create default test users with roles + createUserWithRole("admin", "admin123", "admin@techtorque.com", RoleName.ADMIN); + createUserWithRole("employee", "emp123", "employee@techtorque.com", RoleName.EMPLOYEE); + createUserWithRole("customer", "cust123", "customer@techtorque.com", RoleName.CUSTOMER); - logger.info("Data seeding completed successfully!"); + // Keep your original test users as customers + createUserWithRole("user", "password", "user@techtorque.com", RoleName.CUSTOMER); + createUserWithRole("testuser", "test123", "test@techtorque.com", RoleName.CUSTOMER); + createUserWithRole("demo", "demo123", "demo@techtorque.com", RoleName.CUSTOMER); } - private void createUserIfNotExists(String username, String password, String email) { + /** + * Create user with encoded password and assigned role + * @param username Username for the user + * @param password Plain text password (will be encoded) + * @param email User's email + * @param roleName Role to assign to the user + */ + private void createUserWithRole(String username, String password, String email, RoleName roleName) { if (!userRepository.existsByUsername(username)) { - User user = new User(username, password, email); + // Create user with encoded password + User user = new User(username, passwordEncoder.encode(password), email); + + // Assign role + Set roles = new HashSet<>(); + Role role = roleRepository.findByName(roleName) + .orElseThrow(() -> new RuntimeException("Role " + roleName + " not found")); + roles.add(role); + user.setRoles(roles); + + // Save user userRepository.save(user); - logger.info("Created user: {} with email: {}", username, email); + logger.info("Created user: {} with email: {} and role: {}", username, email, roleName); } else { logger.info("User {} already exists, skipping...", username); } diff --git a/auth-service/src/main/java/com/techtorque/auth_service/config/SecurityConfig.java b/auth-service/src/main/java/com/techtorque/auth_service/config/SecurityConfig.java index c23f45d..2851528 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/config/SecurityConfig.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/config/SecurityConfig.java @@ -1,36 +1,81 @@ package com.techtorque.auth_service.config; +import com.techtorque.auth_service.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +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.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity +@EnableMethodSecurity // The 'prePostEnabled = true' is the default and not needed public class SecurityConfig { - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .csrf(csrf -> csrf.disable()) - .authorizeHttpRequests(authz -> authz - // Allow unauthenticated access to login and health endpoints - .requestMatchers("/login", "/health").permitAll() - // Allow unauthenticated access to OpenAPI and Swagger UI endpoints - .requestMatchers( - "/v3/api-docs/**", - "/v3/api-docs", - "/swagger-ui/**", - "/swagger-ui.html", - "/swagger-ui/index.html", - "/swagger-ui/index.html/**", - "/swagger-resources/**", - "/webjars/**" - ).permitAll() - .anyRequest().authenticated() + + @Autowired + private UserService userDetailsService; + + @Autowired + private AuthEntryPointJwt unauthorizedHandler; + + @Bean + public AuthTokenFilter authenticationJwtTokenFilter() { + return new AuthTokenFilter(); + } + + @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(AuthenticationConfiguration authConfig) throws Exception { + return authConfig.getAuthenticationManager(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + // 1. Disable CSRF and CORS using the new lambda style + .csrf(AbstractHttpConfigurer::disable) + .cors(AbstractHttpConfigurer::disable) // For production, you should configure this properly + + // 2. Set up exception handling + .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler)) + + // 3. Set the session management to stateless + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + + // 4. Set up authorization rules + .authorizeHttpRequests(auth -> auth + // Be specific with your paths. Your controller is likely under /api/v1/auth + .requestMatchers("/api/v1/auth/**").permitAll() + .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").permitAll() + .anyRequest().authenticated() ); - - return http.build(); - } + + // 5. Add your custom provider and filter + http.authenticationProvider(authenticationProvider()); + http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } } \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/controller/AuthController.java b/auth-service/src/main/java/com/techtorque/auth_service/controller/AuthController.java index 9cfb63e..d7a3f41 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/controller/AuthController.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/controller/AuthController.java @@ -2,42 +2,92 @@ import com.techtorque.auth_service.dto.LoginRequest; import com.techtorque.auth_service.dto.LoginResponse; -import com.techtorque.auth_service.service.UserService; -import com.techtorque.auth_service.util.JwtUtil; +import com.techtorque.auth_service.dto.RegisterRequest; +import com.techtorque.auth_service.service.AuthService; +import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +/** + * REST Controller for authentication endpoints + * Handles login, registration, and health check requests + */ @RestController -@RequestMapping("/") +@RequestMapping("/api/auth") +@CrossOrigin(origins = "*", maxAge = 3600) public class AuthController { @Autowired - private UserService userService; - - @Autowired - private JwtUtil jwtUtil; + private AuthService authService; + /** + * User login endpoint + * @param loginRequest Login credentials + * @return JWT token and user details + */ @PostMapping("/login") - public ResponseEntity login(@RequestBody LoginRequest loginRequest) { + public ResponseEntity authenticateUser(@Valid @RequestBody LoginRequest loginRequest) { + try { + LoginResponse loginResponse = authService.authenticateUser(loginRequest); + return ResponseEntity.ok(loginResponse); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(new MessageResponse("Error: " + e.getMessage())); + } + } + + /** + * User registration endpoint + * @param registerRequest Registration details + * @return Success message + */ + @PostMapping("/register") + public ResponseEntity registerUser(@Valid @RequestBody RegisterRequest registerRequest) { try { - if (userService.authenticate(loginRequest.getUsername(), loginRequest.getPassword())) { - String token = jwtUtil.generateToken(loginRequest.getUsername()); - LoginResponse response = new LoginResponse(token, loginRequest.getUsername()); - return ResponseEntity.ok(response); - } else { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body("{\"error\":\"Invalid credentials\"}"); - } + String message = authService.registerUser(registerRequest); + return ResponseEntity.ok(new MessageResponse(message)); } catch (Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body("{\"error\":\"Internal server error\"}"); + return ResponseEntity.badRequest() + .body(new MessageResponse("Error: " + e.getMessage())); } } + /** + * Health check endpoint + * @return Service status + */ @GetMapping("/health") - public ResponseEntity health() { - return ResponseEntity.ok("{\"status\":\"UP\"}"); + public ResponseEntity health() { + return ResponseEntity.ok(new MessageResponse("Authentication Service is running!")); + } + + /** + * Test endpoint for authenticated users + * @return Test message + */ + @GetMapping("/test") + public ResponseEntity test() { + return ResponseEntity.ok(new MessageResponse("Test endpoint accessible!")); + } + + /** + * Inner class for simple message responses + */ + public static class MessageResponse { + private String message; + + public MessageResponse(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } } } \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/dto/CreateAdminRequest.java b/auth-service/src/main/java/com/techtorque/auth_service/dto/CreateAdminRequest.java new file mode 100644 index 0000000..c628c42 --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/dto/CreateAdminRequest.java @@ -0,0 +1,25 @@ +package com.techtorque.auth_service.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Request DTO for creating admin accounts + * Only existing admins can use this endpoint + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CreateAdminRequest { + + private String username; + private String email; + private String password; + + // Optional: Additional admin-specific fields + private String firstName; + private String lastName; +} \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/dto/CreateEmployeeRequest.java b/auth-service/src/main/java/com/techtorque/auth_service/dto/CreateEmployeeRequest.java new file mode 100644 index 0000000..f8713db --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/dto/CreateEmployeeRequest.java @@ -0,0 +1,26 @@ +package com.techtorque.auth_service.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Request DTO for creating employee accounts + * Only admins can use this endpoint + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CreateEmployeeRequest { + + private String username; + private String email; + private String password; + + // Optional: Additional employee-specific fields + private String firstName; + private String lastName; + private String department; +} \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/dto/LoginRequest.java b/auth-service/src/main/java/com/techtorque/auth_service/dto/LoginRequest.java index bef262c..040e37a 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/dto/LoginRequest.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/dto/LoginRequest.java @@ -1,13 +1,21 @@ package com.techtorque.auth_service.dto; +import jakarta.validation.constraints.NotBlank; import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; +/** + * DTO for login request containing user credentials + */ @Data @NoArgsConstructor @AllArgsConstructor public class LoginRequest { + + @NotBlank(message = "Username is required") private String username; + + @NotBlank(message = "Password is required") private String password; } \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/dto/LoginResponse.java b/auth-service/src/main/java/com/techtorque/auth_service/dto/LoginResponse.java index 1d3acfc..975c979 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/dto/LoginResponse.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/dto/LoginResponse.java @@ -3,11 +3,27 @@ import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; +import lombok.Builder; + +import java.util.Set; @Data @NoArgsConstructor @AllArgsConstructor +@Builder public class LoginResponse { + private String token; + private String type = "Bearer"; private String username; + private String email; + private Set roles; + + public LoginResponse(String token, String username, String email, Set roles) { + this.token = token; + this.type = "Bearer"; + this.username = username; + this.email = email; + this.roles = roles; + } } \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/dto/RegisterRequest.java b/auth-service/src/main/java/com/techtorque/auth_service/dto/RegisterRequest.java new file mode 100644 index 0000000..e950f19 --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/dto/RegisterRequest.java @@ -0,0 +1,34 @@ +package com.techtorque.auth_service.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.util.Set; + +/** + * DTO for user registration request + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class RegisterRequest { + + @NotBlank(message = "Username is required") + @Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters") + private String username; + + @NotBlank(message = "Email is required") + @Email(message = "Email should be valid") + private String email; + + @NotBlank(message = "Password is required") + @Size(min = 6, max = 40, message = "Password must be between 6 and 40 characters") + private String password; + + // Set of role names to assign to the user (optional) + private Set roles; +} \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/dto/UserDto.java b/auth-service/src/main/java/com/techtorque/auth_service/dto/UserDto.java new file mode 100644 index 0000000..14d909f --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/dto/UserDto.java @@ -0,0 +1,28 @@ +package com.techtorque.auth_service.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.Set; + +/** + * Data Transfer Object for User information + * Used to transfer user data without exposing sensitive information + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class UserDto { + + private Long id; + private String username; + private String email; + private Boolean enabled; + private LocalDateTime createdAt; + private Set roles; // Role names as strings + private Set permissions; // Permission names as strings +} \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/entity/Permission.java b/auth-service/src/main/java/com/techtorque/auth_service/entity/Permission.java new file mode 100644 index 0000000..ea9f618 --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/entity/Permission.java @@ -0,0 +1,37 @@ +package com.techtorque.auth_service.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * Permission entity to represent individual permissions in the system + * Each permission represents a specific action that can be performed + */ +@Entity +@Table(name = "permissions") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Permission { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + // Unique permission name (e.g., CREATE_USER, VIEW_REPORTS) + @Column(unique = true, nullable = false) + private String name; + + // Human-readable description of what this permission allows + private String description; + + // Many-to-Many relationship with Role - a permission can be assigned to multiple roles + @ManyToMany(mappedBy = "permissions") + private Set roles; +} \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/entity/PermissionConstants.java b/auth-service/src/main/java/com/techtorque/auth_service/entity/PermissionConstants.java new file mode 100644 index 0000000..8884696 --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/entity/PermissionConstants.java @@ -0,0 +1,39 @@ +package com.techtorque.auth_service.entity; + +/** + * Constants class containing all permission names used in the system + * This centralizes permission management and prevents typos + */ +public class PermissionConstants { + + // ============ ADMIN PERMISSIONS ============ + // User management permissions - only admins can create/update/delete users + public static final String CREATE_EMPLOYEE = "CREATE_EMPLOYEE"; // Only admins can create employees + public static final String CREATE_ADMIN = "CREATE_ADMIN"; // Only admins can create other admins + public static final String UPDATE_USER = "UPDATE_USER"; + public static final String DELETE_USER = "DELETE_USER"; + public static final String VIEW_ALL_USERS = "VIEW_ALL_USERS"; + + // Role management - only admins can assign/remove roles + public static final String MANAGE_ROLES = "MANAGE_ROLES"; + + // System administration + public static final String SYSTEM_ADMIN = "SYSTEM_ADMIN"; + + // ============ EMPLOYEE PERMISSIONS ============ + // Employee can view and update customer data for support purposes + public static final String VIEW_CUSTOMER_DATA = "VIEW_CUSTOMER_DATA"; + public static final String UPDATE_CUSTOMER_DATA = "UPDATE_CUSTOMER_DATA"; + + // Employee can access business reports + public static final String VIEW_REPORTS = "VIEW_REPORTS"; + + // ============ CUSTOMER PERMISSIONS ============ + // Basic profile management - all users can view/update their own profile + public static final String VIEW_OWN_PROFILE = "VIEW_OWN_PROFILE"; + public static final String UPDATE_OWN_PROFILE = "UPDATE_OWN_PROFILE"; + + // Customer-specific actions + public static final String PLACE_ORDER = "PLACE_ORDER"; + public static final String VIEW_ORDER_HISTORY = "VIEW_ORDER_HISTORY"; +} \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/entity/Role.java b/auth-service/src/main/java/com/techtorque/auth_service/entity/Role.java new file mode 100644 index 0000000..f65ab4b --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/entity/Role.java @@ -0,0 +1,48 @@ +package com.techtorque.auth_service.entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Set; + +/** + * Role entity representing user roles in the system + * Each role contains multiple permissions that define what actions can be performed + */ +@Entity +@Table(name = "roles") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Role { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + // Role name from the RoleName enum (ADMIN, EMPLOYEE, CUSTOMER) + @Column(unique = true, nullable = false) + @Enumerated(EnumType.STRING) + private RoleName name; + + // Human-readable description of the role + private String description; + + // Many-to-Many relationship with User - a role can be assigned to multiple users + @ManyToMany(mappedBy = "roles") + private Set users; + + // Many-to-Many relationship with Permission - a role contains multiple permissions + // EAGER fetch ensures permissions are loaded when we load a role + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "role_permissions", // Junction table name + joinColumns = @JoinColumn(name = "role_id"), // Foreign key to role + inverseJoinColumns = @JoinColumn(name = "permission_id") // Foreign key to permission + ) + private Set permissions; +} \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/entity/RoleName.java b/auth-service/src/main/java/com/techtorque/auth_service/entity/RoleName.java new file mode 100644 index 0000000..9221107 --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/entity/RoleName.java @@ -0,0 +1,23 @@ +package com.techtorque.auth_service.entity; + +/** + * Enum defining the three roles in the system + * ADMIN - Full system access + * EMPLOYEE - Limited access for staff operations + * CUSTOMER - Access to customer-specific features + */ +public enum RoleName { + ADMIN("Administrator - Full system access"), + EMPLOYEE("Employee - Limited system access for staff operations"), + CUSTOMER("Customer - Access to customer-specific features"); + + private final String description; + + RoleName(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/entity/User.java b/auth-service/src/main/java/com/techtorque/auth_service/entity/User.java index 6a19307..2273d18 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/entity/User.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/entity/User.java @@ -7,7 +7,13 @@ import lombok.Builder; import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; +/** + * User entity representing users in the authentication system + * Contains user credentials and role assignments + */ @Entity @Table(name = "users") @Data @@ -26,7 +32,7 @@ public class User { @Column(nullable = false) private String password; - @Column(nullable = false) + @Column(unique = true, nullable = false) private String email; @Column(nullable = false) @@ -37,11 +43,28 @@ public class User { @Builder.Default private LocalDateTime createdAt = LocalDateTime.now(); + // Many-to-Many relationship with Role entity + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable( + name = "user_roles", + joinColumns = @JoinColumn(name = "user_id"), + inverseJoinColumns = @JoinColumn(name = "role_id") + ) + @Builder.Default + private Set roles = new HashSet<>(); + + // Constructor for easy user creation public User(String username, String password, String email) { this.username = username; this.password = password; this.email = email; this.enabled = true; this.createdAt = LocalDateTime.now(); + this.roles = new HashSet<>(); + } + + // Helper method to add roles + public void addRole(Role role) { + this.roles.add(role); } } \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/repository/PermissionRepository.java b/auth-service/src/main/java/com/techtorque/auth_service/repository/PermissionRepository.java new file mode 100644 index 0000000..4ff500c --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/repository/PermissionRepository.java @@ -0,0 +1,39 @@ +package com.techtorque.auth_service.repository; + +import com.techtorque.auth_service.entity.Permission; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.Set; + +/** + * Repository interface for Permission entity + * Provides database operations for permission management + */ +@Repository +public interface PermissionRepository extends JpaRepository { + + /** + * Find a permission by its name + * @param name The permission name to search for (e.g., "CREATE_EMPLOYEE") + * @return Optional containing the permission if found + */ + Optional findByName(String name); + + /** + * Find multiple permissions by their names + * Useful when assigning multiple permissions to a role + * @param names Set of permission names to search for + * @return Set of permissions found + */ + Set findByNameIn(Set names); + + /** + * Check if a permission exists by name + * Useful for validation before creating new permissions + * @param name The permission name to check + * @return true if permission exists, false otherwise + */ + boolean existsByName(String name); +} \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/repository/RoleRepository.java b/auth-service/src/main/java/com/techtorque/auth_service/repository/RoleRepository.java new file mode 100644 index 0000000..2185c65 --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/repository/RoleRepository.java @@ -0,0 +1,30 @@ +package com.techtorque.auth_service.repository; + +import com.techtorque.auth_service.entity.Role; +import com.techtorque.auth_service.entity.RoleName; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * Repository interface for Role entity operations + * Provides database access methods for role-related queries + */ +@Repository +public interface RoleRepository extends JpaRepository { + + /** + * Find role by role name + * @param name the role name to search for + * @return Optional containing role if found + */ + Optional findByName(RoleName name); + + /** + * Check if role exists by name + * @param name the role name to check + * @return true if role exists, false otherwise + */ + boolean existsByName(RoleName name); +} \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/repository/UserRepository.java b/auth-service/src/main/java/com/techtorque/auth_service/repository/UserRepository.java index 3a374e9..6d8d9af 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/repository/UserRepository.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/repository/UserRepository.java @@ -2,16 +2,52 @@ import com.techtorque.auth_service.entity.User; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.Optional; +/** + * Repository interface for User entity operations + * Provides database access methods for user-related queries + */ @Repository public interface UserRepository extends JpaRepository { + /** + * Find user by username + * @param username the username to search for + * @return Optional containing user if found + */ Optional findByUsername(String username); + /** + * Find user by email + * @param email the email to search for + * @return Optional containing user if found + */ + Optional findByEmail(String email); + + /** + * Check if username exists + * @param username the username to check + * @return true if username exists, false otherwise + */ boolean existsByUsername(String username); + /** + * Check if email exists + * @param email the email to check + * @return true if email exists, false otherwise + */ boolean existsByEmail(String email); + + /** + * Find user with roles by username (explicit fetch) + * @param username the username to search for + * @return Optional containing user with roles if found + */ + @Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.username = :username") + Optional findByUsernameWithRoles(@Param("username") String username); } \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/service/AuthService.java b/auth-service/src/main/java/com/techtorque/auth_service/service/AuthService.java new file mode 100644 index 0000000..d085215 --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/service/AuthService.java @@ -0,0 +1,128 @@ +package com.techtorque.auth_service.service; + +import com.techtorque.auth_service.dto.LoginRequest; +import com.techtorque.auth_service.dto.LoginResponse; +import com.techtorque.auth_service.dto.RegisterRequest; +import com.techtorque.auth_service.entity.Role; +import com.techtorque.auth_service.entity.RoleName; +import com.techtorque.auth_service.entity.User; +import com.techtorque.auth_service.repository.RoleRepository; +import com.techtorque.auth_service.repository.UserRepository; +import com.techtorque.auth_service.util.JwtUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +@Transactional +public class AuthService { + + @Autowired + private AuthenticationManager authenticationManager; + + @Autowired + private UserRepository userRepository; + + @Autowired + private RoleRepository roleRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Autowired + private JwtUtil jwtUtil; + + public LoginResponse authenticateUser(LoginRequest loginRequest) { + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + loginRequest.getUsername(), + loginRequest.getPassword() + ) + ); + + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + + List roles = userDetails.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .map(auth -> auth.replace("ROLE_", "")) + .collect(Collectors.toList()); + + String jwt = jwtUtil.generateJwtToken(userDetails, roles); + + User user = userRepository.findByUsername(userDetails.getUsername()) + .orElseThrow(() -> new RuntimeException("User not found")); + + Set roleNames = user.getRoles().stream() + .map(role -> role.getName().name()) + .collect(Collectors.toSet()); + + return LoginResponse.builder() + .token(jwt) + .username(user.getUsername()) + .email(user.getEmail()) // This was missing in the error + .roles(roleNames) + .build(); + } + + public String registerUser(RegisterRequest registerRequest) { + if (userRepository.existsByUsername(registerRequest.getUsername())) { + throw new RuntimeException("Error: Username is already taken!"); + } + + if (userRepository.existsByEmail(registerRequest.getEmail())) { + throw new RuntimeException("Error: Email is already in use!"); + } + + User user = User.builder() + .username(registerRequest.getUsername()) + .email(registerRequest.getEmail()) + .password(passwordEncoder.encode(registerRequest.getPassword())) + .enabled(true) + .roles(new HashSet<>()) + .build(); + + Set strRoles = registerRequest.getRoles(); + Set roles = new HashSet<>(); + + if (strRoles == null || strRoles.isEmpty()) { + Role customerRole = roleRepository.findByName(RoleName.CUSTOMER) + .orElseThrow(() -> new RuntimeException("Error: Customer Role not found.")); + roles.add(customerRole); + } else { + strRoles.forEach(roleName -> { + switch (roleName) { + case "admin": + Role adminRole = roleRepository.findByName(RoleName.ADMIN) + .orElseThrow(() -> new RuntimeException("Error: Admin Role not found.")); + roles.add(adminRole); + break; + case "employee": + Role employeeRole = roleRepository.findByName(RoleName.EMPLOYEE) + .orElseThrow(() -> new RuntimeException("Error: Employee Role not found.")); + roles.add(employeeRole); + break; + default: + Role customerRole = roleRepository.findByName(RoleName.CUSTOMER) + .orElseThrow(() -> new RuntimeException("Error: Customer Role not found.")); + roles.add(customerRole); + } + }); + } + + user.setRoles(roles); + userRepository.save(user); + + return "User registered successfully!"; + } +} \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/service/UserService.java b/auth-service/src/main/java/com/techtorque/auth_service/service/UserService.java index ca1608c..d8b02de 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/service/UserService.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/service/UserService.java @@ -1,46 +1,313 @@ package com.techtorque.auth_service.service; +import com.techtorque.auth_service.entity.Role; +import com.techtorque.auth_service.entity.RoleName; import com.techtorque.auth_service.entity.User; +import com.techtorque.auth_service.repository.RoleRepository; import com.techtorque.auth_service.repository.UserRepository; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.security.core.GrantedAuthority; +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.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; +import java.util.*; +import java.util.stream.Collectors; +/** + * Service class for user management with restricted registration + * - Only customers can register publicly + * - Only admins can create employees and other admins + * - Implements Spring Security's UserDetailsService for authentication + */ @Service -public class UserService { - - @Autowired - private UserRepository userRepository; - - public boolean authenticate(String username, String password) { - Optional userOpt = userRepository.findByUsername(username); - if (userOpt.isPresent()) { - User user = userOpt.get(); - // For now, we'll do plain text password comparison - // In production, you should use BCrypt or similar - return user.getPassword().equals(password) && user.getEnabled(); - } - return false; +@Transactional +public class UserService implements UserDetailsService { + + private final UserRepository userRepository; + private final RoleRepository roleRepository; + private final PasswordEncoder passwordEncoder; + + @Autowired + public UserService(UserRepository userRepository, RoleRepository roleRepository, @Lazy PasswordEncoder passwordEncoder) { + this.userRepository = userRepository; + this.roleRepository = roleRepository; + this.passwordEncoder = passwordEncoder; + } + + /** + * Load user by username for Spring Security authentication + * This method is called during login to authenticate the user + * @param username The username to authenticate + * @return UserDetails object with user info and authorities + * @throws UsernameNotFoundException if user not found + */ + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); + + return org.springframework.security.core.userdetails.User.builder() + .username(user.getUsername()) + .password(user.getPassword()) + .authorities(getAuthorities(user)) // Convert roles/permissions to Spring Security authorities + .accountExpired(false) + .accountLocked(!user.getEnabled()) // Account locked if user is disabled + .credentialsExpired(false) + .disabled(!user.getEnabled()) + .build(); } - - public boolean userExists(String username) { - return userRepository.existsByUsername(username); + + /** + * Convert user roles and permissions to Spring Security GrantedAuthority objects + * This enables role-based and permission-based security checks + * @param user The user whose authorities to build + * @return Collection of granted authorities + */ + private Collection getAuthorities(User user) { + Set authorities = new HashSet<>(); + + // Add role-based authorities (prefixed with ROLE_) + user.getRoles().forEach(role -> { + authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName().name())); + + // Add permission-based authorities (used for @PreAuthorize checks) + role.getPermissions().forEach(permission -> + authorities.add(new SimpleGrantedAuthority(permission.getName())) + ); + }); + + return authorities; } - - public User createUser(String username, String password, String email) { - if (userRepository.existsByUsername(username)) { - throw new RuntimeException("Username already exists"); + + /** + * Register a new customer (public registration) + * Only allows CUSTOMER role creation through public endpoint + * @param username Unique username + * @param email Unique email + * @param password Plain text password (will be encoded) + * @return The created customer user + * @throws RuntimeException if username or email already exists + */ + public User registerCustomer(String username, String email, String password) { + // Validate username doesn't exist + if (userRepository.findByUsername(username).isPresent()) { + throw new RuntimeException("Username already exists: " + username); } - if (userRepository.existsByEmail(email)) { - throw new RuntimeException("Email already exists"); + + // Validate email doesn't exist + if (userRepository.findByEmail(email).isPresent()) { + throw new RuntimeException("Email already exists: " + email); + } + + // Get CUSTOMER role from database + Role customerRole = roleRepository.findByName(RoleName.CUSTOMER) + .orElseThrow(() -> new RuntimeException("Customer role not found")); + + // Create user with CUSTOMER role only + User user = User.builder() + .username(username) + .email(email) + .password(passwordEncoder.encode(password)) + .enabled(true) + .roles(Set.of(customerRole)) // Only CUSTOMER role + .build(); + + return userRepository.save(user); + } + + /** + * Create an employee account (admin only) + * Only admins can call this method + * @param username Unique username + * @param email Unique email + * @param password Plain text password (will be encoded) + * @return The created employee user + * @throws RuntimeException if username or email already exists + */ + public User createEmployee(String username, String email, String password) { + // Validate username doesn't exist + if (userRepository.findByUsername(username).isPresent()) { + throw new RuntimeException("Username already exists: " + username); } - User user = new User(username, password, email); + // Validate email doesn't exist + if (userRepository.findByEmail(email).isPresent()) { + throw new RuntimeException("Email already exists: " + email); + } + + // Get EMPLOYEE role from database + Role employeeRole = roleRepository.findByName(RoleName.EMPLOYEE) + .orElseThrow(() -> new RuntimeException("Employee role not found")); + + // Create user with EMPLOYEE role + User user = User.builder() + .username(username) + .email(email) + .password(passwordEncoder.encode(password)) + .enabled(true) + .roles(Set.of(employeeRole)) // Only EMPLOYEE role + .build(); + return userRepository.save(user); } - + + /** + * Create an admin account (admin only) + * Only existing admins can call this method + * @param username Unique username + * @param email Unique email + * @param password Plain text password (will be encoded) + * @return The created admin user + * @throws RuntimeException if username or email already exists + */ + public User createAdmin(String username, String email, String password) { + // Validate username doesn't exist + if (userRepository.findByUsername(username).isPresent()) { + throw new RuntimeException("Username already exists: " + username); + } + + // Validate email doesn't exist + if (userRepository.findByEmail(email).isPresent()) { + throw new RuntimeException("Email already exists: " + email); + } + + // Get ADMIN role from database + Role adminRole = roleRepository.findByName(RoleName.ADMIN) + .orElseThrow(() -> new RuntimeException("Admin role not found")); + + // Create user with ADMIN role + User user = User.builder() + .username(username) + .email(email) + .password(passwordEncoder.encode(password)) + .enabled(true) + .roles(Set.of(adminRole)) // Only ADMIN role + .build(); + + return userRepository.save(user); + } + + /** + * Find user by username + * @param username Username to search for + * @return Optional containing user if found + */ public Optional findByUsername(String username) { return userRepository.findByUsername(username); } + + /** + * Find user by email + * @param email Email to search for + * @return Optional containing user if found + */ + public Optional findByEmail(String email) { + return userRepository.findByEmail(email); + } + + /** + * Get all users in the system (admin only) + * @return List of all users + */ + public List findAllUsers() { + return userRepository.findAll(); + } + + /** + * Get all permissions for a user (from all their roles) + * @param username Username to get permissions for + * @return Set of permission names + */ + public Set getUserPermissions(String username) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found: " + username)); + + return user.getRoles().stream() + .flatMap(role -> role.getPermissions().stream()) + .map(permission -> permission.getName()) + .collect(Collectors.toSet()); + } + + /** + * Get all roles for a user + * @param username Username to get roles for + * @return Set of role names + */ + public Set getUserRoles(String username) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found: " + username)); + + return user.getRoles().stream() + .map(role -> role.getName().name()) + .collect(Collectors.toSet()); + } + + /** + * Enable a user account (admin only) + * @param username Username to enable + */ + public void enableUser(String username) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found: " + username)); + user.setEnabled(true); + userRepository.save(user); + } + + /** + * Disable a user account (admin only) + * @param username Username to disable + */ + public void disableUser(String username) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found: " + username)); + user.setEnabled(false); + userRepository.save(user); + } + + /** + * Delete a user from the system (admin only) + * @param username Username to delete + * @throws RuntimeException if user not found + */ + public void deleteUser(String username) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found: " + username)); + userRepository.delete(user); + } + + /** + * Check if a user has a specific role + * @param username Username to check + * @param roleName Role to check for + * @return true if user has the role + */ + public boolean hasRole(String username, RoleName roleName) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found: " + username)); + + return user.getRoles().stream() + .anyMatch(role -> role.getName().equals(roleName)); + } + + /** + * Check if a user has a specific permission + * @param username Username to check + * @param permissionName Permission to check for + * @return true if user has the permission through any of their roles + */ + public boolean hasPermission(String username, String permissionName) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found: " + username)); + + return user.getRoles().stream() + .flatMap(role -> role.getPermissions().stream()) + .anyMatch(permission -> permission.getName().equals(permissionName)); + } } \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/util/JwtUtil.java b/auth-service/src/main/java/com/techtorque/auth_service/util/JwtUtil.java index ca05db1..97d6388 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/util/JwtUtil.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/util/JwtUtil.java @@ -1,61 +1,129 @@ +// In: /src/main/java/com/techtorque/auth_service/util/JwtUtil.java package com.techtorque.auth_service.util; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; -import java.nio.charset.StandardCharsets; import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; @Component public class JwtUtil { + private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class); + @Value("${jwt.secret}") - private String secret; + private String jwtSecret; + + @Value("${jwt.expiration}") + private long jwtExpirationMs; - @Value("${jwt.expiration.time}") - private long expirationTime; + /** + * Generates a JWT token for a user with their roles. + */ + public String generateJwtToken(UserDetails userDetails, List roles) { + Map claims = new HashMap<>(); + claims.put("roles", roles); + return generateToken(claims, userDetails.getUsername()); + } - public String generateToken(String username) { - SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + /** + * Creates the token using the modern builder pattern. + * This fixes all the deprecation warnings. + */ + public String generateToken(Map extraClaims, String username) { + Date now = new Date(); + Date expirationDate = new Date(now.getTime() + jwtExpirationMs); return Jwts.builder() + .claims(extraClaims) // Modern way to set claims .subject(username) - .issuedAt(new Date()) - .expiration(new Date(System.currentTimeMillis() + expirationTime)) - .signWith(key) + .issuedAt(now) + .expiration(expirationDate) + .signWith(getSignInKey()) // Modern way to sign (algorithm is inferred from the key) .compact(); } - public boolean validateToken(String token, String username) { - try { - SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); - String tokenUsername = Jwts.parser() - .verifyWith(key) - .build() - .parseSignedClaims(token) - .getPayload() - .getSubject(); + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } - return tokenUsername.equals(username); - } catch (Exception e) { - return false; - } + @SuppressWarnings("unchecked") + public List extractRoles(String token) { + return extractClaim(token, claims -> (List) claims.get("roles")); } - public String extractUsername(String token) { + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + /** + * Extracts all claims using the modern parser builder. + * This part of your code was already correct. + */ + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith(getSignInKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + private Boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + public Boolean validateToken(String token, UserDetails userDetails) { + final String username = extractUsername(token); + return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } + + /** + * Validates the token structure and signature without checking expiration against UserDetails. + */ + public boolean validateJwtToken(String token) { try { - SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); - return Jwts.parser() - .verifyWith(key) + Jwts.parser() + .verifyWith(getSignInKey()) .build() - .parseSignedClaims(token) - .getPayload() - .getSubject(); - } catch (Exception e) { - return null; + .parseSignedClaims(token); + return true; + } catch (MalformedJwtException e) { + logger.error("Invalid JWT token: {}", e.getMessage()); + } catch (ExpiredJwtException e) { + logger.error("JWT token is expired: {}", e.getMessage()); + } catch (UnsupportedJwtException e) { + logger.error("JWT token is unsupported: {}", e.getMessage()); + } catch (IllegalArgumentException e) { + logger.error("JWT claims string is empty: {}", e.getMessage()); } + return false; + } + + /** + * Generates a SecretKey object from the Base64 encoded secret string. + */ + private SecretKey getSignInKey() { + byte[] keyBytes = Decoders.BASE64.decode(jwtSecret); + return Keys.hmacShaKeyFor(keyBytes); } } \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/util/RegisterRequest.java b/auth-service/src/main/java/com/techtorque/auth_service/util/RegisterRequest.java new file mode 100644 index 0000000..f4d37b9 --- /dev/null +++ b/auth-service/src/main/java/com/techtorque/auth_service/util/RegisterRequest.java @@ -0,0 +1,35 @@ +package com.techtorque.auth_service.util; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.util.Set; + +/** + * DTO for user registration request + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class RegisterRequest { + + @NotBlank(message = "Username is required") + @Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters") + private String username; + + @NotBlank(message = "Email is required") + @Email(message = "Email should be valid") + private String email; + + @NotBlank(message = "Password is required") + @Size(min = 6, max = 40, message = "Password must be between 6 and 40 characters") + private String password; + + // Set of role names to assign to the user (optional) + // Valid values: "admin", "employee", "customer" + private Set roles; +} \ No newline at end of file diff --git a/auth-service/src/main/resources/application.properties b/auth-service/src/main/resources/application.properties index 054e02c..9a9fa67 100644 --- a/auth-service/src/main/resources/application.properties +++ b/auth-service/src/main/resources/application.properties @@ -4,7 +4,7 @@ server.port=8081 # JWT Configuration jwt.secret=${JWT_SECRET:YourSuperSecretKeyForJWTGoesHereAndItMustBeVeryLongForSecurityPurposes} -jwt.expiration.time=86400000 +jwt.expiration=86400000 # Database Configuration for local testing, it is recommended to make a database, using the following name, and make a user with the given username and password. spring.datasource.url=jdbc:postgresql://localhost:5432/techtorque