chore: fix ai service

This commit is contained in:
mahyargdz
2025-07-26 13:02:30 +03:30
parent 61e3be9f4a
commit a753badcef
12 changed files with 5 additions and 2547 deletions
File diff suppressed because it is too large Load Diff
-579
View File
@@ -1,579 +0,0 @@
# Web Push Notifications Setup Guide
This guide explains how to implement and use **Web Push notifications** in the Dmail API. Web Push allows you to send notifications directly to users' browsers, even when your web app is not open.
## Overview
The Web Push notification system sends real-time notifications to users' browsers when new emails arrive. It uses the standard **Web Push API** supported by all modern browsers and works with service workers.
## Architecture
```
Email Received → EmailMonitoringService → NotificationQueue → NewEmailHandler → PushNotificationHandler → Browser Push Service → User's Browser
```
## Setup Instructions
### 1. Generate VAPID Keys
VAPID (Voluntary Application Server Identification) keys are required for Web Push notifications:
```bash
# Generate VAPID keys using Node.js
node -e "
const webpush = require('web-push');
const vapidKeys = webpush.generateVAPIDKeys();
console.log('Public Key:', vapidKeys.publicKey);
console.log('Private Key:', vapidKeys.privateKey);
"
```
### 2. Configure Environment Variables
Add these environment variables to your `.env` file:
```bash
# Web Push Notifications Configuration
PUSH_NOTIFICATIONS_ENABLED=true
VAPID_PUBLIC_KEY=your_generated_public_key_here
VAPID_PRIVATE_KEY=your_generated_private_key_here
VAPID_SUBJECT=mailto:admin@yourdomain.com
# Optional: Push notification settings
PUSH_TTL=86400
PUSH_URGENCY=normal
PUSH_RETRY_ATTEMPTS=3
PUSH_RETRY_DELAY=5000
# Frontend URL for notification actions
FRONTEND_URL=https://mail.yourdomain.com
```
### 3. Install Dependencies
```bash
# Install web-push library
npm install web-push
# or
pnpm add web-push
```
### 4. Run Database Migration
```bash
# Apply the database migration for web push notification fields
psql -d your_database -f database/migrations/add_push_notifications_to_users.sql
```
### 5. Update App Module
Add the push notifications config to your `app.module.ts`:
```typescript
import { pushNotificationsConfig } from "./configs/push-notifications.config";
@Module({
imports: [
ConfigModule.forRoot({
load: [
// ... other configs
pushNotificationsConfig,
],
}),
// ... other modules
],
})
export class AppModule {}
```
## Frontend Implementation
### 1. Service Worker Setup
Create `public/sw.js` in your frontend project:
```javascript
// Service Worker for handling push notifications
self.addEventListener("push", function (event) {
if (event.data) {
const data = event.data.json();
const options = {
body: data.body,
icon: data.icon || "/assets/email-icon.png",
badge: data.badge || "/assets/email-badge.png",
tag: data.tag || "email-notification",
requireInteraction: data.requireInteraction || false,
actions: data.actions || [],
data: data.data || {},
};
event.waitUntil(self.registration.showNotification(data.title, options));
}
});
// Handle notification clicks
self.addEventListener("notificationclick", function (event) {
event.notification.close();
if (event.action === "view") {
// Open the email in the app
event.waitUntil(clients.openWindow(event.notification.data.url || "/"));
} else if (event.action === "dismiss") {
// Just close the notification
return;
} else {
// Default action - open the app
event.waitUntil(clients.openWindow(event.notification.data.url || "/"));
}
});
```
### 2. Client-side JavaScript
Add this to your main JavaScript file:
```javascript
class WebPushManager {
constructor(apiBaseUrl, getAuthToken) {
this.apiBaseUrl = apiBaseUrl;
this.getAuthToken = getAuthToken;
this.vapidPublicKey = null;
}
// Initialize and register service worker
async init() {
if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
console.warn("Push messaging is not supported");
return false;
}
try {
// Register service worker
const registration = await navigator.serviceWorker.register("/sw.js");
console.log("Service Worker registered:", registration);
// Get VAPID public key
await this.getVAPIDKey();
return true;
} catch (error) {
console.error("Service Worker registration failed:", error);
return false;
}
}
// Get VAPID public key from server
async getVAPIDKey() {
try {
const response = await fetch(`${this.apiBaseUrl}/users/push-notifications/vapid-key`, {
headers: {
Authorization: `Bearer ${this.getAuthToken()}`,
},
});
if (response.ok) {
const data = await response.json();
this.vapidPublicKey = data.vapidPublicKey;
return this.vapidPublicKey;
}
} catch (error) {
console.error("Failed to get VAPID key:", error);
}
return null;
}
// Request permission and subscribe to push notifications
async subscribeToPush() {
if (!this.vapidPublicKey) {
await this.getVAPIDKey();
}
if (!this.vapidPublicKey) {
throw new Error("VAPID public key not available");
}
// Request permission
const permission = await Notification.requestPermission();
if (permission !== "granted") {
throw new Error("Push notification permission denied");
}
// Get service worker registration
const registration = await navigator.serviceWorker.ready;
try {
// Subscribe to push notifications
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey),
});
// Send subscription to server
await this.sendSubscriptionToServer(subscription);
console.log("Successfully subscribed to push notifications");
return subscription;
} catch (error) {
console.error("Failed to subscribe to push notifications:", error);
throw error;
}
}
// Send subscription to server
async sendSubscriptionToServer(subscription) {
const response = await fetch(`${this.apiBaseUrl}/users/push-subscriptions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.getAuthToken()}`,
},
body: JSON.stringify({
subscription: subscription.toJSON(),
deviceType: "web",
deviceName: navigator.userAgent.slice(0, 50),
}),
});
if (!response.ok) {
throw new Error("Failed to send subscription to server");
}
return response.json();
}
// Test push notification
async testNotification() {
const response = await fetch(`${this.apiBaseUrl}/users/push-notifications/test`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.getAuthToken()}`,
},
body: JSON.stringify({
message: "Test notification from Dmail Web App",
}),
});
if (!response.ok) {
throw new Error("Failed to send test notification");
}
return response.json();
}
// Check if user is subscribed
async isSubscribed() {
if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
return false;
}
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
return !!subscription;
}
// Unsubscribe from push notifications
async unsubscribe() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
// Unsubscribe from browser
await subscription.unsubscribe();
// Remove from server
await fetch(`${this.apiBaseUrl}/users/push-subscriptions`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.getAuthToken()}`,
},
body: JSON.stringify({
endpoint: subscription.endpoint,
}),
});
console.log("Successfully unsubscribed from push notifications");
}
}
// Utility function to convert VAPID key
urlBase64ToUint8Array(base64String) {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
}
// Usage example
const pushManager = new WebPushManager("https://api.yourdomain.com", () => localStorage.getItem("authToken"));
// Initialize on page load
pushManager.init().then((success) => {
if (success) {
console.log("Web Push initialized successfully");
}
});
// Export for use in your app
window.webPushManager = pushManager;
```
### 3. User Interface Integration
Add UI components to your app:
```html
<!-- Notification permission UI -->
<div id="push-notification-ui" style="display: none;">
<div class="notification-prompt">
<h3>📬 Enable Email Notifications</h3>
<p>Get instant notifications when new emails arrive, even when the app is closed.</p>
<button id="enable-notifications" class="btn btn-primary">Enable Notifications</button>
<button id="test-notification" class="btn btn-secondary" style="display: none;">Test Notification</button>
<button id="disable-notifications" class="btn btn-danger" style="display: none;">Disable</button>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", async () => {
const ui = document.getElementById("push-notification-ui");
const enableBtn = document.getElementById("enable-notifications");
const testBtn = document.getElementById("test-notification");
const disableBtn = document.getElementById("disable-notifications");
// Show UI if push is supported
if ("serviceWorker" in navigator && "PushManager" in window) {
ui.style.display = "block";
// Check current subscription status
const isSubscribed = await window.webPushManager.isSubscribed();
if (isSubscribed) {
enableBtn.style.display = "none";
testBtn.style.display = "inline-block";
disableBtn.style.display = "inline-block";
}
// Enable notifications
enableBtn.addEventListener("click", async () => {
try {
await window.webPushManager.subscribeToPush();
enableBtn.style.display = "none";
testBtn.style.display = "inline-block";
disableBtn.style.display = "inline-block";
alert("Push notifications enabled successfully!");
} catch (error) {
alert("Failed to enable notifications: " + error.message);
}
});
// Test notification
testBtn.addEventListener("click", async () => {
try {
await window.webPushManager.testNotification();
alert("Test notification sent!");
} catch (error) {
alert("Failed to send test notification: " + error.message);
}
});
// Disable notifications
disableBtn.addEventListener("click", async () => {
try {
await window.webPushManager.unsubscribe();
enableBtn.style.display = "inline-block";
testBtn.style.display = "none";
disableBtn.style.display = "none";
alert("Push notifications disabled");
} catch (error) {
alert("Failed to disable notifications: " + error.message);
}
});
}
});
</script>
```
## API Endpoints
### Push Subscription Management
- `POST /users/push-subscriptions` - Register a new push subscription
- `PATCH /users/push-subscriptions` - Update push subscriptions
- `DELETE /users/push-subscriptions` - Remove a push subscription
### Push Notification Settings
- `PATCH /users/push-notifications/settings` - Enable/disable push notifications
- `GET /users/push-notifications/info` - Get push notification info and settings
- `POST /users/push-notifications/test` - Send a test notification
- `GET /users/push-notifications/vapid-key` - Get VAPID public key
## Usage Examples
### Register Push Subscription
```bash
curl -X POST https://api.yourdomain.com/users/push-subscriptions \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"subscription": {
"endpoint": "https://fcm.googleapis.com/fcm/send/...",
"keys": {
"p256dh": "BNbxxx...",
"auth": "abc123..."
}
},
"deviceType": "web",
"deviceName": "Chrome on MacBook Pro"
}'
```
### Test Push Notification
```bash
curl -X POST https://api.yourdomain.com/users/push-notifications/test \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": "Test notification from Dmail API"
}'
```
### Get VAPID Public Key
```bash
curl -X GET https://api.yourdomain.com/users/push-notifications/vapid-key \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
```
## How It Works
1. **Service Worker Registration**: Your web app registers a service worker that can receive push messages
2. **User Permission**: The user grants permission for notifications
3. **Subscription Creation**: The browser creates a push subscription with a unique endpoint
4. **Server Registration**: The subscription is sent to your API and stored in the database
5. **Email Detection**: When new emails arrive, the EmailMonitoringService detects them
6. **Notification Processing**: The NewEmailHandler triggers the PushNotificationHandler
7. **Push Message**: The server sends a push message to the browser's push service
8. **Notification Display**: The service worker receives the message and shows the notification
## Browser Support
Web Push is supported by:
- ✅ Chrome 50+
- ✅ Firefox 44+
- ✅ Safari 16.0+
- ✅ Edge 17+
- ✅ Opera 37+
## Notification Flow
```mermaid
sequenceDiagram
participant U as User's Browser
participant SW as Service Worker
participant API as Dmail API
participant PS as Push Service
participant E as Email System
U->>SW: Register Service Worker
U->>API: Request VAPID key
API->>U: Return VAPID public key
U->>U: Request notification permission
U->>PS: Create push subscription
PS->>U: Return subscription object
U->>API: Register subscription
API->>API: Store subscription
E->>API: New email detected
API->>PS: Send push message
PS->>SW: Deliver push message
SW->>U: Show notification
```
## Security Considerations
1. **VAPID Keys**: Keep your private VAPID key secure and never expose it to the client
2. **Subscription Validation**: Always validate subscription objects before storing them
3. **Rate Limiting**: Implement rate limiting to prevent notification spam
4. **Data Privacy**: Be mindful of what data you include in push notifications
5. **User Consent**: Always request explicit user permission before subscribing to push notifications
## Troubleshooting
### Common Issues
1. **Notifications not received**:
- Check if the user has granted notification permission
- Verify the service worker is registered correctly
- Ensure VAPID keys are properly configured
- Check browser console for errors
2. **VAPID key errors**:
- Ensure VAPID keys are correctly generated and configured
- Verify the VAPID subject is a valid mailto: or https: URL
3. **Service worker issues**:
- Check if the service worker file is accessible
- Verify the service worker scope includes your web app
- Ensure HTTPS is used (required for service workers)
### Debug Commands
```bash
# Test VAPID key configuration
curl -X GET https://api.yourdomain.com/users/push-notifications/vapid-key \
-H "Authorization: Bearer YOUR_TOKEN"
# Check user's notification settings
curl -X GET https://api.yourdomain.com/users/push-notifications/info \
-H "Authorization: Bearer YOUR_TOKEN"
# Send test notification
curl -X POST https://api.yourdomain.com/users/push-notifications/test \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"message": "Test"}'
```
## Monitoring and Analytics
- **Subscription Management**: Track active subscriptions per user
- **Delivery Success**: Monitor push notification delivery rates
- **User Engagement**: Track notification click-through rates
- **Error Handling**: Log and monitor push notification failures
## Performance Considerations
- **Payload Size**: Keep push notification payloads small (max 4KB)
- **Batch Processing**: Process multiple subscriptions efficiently
- **TTL Settings**: Configure appropriate Time-To-Live for notifications
- **Cleanup**: Regularly remove invalid/expired subscriptions
## Future Enhancements
- **Rich Notifications**: Add images and action buttons
- **Notification Categories**: Different notification types for different email categories
- **User Preferences**: Granular control over notification timing and content
- **Analytics Dashboard**: Web interface for monitoring push notification metrics
- **A/B Testing**: Test different notification strategies and content
## Support
For issues and questions:
1. Check browser developer console for errors
2. Verify service worker registration and subscription
3. Test VAPID key configuration
4. Review application logs for error messages
@@ -11,24 +11,3 @@ export class WriteEmailAssistanceDto {
}) })
prompt: string; prompt: string;
} }
export class EmailSuggestionDto {
@ApiProperty({
description: "Suggested email subject",
example: "Re: Project Timeline Discussion - Meeting Request",
})
subject: string;
@ApiProperty({
description: "Generated email content",
example: "Dear John,\n\nThank you for your email regarding the project timeline...",
})
content: string;
@ApiProperty({
description: "Alternative variations of the email",
type: [String],
example: ["More formal version", "Shorter version", "More detailed version"],
})
alternatives: string[];
}
@@ -34,7 +34,7 @@ export class AiAssistantService {
async assistEmailWriting(writeAssistanceDto: WriteEmailAssistanceDto, userId: string) { async assistEmailWriting(writeAssistanceDto: WriteEmailAssistanceDto, userId: string) {
const assistance = await this.generateEmailWritingAssistance(writeAssistanceDto, userId); const assistance = await this.generateEmailWritingAssistance(writeAssistanceDto, userId);
return assistance; return { assistance };
} }
//************************************************* */ //************************************************* */
@@ -116,10 +116,6 @@ export class EmailService {
wildDuckQuery.query = query.search; wildDuckQuery.query = query.search;
wildDuckQuery.from = query.search; wildDuckQuery.from = query.search;
} }
if (query.q) {
wildDuckQuery.query = query.q;
wildDuckQuery.from = query.q;
}
if (query.from) wildDuckQuery.from = query.from; if (query.from) wildDuckQuery.from = query.from;
if (query.to) wildDuckQuery.to = query.to; if (query.to) wildDuckQuery.to = query.to;
@@ -2,7 +2,6 @@ import { EntityManager } from "@mikro-orm/postgresql";
import { Injectable, Logger } from "@nestjs/common"; import { Injectable, Logger } from "@nestjs/common";
import { BaseNotificationHandler } from "./base-notification.handler"; import { BaseNotificationHandler } from "./base-notification.handler";
import { PushNotificationHandler } from "./push-notification.handler";
import { INewEmailNotificationData } from "../interfaces/ISendNotificationData"; import { INewEmailNotificationData } from "../interfaces/ISendNotificationData";
import { NotificationsService } from "../services/notifications.service"; import { NotificationsService } from "../services/notifications.service";
@@ -10,21 +9,13 @@ import { NotificationsService } from "../services/notifications.service";
export class NewEmailHandler extends BaseNotificationHandler<INewEmailNotificationData> { export class NewEmailHandler extends BaseNotificationHandler<INewEmailNotificationData> {
protected readonly logger = new Logger(NewEmailHandler.name); protected readonly logger = new Logger(NewEmailHandler.name);
constructor( constructor(private readonly notificationsService: NotificationsService) {
private readonly notificationsService: NotificationsService,
private readonly pushNotificationHandler: PushNotificationHandler,
) {
super(); super();
} }
protected async handle(recipientId: string, data: INewEmailNotificationData, em: EntityManager): Promise<void> { protected async handle(recipientId: string, data: INewEmailNotificationData, em: EntityManager): Promise<void> {
try { try {
// Create in-app notification
await this.notificationsService.createNewEmailNotification(recipientId, data, em); await this.notificationsService.createNewEmailNotification(recipientId, data, em);
// Also send push notification
await this.pushNotificationHandler.processWithTransaction(recipientId, data, em);
this.logger.log(`New email notifications (in-app + push) processed for user: ${recipientId}`); this.logger.log(`New email notifications (in-app + push) processed for user: ${recipientId}`);
} catch (error) { } catch (error) {
this.logger.error(`Error processing new email notifications for user ${recipientId}:`, error); this.logger.error(`Error processing new email notifications for user ${recipientId}:`, error);
@@ -1,75 +0,0 @@
import { EntityManager } from "@mikro-orm/postgresql";
import { Injectable, Logger } from "@nestjs/common";
import { BaseNotificationHandler } from "./base-notification.handler";
import { User } from "../../users/entities/user.entity";
import { INewEmailNotificationData } from "../interfaces/ISendNotificationData";
import { PushNotificationService, WebPushSubscription } from "../services/push-notification.service";
@Injectable()
export class PushNotificationHandler extends BaseNotificationHandler<INewEmailNotificationData> {
protected readonly logger = new Logger(PushNotificationHandler.name);
constructor(private readonly pushNotificationService: PushNotificationService) {
super();
}
protected async handle(recipientId: string, data: INewEmailNotificationData, em: EntityManager): Promise<void> {
try {
// Get user with push notification settings
const user = await em.findOne(User, {
id: recipientId,
deletedAt: null,
});
if (!user) {
this.logger.warn(`User not found: ${recipientId}`);
return;
}
// Check if push notifications are enabled for this user
if (!user.pushNotificationsEnabled || !user.pushTokens || user.pushTokens.length === 0) {
this.logger.debug(`Push notifications not enabled or no subscriptions for user: ${recipientId}`);
return;
}
// Check if push notification service is enabled
if (!this.pushNotificationService.isEnabled()) {
this.logger.debug("Push notification service is disabled");
return;
}
// Convert stored subscriptions to WebPushSubscription format
const subscriptions: WebPushSubscription[] = user.pushTokens.map((sub) => ({
endpoint: sub.endpoint,
keys: sub.keys,
}));
// Send push notification using the data from INewEmailNotificationData
const success = await this.pushNotificationService.sendNewEmailNotification(
subscriptions,
"New Email", // Default subject - can be enhanced with more email data
data.fromName || "Unknown Sender",
data.fromEmail,
user.id,
);
if (success) {
// Update last push notification timestamp
user.lastPushNotificationAt = new Date();
await em.flush();
this.logger.log(`Push notification sent successfully for user: ${recipientId}`);
} else {
this.logger.warn(`Failed to send push notification for user: ${recipientId}`);
}
} catch (error) {
this.logger.error(`Error in push notification handler for user ${recipientId}:`, error);
throw error;
}
}
protected getHandlerName(): string {
return "PushNotification";
}
}
@@ -8,12 +8,10 @@ import { Notification } from "./entities/notification.entity";
import { ChangePasswordHandler } from "./handlers/change-password.handler"; import { ChangePasswordHandler } from "./handlers/change-password.handler";
import { NewEmailHandler } from "./handlers/new-email.handler"; import { NewEmailHandler } from "./handlers/new-email.handler";
import { NotificationHandlerFactory } from "./handlers/notification-handler.factory"; import { NotificationHandlerFactory } from "./handlers/notification-handler.factory";
import { PushNotificationHandler } from "./handlers/push-notification.handler";
import { NotificationController } from "./notifications.controller"; import { NotificationController } from "./notifications.controller";
import { NotificationProcessor } from "./queue/notification.processor"; import { NotificationProcessor } from "./queue/notification.processor";
import { NotificationQueue } from "./queue/notification.queue"; import { NotificationQueue } from "./queue/notification.queue";
import { NotificationsService } from "./services/notifications.service"; import { NotificationsService } from "./services/notifications.service";
import { PushNotificationService } from "./services/push-notification.service";
import { pushNotificationsConfig } from "../../configs/push-notifications.config"; import { pushNotificationsConfig } from "../../configs/push-notifications.config";
import { NotificationSetting } from "../settings/entities/notification-setting.entity"; import { NotificationSetting } from "../settings/entities/notification-setting.entity";
import { SettingModule } from "../settings/settings.module"; import { SettingModule } from "../settings/settings.module";
@@ -44,15 +42,13 @@ import { UserLoginHandler } from "./handlers/user-login.handler";
NotificationsService, NotificationsService,
NotificationQueue, NotificationQueue,
NotificationProcessor, NotificationProcessor,
PushNotificationService,
// notification handlers // notification handlers
NotificationHandlerFactory, NotificationHandlerFactory,
UserLoginHandler, UserLoginHandler,
NewEmailHandler, NewEmailHandler,
ChangePasswordHandler, ChangePasswordHandler,
PushNotificationHandler,
], ],
controllers: [NotificationController], controllers: [NotificationController],
exports: [NotificationsService, NotificationQueue, PushNotificationService], exports: [NotificationsService, NotificationQueue],
}) })
export class NotificationModule {} export class NotificationModule {}
@@ -1,277 +0,0 @@
import { Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import * as webpush from "web-push";
import { PushNotificationsConfigType } from "../../../configs/push-notifications.config";
export interface WebPushSubscription {
endpoint: string;
keys: {
p256dh: string;
auth: string;
};
}
export interface WebPushNotificationPayload {
title: string;
body: string;
icon?: string;
badge?: string;
image?: string;
data?: any;
actions?: Array<{
action: string;
title: string;
icon?: string;
}>;
tag?: string;
requireInteraction?: boolean;
silent?: boolean;
vibrate?: number[];
timestamp?: number;
}
export interface BulkWebPushPayload extends WebPushNotificationPayload {
subscriptions: WebPushSubscription[];
}
@Injectable()
export class PushNotificationService {
private readonly logger = new Logger(PushNotificationService.name);
private readonly config: PushNotificationsConfigType;
constructor(private readonly configService: ConfigService) {
this.config = this.configService.get<PushNotificationsConfigType>("pushNotifications")!;
// Configure web-push with VAPID keys
if (this.config.webPush.vapidPublicKey && this.config.webPush.vapidPrivateKey) {
webpush.setVapidDetails(this.config.webPush.vapidSubject, this.config.webPush.vapidPublicKey, this.config.webPush.vapidPrivateKey);
this.logger.log("Web Push service initialized with VAPID keys");
} else {
this.logger.warn("Web Push service initialized without VAPID keys - push notifications will not work");
}
}
/**
* Send push notification to a single subscription
*/
async sendPushNotification(subscription: WebPushSubscription, payload: WebPushNotificationPayload, userId?: string): Promise<boolean> {
if (!this.config.enabled) {
this.logger.debug("Push notifications are disabled");
return false;
}
if (!this.isConfigured()) {
this.logger.warn("Web Push is not properly configured - missing VAPID keys");
return false;
}
try {
const options = {
TTL: this.config.ttl,
urgency: this.config.urgency as webpush.Urgency,
headers: {},
};
const payloadString = JSON.stringify({
...payload,
timestamp: payload.timestamp || Date.now(),
});
await webpush.sendNotification(subscription, payloadString, options);
this.logger.log(`Push notification sent successfully${userId ? ` to user: ${userId}` : ""}`);
return true;
} catch (error) {
this.logger.error(`Failed to send push notification${userId ? ` to user ${userId}` : ""}:`, error);
// Handle specific web push errors
if (error instanceof Error && "statusCode" in error) {
const webPushError = error as any;
if (webPushError.statusCode === 410 || webPushError.statusCode === 404) {
this.logger.warn("Subscription is no longer valid - should be removed from database");
}
}
return false;
}
}
/**
* Send push notifications to multiple subscriptions
*/
async sendBulkPushNotifications(bulkPayload: BulkWebPushPayload): Promise<{
successful: number;
failed: number;
invalidSubscriptions: WebPushSubscription[];
}> {
const results = {
successful: 0,
failed: 0,
invalidSubscriptions: [] as WebPushSubscription[],
};
const promises = bulkPayload.subscriptions.map(async (subscription) => {
try {
const success = await this.sendPushNotification(subscription, bulkPayload);
if (success) {
results.successful++;
} else {
results.failed++;
}
} catch (error) {
results.failed++;
if (error instanceof Error && "statusCode" in error) {
const webPushError = error as any;
if (webPushError.statusCode === 410 || webPushError.statusCode === 404) {
results.invalidSubscriptions.push(subscription);
}
}
}
});
await Promise.all(promises);
this.logger.log(
`Bulk push notifications completed. Successful: ${results.successful}, Failed: ${results.failed}, Invalid: ${results.invalidSubscriptions.length}`,
);
return results;
}
/**
* Send new email notification
*/
async sendNewEmailNotification(
subscriptions: WebPushSubscription[],
emailSubject: string,
senderName: string,
senderEmail: string,
userId?: string,
): Promise<boolean> {
const truncatedSubject = emailSubject.length > 50 ? emailSubject.substring(0, 47) + "..." : emailSubject;
const payload: WebPushNotificationPayload = {
title: "📧 New Email",
body: `From: ${senderName || senderEmail}\nSubject: ${truncatedSubject}`,
icon: `${process.env.FRONTEND_URL || "https://mail.danakcorp.com"}/assets/email-icon.png`,
badge: `${process.env.FRONTEND_URL || "https://mail.danakcorp.com"}/assets/email-badge.png`,
tag: "new-email",
requireInteraction: true,
data: {
type: "new-email",
userId,
sender: senderEmail,
subject: emailSubject,
url: `${process.env.FRONTEND_URL || "https://mail.danakcorp.com"}/inbox`,
},
actions: [
{
action: "view",
title: "View Email",
icon: `${process.env.FRONTEND_URL || "https://mail.danakcorp.com"}/assets/view-icon.png`,
},
{
action: "dismiss",
title: "Dismiss",
},
],
vibrate: [200, 100, 200],
};
if (subscriptions.length === 1) {
return this.sendPushNotification(subscriptions[0], payload, userId);
} else {
const results = await this.sendBulkPushNotifications({
...payload,
subscriptions,
});
return results.successful > 0;
}
}
/**
* Send test notification
*/
async sendTestNotification(subscriptions: WebPushSubscription[], userId?: string): Promise<boolean> {
const payload: WebPushNotificationPayload = {
title: "🧪 Test Notification",
body: "Your web push notifications are working correctly!",
icon: `${process.env.FRONTEND_URL || "https://mail.danakcorp.com"}/assets/test-icon.png`,
tag: "test",
data: {
type: "test",
userId,
url: `${process.env.FRONTEND_URL || "https://mail.danakcorp.com"}`,
},
};
if (subscriptions.length === 1) {
return this.sendPushNotification(subscriptions[0], payload, userId);
} else {
const results = await this.sendBulkPushNotifications({
...payload,
subscriptions,
});
return results.successful > 0;
}
}
/**
* Generate VAPID keys (for initial setup)
*/
generateVAPIDKeys(): { publicKey: string; privateKey: string } {
const vapidKeys = webpush.generateVAPIDKeys();
return {
publicKey: vapidKeys.publicKey,
privateKey: vapidKeys.privateKey,
};
}
/**
* Get VAPID public key for client-side subscription
*/
getVAPIDPublicKey(): string {
return this.config.webPush.vapidPublicKey;
}
/**
* Check if push notifications are enabled and configured
*/
isEnabled(): boolean {
return this.config.enabled && this.isConfigured();
}
/**
* Check if VAPID keys are properly configured
*/
isConfigured(): boolean {
return !!(this.config.webPush.vapidPublicKey && this.config.webPush.vapidPrivateKey);
}
/**
* Validate a web push subscription object
*/
validateSubscription(subscription: any): subscription is WebPushSubscription {
return (
subscription &&
typeof subscription.endpoint === "string" &&
subscription.keys &&
typeof subscription.keys.p256dh === "string" &&
typeof subscription.keys.auth === "string"
);
}
/**
* Get service configuration info
*/
getServiceInfo() {
return {
enabled: this.isEnabled(),
configured: this.isConfigured(),
vapidPublicKey: this.config.webPush.vapidPublicKey,
ttl: this.config.ttl,
urgency: this.config.urgency,
};
}
}
@@ -1,112 +0,0 @@
import { ApiProperty } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { ArrayMaxSize, ArrayMinSize, IsArray, IsBoolean, IsNotEmpty, IsObject, IsOptional, IsString, ValidateNested } from "class-validator";
export class WebPushKeysDto {
@ApiProperty({
description: "p256dh key for web push subscription",
example: "BNbxxx...",
})
@IsNotEmpty()
@IsString()
p256dh: string;
@ApiProperty({
description: "auth key for web push subscription",
example: "abc123...",
})
@IsNotEmpty()
@IsString()
auth: string;
}
export class WebPushSubscriptionDto {
@ApiProperty({
description: "Push subscription endpoint URL",
example: "https://fcm.googleapis.com/fcm/send/...",
})
@IsNotEmpty()
@IsString()
endpoint: string;
@ApiProperty({
description: "Subscription keys",
type: WebPushKeysDto,
})
@ValidateNested()
@Type(() => WebPushKeysDto)
@IsObject()
keys: WebPushKeysDto;
}
export class RegisterPushSubscriptionDto {
@ApiProperty({
description: "Web push subscription object",
type: WebPushSubscriptionDto,
})
@ValidateNested()
@Type(() => WebPushSubscriptionDto)
@IsObject()
subscription: WebPushSubscriptionDto;
@ApiProperty({
description: "Device type or platform",
example: "desktop",
required: false,
})
@IsOptional()
@IsString()
deviceType?: string;
@ApiProperty({
description: "Device name or identifier",
example: "Chrome on MacBook Pro",
required: false,
})
@IsOptional()
@IsString()
deviceName?: string;
}
export class UpdatePushSubscriptionsDto {
@ApiProperty({
description: "Array of web push subscriptions",
type: [WebPushSubscriptionDto],
})
@IsArray()
@ArrayMinSize(0)
@ArrayMaxSize(10) // Limit to 10 subscriptions per user
@ValidateNested({ each: true })
@Type(() => WebPushSubscriptionDto)
subscriptions: WebPushSubscriptionDto[];
}
export class UpdatePushNotificationSettingsDto {
@ApiProperty({
description: "Enable or disable push notifications",
example: true,
})
@IsBoolean()
enabled: boolean;
}
export class TestPushNotificationDto {
@ApiProperty({
description: "Test message to send",
example: "Test notification from Dmail",
required: false,
})
@IsOptional()
@IsString()
message?: string;
}
export class RemovePushSubscriptionDto {
@ApiProperty({
description: "Endpoint URL of the subscription to remove",
example: "https://fcm.googleapis.com/fcm/send/...",
})
@IsNotEmpty()
@IsString()
endpoint: string;
}
+2 -254
View File
@@ -1,16 +1,8 @@
import { EntityManager } from "@mikro-orm/postgresql"; import { EntityManager } from "@mikro-orm/postgresql";
import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common"; import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { import { BusinessMessage, DomainMessage, MailServerMessage, RoleMessage, TemplateMessage, UserMessage } from "../../../common/enums/message.enum";
BusinessMessage,
CommonMessage,
DomainMessage,
MailServerMessage,
RoleMessage,
TemplateMessage,
UserMessage,
} from "../../../common/enums/message.enum";
import { QUOTA_CONSTANTS } from "../../businesses/constant"; import { QUOTA_CONSTANTS } from "../../businesses/constant";
import { Business } from "../../businesses/entities/business.entity"; import { Business } from "../../businesses/entities/business.entity";
import { DomainStatus } from "../../domains/enums/domain-status.enum"; import { DomainStatus } from "../../domains/enums/domain-status.enum";
@@ -18,18 +10,11 @@ import { DomainAutomationService } from "../../domains/services/domain-automatio
import { DomainsService } from "../../domains/services/domains.service"; import { DomainsService } from "../../domains/services/domains.service";
import { UpdateUserDto } from "../../mail-server/DTO/update-user.dto"; import { UpdateUserDto } from "../../mail-server/DTO/update-user.dto";
import { MailServerService } from "../../mail-server/services/mail-server.service"; import { MailServerService } from "../../mail-server/services/mail-server.service";
import { PushNotificationService } from "../../notifications/services/push-notification.service";
import { QuotaSyncService } from "../../quota-sync/services/quota-sync.service"; import { QuotaSyncService } from "../../quota-sync/services/quota-sync.service";
import { UserSettingsService } from "../../settings/services/user-settings.service"; import { UserSettingsService } from "../../settings/services/user-settings.service";
import { Template } from "../../templates/entities/template.entity"; import { Template } from "../../templates/entities/template.entity";
import { PasswordService } from "../../utils/services/password.service"; import { PasswordService } from "../../utils/services/password.service";
import { CreateEmailUserDto } from "../DTO/create-email-user.dto"; import { CreateEmailUserDto } from "../DTO/create-email-user.dto";
import {
RegisterPushSubscriptionDto,
TestPushNotificationDto,
UpdatePushNotificationSettingsDto,
UpdatePushSubscriptionsDto,
} from "../DTO/push-notification.dto";
import { UpdateEmailUserDto } from "../DTO/update-email-user.dto"; import { UpdateEmailUserDto } from "../DTO/update-email-user.dto";
import { UpdateUserProfileDto } from "../DTO/update-user-profile.dto"; import { UpdateUserProfileDto } from "../DTO/update-user-profile.dto";
import { UserListQueryDto } from "../DTO/user-list-query.dto"; import { UserListQueryDto } from "../DTO/user-list-query.dto";
@@ -51,7 +36,6 @@ export class UsersService {
private readonly domainAutomationService: DomainAutomationService, private readonly domainAutomationService: DomainAutomationService,
private readonly quotaSyncService: QuotaSyncService, private readonly quotaSyncService: QuotaSyncService,
private readonly userSettingsService: UserSettingsService, private readonly userSettingsService: UserSettingsService,
private readonly pushNotificationService: PushNotificationService,
) {} ) {}
async getUserEmailIdFromId(userId: string) { async getUserEmailIdFromId(userId: string) {
@@ -491,240 +475,4 @@ export class UsersService {
this.logger.log(`Updated business quota for ${business.name}: used=${business.usedQuota}, remaining=${business.remainingQuota}`); this.logger.log(`Updated business quota for ${business.name}: used=${business.usedQuota}, remaining=${business.remainingQuota}`);
} }
// Push Notification Methods
async registerPushSubscription(userId: string, registerPushSubscriptionDto: RegisterPushSubscriptionDto) {
try {
const user = await this.userRepository.findOne({ id: userId, deletedAt: null });
if (!user) {
throw new NotFoundException(UserMessage.USER_NOT_FOUND);
}
// Validate the subscription
if (!this.pushNotificationService.validateSubscription(registerPushSubscriptionDto.subscription)) {
throw new BadRequestException("Invalid push subscription format");
}
// Initialize pushTokens array if it doesn't exist (now stores subscriptions)
if (!user.pushTokens) {
user.pushTokens = [];
}
// Check if subscription endpoint already exists
const existingIndex = user.pushTokens.findIndex((sub: any) => sub.endpoint === registerPushSubscriptionDto.subscription.endpoint);
if (existingIndex !== -1) {
// Update existing subscription
user.pushTokens[existingIndex] = {
...registerPushSubscriptionDto.subscription,
deviceType: registerPushSubscriptionDto.deviceType,
deviceName: registerPushSubscriptionDto.deviceName,
registeredAt: new Date(),
};
} else {
// Add new subscription (limit to 10 subscriptions per user)
if (user.pushTokens.length >= 10) {
// Remove oldest subscription
user.pushTokens.shift();
}
user.pushTokens.push({
...registerPushSubscriptionDto.subscription,
deviceType: registerPushSubscriptionDto.deviceType,
deviceName: registerPushSubscriptionDto.deviceName,
registeredAt: new Date(),
});
}
await this.em.flush();
this.logger.log(`Push subscription registered for user ${userId}: ${registerPushSubscriptionDto.deviceType || "unknown device"}`);
return {
message: "Subscription registered successfully",
subscriptionCount: user.pushTokens.length,
endpoint: registerPushSubscriptionDto.subscription.endpoint,
};
} catch (error) {
this.logger.error(`Error registering push subscription for user ${userId}:`, error);
throw error;
}
}
async updatePushSubscriptions(userId: string, updatePushSubscriptionsDto: UpdatePushSubscriptionsDto) {
try {
const user = await this.userRepository.findOne({ id: userId, deletedAt: null });
if (!user) {
throw new NotFoundException(UserMessage.USER_NOT_FOUND);
}
// Validate all subscriptions
for (const subscription of updatePushSubscriptionsDto.subscriptions) {
if (!this.pushNotificationService.validateSubscription(subscription)) {
throw new BadRequestException(`Invalid subscription format for endpoint: ${(subscription as any).endpoint}`);
}
}
user.pushTokens = updatePushSubscriptionsDto.subscriptions.map((sub) => ({
...sub,
updatedAt: new Date(),
}));
await this.em.flush();
this.logger.log(`Push subscriptions updated for user ${userId}: ${updatePushSubscriptionsDto.subscriptions.length} subscriptions`);
return {
message: CommonMessage.UPDATE_SUCCESS,
subscriptionCount: user.pushTokens.length,
};
} catch (error) {
this.logger.error(`Error updating push subscriptions for user ${userId}:`, error);
throw error;
}
}
async removePushSubscription(userId: string, endpoint: string) {
try {
const user = await this.userRepository.findOne({ id: userId, deletedAt: null });
if (!user) {
throw new NotFoundException(UserMessage.USER_NOT_FOUND);
}
if (!user.pushTokens || user.pushTokens.length === 0) {
throw new BadRequestException("No subscriptions found");
}
const subscriptions = user.pushTokens;
const initialCount = subscriptions.length;
user.pushTokens = subscriptions.filter((sub) => sub.endpoint !== endpoint);
if (user.pushTokens.length === initialCount) {
throw new BadRequestException("Subscription not found");
}
await this.em.flush();
this.logger.log(`Push subscription removed for user ${userId}`);
return {
message: "Subscription removed successfully",
subscriptionCount: user.pushTokens.length,
};
} catch (error) {
this.logger.error(`Error removing push subscription for user ${userId}:`, error);
throw error;
}
}
async updatePushNotificationSettings(userId: string, settingsDto: UpdatePushNotificationSettingsDto) {
try {
const user = await this.userRepository.findOne({ id: userId, deletedAt: null });
if (!user) {
throw new NotFoundException(UserMessage.USER_NOT_FOUND);
}
user.pushNotificationsEnabled = settingsDto.enabled;
await this.em.flush();
this.logger.log(`Push notification settings updated for user ${userId}: ${settingsDto.enabled ? "enabled" : "disabled"}`);
return {
message: CommonMessage.UPDATE_SUCCESS,
pushNotificationsEnabled: user.pushNotificationsEnabled,
};
} catch (error) {
this.logger.error(`Error updating push notification settings for user ${userId}:`, error);
throw error;
}
}
async testPushNotification(userId: string, _testDto: TestPushNotificationDto) {
try {
const user = await this.userRepository.findOne({ id: userId, deletedAt: null });
if (!user) {
throw new NotFoundException(UserMessage.USER_NOT_FOUND);
}
if (!user.pushNotificationsEnabled) {
throw new BadRequestException("Push notifications are disabled for this user");
}
if (!user.pushTokens || user.pushTokens.length === 0) {
throw new BadRequestException("No push subscriptions registered for this user");
}
if (!this.pushNotificationService.isEnabled()) {
throw new BadRequestException("Push notification service is disabled");
}
// Convert stored subscriptions to proper format
const subscriptions = user.pushTokens.map((sub) => ({
endpoint: sub.endpoint,
keys: sub.keys,
}));
const success = await this.pushNotificationService.sendTestNotification(subscriptions, user.id);
if (!success) {
throw new BadRequestException("Failed to send test notification");
}
this.logger.log(`Test push notification sent for user ${userId}`);
return {
message: "Test notification sent successfully",
subscriptionCount: subscriptions.length,
serviceInfo: this.pushNotificationService.getServiceInfo(),
};
} catch (error) {
this.logger.error(`Error sending test push notification for user ${userId}:`, error);
throw error;
}
}
async getPushNotificationInfo(userId: string) {
try {
const user = await this.userRepository.findOne({ id: userId, deletedAt: null });
if (!user) {
throw new NotFoundException(UserMessage.USER_NOT_FOUND);
}
return {
pushNotificationsEnabled: user.pushNotificationsEnabled,
subscriptionCount: user.pushTokens?.length || 0,
lastPushNotificationAt: user.lastPushNotificationAt,
serviceEnabled: this.pushNotificationService.isEnabled(),
vapidPublicKey: this.pushNotificationService.getVAPIDPublicKey(),
serviceInfo: this.pushNotificationService.getServiceInfo(),
setup: {
instructions: "Register your browser for push notifications using the service worker",
documentation: {
webPushAPI: "https://developer.mozilla.org/en-US/docs/Web/API/Push_API",
serviceWorkers: "https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API",
},
},
};
} catch (error) {
this.logger.error(`Error getting push notification info for user ${userId}:`, error);
throw error;
}
}
async getVAPIDPublicKey() {
try {
const vapidPublicKey = this.pushNotificationService.getVAPIDPublicKey();
if (!vapidPublicKey) {
throw new BadRequestException("VAPID public key is not configured");
}
return {
vapidPublicKey,
serviceInfo: this.pushNotificationService.getServiceInfo(),
};
} catch (error) {
this.logger.error("Error getting VAPID public key:", error);
throw error;
}
}
} }
-56
View File
@@ -2,12 +2,6 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseIntercepto
import { ApiOperation, ApiResponse } from "@nestjs/swagger"; import { ApiOperation, ApiResponse } from "@nestjs/swagger";
import { CreateEmailUserDto } from "./DTO/create-email-user.dto"; import { CreateEmailUserDto } from "./DTO/create-email-user.dto";
import {
RegisterPushSubscriptionDto,
TestPushNotificationDto,
UpdatePushNotificationSettingsDto,
UpdatePushSubscriptionsDto,
} from "./DTO/push-notification.dto";
import { UpdateEmailUserDto } from "./DTO/update-email-user.dto"; import { UpdateEmailUserDto } from "./DTO/update-email-user.dto";
import { UpdateUserProfileDto } from "./DTO/update-user-profile.dto"; import { UpdateUserProfileDto } from "./DTO/update-user-profile.dto";
import { UserListQueryDto } from "./DTO/user-list-query.dto"; import { UserListQueryDto } from "./DTO/user-list-query.dto";
@@ -45,56 +39,6 @@ export class UsersController {
return this.usersService.updateProfile(userId, updateMeDto); return this.usersService.updateProfile(userId, updateMeDto);
} }
// Push Notification Endpoints
@Post("push-subscriptions")
@ApiOperation({ summary: "Register web push subscription" })
@ApiResponse({ status: 201, description: "Push subscription registered successfully" })
registerPushSubscription(@UserDec("id") userId: string, @Body() registerPushSubscriptionDto: RegisterPushSubscriptionDto) {
return this.usersService.registerPushSubscription(userId, registerPushSubscriptionDto);
}
@Patch("push-subscriptions")
@ApiOperation({ summary: "Update web push subscriptions" })
@ApiResponse({ status: 200, description: "Push subscriptions updated successfully" })
updatePushSubscriptions(@UserDec("id") userId: string, @Body() updatePushSubscriptionsDto: UpdatePushSubscriptionsDto) {
return this.usersService.updatePushSubscriptions(userId, updatePushSubscriptionsDto);
}
@Delete("push-subscriptions")
@ApiOperation({ summary: "Remove web push subscription by endpoint" })
@ApiResponse({ status: 200, description: "Push subscription removed successfully" })
removePushSubscription(@UserDec("id") userId: string, @Body() body: { endpoint: string }) {
return this.usersService.removePushSubscription(userId, body.endpoint);
}
@Patch("push-notifications/settings")
@ApiOperation({ summary: "Update push notification settings" })
@ApiResponse({ status: 200, description: "Push notification settings updated successfully" })
updatePushNotificationSettings(@UserDec("id") userId: string, @Body() settingsDto: UpdatePushNotificationSettingsDto) {
return this.usersService.updatePushNotificationSettings(userId, settingsDto);
}
@Post("push-notifications/test")
@ApiOperation({ summary: "Test web push notification" })
@ApiResponse({ status: 200, description: "Test notification sent successfully" })
testPushNotification(@UserDec("id") userId: string, @Body() testDto: TestPushNotificationDto) {
return this.usersService.testPushNotification(userId, testDto);
}
@Get("push-notifications/info")
@ApiOperation({ summary: "Get web push notification info and settings" })
@ApiResponse({ status: 200, description: "Push notification info retrieved successfully" })
getPushNotificationInfo(@UserDec("id") userId: string) {
return this.usersService.getPushNotificationInfo(userId);
}
@Get("push-notifications/vapid-key")
@ApiOperation({ summary: "Get VAPID public key for client-side subscription" })
@ApiResponse({ status: 200, description: "VAPID public key retrieved successfully" })
getVAPIDPublicKey(@UserDec("id") _userId: string) {
return this.usersService.getVAPIDPublicKey();
}
@Post() @Post()
@UseInterceptors(BusinessInterceptor) @UseInterceptors(BusinessInterceptor)
@ApiOperation({ summary: "Create a new email user" }) @ApiOperation({ summary: "Create a new email user" })