Skip to content
This repository was archived by the owner on Nov 23, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
06f3085
feat(auth): Add user management endpoints and employee creation funct…
RandithaK Sep 27, 2025
9f58fe6
fix(config): Update database configuration to use environment variabl…
RandithaK Sep 27, 2025
0a7e73b
feat(auth): Implement full RBAC with SUPER_ADMIN and user management API
RandithaK Sep 27, 2025
0721994
fix(config): Update JPA ddl-auto property to use environment variable…
RandithaK Sep 27, 2025
be73e4e
feat(security): Add WebSecurityCustomizer to ignore static resources …
RandithaK Sep 27, 2025
78649ac
feat(api): Implement comprehensive user management and self-service.
RandithaK Sep 27, 2025
93f427d
feat(exception): Add global exception handling for improved error res…
RandithaK Sep 27, 2025
231f578
feat(favicon): Add FaviconController to handle favicon.ico requests
RandithaK Sep 27, 2025
479bad1
fix(auth): update JWT entry point and security config; refine control…
RandithaK Sep 27, 2025
55005fc
feat(security): Implement login lockout policy with audit logging for…
RandithaK Sep 27, 2025
52c0027
standardize: replace message maps/MessageResponse with ApiSuccess in …
RandithaK Sep 27, 2025
0104173
docs(properties): enhance comments for database schema configuration …
RandithaK Sep 27, 2025
52d125b
feat(auth): enhance user authentication to support login by email or …
RandithaK Sep 28, 2025
4654e46
feat(api): add OpenAPI configuration and enhance Swagger documentatio…
RandithaK Sep 28, 2025
164023c
feat(dependencies): add Spring Boot DevTools for hot reload and Actua…
RandithaK Sep 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions auth-service/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@
<optional>true</optional>
</dependency>

<!-- Spring Boot DevTools for hot reload during development -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand Down Expand Up @@ -89,6 +97,10 @@
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.13</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

</dependencies>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ public class AuthEntryPointJwt implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
logger.error("Unauthorized error: {}", authException.getMessage());
// Log as INFO instead of ERROR for expected unauthorized requests
// ERROR level would be more appropriate for actual application errors
logger.info("Unauthorized access attempt to {}: {}", request.getRequestURI(), authException.getMessage());

