From 97fca6beb193dd89f1e6fcd515e6f474772a740f Mon Sep 17 00:00:00 2001 From: RandithaK Date: Tue, 11 Nov 2025 12:05:39 +0530 Subject: [PATCH 1/3] chore: commit all changes (automated) --- COMPLETE_FIX.sh | 136 ++++++++++ LOGIN_ISSUE_FIX.md | 239 +++++++++++++++++ auth-service/pom.xml | 55 +++- .../auth_service/config/DataSeeder.java | 240 +++++++++--------- .../auth_service/config/EmailConfig.java | 11 +- .../config/GatewayHeaderFilter.java | 18 +- .../techtorque/auth_service/entity/User.java | 2 +- .../auth_service/service/AuthService.java | 16 +- .../auth_service/service/EmailService.java | 190 ++++++-------- .../src/main/proto/notification/email.proto | 38 +++ .../src/main/resources/application.properties | 7 + check_user_roles.sql | 13 + fix_user_roles.sql | 29 +++ 13 files changed, 743 insertions(+), 251 deletions(-) create mode 100644 COMPLETE_FIX.sh create mode 100644 LOGIN_ISSUE_FIX.md create mode 100644 auth-service/src/main/proto/notification/email.proto create mode 100644 check_user_roles.sql create mode 100644 fix_user_roles.sql diff --git a/COMPLETE_FIX.sh b/COMPLETE_FIX.sh new file mode 100644 index 0000000..0186639 --- /dev/null +++ b/COMPLETE_FIX.sh @@ -0,0 +1,136 @@ +#!/bin/bash + +echo "==========================================" +echo "๐Ÿ”ง Complete Fix for User Roles Issue" +echo "==========================================" +echo "" + +# Database connection details (adjust if needed) +DB_HOST="${DB_HOST:-localhost}" +DB_PORT="${DB_PORT:-5432}" +DB_NAME="${DB_NAME:-techtorque}" +DB_USER="${DB_USER:-techtorque}" +DB_PASS="${DB_PASS:-techtorque123}" + +echo "Step 1: Fixing database - Assigning roles to existing users" +echo "Database: $DB_NAME @ $DB_HOST:$DB_PORT" +echo "" + +# SQL to fix existing data +SQL=$(cat <<'EOSQL' +-- Display current state +SELECT 'BEFORE FIX:' as status; +SELECT u.username, COUNT(ur.role_id) as role_count +FROM users u +LEFT JOIN user_roles ur ON u.id = ur.user_id +GROUP BY u.id, u.username +ORDER BY u.id; + +-- Assign roles to all users based on their username +-- Assign SUPER_ADMIN role +INSERT INTO user_roles (user_id, role_id) +SELECT u.id, r.id +FROM users u +CROSS JOIN roles r +WHERE u.username = 'superadmin' + AND r.name = 'SUPER_ADMIN' + AND NOT EXISTS ( + SELECT 1 FROM user_roles ur + WHERE ur.user_id = u.id AND ur.role_id = r.id + ); + +-- Assign ADMIN role +INSERT INTO user_roles (user_id, role_id) +SELECT u.id, r.id +FROM users u +CROSS JOIN roles r +WHERE u.username = 'admin' + AND r.name = 'ADMIN' + AND NOT EXISTS ( + SELECT 1 FROM user_roles ur + WHERE ur.user_id = u.id AND ur.role_id = r.id + ); + +-- Assign EMPLOYEE role +INSERT INTO user_roles (user_id, role_id) +SELECT u.id, r.id +FROM users u +CROSS JOIN roles r +WHERE u.username = 'employee' + AND r.name = 'EMPLOYEE' + AND NOT EXISTS ( + SELECT 1 FROM user_roles ur + WHERE ur.user_id = u.id AND ur.role_id = r.id + ); + +-- Assign CUSTOMER role to customer and test users +INSERT INTO user_roles (user_id, role_id) +SELECT u.id, r.id +FROM users u +CROSS JOIN roles r +WHERE u.username IN ('customer', 'user', 'testuser', 'demo', 'test') + AND r.name = 'CUSTOMER' + AND NOT EXISTS ( + SELECT 1 FROM user_roles ur + WHERE ur.user_id = u.id AND ur.role_id = r.id + ); + +-- Display fixed state +SELECT 'AFTER FIX:' as status; +SELECT u.username, u.email, r.name as role_name +FROM users u +INNER JOIN user_roles ur ON u.id = ur.user_id +INNER JOIN roles r ON ur.role_id = r.id +ORDER BY u.id, r.name; + +-- Summary +SELECT 'SUMMARY:' as status; +SELECT u.username, + ARRAY_AGG(r.name ORDER BY r.name) as roles +FROM users u +LEFT JOIN user_roles ur ON u.id = ur.user_id +LEFT JOIN roles r ON ur.role_id = r.id +GROUP BY u.id, u.username +ORDER BY u.id; +EOSQL +) + +# Execute SQL +echo "Executing SQL fixes..." +PGPASSWORD="$DB_PASS" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" << EOF +$SQL +EOF + +echo "" +echo "==========================================" +echo "Step 2: Restart Authentication Service" +echo "==========================================" +echo "" +echo "The User entity has been fixed with cascade settings." +echo "You need to rebuild and restart the service:" +echo "" +echo " cd Authentication/auth-service" +echo " mvn clean install -DskipTests" +echo " mvn spring-boot:run" +echo "" +echo "Or if running in Docker:" +echo " docker-compose restart auth-service" +echo "" +echo "==========================================" +echo "โœ… Fix Complete!" +echo "==========================================" +echo "" +echo "What was fixed:" +echo "1. โœ… Added cascade settings to User entity @ManyToMany relationship" +echo "2. โœ… Assigned roles to all existing users in database" +echo "" +echo "Test the fix:" +echo "1. Login again:" +echo ' curl -X POST http://localhost:8081/login \' +echo ' -H "Content-Type: application/json" \' +echo ' -d '"'"'{"username":"customer","password":"cust123"}'"'" +echo "" +echo "2. Decode JWT at https://jwt.io - should show roles" +echo "" +echo "3. Test /users/me with the new token" +echo "" diff --git a/LOGIN_ISSUE_FIX.md b/LOGIN_ISSUE_FIX.md new file mode 100644 index 0000000..b419e08 --- /dev/null +++ b/LOGIN_ISSUE_FIX.md @@ -0,0 +1,239 @@ +# ๐Ÿ”ง Login Issue Fix - Empty Roles in JWT Token + +## ๐ŸŽฏ Problem Summary + +**Symptom**: User can login successfully but gets `403 Forbidden` when accessing `/api/v1/users/me` + +**Root Cause**: The JWT token contains `"roles":[]` (empty array) because the user in the database has no roles assigned in the `user_roles` table. + +**Error in logs**: +``` +Access denied: Access Denied +AuthorizationDeniedException: Access Denied +``` + +## ๐Ÿ” Diagnosis + +Your JWT token shows: +```json +{ + "roles": [], // <-- EMPTY! This is the problem + "sub": "customer", + "iat": 1762805825, + "exp": 1762892225 +} +``` + +The endpoint requires: +```java +@PreAuthorize("hasRole('CUSTOMER') or hasRole('EMPLOYEE') or hasRole('ADMIN') or hasRole('SUPER_ADMIN')") +``` + +Since the roles array is empty, Spring Security denies access. + +## โœ… Solution Options + +### Option 1: Fix via SQL (Immediate Fix) + +Connect to your database and run: + +```bash +# Connect to MySQL/MariaDB +mysql -u root -p techtorque + +# Or connect to PostgreSQL +# psql -U postgres -d techtorque +``` + +Then execute: +```sql +-- Check current user roles +SELECT u.username, u.email, r.name as role_name +FROM users u +LEFT JOIN user_roles ur ON u.id = ur.user_id +LEFT JOIN roles r ON ur.role_id = r.id +WHERE u.username = 'customer'; + +-- Assign CUSTOMER role if missing +INSERT INTO user_roles (user_id, role_id) +SELECT u.id, r.id +FROM users u, roles r +WHERE u.username = 'customer' + AND r.name = 'CUSTOMER' + AND NOT EXISTS ( + SELECT 1 FROM user_roles ur + WHERE ur.user_id = u.id AND ur.role_id = r.id + ); + +-- Verify fix +SELECT u.username, r.name as role_name +FROM users u +INNER JOIN user_roles ur ON u.id = ur.user_id +INNER JOIN roles r ON ur.role_id = r.id +WHERE u.username = 'customer'; +``` + +**Quick script included**: Run `Authentication/fix_user_roles.sql` + +### Option 2: Delete and Recreate Users (Clean Slate) + +If you want to start fresh: + +```sql +-- Delete all users (roles and other tables will be preserved) +DELETE FROM user_roles; +DELETE FROM users; + +-- Restart your Authentication service to trigger DataSeeder +# The DataSeeder will recreate all default users with proper roles +``` + +Then restart the Authentication service: +```bash +cd Authentication/auth-service +mvn spring-boot:run +``` + +### Option 3: Use Admin Endpoint to Assign Role + +If you have access to an admin account with a valid token: + +```bash +ADMIN_TOKEN="your-admin-jwt-token" + +curl -X POST "http://localhost:8081/users/customer/roles" \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "role": "CUSTOMER", + "action": "ASSIGN" + }' +``` + +## ๐Ÿงช Verify the Fix + +After applying the fix: + +### 1. Login Again +```bash +curl -X POST http://localhost:8081/login \ + -H "Content-Type: application/json" \ + -d '{ + "username": "customer", + "password": "cust123" + }' | jq +``` + +### 2. Check JWT Token +Decode the token at https://jwt.io and verify it now shows: +```json +{ + "roles": ["CUSTOMER"], // <-- Should have role now! + "sub": "customer", + ... +} +``` + +### 3. Test /users/me Endpoint +```bash +TOKEN="your-new-jwt-token" + +curl -X GET http://localhost:8081/users/me \ + -H "Authorization: Bearer $TOKEN" | jq +``` + +**Expected**: Should return `200 OK` with user profile + +### 4. Test via API Gateway +```bash +curl -X GET http://localhost:8080/api/v1/users/me \ + -H "Authorization: Bearer $TOKEN" | jq +``` + +## ๐Ÿ”ง Prevention - Ensure Data Seeder Works + +Check your `application.properties` or `application.yml`: + +```properties +# Make sure dev profile is active for development +spring.profiles.active=dev + +# Or set environment variable +# SPRING_PROFILES_ACTIVE=dev +``` + +The `DataSeeder` only creates test users in `dev` profile. Make sure it's active: + +```bash +# Check if seeder ran on startup +# Look for these logs when starting the service: +# "Starting data seeding..." +# "Created role: CUSTOMER" +# "Created user: customer with role CUSTOMER" +# "Data seeding completed successfully!" +``` + +## ๐Ÿ“Š Database Schema Reference + +Correct structure for user-role assignment: + +``` +users table: ++----+----------+-------------------+ +| id | username | email | ++----+----------+-------------------+ +| 1 | customer | customer@... | ++----+----------+-------------------+ + +roles table: ++----+----------+ +| id | name | ++----+----------+ +| 1 | CUSTOMER | ++----+----------+ + +user_roles table (join table): ++---------+---------+ +| user_id | role_id | ++---------+---------+ +| 1 | 1 | <-- This row MUST exist! ++---------+---------+ +``` + +## ๐Ÿšจ Common Causes + +1. **DataSeeder didn't run** - Not in dev profile +2. **Database was reset** - Roles exist but user_roles table is empty +3. **Manual user creation** - User created without assigning roles +4. **Transaction rollback** - Role assignment failed during user creation + +## ๐Ÿ“ž Still Having Issues? + +Check Authentication service logs for: +``` +Hibernate: select r1_0.user_id ... from user_roles r1_0 ... where r1_0.user_id=? +``` + +If this query returns 0 rows, the user has no roles assigned. + +Enable debug logging in `application.properties`: +```properties +logging.level.com.techtorque.auth_service=DEBUG +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.springframework.security=DEBUG +``` + +## โœ… Summary Checklist + +- [ ] User exists in database +- [ ] Roles exist in database (CUSTOMER, EMPLOYEE, ADMIN, SUPER_ADMIN) +- [ ] User-role mapping exists in `user_roles` table +- [ ] JWT token contains roles array with at least one role +- [ ] Can access `/users/me` endpoint with 200 response +- [ ] DataSeeder ran successfully on service startup + +--- + +**Created**: 2025-11-11 +**Issue**: Empty roles in JWT causing 403 Forbidden on authenticated endpoints +**Resolution**: Ensure user has roles assigned in user_roles table diff --git a/auth-service/pom.xml b/auth-service/pom.xml index 1021797..1fb5c28 100644 --- a/auth-service/pom.xml +++ b/auth-service/pom.xml @@ -28,6 +28,9 @@ 17 + 1.63.0 + 3.25.3 + 3.1.0.RELEASE @@ -107,10 +110,30 @@ spring-boot-starter-actuator - - org.springframework.boot - spring-boot-starter-mail + net.devh + grpc-client-spring-boot-starter + ${grpc.spring.boot.version} + + + io.grpc + grpc-protobuf + ${grpc.version} + + + io.grpc + grpc-stub + ${grpc.version} + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + javax.annotation + javax.annotation-api + 1.3.2 @@ -123,7 +146,33 @@ + + + kr.motd.maven + os-maven-plugin + 1.7.1 + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.6.1 + + ${project.basedir}/src/main/proto + com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + + + + + compile + compile-custom + + + + org.springframework.boot spring-boot-maven-plugin 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 e726714..5f32b5e 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 @@ -12,6 +12,7 @@ import org.springframework.boot.CommandLineRunner; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; // <-- IMPORT ADDED import java.util.HashSet; import java.util.Set; @@ -22,67 +23,68 @@ */ @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; - - @Autowired - private Environment env; - - @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 depending on active profile - seedUsersByProfile(); - - logger.info("Data seeding completed successfully!"); - } - - /** - * Create all required roles in the system - */ - private void seedRoles() { - createRoleIfNotExists(RoleName.SUPER_ADMIN); - createRoleIfNotExists(RoleName.ADMIN); - createRoleIfNotExists(RoleName.EMPLOYEE); - createRoleIfNotExists(RoleName.CUSTOMER); + + private static final Logger logger = LoggerFactory.getLogger(DataSeeder.class); + + @Autowired + private RoleRepository roleRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Autowired + private Environment env; + + @Override + @Transactional // <-- FIX: Ensures all seeding runs in one transaction + 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 depending on active profile + seedUsersByProfile(); + + logger.info("Data seeding completed successfully!"); + } + + /** + * Create all required roles in the system + */ + private void seedRoles() { + createRoleIfNotExists(RoleName.SUPER_ADMIN); + 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 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() { + // Check if users already exist to avoid duplicates + if (userRepository.count() > 0) { + logger.info("Users already exist in database. Skipping user seeding."); + return; } - - /** - * Create default users with proper password encoding and role assignments - */ - private void seedUsers() { - // Check if users already exist to avoid duplicates - if (userRepository.count() > 0) { - logger.info("Users already exist in database. Skipping user seeding."); - return; - } - + // 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); @@ -92,65 +94,71 @@ private void seedUsers() { 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); + + // 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; + } } - /** - * 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); - } + 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 + // (Check count first to avoid duplicate superadmin if seeder runs again on prod) + if (userRepository.count() == 0) { + logger.info("Active profile is non-dev. Seeding SUPER_ADMIN only."); + 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); + } else { + logger.info("Active profile is non-dev. Users already exist, skipping superadmin seed."); + } } - - /** - * 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)) { - // 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: {} and role: {}", username, email, roleName); - } else { - logger.info("User {} already exists, skipping...", username); - } + } + + /** + * 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)) { + // 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: {} and role: {}", username, email, roleName); + } else { + logger.info("User {} already exists, skipping...", username); } + } } \ No newline at end of file diff --git a/auth-service/src/main/java/com/techtorque/auth_service/config/EmailConfig.java b/auth-service/src/main/java/com/techtorque/auth_service/config/EmailConfig.java index fe81fc9..045afad 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/config/EmailConfig.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/config/EmailConfig.java @@ -1,19 +1,12 @@ package com.techtorque.auth_service.config; -import com.techtorque.auth_service.service.EmailService; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** - * Configuration for Email Service - * Ensures EmailService bean is properly registered + * Placeholder for future email/gRPC specific configuration. */ @Configuration public class EmailConfig { - - @Bean - public EmailService emailService() { - return new EmailService(); - } + // No explicit beans required at the moment; this class keeps the configuration namespace ready. } diff --git a/auth-service/src/main/java/com/techtorque/auth_service/config/GatewayHeaderFilter.java b/auth-service/src/main/java/com/techtorque/auth_service/config/GatewayHeaderFilter.java index d14b2e7..62707c3 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/config/GatewayHeaderFilter.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/config/GatewayHeaderFilter.java @@ -28,26 +28,32 @@ public class GatewayHeaderFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + // Only process gateway headers if there's no JWT Authorization header + // This allows direct service calls with JWT to work properly + String authHeader = request.getHeader("Authorization"); + boolean hasJwtToken = authHeader != null && authHeader.startsWith("Bearer "); + String userId = request.getHeader("X-User-Subject"); String rolesHeader = request.getHeader("X-User-Roles"); - log.debug("Processing request - Path: {}, User-Subject: {}, User-Roles: {}", - request.getRequestURI(), userId, rolesHeader); + log.debug("Processing request - Path: {}, Has JWT: {}, User-Subject: {}, User-Roles: {}", + request.getRequestURI(), hasJwtToken, userId, rolesHeader); - if (userId != null && !userId.isEmpty()) { + // Only use gateway headers if there's no JWT token present + if (!hasJwtToken && userId != null && !userId.isEmpty()) { List authorities = rolesHeader == null ? Collections.emptyList() : Arrays.stream(rolesHeader.split(",")) .map(role -> new SimpleGrantedAuthority("ROLE_" + role.trim().toUpperCase())) .collect(Collectors.toList()); - log.debug("Authenticated user: {} with authorities: {}", userId, authorities); + log.debug("Authenticated user via gateway headers: {} with authorities: {}", userId, authorities); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userId, null, authorities); SecurityContextHolder.getContext().setAuthentication(authentication); - } else { - log.debug("No X-User-Subject header found in request to {}", request.getRequestURI()); + } else if (!hasJwtToken) { + log.debug("No X-User-Subject header found and no JWT token in request to {}", request.getRequestURI()); } filterChain.doFilter(request, response); 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 0ae0545..76ae2f8 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 @@ -81,7 +81,7 @@ public class User { private LocalDateTime createdAt = LocalDateTime.now(); // This is the other side of the relationship - @ManyToMany(fetch = FetchType.EAGER) + @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.MERGE}) @JoinTable( name = "user_roles", joinColumns = @JoinColumn(name = "user_id"), 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 index 8a57b2b..a61d906 100644 --- 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 @@ -74,6 +74,20 @@ public class AuthService { public LoginResponse authenticateUser(LoginRequest loginRequest, HttpServletRequest request) { String uname = loginRequest.getUsername(); + // Check if user exists and is not verified + java.util.Optional userOpt = userRepository.findByUsername(uname); + if (userOpt.isEmpty()) { + userOpt = userRepository.findByEmail(uname); + } + + if (userOpt.isPresent()) { + User user = userOpt.get(); + if (!user.getEnabled() && !user.getEmailVerified()) { + throw new org.springframework.security.authentication.DisabledException( + "Please verify your email address before logging in. Check your inbox for the verification link."); + } + } + // load or create lock record com.techtorque.auth_service.entity.LoginLock lock = loginLockRepository.findByUsername(uname) .orElseGet(() -> com.techtorque.auth_service.entity.LoginLock.builder().username(uname).failedAttempts(0).build()); @@ -174,7 +188,7 @@ public String registerUser(RegisterRequest registerRequest) { .fullName(registerRequest.getFullName()) .phone(registerRequest.getPhone()) .address(registerRequest.getAddress()) - .enabled(true) // Allow login without email verification + .enabled(false) // Require email verification before login .emailVerified(false) // Track verification status separately .emailVerificationDeadline(LocalDateTime.now().plus(7, ChronoUnit.DAYS)) // 1 week deadline .roles(new HashSet<>()) diff --git a/auth-service/src/main/java/com/techtorque/auth_service/service/EmailService.java b/auth-service/src/main/java/com/techtorque/auth_service/service/EmailService.java index 4655161..bda544e 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/service/EmailService.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/service/EmailService.java @@ -1,144 +1,104 @@ package com.techtorque.auth_service.service; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.mail.SimpleMailMessage; -import org.springframework.mail.javamail.JavaMailSender; +import com.techtorque.notification.grpc.DeliveryStatus; +import com.techtorque.notification.grpc.EmailType; +import com.techtorque.notification.grpc.NotificationEmailServiceGrpc; +import com.techtorque.notification.grpc.SendEmailRequest; +import com.techtorque.notification.grpc.SendEmailResponse; +import io.grpc.StatusRuntimeException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.inject.GrpcClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; /** - * Service for sending emails + * Bridges auth-service events to the notification-service via gRPC. */ +@Service @Slf4j public class EmailService { - - @Autowired(required = false) - private JavaMailSender mailSender; - - @Value("${spring.mail.username:noreply@techtorque.com}") - private String fromEmail; - + + @GrpcClient("notification-email") + private NotificationEmailServiceGrpc.NotificationEmailServiceBlockingStub emailStub; + + @Value("${app.email.enabled:true}") + private boolean emailEnabled; + @Value("${app.frontend.url:http://localhost:3000}") private String frontendUrl; - - @Value("${app.email.enabled:false}") - private boolean emailEnabled; - + + @Value("${notification.grpc.deadline-ms:5000}") + private long deadlineMs; + + @Value("${notification.grpc.enabled:true}") + private boolean grpcEnabled; + /** - * Send email verification link + * Initiates email verification workflow through notification-service. */ public void sendVerificationEmail(String toEmail, String username, String token) { - if (!emailEnabled) { - log.info("Email disabled. Verification token for {}: {}", username, token); - return; - } - - try { - String verificationUrl = frontendUrl + "/auth/verify-email?token=" + token; - - SimpleMailMessage message = new SimpleMailMessage(); - message.setFrom(fromEmail); - message.setTo(toEmail); - message.setSubject("TechTorque - Verify Your Email Address"); - message.setText(String.format( - "Hello %s,\n\n" + - "Thank you for registering with TechTorque!\n\n" + - "Please click the link below to verify your email address:\n" + - "%s\n\n" + - "This link will expire in 24 hours.\n\n" + - "If you did not create an account, please ignore this email.\n\n" + - "Best regards,\n" + - "TechTorque Team", - username, verificationUrl - )); - - if (mailSender != null) { - mailSender.send(message); - log.info("Verification email sent to: {}", toEmail); - } else { - log.warn("Mail sender not configured. Email not sent to: {}", toEmail); - } - } catch (Exception e) { - log.error("Failed to send verification email to {}: {}", toEmail, e.getMessage()); - } + Map variables = new HashMap<>(); + variables.put("token", token); + variables.put("verificationUrl", frontendUrl + "/auth/verify-email?token=" + token); + sendEmail(toEmail, username, EmailType.EMAIL_TYPE_VERIFICATION, variables); } - + /** - * Send password reset email + * Requests a password reset email from notification-service. */ public void sendPasswordResetEmail(String toEmail, String username, String token) { - if (!emailEnabled) { - log.info("Email disabled. Password reset token for {}: {}", username, token); - return; - } - - try { - String resetUrl = frontendUrl + "/auth/reset-password?token=" + token; - - SimpleMailMessage message = new SimpleMailMessage(); - message.setFrom(fromEmail); - message.setTo(toEmail); - message.setSubject("TechTorque - Password Reset Request"); - message.setText(String.format( - "Hello %s,\n\n" + - "We received a request to reset your password.\n\n" + - "Please click the link below to reset your password:\n" + - "%s\n\n" + - "This link will expire in 1 hour.\n\n" + - "If you did not request a password reset, please ignore this email " + - "and your password will remain unchanged.\n\n" + - "Best regards,\n" + - "TechTorque Team", - username, resetUrl - )); - - if (mailSender != null) { - mailSender.send(message); - log.info("Password reset email sent to: {}", toEmail); - } else { - log.warn("Mail sender not configured. Email not sent to: {}", toEmail); - } - } catch (Exception e) { - log.error("Failed to send password reset email to {}: {}", toEmail, e.getMessage()); - } + Map variables = new HashMap<>(); + variables.put("token", token); + variables.put("resetUrl", frontendUrl + "/auth/reset-password?token=" + token); + sendEmail(toEmail, username, EmailType.EMAIL_TYPE_PASSWORD_RESET, variables); } - + /** - * Send welcome email after verification + * Dispatches welcome email once the account is verified. */ public void sendWelcomeEmail(String toEmail, String username) { - if (!emailEnabled) { - log.info("Email disabled. Welcome email skipped for: {}", username); + Map variables = new HashMap<>(); + variables.put("dashboardUrl", frontendUrl + "/dashboard"); + sendEmail(toEmail, username, EmailType.EMAIL_TYPE_WELCOME, variables); + } + + private void sendEmail(String toEmail, String username, EmailType type, Map variables) { + if (!emailEnabled || !grpcEnabled) { + log.info("Notification email dispatch disabled. Skipping {} email for {}", type, username); return; } - + + NotificationEmailServiceGrpc.NotificationEmailServiceBlockingStub stubToUse = emailStub; + if (deadlineMs > 0) { + stubToUse = stubToUse.withDeadlineAfter(deadlineMs, TimeUnit.MILLISECONDS); + } + + SendEmailRequest.Builder builder = SendEmailRequest.newBuilder() + .setTo(toEmail) + .setUsername(username == null ? "" : username) + .setType(type) + .setCorrelationId(UUID.randomUUID().toString()); + + if (!CollectionUtils.isEmpty(variables)) { + builder.putAllVariables(variables); + } + try { - SimpleMailMessage message = new SimpleMailMessage(); - message.setFrom(fromEmail); - message.setTo(toEmail); - message.setSubject("Welcome to TechTorque!"); - message.setText(String.format( - "Hello %s,\n\n" + - "Welcome to TechTorque! Your email has been successfully verified.\n\n" + - "You can now:\n" + - "- Register your vehicles\n" + - "- Book service appointments\n" + - "- Track service progress\n" + - "- Request custom modifications\n\n" + - "Visit %s to get started.\n\n" + - "Best regards,\n" + - "TechTorque Team", - username, frontendUrl - )); - - if (mailSender != null) { - mailSender.send(message); - log.info("Welcome email sent to: {}", toEmail); + SendEmailResponse response = stubToUse.sendTransactionalEmail(builder.build()); + if (response.getStatus() == DeliveryStatus.DELIVERY_STATUS_ACCEPTED) { + log.info("Notification-service accepted {} email for {} (id={})", type, toEmail, response.getMessageId()); } else { - log.warn("Mail sender not configured. Email not sent to: {}", toEmail); + log.warn("Notification-service rejected {} email for {}: {}", type, toEmail, response.getDetail()); } - } catch (Exception e) { - log.error("Failed to send welcome email to {}: {}", toEmail, e.getMessage()); + } catch (StatusRuntimeException ex) { + log.error("gRPC call failed while sending {} email to {}: {}", type, toEmail, ex.getStatus(), ex); + } catch (Exception ex) { + log.error("Unexpected error while sending {} email to {}: {}", type, toEmail, ex.getMessage(), ex); } } } diff --git a/auth-service/src/main/proto/notification/email.proto b/auth-service/src/main/proto/notification/email.proto new file mode 100644 index 0000000..7f318cc --- /dev/null +++ b/auth-service/src/main/proto/notification/email.proto @@ -0,0 +1,38 @@ +syntax = "proto3"; + +package notification.v1; + +option java_multiple_files = true; +option java_package = "com.techtorque.notification.grpc"; +option java_outer_classname = "NotificationEmailProto"; + +enum EmailType { + EMAIL_TYPE_UNSPECIFIED = 0; + EMAIL_TYPE_VERIFICATION = 1; + EMAIL_TYPE_PASSWORD_RESET = 2; + EMAIL_TYPE_WELCOME = 3; +} + +enum DeliveryStatus { + DELIVERY_STATUS_UNSPECIFIED = 0; + DELIVERY_STATUS_ACCEPTED = 1; + DELIVERY_STATUS_REJECTED = 2; +} + +message SendEmailRequest { + string to = 1; + string username = 2; + EmailType type = 3; + map variables = 4; + string correlation_id = 5; +} + +message SendEmailResponse { + DeliveryStatus status = 1; + string message_id = 2; + string detail = 3; +} + +service NotificationEmailService { + rpc SendTransactionalEmail(SendEmailRequest) returns (SendEmailResponse); +} diff --git a/auth-service/src/main/resources/application.properties b/auth-service/src/main/resources/application.properties index 1978ed6..618040c 100644 --- a/auth-service/src/main/resources/application.properties +++ b/auth-service/src/main/resources/application.properties @@ -58,6 +58,13 @@ app.token.verification.expiry-hours=${VERIFICATION_TOKEN_EXPIRY:24} app.token.password-reset.expiry-hours=${PASSWORD_RESET_TOKEN_EXPIRY:1} app.token.refresh.expiry-days=${REFRESH_TOKEN_EXPIRY:7} +# Notification gRPC configuration +grpc.client.notification-email.address=${NOTIFICATION_GRPC_TARGET:static://localhost:9090} +grpc.client.notification-email.negotiationType=${NOTIFICATION_GRPC_NEGOTIATION:PLAINTEXT} +grpc.client.notification-email.enableKeepAlive=true +notification.grpc.deadline-ms=${NOTIFICATION_GRPC_TIMEOUT_MS:5000} +notification.grpc.enabled=${NOTIFICATION_GRPC_ENABLED:${EMAIL_ENABLED:true}} + # Session Configuration - Disable sessions for stateless API server.servlet.session.tracking-modes=COOKIE spring.session.store-type=none diff --git a/check_user_roles.sql b/check_user_roles.sql new file mode 100644 index 0000000..38bea5f --- /dev/null +++ b/check_user_roles.sql @@ -0,0 +1,13 @@ +-- Check if user 'customer' exists and has roles +SELECT u.id, u.username, u.email, r.name as role_name +FROM users u +LEFT JOIN user_roles ur ON u.id = ur.user_id +LEFT JOIN roles r ON ur.role_id = r.id +WHERE u.username = 'customer'; + +-- Check all users and their roles +SELECT u.username, u.email, GROUP_CONCAT(r.name) as roles +FROM users u +LEFT JOIN user_roles ur ON u.id = ur.user_id +LEFT JOIN roles r ON ur.role_id = r.id +GROUP BY u.id, u.username, u.email; diff --git a/fix_user_roles.sql b/fix_user_roles.sql new file mode 100644 index 0000000..460652e --- /dev/null +++ b/fix_user_roles.sql @@ -0,0 +1,29 @@ +-- Fix: Assign CUSTOMER role to user 'customer' if missing +-- Run this SQL script in your database + +-- First, verify the issue +SELECT 'Checking user roles...' as status; +SELECT u.username, u.email, r.name as role_name +FROM users u +LEFT JOIN user_roles ur ON u.id = ur.user_id +LEFT JOIN roles r ON ur.role_id = r.id +WHERE u.username = 'customer'; + +-- Insert CUSTOMER role for user 'customer' if not exists +INSERT INTO user_roles (user_id, role_id) +SELECT u.id, r.id +FROM users u, roles r +WHERE u.username = 'customer' + AND r.name = 'CUSTOMER' + AND NOT EXISTS ( + SELECT 1 FROM user_roles ur + WHERE ur.user_id = u.id AND ur.role_id = r.id + ); + +-- Verify the fix +SELECT 'After fix:' as status; +SELECT u.username, u.email, r.name as role_name +FROM users u +LEFT JOIN user_roles ur ON u.id = ur.user_id +LEFT JOIN roles r ON ur.role_id = r.id +WHERE u.username = 'customer'; From dfbcf7fa4672d06113d41b951ee89e5952b6a3dc Mon Sep 17 00:00:00 2001 From: RandithaK Date: Tue, 11 Nov 2025 14:21:37 +0530 Subject: [PATCH 2/3] Enhance login error handling for disabled accounts with specific messages --- .../auth_service/service/AuthService.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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 index a61d906..4a83235 100644 --- 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 @@ -82,9 +82,16 @@ public LoginResponse authenticateUser(LoginRequest loginRequest, HttpServletRequ if (userOpt.isPresent()) { User user = userOpt.get(); - if (!user.getEnabled() && !user.getEmailVerified()) { - throw new org.springframework.security.authentication.DisabledException( - "Please verify your email address before logging in. Check your inbox for the verification link."); + + // Check if account is disabled (deactivated by admin) + if (!user.getEnabled()) { + if (!user.getEmailVerified()) { + throw new org.springframework.security.authentication.DisabledException( + "Please verify your email address before logging in. Check your inbox for the verification link."); + } else { + throw new org.springframework.security.authentication.DisabledException( + "Your account has been deactivated. Please contact the administrator for assistance."); + } } } From 5b84c00b17df6ade6cc328f2fdab140199e38c25 Mon Sep 17 00:00:00 2001 From: Akith-002 Date: Tue, 11 Nov 2025 18:45:48 +0530 Subject: [PATCH 3/3] feat: Add role-based access control for user role management and update API base URL configuration --- .../com/techtorque/auth_service/controller/UserController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth-service/src/main/java/com/techtorque/auth_service/controller/UserController.java b/auth-service/src/main/java/com/techtorque/auth_service/controller/UserController.java index b1dbc8e..9a1e837 100644 --- a/auth-service/src/main/java/com/techtorque/auth_service/controller/UserController.java +++ b/auth-service/src/main/java/com/techtorque/auth_service/controller/UserController.java @@ -151,6 +151,7 @@ public ResponseEntity resetUserPassword(@PathVariable String username, * POST /api/v1/users/{username}/roles */ @PostMapping("/{username}/roles") + @PreAuthorize("hasRole('ADMIN') or hasRole('SUPER_ADMIN')") public ResponseEntity manageUserRole(@PathVariable String username, @Valid @RequestBody RoleAssignmentRequest roleRequest) { try { @@ -396,4 +397,3 @@ private UserDto convertToDto(com.techtorque.auth_service.entity.User user) { .build(); } } -