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/CorsFilter.java b/auth-service/src/main/java/com/techtorque/auth_service/config/CorsFilter.java
index 3a4328e..9f455f2 100644
--- a/auth-service/src/main/java/com/techtorque/auth_service/config/CorsFilter.java
+++ b/auth-service/src/main/java/com/techtorque/auth_service/config/CorsFilter.java
@@ -1,33 +1,31 @@
package com.techtorque.auth_service.config;
import jakarta.servlet.*;
-import jakarta.servlet.http.HttpServletResponse;
-import jakarta.servlet.http.HttpServletRequest;
-import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
-import org.springframework.stereotype.Component;
import java.io.IOException;
/**
- * Custom CORS filter that ensures CORS headers are added to ALL responses,
- * including redirects and error responses.
- *
- * This filter runs at the servlet level (before Spring Security) with high priority
- * to ensure CORS headers are included on every response regardless of what happens downstream.
- *
- * NOTE: This filter is DISABLED because CORS is handled centrally by the API Gateway.
- * The API Gateway applies CORS headers to all responses, so backend services should not
- * add CORS headers to avoid duplication.
+ * โ ๏ธ DO NOT ENABLE THIS FILTER โ ๏ธ
+ *
+ * CORS is handled centrally by the API Gateway (API_Gateway/cmd/gateway/main.go).
+ * All client requests go through the API Gateway, which applies CORS headers.
+ *
+ * Enabling CORS at the microservice level will:
+ * - Create duplicate CORS headers
+ * - Cause CORS preflight issues
+ * - Break browser security
+ *
+ * This class is kept for reference only and should remain DISABLED.
+ *
+ * @deprecated CORS must be handled by API Gateway only
*/
-@Component
+// @Component - DO NOT ENABLE: CORS is handled by API Gateway
+@Deprecated
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {
- @Value("${app.cors.allowed-origins:http://localhost:3000,http://127.0.0.1:3000}")
- private String allowedOrigins;
-
@Override
public void init(FilterConfig filterConfig) {
// Initialize filter
@@ -36,9 +34,8 @@ public void init(FilterConfig filterConfig) {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
-
- // CORS is handled by the API Gateway, so we skip CORS header injection here
- // Just pass the request through without adding CORS headers
+ // This filter is DISABLED and should never be called
+ // CORS is handled by the API Gateway
chain.doFilter(request, response);
}
@@ -46,17 +43,4 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
public void destroy() {
// Cleanup
}
-
- /**
- * Check if the given origin is in the allowed list
- */
- private boolean isOriginAllowed(String origin) {
- String[] origins = allowedOrigins.split(",");
- for (String allowed : origins) {
- if (allowed.trim().equals(origin)) {
- return true;
- }
- }
- return false;
- }
}
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/controller/UserController.java b/auth-service/src/main/java/com/techtorque/auth_service/controller/UserController.java
index 83beb54..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
@@ -44,9 +44,10 @@ public class UserController {
* Get a list of all users in the system.
*/
@GetMapping
- public ResponseEntity> getAllUsers() {
+ public ResponseEntity> getAllUsers(@RequestParam(required = false) String role) {
List users = userService.findAllUsers().stream()
.map(this::convertToDto)
+ .filter(user -> role == null || user.getRoles().contains(role))
.collect(Collectors.toList());
return ResponseEntity.ok(users);
}
@@ -150,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 {
@@ -395,4 +397,3 @@ private UserDto convertToDto(com.techtorque.auth_service.entity.User user) {
.build();
}
}
-
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..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
@@ -74,6 +74,27 @@ 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();
+
+ // 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.");
+ }
+ }
+ }
+
// 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 +195,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 0072a8d..618040c 100644
--- a/auth-service/src/main/resources/application.properties
+++ b/auth-service/src/main/resources/application.properties
@@ -58,9 +58,17 @@ 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
-# CORS Configuration
-app.cors.allowed-origins=http://localhost:3000,http://127.0.0.1:3000
+# CORS Configuration - HANDLED BY API GATEWAY
+# CORS is managed centrally by the API Gateway. Do not add CORS configuration here.
+# See: API_Gateway/cmd/gateway/main.go for CORS settings
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';