response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
Expand All @@ -32,6 +33,9 @@ public class DataSeeder implements CommandLineRunner {

@Autowired
private PasswordEncoder passwordEncoder;

@Autowired
private Environment env;

@Override
public void run(String... args) throws Exception {
Expand All @@ -40,8 +44,8 @@ public void run(String... args) throws Exception {
// First, create roles if they don't exist
seedRoles();

// Then, seed users with proper roles
seedUsers();
// Then, seed users with proper roles depending on active profile
seedUsersByProfile();

logger.info("Data seeding completed successfully!");
}
Expand All @@ -50,6 +54,7 @@ public void run(String... args) throws Exception {
* Create all required roles in the system
*/
private void seedRoles() {
createRoleIfNotExists(RoleName.SUPER_ADMIN);
createRoleIfNotExists(RoleName.ADMIN);
createRoleIfNotExists(RoleName.EMPLOYEE);
createRoleIfNotExists(RoleName.CUSTOMER);
Expand Down Expand Up @@ -78,16 +83,49 @@ private void seedUsers() {
return;
}

// 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);
// Create default test users with roles
// The first privileged account is the SUPER_ADMIN (created only once by the seeder)
createUserWithRole("superadmin", "superadmin123", "superadmin@techtorque.com", RoleName.SUPER_ADMIN);

// Create a regular ADMIN for day-to-day management (cannot create other ADMINs)
createUserWithRole("admin", "admin123", "admin@techtorque.com", RoleName.ADMIN);

createUserWithRole("employee", "emp123", "employee@techtorque.com", RoleName.EMPLOYEE);
createUserWithRole("customer", "cust123", "customer@techtorque.com", RoleName.CUSTOMER);

// 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);
}

/**
* Seed users based on active Spring profile.
* - In 'dev' profile: create all test users (superadmin, admin, employee, customer, etc.)
* - In non-dev (e.g., production): only create the superadmin account to avoid seeding test data
*/
private void seedUsersByProfile() {
String[] activeProfiles = env.getActiveProfiles();
boolean isDev = false;
for (String p : activeProfiles) {
if ("dev".equalsIgnoreCase(p)) {
isDev = true;
break;
}
}

if (isDev) {
// Full seeding for development
logger.info("Active profile is 'dev' — seeding development users (including test accounts).");
System.out.println("[DEV MODE] Seeding development users: superadmin, admin, employee, customer, test users.");
seedUsers();
} else {
// Production/non-dev: only ensure SUPER_ADMIN exists
createUserWithRole("superadmin", "superadmin123", "superadmin@techtorque.com", RoleName.SUPER_ADMIN);
// Optionally create a day-to-day admin (commented out by default for production)
// createUserWithRole("admin", "admin123", "admin@techtorque.com", RoleName.ADMIN);
}
}

/**
* Create user with encoded password and assigned role
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.techtorque.auth_service.config;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Contact;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.info.License;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.servers.Server;
import org.springframework.context.annotation.Configuration;

/**
* OpenAPI 3.0 configuration for the TechTorque Authentication Service
*
* This configuration:
* 1. Defines API documentation metadata
* 2. Sets up JWT Bearer token authentication scheme
* 3. Configures the authorization button in Swagger UI
* 4. Defines server information
*/
@Configuration
@OpenAPIDefinition(
info = @Info(
title = "TechTorque Auth Service API",
version = "1.0.0",
description = """
Authentication and User Management API for TechTorque Auto Service Platform.

This API provides:
- User authentication (login/logout)
- User registration and management
- Role-based access control (RBAC)
- JWT token management
- Account security features (rate limiting, lockout protection)

## Security
Most endpoints require JWT authentication. Use the 'Authorize' button above to provide your Bearer token.

## Rate Limiting
Login attempts are rate-limited to prevent brute force attacks:
- Maximum 3 failed attempts per account
- 15-minute lockout after exceeding limit
- Automatic reset on successful login
""",
contact = @Contact(
name = "TechTorque Development Team",
email = "dev@techtorque.com",
url = "https://github.com/TechTorque-2025"
),
license = @License(
name = "MIT License",
url = "https://opensource.org/licenses/MIT"
)
),
servers = {
@Server(
url = "http://localhost:8081",
description = "Development Server"
),
@Server(
url = "https://api.techtorque.com",
description = "Production Server"
)
},
security = {
@SecurityRequirement(name = "bearerAuth")
}
)
@SecurityScheme(
name = "bearerAuth",
description = """
JWT Bearer Token Authentication

To obtain a token:
1. Use the /api/v1/auth/login endpoint with valid credentials
2. Copy the 'token' value from the response
3. Click the 'Authorize' button above
4. Enter: <your-token-here>
5. Click 'Authorize' to apply the token to all requests

Example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
""",
type = SecuritySchemeType.HTTP,
scheme = "bearer",
bearerFormat = "JWT",
in = SecuritySchemeIn.HEADER
)
public class OpenApiConfig {
// This configuration class uses annotations only
// Spring Boot will automatically pick up the @OpenAPIDefinition and @SecurityScheme annotations
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity // The 'prePostEnabled = true' is the default and not needed
@EnableMethodSecurity
public class SecurityConfig {

@Autowired
Expand Down Expand Up @@ -51,31 +56,74 @@ public AuthenticationManager authenticationManager(AuthenticationConfiguration a
return authConfig.getAuthenticationManager();
}

// NOTE: The WebSecurityCustomizer bean has been completely removed.

@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
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.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()
.requestMatchers(
// Public API endpoints
"/api/v1/auth/**", // Fixed: more specific auth path
"/api/auth/**", // Keep both for backward compatibility

// Public controller endpoints
"/favicon.ico",
"/error", // Add error page

// Health check and actuator endpoints (if needed)
"/actuator/**",

// All OpenAPI and Swagger UI resources
"/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/swagger-resources/**", // Include swagger-resources
"/webjars/**", // Include webjars
"/api-docs/**" // Additional swagger endpoint pattern
).permitAll()

// All other requests require authentication.
.anyRequest().authenticated()
);

// 5. Add your custom provider and filter
http.authenticationProvider(authenticationProvider());
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();

// Allow specific origins
configuration.setAllowedOrigins(Arrays.asList(
"http://localhost:3000", // Next.js dev server
"http://127.0.0.1:3000" // Alternative localhost
));

// Allow all headers
configuration.setAllowedHeaders(Arrays.asList("*"));

// Allow specific HTTP methods
configuration.setAllowedMethods(Arrays.asList(
"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"
));

// Allow credentials (important for cookies/auth tokens)
configuration.setAllowCredentials(true);

// Cache preflight response for 1 hour
configuration.setMaxAge(3600L);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);

return source;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.techtorque.auth_service.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.event.EventListener;
import org.springframework.core.env.Environment;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Prints a small startup banner/message when the application is ready.
* Only prints when running with the 'dev' profile and when enabled via config.
*/
@Component
public class StartupBanner {

private static final Logger logger = LoggerFactory.getLogger(StartupBanner.class);

private final Environment env;

private final boolean bannerEnabled;

public StartupBanner(Environment env, @Value("${app.banner.enabled:true}") boolean bannerEnabled) {
this.env = env;
this.bannerEnabled = bannerEnabled;
}

@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
if (!bannerEnabled) {
return;
}

String[] active = env.getActiveProfiles();
boolean isDev = false;
for (String p : active) {
if ("dev".equalsIgnoreCase(p)) {
isDev = true;
break;
}
}

if (isDev) {
String[] banner = new String[] {
"========================================",
"= DEVELOPMENT MODE - TECHTORQUE =",
"= Seeding development users now =",
"========================================"
};

for (String line : banner) {
// Log and also print to stdout for immediate CLI visibility
logger.info(line);
System.out.println(line);
}
} else {
logger.info("Application started with profiles: {}", String.join(",", active));
}
}
}
Loading