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 extends GrantedAuthority> 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