From 247b81c0de14abf43539729b8776af10b7dca627 Mon Sep 17 00:00:00 2001 From: Paradoxrc Date: Sun, 9 Nov 2025 17:27:42 +0530 Subject: [PATCH 1/3] feat: Implement WebSocket support for real-time notifications and update notification service configuration --- EMAIL_CONFIGURATION.md | 177 ------------------ EMAIL_FIX_SUMMARY.md | 166 ---------------- README.md | 1 - notification-service/.env.example | 16 ++ notification-service/pom.xml | 4 + .../config/SecurityConfig.java | 4 +- .../config/WebSocketConfig.java | 53 ++++++ .../controller/NotificationApiController.java | 72 +++++++ .../controller/NotificationController.java | 2 + .../WebSocketNotificationController.java | 90 +++++++++ .../request/CreateNotificationRequest.java | 29 +++ .../service/impl/NotificationServiceImpl.java | 42 ++++- test_email_config.sh | 48 ----- 13 files changed, 301 insertions(+), 403 deletions(-) delete mode 100644 EMAIL_CONFIGURATION.md delete mode 100644 EMAIL_FIX_SUMMARY.md delete mode 100644 README.md create mode 100644 notification-service/.env.example create mode 100644 notification-service/src/main/java/com/techtorque/notification_service/config/WebSocketConfig.java create mode 100644 notification-service/src/main/java/com/techtorque/notification_service/controller/NotificationApiController.java create mode 100644 notification-service/src/main/java/com/techtorque/notification_service/controller/WebSocketNotificationController.java create mode 100644 notification-service/src/main/java/com/techtorque/notification_service/dto/request/CreateNotificationRequest.java delete mode 100755 test_email_config.sh diff --git a/EMAIL_CONFIGURATION.md b/EMAIL_CONFIGURATION.md deleted file mode 100644 index 1e9ca25..0000000 --- a/EMAIL_CONFIGURATION.md +++ /dev/null @@ -1,177 +0,0 @@ -# Email Configuration Guide for Notification Service - -## Development Mode (Default) - -By default, the service runs with the **dev profile** which **disables email health checks**. This prevents authentication failures during local development. - -### Running in Development Mode - -```bash -# Using Maven -mvn spring-boot:run - -# Using IDE - the dev profile is active by default -# Just run NotificationServiceApplication.java -``` - -**Note:** Email sending will be attempted but won't fail the health check if credentials are invalid. - ---- - -## Testing Email Functionality - -If you want to test actual email sending during development, you need valid Gmail credentials with an App Password. - -### Step 1: Generate Gmail App Password - -1. Go to your Google Account: https://myaccount.google.com/ -2. Navigate to **Security** -3. Enable **2-Step Verification** (if not already enabled) -4. Go to **App Passwords**: https://myaccount.google.com/apppasswords -5. Generate a new app password for "Mail" -6. Copy the 16-character password - -### Step 2: Set Environment Variables - -```bash -# Linux/Mac -export EMAIL_USERNAME="your-email@gmail.com" -export EMAIL_PASSWORD="your-16-char-app-password" - -# Windows (PowerShell) -$env:EMAIL_USERNAME="your-email@gmail.com" -$env:EMAIL_PASSWORD="your-16-char-app-password" -``` - -### Step 3: Enable Mail Health Check (Optional) - -In `application-dev.properties`, change: -```properties -management.health.mail.enabled=true -``` - -### Step 4: Run the Service - -```bash -mvn spring-boot:run -``` - ---- - -## Production Mode - -For production deployments, use the **prod profile** with proper credentials: - -```bash -# Set all required environment variables -export SPRING_PROFILE=prod -export DB_URL=jdbc:postgresql://prod-host:5432/notification_db -export DB_USERNAME=prod_user -export DB_PASSWORD=prod_password -export EMAIL_USERNAME=noreply@techtorque.com -export EMAIL_PASSWORD=production-app-password - -# Run the application -java -jar notification-service.jar --spring.profiles.active=prod -``` - -**Production profile automatically:** -- Enables mail health checks -- Uses environment variables for sensitive data -- Reduces logging verbosity -- Sets JPA to validate-only mode - ---- - -## Troubleshooting - -### Issue: "Username and Password not accepted" - -**Cause:** Invalid Gmail credentials or regular password used instead of App Password. - -**Solutions:** -1. Generate an App Password (see above) -2. Disable mail health check: `management.health.mail.enabled=false` -3. Use dev profile (mail health check disabled by default) - -### Issue: "535-5.7.8 BadCredentials" - -**Cause:** Gmail blocking login attempt. - -**Solutions:** -1. Ensure 2-Step Verification is enabled -2. Use an App Password, not your regular password -3. Check if "Less secure app access" is required (deprecated by Google) - -### Issue: Email sending works but health check fails - -**Cause:** Transient connection issues or rate limiting. - -**Solution:** Disable health check in development: -```properties -management.health.mail.enabled=false -``` - ---- - -## Configuration Summary - -| Profile | Mail Health Check | Use Case | -|---------|------------------|----------| -| **dev** (default) | Disabled | Local development without email | -| **prod** | Enabled | Production with valid credentials | - ---- - -## Quick Commands - -```bash -# Development (no email credentials needed) -mvn spring-boot:run - -# Development with email testing -export EMAIL_USERNAME="your@gmail.com" -export EMAIL_PASSWORD="app-password" -mvn spring-boot:run - -# Production -export SPRING_PROFILE=prod -export EMAIL_USERNAME="prod@techtorque.com" -export EMAIL_PASSWORD="prod-password" -java -jar notification-service.jar -``` - ---- - -## Security Notes - -⚠️ **Never commit email credentials to version control!** - -- Always use environment variables -- Add `.env` files to `.gitignore` -- Use secret management in production (AWS Secrets Manager, Azure Key Vault, etc.) -- Rotate credentials regularly - ---- - -## Alternative: Using MailHog for Development - -For local email testing without real credentials: - -```bash -# Start MailHog -docker run -d -p 1025:1025 -p 8025:8025 mailhog/mailhog - -# Update application-dev.properties -spring.mail.host=localhost -spring.mail.port=1025 -spring.mail.username= -spring.mail.password= -spring.mail.properties.mail.smtp.auth=false -spring.mail.properties.mail.smtp.starttls.enable=false -management.health.mail.enabled=false - -# Access web UI at http://localhost:8025 -``` - -This captures all emails locally without sending them to real addresses. diff --git a/EMAIL_FIX_SUMMARY.md b/EMAIL_FIX_SUMMARY.md deleted file mode 100644 index b0c6a52..0000000 --- a/EMAIL_FIX_SUMMARY.md +++ /dev/null @@ -1,166 +0,0 @@ -# Email Configuration Quick Reference - -## ✅ SOLUTION IMPLEMENTED - -The notification service has been configured to **disable email health checks during development**, eliminating the authentication warning you encountered. - ---- - -## What Was Done - -### 1. **Disabled Mail Health Check** (Primary Solution) -Added to `application.properties`: -```properties -management.health.mail.enabled=false -``` - -This prevents Spring Boot Actuator from attempting to connect to Gmail SMTP during health checks. - -### 2. **Created Development Profile** -Created `application-dev.properties` with mail health check disabled by default. - -### 3. **Created Production Profile** -Created `application-prod.properties` that enables health checks and uses environment variables. - ---- - -## Running the Service - -### Development Mode (No Email Credentials Needed) -```bash -cd Notification_Service/notification-service -mvn spring-boot:run -``` - -✅ **No more email warnings!** The service will start cleanly. - -### With Real Email Testing -```bash -export EMAIL_USERNAME="your-email@gmail.com" -export EMAIL_PASSWORD="your-app-password" # Gmail App Password -mvn spring-boot:run -``` - -### Production Mode -```bash -export SPRING_PROFILE=prod -export EMAIL_USERNAME="prod@techtorque.com" -export EMAIL_PASSWORD="prod-password" -java -jar notification-service.jar -``` - ---- - -## Understanding the Warning - -The warning you saw: -``` -jakarta.mail.AuthenticationFailedException: 535-5.7.8 Username and Password not accepted -``` - -**Cause:** Spring Boot Actuator's health endpoint was trying to verify SMTP connection with placeholder credentials. - -**Impact:** -- ⚠️ Warning in logs -- ✅ Service still starts successfully -- ✅ All endpoints work normally -- ❌ Only email sending would fail (if attempted) - ---- - -## Configuration Files Created - -| File | Purpose | -|------|---------| -| `application.properties` | Base config with mail health disabled | -| `application-dev.properties` | Development profile (no real email needed) | -| `application-prod.properties` | Production profile (real credentials required) | -| `EMAIL_CONFIGURATION.md` | Detailed setup guide | -| `test_email_config.sh` | Verification script | - ---- - -## Testing Your Setup - -Run the verification script: -```bash -cd Notification_Service -./test_email_config.sh -``` - -Expected output: -``` -✓ Mail health check is DISABLED in application.properties -✓ Development profile exists -✓ Production profile exists -``` - ---- - -## Common Scenarios - -### Scenario 1: Local Development (Current) -**Status:** ✅ Configured -**Action:** None needed - just run `mvn spring-boot:run` -**Email:** Won't send (no valid credentials) -**Health Check:** Disabled (no warnings) - -### Scenario 2: Testing Email Functionality -**Status:** Need valid Gmail App Password -**Action:** Set EMAIL_USERNAME and EMAIL_PASSWORD env vars -**Email:** Will send to real addresses -**Health Check:** Keep disabled to avoid startup delays - -### Scenario 3: Production Deployment -**Status:** Use prod profile -**Action:** Set all env vars, use `--spring.profiles.active=prod` -**Email:** Fully functional with monitoring -**Health Check:** Enabled for monitoring - ---- - -## Quick Troubleshooting - -| Issue | Solution | -|-------|----------| -| Email warning on startup | ✅ Already fixed - health check disabled | -| Need to test email | Set EMAIL_USERNAME/PASSWORD env vars | -| Want to mock emails | Use MailHog (see EMAIL_CONFIGURATION.md) | -| Production setup | Use prod profile + env vars | - ---- - -## Next Steps - -Your notification service is now properly configured for development! - -**To resume work:** -```bash -cd Notification_Service/notification-service -mvn spring-boot:run -``` - -**To test endpoints:** -```bash -# Health check (should be UP without mail health) -curl http://localhost:8088/actuator/health - -# API docs -open http://localhost:8088/swagger-ui/index.html -``` - ---- - -## Files Modified - -- ✏️ `application.properties` - Added `management.health.mail.enabled=false` -- ➕ `application-dev.properties` - New development profile -- ➕ `application-prod.properties` - New production profile -- ➕ `EMAIL_CONFIGURATION.md` - Detailed guide -- ➕ `test_email_config.sh` - Verification script - ---- - -**Status:** ✅ **Email configuration issue RESOLVED** - -The service will now start without email authentication warnings during development. diff --git a/README.md b/README.md deleted file mode 100644 index 957a8c0..0000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -# Notification_Service \ No newline at end of file diff --git a/notification-service/.env.example b/notification-service/.env.example new file mode 100644 index 0000000..b929908 --- /dev/null +++ b/notification-service/.env.example @@ -0,0 +1,16 @@ +# Database Configuration +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=techtorque_notification +DB_USER=techtorque +DB_PASS=techtorque123 + +# Spring Profile +SPRING_PROFILE=dev + +# Email Configuration (Gmail example) +EMAIL_USERNAME=your-email@gmail.com +EMAIL_PASSWORD=your-app-password + +# Notification Configuration +NOTIFICATION_RETENTION_DAYS=30 diff --git a/notification-service/pom.xml b/notification-service/pom.xml index 99d0067..f955654 100644 --- a/notification-service/pom.xml +++ b/notification-service/pom.xml @@ -54,6 +54,10 @@ org.springframework.boot spring-boot-starter-actuator + + org.springframework.boot + spring-boot-starter-websocket + diff --git a/notification-service/src/main/java/com/techtorque/notification_service/config/SecurityConfig.java b/notification-service/src/main/java/com/techtorque/notification_service/config/SecurityConfig.java index ff37730..601b2db 100644 --- a/notification-service/src/main/java/com/techtorque/notification_service/config/SecurityConfig.java +++ b/notification-service/src/main/java/com/techtorque/notification_service/config/SecurityConfig.java @@ -25,7 +25,9 @@ public class SecurityConfig { "/actuator/**", "/health", "/favicon.ico", - "/error" + "/error", + "/ws/**", // Allow WebSocket endpoints + "/api/v1/notifications/**" // Allow notification REST API endpoints }; @Bean diff --git a/notification-service/src/main/java/com/techtorque/notification_service/config/WebSocketConfig.java b/notification-service/src/main/java/com/techtorque/notification_service/config/WebSocketConfig.java new file mode 100644 index 0000000..d239f9f --- /dev/null +++ b/notification-service/src/main/java/com/techtorque/notification_service/config/WebSocketConfig.java @@ -0,0 +1,53 @@ +package com.techtorque.notification_service.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +/** + * WebSocket configuration for real-time notifications + * Uses STOMP (Simple Text Oriented Messaging Protocol) over WebSocket + * + * Industry standard configuration following Spring Boot best practices + */ +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + /** + * Configure message broker for handling messages + * - /topic: for broadcasting to multiple subscribers (pub-sub pattern) + * - /queue: for point-to-point messaging (user-specific) + * - /app: prefix for messages from client to server + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + // Enable simple in-memory message broker for pub-sub + config.enableSimpleBroker("/topic", "/queue"); + + // Prefix for messages from client to server + config.setApplicationDestinationPrefixes("/app"); + + // Prefix for user-specific messages + config.setUserDestinationPrefix("/user"); + } + + /** + * Register STOMP endpoints that clients will connect to + * - /ws/notifications: Main WebSocket endpoint + * - SockJS fallback for browsers that don't support WebSocket + * - CORS allowed for development (configure properly for production) + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws/notifications") + .setAllowedOriginPatterns("*") // Allow all origins for development + .withSockJS(); // Enable SockJS fallback for older browsers + + // Plain WebSocket endpoint without SockJS (for modern clients) + registry.addEndpoint("/ws/notifications") + .setAllowedOriginPatterns("*"); // Allow all origins for development + } +} diff --git a/notification-service/src/main/java/com/techtorque/notification_service/controller/NotificationApiController.java b/notification-service/src/main/java/com/techtorque/notification_service/controller/NotificationApiController.java new file mode 100644 index 0000000..f63643a --- /dev/null +++ b/notification-service/src/main/java/com/techtorque/notification_service/controller/NotificationApiController.java @@ -0,0 +1,72 @@ +package com.techtorque.notification_service.controller; + +import com.techtorque.notification_service.dto.request.CreateNotificationRequest; +import com.techtorque.notification_service.dto.response.ApiResponse; +import com.techtorque.notification_service.dto.response.NotificationResponse; +import com.techtorque.notification_service.entity.Notification; +import com.techtorque.notification_service.service.NotificationService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.*; + +/** + * API Controller for other services to create notifications + * This is separate from the user-facing NotificationController + */ +@RestController +@RequestMapping("/api/v1/notifications") +@RequiredArgsConstructor +@Slf4j +public class NotificationApiController { + + private final NotificationService notificationService; + private final SimpMessagingTemplate messagingTemplate; + + /** + * Create a notification (called by other microservices) + * This endpoint is whitelisted in SecurityConfig for service-to-service communication + */ + @PostMapping("/create") + public ResponseEntity createNotification(@Valid @RequestBody CreateNotificationRequest request) { + log.info("Creating notification for user: {} with type: {}", request.getUserId(), request.getType()); + + try { + // Parse notification type + Notification.NotificationType notificationType = Notification.NotificationType.valueOf(request.getType()); + + // Create notification using service + NotificationResponse response = notificationService.createNotification( + request.getUserId(), + notificationType, + request.getMessage(), + request.getDetails() + ); + + // Send real-time notification via WebSocket + String destination = "/user/" + request.getUserId() + "/queue/notifications"; + log.info("Sending WebSocket notification to destination: {}", destination); + messagingTemplate.convertAndSend(destination, response); + + log.info("Notification created successfully for user: {}", request.getUserId()); + return ResponseEntity.ok(ApiResponse.success("Notification created", response)); + + } catch (IllegalArgumentException e) { + log.error("Invalid notification type: {}", request.getType(), e); + return ResponseEntity.badRequest().body( + ApiResponse.builder() + .message("Invalid notification type: " + request.getType()) + .build() + ); + } catch (Exception e) { + log.error("Failed to create notification for user: {}", request.getUserId(), e); + return ResponseEntity.internalServerError().body( + ApiResponse.builder() + .message("Failed to create notification") + .build() + ); + } + } +} diff --git a/notification-service/src/main/java/com/techtorque/notification_service/controller/NotificationController.java b/notification-service/src/main/java/com/techtorque/notification_service/controller/NotificationController.java index e789726..144310c 100644 --- a/notification-service/src/main/java/com/techtorque/notification_service/controller/NotificationController.java +++ b/notification-service/src/main/java/com/techtorque/notification_service/controller/NotificationController.java @@ -1,11 +1,13 @@ package com.techtorque.notification_service.controller; +import com.techtorque.notification_service.dto.request.CreateNotificationRequest; import com.techtorque.notification_service.dto.request.MarkAsReadRequest; import com.techtorque.notification_service.dto.request.SubscribeRequest; import com.techtorque.notification_service.dto.request.UnsubscribeRequest; import com.techtorque.notification_service.dto.response.ApiResponse; import com.techtorque.notification_service.dto.response.NotificationResponse; import com.techtorque.notification_service.dto.response.SubscriptionResponse; +import com.techtorque.notification_service.entity.Notification; import com.techtorque.notification_service.entity.Subscription; import com.techtorque.notification_service.service.NotificationService; import com.techtorque.notification_service.service.SubscriptionService; diff --git a/notification-service/src/main/java/com/techtorque/notification_service/controller/WebSocketNotificationController.java b/notification-service/src/main/java/com/techtorque/notification_service/controller/WebSocketNotificationController.java new file mode 100644 index 0000000..51e0d80 --- /dev/null +++ b/notification-service/src/main/java/com/techtorque/notification_service/controller/WebSocketNotificationController.java @@ -0,0 +1,90 @@ +package com.techtorque.notification_service.controller; + +import com.techtorque.notification_service.dto.response.NotificationResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.handler.annotation.SendTo; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Controller; + +/** + * WebSocket controller for real-time notification broadcasting + * + * Message destinations: + * - /app/notifications.subscribe -> Client subscribes to notifications + * - /topic/notifications -> Broadcast to all connected clients + * - /user/{userId}/queue/notifications -> Send to specific user + * + * Industry standard WebSocket messaging with STOMP protocol + */ +@Controller +@RequiredArgsConstructor +@Slf4j +public class WebSocketNotificationController { + + private final SimpMessagingTemplate messagingTemplate; + + /** + * Handle client subscription to notification stream + * Clients send message to /app/notifications.subscribe + */ + @MessageMapping("/notifications.subscribe") + @SendTo("/topic/notifications") + public String subscribe(@Payload String userId, SimpMessageHeaderAccessor headerAccessor) { + // Store userId in session attributes for user-specific routing + headerAccessor.getSessionAttributes().put("userId", userId); + log.info("User {} subscribed to WebSocket notifications", userId); + return "Subscription successful"; + } + + /** + * Send notification to specific user + * Called by service layer when new notification is created + * + * @param userId Target user ID + * @param notification Notification to send + */ + public void sendNotificationToUser(String userId, NotificationResponse notification) { + log.info("Sending WebSocket notification to user: {} - {}", userId, notification.getMessage()); + + // Send to user-specific queue: /user/{userId}/queue/notifications + messagingTemplate.convertAndSendToUser( + userId, + "/queue/notifications", + notification + ); + } + + /** + * Broadcast notification to all connected users + * Used for system-wide announcements + * + * @param notification Notification to broadcast + */ + public void broadcastNotification(NotificationResponse notification) { + log.info("Broadcasting WebSocket notification: {}", notification.getMessage()); + + // Send to topic: /topic/notifications (all subscribers receive) + messagingTemplate.convertAndSend("/topic/notifications", notification); + } + + /** + * Send notification count update to user + * Called when unread count changes + * + * @param userId Target user ID + * @param count New unread count + */ + public void sendUnreadCountUpdate(String userId, Long count) { + log.debug("Sending unread count update to user {}: {}", userId, count); + + messagingTemplate.convertAndSendToUser( + userId, + "/queue/notifications/count", + count + ); + } +} diff --git a/notification-service/src/main/java/com/techtorque/notification_service/dto/request/CreateNotificationRequest.java b/notification-service/src/main/java/com/techtorque/notification_service/dto/request/CreateNotificationRequest.java new file mode 100644 index 0000000..6ba54df --- /dev/null +++ b/notification-service/src/main/java/com/techtorque/notification_service/dto/request/CreateNotificationRequest.java @@ -0,0 +1,29 @@ +package com.techtorque.notification_service.dto.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CreateNotificationRequest { + + @NotBlank(message = "User ID is required") + private String userId; + + @NotBlank(message = "Type is required") + private String type; // INFO, WARNING, ERROR, SUCCESS + + @NotBlank(message = "Message is required") + private String message; + + private String details; + + private String relatedEntityId; + + private String relatedEntityType; +} diff --git a/notification-service/src/main/java/com/techtorque/notification_service/service/impl/NotificationServiceImpl.java b/notification-service/src/main/java/com/techtorque/notification_service/service/impl/NotificationServiceImpl.java index a0778ae..aba8631 100644 --- a/notification-service/src/main/java/com/techtorque/notification_service/service/impl/NotificationServiceImpl.java +++ b/notification-service/src/main/java/com/techtorque/notification_service/service/impl/NotificationServiceImpl.java @@ -1,11 +1,12 @@ package com.techtorque.notification_service.service.impl; +import com.techtorque.notification_service.controller.WebSocketNotificationController; import com.techtorque.notification_service.dto.response.NotificationResponse; import com.techtorque.notification_service.entity.Notification; import com.techtorque.notification_service.repository.NotificationRepository; import com.techtorque.notification_service.service.NotificationService; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,11 +15,18 @@ import java.util.stream.Collectors; @Service -@RequiredArgsConstructor @Slf4j public class NotificationServiceImpl implements NotificationService { private final NotificationRepository notificationRepository; + private final WebSocketNotificationController webSocketController; + + // Use @Lazy to avoid circular dependency issues + public NotificationServiceImpl(NotificationRepository notificationRepository, + @Lazy WebSocketNotificationController webSocketController) { + this.notificationRepository = notificationRepository; + this.webSocketController = webSocketController; + } @Override @Transactional(readOnly = true) @@ -41,22 +49,27 @@ public List getUserNotifications(String userId, Boolean un @Transactional public NotificationResponse markAsRead(String notificationId, String userId, Boolean read) { log.info("Marking notification {} as read: {} for user: {}", notificationId, read, userId); - + Notification notification = notificationRepository.findById(notificationId) .orElseThrow(() -> new RuntimeException("Notification not found")); - + if (!notification.getUserId().equals(userId)) { throw new RuntimeException("Unauthorized access to notification"); } - + notification.setRead(read); if (read) { notification.setReadAt(LocalDateTime.now()); } else { notification.setReadAt(null); } - + Notification updated = notificationRepository.save(notification); + + // Send real-time unread count update via WebSocket + Long unreadCount = getUnreadCount(userId); + webSocketController.sendUnreadCountUpdate(userId, unreadCount); + return convertToResponse(updated); } @@ -78,10 +91,10 @@ public void deleteNotification(String notificationId, String userId) { @Override @Transactional - public NotificationResponse createNotification(String userId, Notification.NotificationType type, + public NotificationResponse createNotification(String userId, Notification.NotificationType type, String message, String details) { log.info("Creating notification for user: {}, type: {}", userId, type); - + Notification notification = Notification.builder() .userId(userId) .type(type) @@ -91,9 +104,18 @@ public NotificationResponse createNotification(String userId, Notification.Notif .deleted(false) .expiresAt(LocalDateTime.now().plusDays(30)) .build(); - + Notification saved = notificationRepository.save(notification); - return convertToResponse(saved); + NotificationResponse response = convertToResponse(saved); + + // Send real-time notification via WebSocket + webSocketController.sendNotificationToUser(userId, response); + + // Send updated unread count via WebSocket + Long unreadCount = getUnreadCount(userId); + webSocketController.sendUnreadCountUpdate(userId, unreadCount); + + return response; } @Override diff --git a/test_email_config.sh b/test_email_config.sh deleted file mode 100755 index 56c8630..0000000 --- a/test_email_config.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/bash - -# Test Notification Service Email Configuration -# This script verifies that the mail health check is properly disabled - -echo "==========================================" -echo "Testing Notification Service Configuration" -echo "==========================================" -echo "" - -cd /home/randitha/Desktop/IT/UoM/TechTorque-2025/Notification_Service/notification-service - -echo "✓ Checking if mail health check is disabled..." -if grep -q "management.health.mail.enabled" src/main/resources/application.properties; then - HEALTH_STATUS=$(grep "management.health.mail.enabled" src/main/resources/application.properties | grep -o "false\|true") - if [ "$HEALTH_STATUS" = "false" ]; then - echo " ✓ Mail health check is DISABLED in application.properties" - else - echo " ✗ Mail health check is enabled (should be disabled for dev)" - fi -else - echo " ⚠ Mail health check setting not found" -fi - -echo "" -echo "✓ Checking profile configuration..." -if [ -f "src/main/resources/application-dev.properties" ]; then - echo " ✓ Development profile exists (application-dev.properties)" -fi - -if [ -f "src/main/resources/application-prod.properties" ]; then - echo " ✓ Production profile exists (application-prod.properties)" -fi - -echo "" -echo "==========================================" -echo "Configuration Test Summary" -echo "==========================================" -echo "✓ Mail health check: DISABLED (development mode)" -echo "✓ This prevents email authentication warnings" -echo "✓ Service will start without SMTP credential errors" -echo "" -echo "To enable email in production:" -echo " 1. Set EMAIL_USERNAME and EMAIL_PASSWORD env variables" -echo " 2. Use --spring.profiles.active=prod" -echo " 3. See EMAIL_CONFIGURATION.md for details" -echo "" -echo "==========================================" From 74340ec20852392906e42174c0173b42b4f1d055 Mon Sep 17 00:00:00 2001 From: RandithaK Date: Tue, 11 Nov 2025 00:56:30 +0530 Subject: [PATCH 2/3] refactor: Use consistent user IDs for seeding notifications and subscriptions to ensure cross-service data integrity --- .../seeder/DataSeeder.java | 66 ++++++++++++------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/notification-service/src/main/java/com/techtorque/notification_service/seeder/DataSeeder.java b/notification-service/src/main/java/com/techtorque/notification_service/seeder/DataSeeder.java index 2df3bd8..fe3e911 100644 --- a/notification-service/src/main/java/com/techtorque/notification_service/seeder/DataSeeder.java +++ b/notification-service/src/main/java/com/techtorque/notification_service/seeder/DataSeeder.java @@ -13,7 +13,6 @@ import java.time.LocalDateTime; import java.util.Arrays; import java.util.List; -import java.util.UUID; @Component @Profile("!test") @@ -24,6 +23,12 @@ public class DataSeeder implements CommandLineRunner { private final NotificationRepository notificationRepository; private final SubscriptionRepository subscriptionRepository; + // Consistent user IDs matching Auth Service seed data + // These usernames are forwarded via X-User-Subject header from the API Gateway + private static final String CUSTOMER_1_ID = "customer"; + private static final String CUSTOMER_2_ID = "testuser"; + private static final String EMPLOYEE_1_ID = "employee"; + @Override public void run(String... args) { log.info("Starting DataSeeder for Notification Service..."); @@ -44,14 +49,13 @@ public void run(String... args) { } private void seedNotifications() { - log.info("Seeding notifications..."); - - String testUserId1 = UUID.randomUUID().toString(); - String testUserId2 = UUID.randomUUID().toString(); + log.info("Seeding notifications for consistent test users..."); + // Use consistent user IDs that match Auth Service seed data + // No more random UUIDs - ensures cross-service data integrity List notifications = Arrays.asList( Notification.builder() - .userId(testUserId1) + .userId(CUSTOMER_1_ID) .type(Notification.NotificationType.APPOINTMENT_CONFIRMED) .message("Your appointment has been confirmed") .details("Appointment scheduled for tomorrow at 10:00 AM") @@ -61,7 +65,7 @@ private void seedNotifications() { .build(), Notification.builder() - .userId(testUserId1) + .userId(CUSTOMER_1_ID) .type(Notification.NotificationType.SERVICE_COMPLETED) .message("Your service has been completed") .details("Oil change and tire rotation completed successfully") @@ -72,7 +76,7 @@ private void seedNotifications() { .build(), Notification.builder() - .userId(testUserId1) + .userId(CUSTOMER_1_ID) .type(Notification.NotificationType.INVOICE_GENERATED) .message("Invoice generated for your service") .details("Total amount: $150.00. Payment due in 7 days.") @@ -82,7 +86,7 @@ private void seedNotifications() { .build(), Notification.builder() - .userId(testUserId2) + .userId(CUSTOMER_2_ID) .type(Notification.NotificationType.APPOINTMENT_REMINDER) .message("Appointment reminder") .details("Your appointment is in 1 hour") @@ -92,7 +96,7 @@ private void seedNotifications() { .build(), Notification.builder() - .userId(testUserId2) + .userId(CUSTOMER_2_ID) .type(Notification.NotificationType.PAYMENT_RECEIVED) .message("Payment received") .details("We've received your payment of $200.00") @@ -100,43 +104,61 @@ private void seedNotifications() { .deleted(false) .readAt(LocalDateTime.now().minusDays(1)) .expiresAt(LocalDateTime.now().plusDays(30)) + .build(), + + Notification.builder() + .userId(EMPLOYEE_1_ID) + .type(Notification.NotificationType.APPOINTMENT_CONFIRMED) + .message("New appointment assigned to you") + .details("Customer appointment scheduled for today at 2:00 PM") + .read(false) + .deleted(false) + .expiresAt(LocalDateTime.now().plusDays(7)) .build() ); notificationRepository.saveAll(notifications); - log.info("Seeded {} notifications", notifications.size()); + log.info("Seeded {} notifications for users: {}, {}, {}", + notifications.size(), CUSTOMER_1_ID, CUSTOMER_2_ID, EMPLOYEE_1_ID); } private void seedSubscriptions() { - log.info("Seeding subscriptions..."); - - String testUserId1 = UUID.randomUUID().toString(); - String testUserId2 = UUID.randomUUID().toString(); + log.info("Seeding subscriptions for consistent test users..."); + // Use consistent user IDs and predictable tokens for testing + // No more random UUIDs - ensures reproducible test data List subscriptions = Arrays.asList( Subscription.builder() - .userId(testUserId1) - .token("web_push_token_" + UUID.randomUUID()) + .userId(CUSTOMER_1_ID) + .token("web_push_token_customer_browser") .platform(Subscription.Platform.WEB) .active(true) .build(), Subscription.builder() - .userId(testUserId1) - .token("ios_device_token_" + UUID.randomUUID()) + .userId(CUSTOMER_1_ID) + .token("ios_device_token_customer_iphone") .platform(Subscription.Platform.IOS) .active(true) .build(), Subscription.builder() - .userId(testUserId2) - .token("android_device_token_" + UUID.randomUUID()) + .userId(CUSTOMER_2_ID) + .token("android_device_token_testuser_phone") .platform(Subscription.Platform.ANDROID) .active(true) + .build(), + + Subscription.builder() + .userId(EMPLOYEE_1_ID) + .token("web_push_token_employee_browser") + .platform(Subscription.Platform.WEB) + .active(true) .build() ); subscriptionRepository.saveAll(subscriptions); - log.info("Seeded {} subscriptions", subscriptions.size()); + log.info("Seeded {} subscriptions for users: {}, {}, {}", + subscriptions.size(), CUSTOMER_1_ID, CUSTOMER_2_ID, EMPLOYEE_1_ID); } } From c776d472ae23a2f1df0dbda64b81c73bb799eaa2 Mon Sep 17 00:00:00 2001 From: RandithaK Date: Tue, 11 Nov 2025 12:05:36 +0530 Subject: [PATCH 3/3] chore: commit all changes (automated) --- notification-service/pom.xml | 54 +++++++ .../grpc/NotificationEmailGrpcService.java | 49 ++++++ .../service/TransactionalEmailService.java | 149 ++++++++++++++++++ .../src/main/proto/notification/email.proto | 38 +++++ .../src/main/resources/application.properties | 5 + 5 files changed, 295 insertions(+) create mode 100644 notification-service/src/main/java/com/techtorque/notification_service/grpc/NotificationEmailGrpcService.java create mode 100644 notification-service/src/main/java/com/techtorque/notification_service/service/TransactionalEmailService.java create mode 100644 notification-service/src/main/proto/notification/email.proto diff --git a/notification-service/pom.xml b/notification-service/pom.xml index f955654..be6bca3 100644 --- a/notification-service/pom.xml +++ b/notification-service/pom.xml @@ -28,6 +28,9 @@ 17 + 1.63.0 + 3.25.3 + 3.1.0.RELEASE @@ -58,6 +61,31 @@ org.springframework.boot spring-boot-starter-websocket + + net.devh + grpc-server-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 + @@ -94,7 +122,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.apache.maven.plugins maven-compiler-plugin diff --git a/notification-service/src/main/java/com/techtorque/notification_service/grpc/NotificationEmailGrpcService.java b/notification-service/src/main/java/com/techtorque/notification_service/grpc/NotificationEmailGrpcService.java new file mode 100644 index 0000000..5c71329 --- /dev/null +++ b/notification-service/src/main/java/com/techtorque/notification_service/grpc/NotificationEmailGrpcService.java @@ -0,0 +1,49 @@ +package com.techtorque.notification_service.grpc; + +import com.techtorque.notification.grpc.NotificationEmailServiceGrpc; +import com.techtorque.notification.grpc.SendEmailRequest; +import com.techtorque.notification.grpc.SendEmailResponse; +import com.techtorque.notification_service.service.TransactionalEmailService; +import io.grpc.Status; +import io.grpc.stub.StreamObserver; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.server.service.GrpcService; + +/** + * Exposes transactional email delivery over gRPC for other services. + */ +@GrpcService +@RequiredArgsConstructor +@Slf4j +public class NotificationEmailGrpcService extends NotificationEmailServiceGrpc.NotificationEmailServiceImplBase { + + private final TransactionalEmailService transactionalEmailService; + + @Override + public void sendTransactionalEmail(SendEmailRequest request, StreamObserver responseObserver) { + try { + log.debug("Received transactional email request for {} using template {}", request.getTo(), request.getType()); + var result = transactionalEmailService.sendTransactionalEmail( + request.getTo(), + request.getUsername(), + request.getType(), + request.getVariablesMap()); + + SendEmailResponse response = SendEmailResponse.newBuilder() + .setStatus(result.status()) + .setMessageId(result.messageId()) + .setDetail(result.detail() == null ? "" : result.detail()) + .build(); + + responseObserver.onNext(response); + responseObserver.onCompleted(); + } catch (Exception ex) { + log.error("Unexpected error while handling transactional email request: {}", ex.getMessage(), ex); + responseObserver.onError(Status.INTERNAL + .withDescription("Failed to process transactional email request") + .withCause(ex) + .asRuntimeException()); + } + } +} diff --git a/notification-service/src/main/java/com/techtorque/notification_service/service/TransactionalEmailService.java b/notification-service/src/main/java/com/techtorque/notification_service/service/TransactionalEmailService.java new file mode 100644 index 0000000..4931d48 --- /dev/null +++ b/notification-service/src/main/java/com/techtorque/notification_service/service/TransactionalEmailService.java @@ -0,0 +1,149 @@ +package com.techtorque.notification_service.service; + +import com.techtorque.notification.grpc.DeliveryStatus; +import com.techtorque.notification.grpc.EmailType; +import com.techtorque.notification_service.config.NotificationProperties; +import java.time.OffsetDateTime; +import java.util.Map; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +/** + * Handles transactional email composition and delivery triggered via gRPC. + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class TransactionalEmailService { + + private final JavaMailSender mailSender; + private final NotificationProperties notificationProperties; + + @Value("${notification.email.enabled:true}") + private boolean emailEnabled; + + public DeliveryResult sendTransactionalEmail(String to, + String username, + EmailType type, + Map variables) { + String fromAddress = resolveFromAddress(); + EmailTemplate template = buildTemplate(username, type, variables); + String messageId = UUID.randomUUID().toString(); + + if (!emailEnabled) { + log.info("Email delivery disabled. Skipping send for {} ({})", to, type); + return new DeliveryResult(messageId, DeliveryStatus.DELIVERY_STATUS_ACCEPTED, + "Email delivery disabled by configuration"); + } + + try { + if (mailSender == null) { + log.warn("Mail sender not configured. Unable to deliver {} email to {}", type, to); + return new DeliveryResult(messageId, DeliveryStatus.DELIVERY_STATUS_REJECTED, + "Mail sender not configured"); + } + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(fromAddress); + message.setTo(to); + message.setSubject(template.subject()); + message.setText(template.body()); + mailSender.send(message); + log.info("Transactional email {} queued to {} at {}", type, to, OffsetDateTime.now()); + return new DeliveryResult(messageId, DeliveryStatus.DELIVERY_STATUS_ACCEPTED, "Email sent"); + } catch (Exception ex) { + log.error("Failed to send {} email to {}: {}", type, to, ex.getMessage(), ex); + return new DeliveryResult(messageId, DeliveryStatus.DELIVERY_STATUS_REJECTED, ex.getMessage()); + } + } + + private String resolveFromAddress() { + String configured = notificationProperties.getEmail() != null + ? notificationProperties.getEmail().getFrom() + : null; + return StringUtils.hasText(configured) ? configured : "noreply@techtorque.com"; + } + + private EmailTemplate buildTemplate(String username, EmailType type, Map variables) { + String safeUsername = StringUtils.hasText(username) ? username : "there"; + return switch (type) { + case EMAIL_TYPE_PASSWORD_RESET -> passwordResetTemplate(safeUsername, variables); + case EMAIL_TYPE_WELCOME -> welcomeTemplate(safeUsername, variables); + case EMAIL_TYPE_VERIFICATION, EMAIL_TYPE_UNSPECIFIED -> verificationTemplate(safeUsername, variables); + default -> verificationTemplate(safeUsername, variables); + }; + } + + private EmailTemplate verificationTemplate(String username, Map variables) { + String verificationUrl = variables.getOrDefault("verificationUrl", ""); + String token = variables.getOrDefault("token", ""); + String subject = "TechTorque - Verify Your Email Address"; + StringBuilder body = new StringBuilder() + .append("Hello ").append(username).append(",\n\n") + .append("Thank you for registering with TechTorque!\n\n"); + + if (StringUtils.hasText(verificationUrl)) { + body.append("Please click the link below to verify your email address:\n") + .append(verificationUrl).append("\n\n"); + } else if (StringUtils.hasText(token)) { + body.append("Use the following verification token to complete your registration:\n") + .append(token).append("\n\n"); + } + + body.append("This link will expire in 24 hours.\n\n") + .append("If you did not create an account, please ignore this email.\n\n") + .append("Best regards,\nTechTorque Team"); + return new EmailTemplate(subject, body.toString()); + } + + private EmailTemplate passwordResetTemplate(String username, Map variables) { + String resetUrl = variables.getOrDefault("resetUrl", ""); + String token = variables.getOrDefault("token", ""); + String subject = "TechTorque - Password Reset Request"; + StringBuilder body = new StringBuilder() + .append("Hello ").append(username).append(",\n\n") + .append("We received a request to reset your password.\n\n"); + + if (StringUtils.hasText(resetUrl)) { + body.append("Please click the link below to reset your password:\n") + .append(resetUrl).append("\n\n"); + } else if (StringUtils.hasText(token)) { + body.append("Use the following password reset token to update your credentials:\n") + .append(token).append("\n\n"); + } + + body.append("This link will expire in 1 hour.\n\n") + .append("If you did not request a password reset, please ignore this email and your password will remain unchanged.\n\n") + .append("Best regards,\nTechTorque Team"); + return new EmailTemplate(subject, body.toString()); + } + + private EmailTemplate welcomeTemplate(String username, Map variables) { + String dashboardUrl = variables.getOrDefault("dashboardUrl", ""); + String subject = "Welcome to TechTorque!"; + StringBuilder body = new StringBuilder() + .append("Hello ").append(username).append(",\n\n") + .append("Welcome to TechTorque! Your email has been successfully verified.\n\n") + .append("You can now:\n") + .append("- Register your vehicles\n") + .append("- Book service appointments\n") + .append("- Track service progress\n") + .append("- Request custom modifications\n\n"); + + if (StringUtils.hasText(dashboardUrl)) { + body.append("Visit ").append(dashboardUrl).append(" to get started.\n\n"); + } + + body.append("Best regards,\nTechTorque Team"); + return new EmailTemplate(subject, body.toString()); + } + + public record DeliveryResult(String messageId, DeliveryStatus status, String detail) {} + + private record EmailTemplate(String subject, String body) {} +} diff --git a/notification-service/src/main/proto/notification/email.proto b/notification-service/src/main/proto/notification/email.proto new file mode 100644 index 0000000..7f318cc --- /dev/null +++ b/notification-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/notification-service/src/main/resources/application.properties b/notification-service/src/main/resources/application.properties index e9f6c60..69486dd 100644 --- a/notification-service/src/main/resources/application.properties +++ b/notification-service/src/main/resources/application.properties @@ -25,11 +25,16 @@ spring.mail.password=${EMAIL_PASSWORD:your-app-password} spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.starttls.required=true +notification.email.enabled=${EMAIL_ENABLED:true} # Notification Configuration notification.email.from=${EMAIL_USERNAME:noreply@techtorque.com} notification.retention.days=30 +# gRPC Configuration +grpc.server.port=${GRPC_PORT:9090} +grpc.server.security.enabled=false + # Actuator Configuration management.endpoints.web.exposure.include=health,info,metrics management.endpoint.health.show-details=always