From a753badcef576fc99f1698988b49d5aa99305951 Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Sat, 26 Jul 2025 13:02:30 +0330 Subject: [PATCH] chore: fix ai service --- REACT_WEB_PUSH_README.md | 1153 ----------------- WEB_PUSH_SETUP.md | 579 --------- .../DTO/write-email-assistance.dto.ts | 21 - .../services/ai-assistant.service.ts | 2 +- src/modules/email/services/email.service.ts | 4 - .../handlers/new-email.handler.ts | 11 +- .../handlers/push-notification.handler.ts | 75 -- .../notifications/notifications.module.ts | 6 +- .../services/push-notification.service.ts | 277 ---- .../users/DTO/push-notification.dto.ts | 112 -- src/modules/users/services/users.service.ts | 256 +--- src/modules/users/users.controller.ts | 56 - 12 files changed, 5 insertions(+), 2547 deletions(-) delete mode 100644 REACT_WEB_PUSH_README.md delete mode 100644 WEB_PUSH_SETUP.md delete mode 100644 src/modules/notifications/handlers/push-notification.handler.ts delete mode 100644 src/modules/notifications/services/push-notification.service.ts delete mode 100644 src/modules/users/DTO/push-notification.dto.ts diff --git a/REACT_WEB_PUSH_README.md b/REACT_WEB_PUSH_README.md deleted file mode 100644 index 0e0ea1f..0000000 --- a/REACT_WEB_PUSH_README.md +++ /dev/null @@ -1,1153 +0,0 @@ -# React Web Push Notifications - Frontend Implementation Guide - -A complete guide for implementing web push notifications in your React application with the Dmail API. - -## ๐Ÿš€ Quick Start - -### Prerequisites - -- React 16.8+ (for hooks support) -- TypeScript (recommended) -- HTTPS-enabled domain (required for service workers) -- Modern browser support (Chrome 50+, Firefox 44+, Safari 16+, Edge 17+) - -### Installation - -```bash -# No additional dependencies needed - uses native Web Push API -# Optional: Install types for better TypeScript support -npm install --save-dev @types/web-push -``` - -## ๐Ÿ“ Project Structure - -``` -src/ -โ”œโ”€โ”€ hooks/ -โ”‚ โ”œโ”€โ”€ usePushNotifications.ts -โ”‚ โ””โ”€โ”€ useServiceWorker.ts -โ”œโ”€โ”€ components/ -โ”‚ โ”œโ”€โ”€ PushNotificationPrompt.tsx -โ”‚ โ”œโ”€โ”€ NotificationSettings.tsx -โ”‚ โ””โ”€โ”€ PushNotificationButton.tsx -โ”œโ”€โ”€ contexts/ -โ”‚ โ””โ”€โ”€ PushNotificationContext.tsx -โ”œโ”€โ”€ services/ -โ”‚ โ””โ”€โ”€ pushNotificationService.ts -โ”œโ”€โ”€ types/ -โ”‚ โ””โ”€โ”€ pushNotifications.ts -โ””โ”€โ”€ utils/ - โ””โ”€โ”€ vapidHelper.ts -public/ -โ””โ”€โ”€ sw.js (Service Worker) -``` - -## ๐Ÿ”ง Implementation - -### 1. Service Worker Setup - -Create `public/sw.js`: - -```javascript -// Service Worker for handling push notifications -const CACHE_NAME = "dmail-push-v1"; - -// Install event -self.addEventListener("install", (event) => { - console.log("Service Worker installing"); - self.skipWaiting(); -}); - -// Activate event -self.addEventListener("activate", (event) => { - console.log("Service Worker activating"); - event.waitUntil(clients.claim()); -}); - -// Push event handler -self.addEventListener("push", (event) => { - console.log("Push message received:", event); - - if (!event.data) { - console.log("Push event but no data"); - return; - } - - try { - const data = event.data.json(); - console.log("Push data:", data); - - const options = { - body: data.body, - icon: data.icon || "/icons/email-icon-192.png", - badge: data.badge || "/icons/email-badge-72.png", - tag: data.tag || "email-notification", - requireInteraction: data.requireInteraction || true, - actions: data.actions || [ - { - action: "view", - title: "View Email", - icon: "/icons/view-icon-32.png", - }, - { - action: "dismiss", - title: "Dismiss", - }, - ], - data: { - ...data.data, - timestamp: Date.now(), - }, - vibrate: data.vibrate || [200, 100, 200], - silent: data.silent || false, - }; - - event.waitUntil(self.registration.showNotification(data.title, options)); - } catch (error) { - console.error("Error parsing push data:", error); - - // Show generic notification if parsing fails - event.waitUntil( - self.registration.showNotification("New Email", { - body: "You have received a new email", - icon: "/icons/email-icon-192.png", - tag: "generic-email", - }), - ); - } -}); - -// Notification click handler -self.addEventListener("notificationclick", (event) => { - console.log("Notification clicked:", event); - - event.notification.close(); - - const action = event.action; - const data = event.notification.data; - - if (action === "view") { - // Open the email in the app - event.waitUntil(clients.openWindow(data.url || "/inbox")); - } else if (action === "dismiss") { - // Just close the notification (already done above) - return; - } else { - // Default click action - open the app - event.waitUntil(clients.openWindow(data.url || "/")); - } -}); - -// Background sync (optional - for future enhancements) -self.addEventListener("sync", (event) => { - if (event.tag === "email-sync") { - event.waitUntil( - // Handle background email sync - console.log("Background sync triggered"), - ); - } -}); -``` - -### 2. TypeScript Types - -Create `src/types/pushNotifications.ts`: - -```typescript -export interface WebPushSubscription { - endpoint: string; - keys: { - p256dh: string; - auth: string; - }; -} - -export interface RegisterPushSubscriptionRequest { - subscription: WebPushSubscription; - deviceType?: string; - deviceName?: string; -} - -export interface PushNotificationSettings { - enabled: boolean; - subscriptionCount: number; - lastPushNotificationAt?: string; - serviceEnabled: boolean; - vapidPublicKey: string; -} - -export interface TestNotificationRequest { - message?: string; -} - -export interface PushNotificationInfo { - pushNotificationsEnabled: boolean; - subscriptionCount: number; - lastPushNotificationAt?: string; - serviceEnabled: boolean; - vapidPublicKey: string; - serviceInfo: { - enabled: boolean; - configured: boolean; - vapidPublicKey: string; - ttl: number; - urgency: string; - }; - setup: { - instructions: string; - documentation: { - webPushAPI: string; - serviceWorkers: string; - }; - }; -} - -export interface PushNotificationContextValue { - isSupported: boolean; - isSubscribed: boolean; - isLoading: boolean; - error: string | null; - subscriptionCount: number; - subscribe: () => Promise; - unsubscribe: () => Promise; - testNotification: () => Promise; - updateSettings: (enabled: boolean) => Promise; - clearError: () => void; -} -``` - -### 3. API Service - -Create `src/services/pushNotificationService.ts`: - -```typescript -import { WebPushSubscription, RegisterPushSubscriptionRequest, PushNotificationInfo, TestNotificationRequest } from "../types/pushNotifications"; - -class PushNotificationService { - private baseURL: string; - private getAuthToken: () => string | null; - - constructor(baseURL: string, getAuthToken: () => string | null) { - this.baseURL = baseURL; - this.getAuthToken = getAuthToken; - } - - private async makeRequest(endpoint: string, options: RequestInit = {}): Promise { - const token = this.getAuthToken(); - - const response = await fetch(`${this.baseURL}${endpoint}`, { - ...options, - headers: { - "Content-Type": "application/json", - ...(token && { Authorization: `Bearer ${token}` }), - ...options.headers, - }, - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`); - } - - return response.json(); - } - - async getVAPIDPublicKey(): Promise<{ vapidPublicKey: string }> { - return this.makeRequest("/users/push-notifications/vapid-key"); - } - - async registerSubscription(data: RegisterPushSubscriptionRequest): Promise { - return this.makeRequest("/users/push-subscriptions", { - method: "POST", - body: JSON.stringify(data), - }); - } - - async removeSubscription(endpoint: string): Promise { - return this.makeRequest("/users/push-subscriptions", { - method: "DELETE", - body: JSON.stringify({ endpoint }), - }); - } - - async updateSettings(enabled: boolean): Promise { - return this.makeRequest("/users/push-notifications/settings", { - method: "PATCH", - body: JSON.stringify({ enabled }), - }); - } - - async getNotificationInfo(): Promise { - return this.makeRequest("/users/push-notifications/info"); - } - - async testNotification(data: TestNotificationRequest = {}): Promise { - return this.makeRequest("/users/push-notifications/test", { - method: "POST", - body: JSON.stringify(data), - }); - } -} - -export default PushNotificationService; -``` - -### 4. Utility Helper - -Create `src/utils/vapidHelper.ts`: - -```typescript -export function urlBase64ToUint8Array(base64String: string): Uint8Array { - 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; -} - -export function getBrowserInfo(): string { - const userAgent = navigator.userAgent; - let browserName = "Unknown"; - - if (userAgent.includes("Chrome")) { - browserName = "Chrome"; - } else if (userAgent.includes("Firefox")) { - browserName = "Firefox"; - } else if (userAgent.includes("Safari")) { - browserName = "Safari"; - } else if (userAgent.includes("Edge")) { - browserName = "Edge"; - } else if (userAgent.includes("Opera")) { - browserName = "Opera"; - } - - return `${browserName} on ${navigator.platform}`; -} - -export function isPushSupported(): boolean { - return "serviceWorker" in navigator && "PushManager" in window && "Notification" in window; -} -``` - -### 5. Service Worker Hook - -Create `src/hooks/useServiceWorker.ts`: - -```typescript -import { useEffect, useState, useCallback } from "react"; - -interface UseServiceWorkerReturn { - registration: ServiceWorkerRegistration | null; - isLoading: boolean; - error: string | null; - register: () => Promise; -} - -export function useServiceWorker(scriptURL = "/sw.js"): UseServiceWorkerReturn { - const [registration, setRegistration] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - - const register = useCallback(async () => { - if (!("serviceWorker" in navigator)) { - setError("Service Workers are not supported in this browser"); - setIsLoading(false); - return; - } - - try { - setIsLoading(true); - setError(null); - - const reg = await navigator.serviceWorker.register(scriptURL); - - // Wait for the service worker to be ready - await navigator.serviceWorker.ready; - - setRegistration(reg); - console.log("Service Worker registered successfully:", reg); - } catch (err) { - const errorMessage = err instanceof Error ? err.message : "Failed to register service worker"; - setError(errorMessage); - console.error("Service Worker registration failed:", err); - } finally { - setIsLoading(false); - } - }, [scriptURL]); - - useEffect(() => { - register(); - }, [register]); - - return { registration, isLoading, error, register }; -} -``` - -### 6. Push Notifications Hook - -Create `src/hooks/usePushNotifications.ts`: - -```typescript -import { useState, useEffect, useCallback } from "react"; -import { WebPushSubscription } from "../types/pushNotifications"; -import PushNotificationService from "../services/pushNotificationService"; -import { urlBase64ToUint8Array, getBrowserInfo, isPushSupported } from "../utils/vapidHelper"; - -interface UsePushNotificationsProps { - apiService: PushNotificationService; -} - -interface UsePushNotificationsReturn { - isSupported: boolean; - isSubscribed: boolean; - isLoading: boolean; - error: string | null; - subscriptionCount: number; - permission: NotificationPermission; - subscribe: () => Promise; - unsubscribe: () => Promise; - testNotification: () => Promise; - checkSubscriptionStatus: () => Promise; - clearError: () => void; -} - -export function usePushNotifications({ apiService }: UsePushNotificationsProps): UsePushNotificationsReturn { - const [isSubscribed, setIsSubscribed] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [subscriptionCount, setSubscriptionCount] = useState(0); - const [permission, setPermission] = useState("default"); - - const isSupported = isPushSupported(); - - const clearError = useCallback(() => { - setError(null); - }, []); - - const checkSubscriptionStatus = useCallback(async () => { - if (!isSupported) return; - - try { - setIsLoading(true); - - // Check browser subscription - const registration = await navigator.serviceWorker.ready; - const subscription = await registration.pushManager.getSubscription(); - setIsSubscribed(!!subscription); - - // Check server-side info - const info = await apiService.getNotificationInfo(); - setSubscriptionCount(info.subscriptionCount); - - // Check permission status - setPermission(Notification.permission); - } catch (err) { - console.error("Error checking subscription status:", err); - setError(err instanceof Error ? err.message : "Failed to check subscription status"); - } finally { - setIsLoading(false); - } - }, [apiService, isSupported]); - - const subscribe = useCallback(async (): Promise => { - if (!isSupported) { - throw new Error("Push notifications are not supported in this browser"); - } - - try { - setIsLoading(true); - setError(null); - - // Request notification permission - const permission = await Notification.requestPermission(); - setPermission(permission); - - if (permission !== "granted") { - throw new Error("Notification permission denied"); - } - - // Get VAPID public key - const { vapidPublicKey } = await apiService.getVAPIDPublicKey(); - - if (!vapidPublicKey) { - throw new Error("VAPID public key not available"); - } - - // Get service worker registration - const registration = await navigator.serviceWorker.ready; - - // Create push subscription - const subscription = await registration.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: urlBase64ToUint8Array(vapidPublicKey), - }); - - // Register subscription with server - await apiService.registerSubscription({ - subscription: subscription.toJSON() as WebPushSubscription, - deviceType: "web", - deviceName: getBrowserInfo(), - }); - - setIsSubscribed(true); - await checkSubscriptionStatus(); // Refresh subscription count - } catch (err) { - const errorMessage = err instanceof Error ? err.message : "Failed to subscribe to push notifications"; - setError(errorMessage); - throw new Error(errorMessage); - } finally { - setIsLoading(false); - } - }, [apiService, isSupported, checkSubscriptionStatus]); - - const unsubscribe = useCallback(async (): Promise => { - if (!isSupported) return; - - try { - setIsLoading(true); - setError(null); - - const registration = await navigator.serviceWorker.ready; - const subscription = await registration.pushManager.getSubscription(); - - if (subscription) { - // Unsubscribe from browser - await subscription.unsubscribe(); - - // Remove from server - await apiService.removeSubscription(subscription.endpoint); - } - - setIsSubscribed(false); - await checkSubscriptionStatus(); // Refresh subscription count - } catch (err) { - const errorMessage = err instanceof Error ? err.message : "Failed to unsubscribe from push notifications"; - setError(errorMessage); - throw new Error(errorMessage); - } finally { - setIsLoading(false); - } - }, [apiService, isSupported, checkSubscriptionStatus]); - - const testNotification = useCallback(async (): Promise => { - try { - setError(null); - await apiService.testNotification({ - message: "Test notification from your React app!", - }); - } catch (err) { - const errorMessage = err instanceof Error ? err.message : "Failed to send test notification"; - setError(errorMessage); - throw new Error(errorMessage); - } - }, [apiService]); - - useEffect(() => { - if (isSupported) { - checkSubscriptionStatus(); - } else { - setIsLoading(false); - } - }, [checkSubscriptionStatus, isSupported]); - - return { - isSupported, - isSubscribed, - isLoading, - error, - subscriptionCount, - permission, - subscribe, - unsubscribe, - testNotification, - checkSubscriptionStatus, - clearError, - }; -} -``` - -### 7. React Context Provider - -Create `src/contexts/PushNotificationContext.tsx`: - -```tsx -import React, { createContext, useContext, ReactNode } from "react"; -import { usePushNotifications } from "../hooks/usePushNotifications"; -import PushNotificationService from "../services/pushNotificationService"; -import { PushNotificationContextValue } from "../types/pushNotifications"; - -const PushNotificationContext = createContext(undefined); - -interface PushNotificationProviderProps { - children: ReactNode; - apiBaseURL: string; - getAuthToken: () => string | null; -} - -export function PushNotificationProvider({ children, apiBaseURL, getAuthToken }: PushNotificationProviderProps) { - const apiService = new PushNotificationService(apiBaseURL, getAuthToken); - - const { isSupported, isSubscribed, isLoading, error, subscriptionCount, subscribe, unsubscribe, testNotification, clearError } = - usePushNotifications({ apiService }); - - const updateSettings = async (enabled: boolean): Promise => { - await apiService.updateSettings(enabled); - }; - - const value: PushNotificationContextValue = { - isSupported, - isSubscribed, - isLoading, - error, - subscriptionCount, - subscribe, - unsubscribe, - testNotification, - updateSettings, - clearError, - }; - - return {children}; -} - -export function usePushNotificationContext(): PushNotificationContextValue { - const context = useContext(PushNotificationContext); - if (context === undefined) { - throw new Error("usePushNotificationContext must be used within a PushNotificationProvider"); - } - return context; -} -``` - -### 8. React Components - -#### Push Notification Button Component - -Create `src/components/PushNotificationButton.tsx`: - -```tsx -import React from "react"; -import { usePushNotificationContext } from "../contexts/PushNotificationContext"; - -interface PushNotificationButtonProps { - className?: string; - enabledText?: string; - disabledText?: string; - loadingText?: string; - unsupportedText?: string; -} - -export function PushNotificationButton({ - className = "btn btn-primary", - enabledText = "Disable Notifications", - disabledText = "Enable Notifications", - loadingText = "Loading...", - unsupportedText = "Not Supported", -}: PushNotificationButtonProps) { - const { isSupported, isSubscribed, isLoading, error, subscribe, unsubscribe, clearError } = usePushNotificationContext(); - - const handleClick = async () => { - try { - clearError(); - if (isSubscribed) { - await unsubscribe(); - } else { - await subscribe(); - } - } catch (err) { - // Error is handled by the context - console.error("Push notification action failed:", err); - } - }; - - if (!isSupported) { - return ( - - ); - } - - return ( -
- - - {error &&
{error}
} -
- ); -} -``` - -#### Notification Settings Component - -Create `src/components/NotificationSettings.tsx`: - -```tsx -import React from "react"; -import { usePushNotificationContext } from "../contexts/PushNotificationContext"; - -export function NotificationSettings() { - const { isSupported, isSubscribed, isLoading, error, subscriptionCount, subscribe, unsubscribe, testNotification, clearError } = - usePushNotificationContext(); - - const handleTest = async () => { - try { - clearError(); - await testNotification(); - alert("Test notification sent! Check your browser notifications."); - } catch (err) { - console.error("Test notification failed:", err); - } - }; - - if (!isSupported) { - return ( -
-

Push Notifications Not Supported

-

- Your browser doesn't support push notifications. Please use a modern browser like Chrome, Firefox, Safari 16+, or Edge. -

-
- ); - } - - return ( -
-

๐Ÿ“ฌ Email Notifications

- -
-
-
-

Desktop Notifications

-

Get notified instantly when new emails arrive, even when the app is closed.

-
- -
- -
-
- - {error && ( -
-

{error}

- -
- )} - - {isSubscribed && ( -
-
-

- โœ… Notifications are enabled! You have {subscriptionCount} active subscription{subscriptionCount !== 1 ? "s" : ""}. -

-
- - -
- )} - -
-

- ๐Ÿ”’ Your notification preferences are stored securely and can be changed at any time. Notifications work even when your browser is closed. -

-
-
-
- ); -} -``` - -#### Notification Prompt Component - -Create `src/components/PushNotificationPrompt.tsx`: - -```tsx -import React, { useState } from "react"; -import { usePushNotificationContext } from "../contexts/PushNotificationContext"; - -interface PushNotificationPromptProps { - onDismiss?: () => void; - autoShow?: boolean; -} - -export function PushNotificationPrompt({ onDismiss, autoShow = true }: PushNotificationPromptProps) { - const [isDismissed, setIsDismissed] = useState(false); - const { isSupported, isSubscribed, isLoading, subscribe, clearError } = usePushNotificationContext(); - - const handleEnable = async () => { - try { - clearError(); - await subscribe(); - } catch (err) { - console.error("Failed to enable notifications:", err); - } - }; - - const handleDismiss = () => { - setIsDismissed(true); - onDismiss?.(); - }; - - // Don't show if not supported, already subscribed, or manually dismissed - if (!isSupported || isSubscribed || isDismissed || !autoShow) { - return null; - } - - return ( -
-
-
-
๐Ÿ“ง
-
-

Stay Updated with Email Notifications

-

Get instant notifications when new emails arrive, even when the app is closed.

-
- -
- -
- - -
-
-
- ); -} -``` - -### 9. App Integration - -Update your main `App.tsx`: - -```tsx -import React from "react"; -import { PushNotificationProvider } from "./contexts/PushNotificationContext"; -import { NotificationSettings } from "./components/NotificationSettings"; -import { PushNotificationPrompt } from "./components/PushNotificationPrompt"; - -function App() { - // Replace with your actual API base URL and auth token logic - const apiBaseURL = process.env.REACT_APP_API_URL || "https://api.yourdomain.com"; - const getAuthToken = () => localStorage.getItem("authToken"); - - return ( - -
- {/* Your existing app content */} -
-

Dmail - Email Client

-
- -
- {/* Your existing routes and components */} - - {/* Add notification settings to your settings page */} -
- -
-
- - {/* Notification prompt - shows automatically for new users */} - -
-
- ); -} - -export default App; -``` - -## ๐ŸŽจ Styling Examples - -### Tailwind CSS Classes - -The components use Tailwind CSS classes. If you're not using Tailwind, here are equivalent CSS styles: - -```css -/* Button styles */ -.btn { - padding: 0.5rem 1rem; - border-radius: 0.375rem; - font-weight: 500; - cursor: pointer; - border: none; - transition: background-color 0.2s; -} - -.btn-primary { - background-color: #3b82f6; - color: white; -} - -.btn-primary:hover { - background-color: #2563eb; -} - -.btn-primary:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -/* Notification prompt */ -.notification-prompt { - position: fixed; - bottom: 1rem; - right: 1rem; - max-width: 24rem; - background: white; - border: 1px solid #d1d5db; - border-radius: 0.5rem; - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); - z-index: 50; - padding: 1rem; -} - -/* Error messages */ -.error-message { - background-color: #fef2f2; - border: 1px solid #fecaca; - color: #dc2626; - padding: 0.75rem; - border-radius: 0.375rem; - font-size: 0.875rem; -} - -/* Success messages */ -.success-message { - background-color: #f0fdf4; - border: 1px solid #bbf7d0; - color: #16a34a; - padding: 0.75rem; - border-radius: 0.375rem; - font-size: 0.875rem; -} -``` - -## ๐Ÿงช Testing - -### Testing Push Notifications - -1. **Development Testing**: - - ```typescript - // Add to your component for testing - const { testNotification } = usePushNotificationContext(); - - const handleTest = async () => { - try { - await testNotification(); - console.log("Test notification sent!"); - } catch (error) { - console.error("Test failed:", error); - } - }; - ``` - -2. **Browser Developer Tools**: - - Open Chrome DevTools โ†’ Application โ†’ Service Workers - - Check if your service worker is registered and active - - Use Application โ†’ Storage โ†’ IndexedDB to inspect subscription data - -3. **Manual Testing Checklist**: - - [ ] Service worker registers successfully - - [ ] Permission request works - - [ ] Subscription creates and stores properly - - [ ] Test notification displays correctly - - [ ] Notification actions work (click to open app) - - [ ] Unsubscribe removes subscription - - [ ] Works when browser is closed - -## ๐Ÿ“ฑ Icons and Assets - -Add these icons to your `public/icons/` directory: - -- `email-icon-192.png` - Main notification icon (192x192px) -- `email-badge-72.png` - Notification badge (72x72px) -- `view-icon-32.png` - Action button icon (32x32px) - -## ๐Ÿ”ง Environment Variables - -Create a `.env` file in your React app: - -```bash -# API Configuration -REACT_APP_API_URL=https://api.yourdomain.com - -# Optional: Enable debug mode -REACT_APP_DEBUG_PUSH=true - -# Optional: Custom service worker path -REACT_APP_SW_PATH=/sw.js -``` - -## ๐Ÿš€ Deployment Considerations - -### HTTPS Requirement - -- Push notifications require HTTPS in production -- Service workers only work over HTTPS (except on localhost) -- Ensure your domain has a valid SSL certificate - -### Service Worker Caching - -- The service worker file (`sw.js`) should not be cached by your CDN -- Add appropriate cache headers: - ``` - Cache-Control: no-cache, no-store, must-revalidate - ``` - -### Build Configuration - -- Ensure `sw.js` is copied to your build output -- Most React build tools automatically copy files from `public/` - -## ๐Ÿ› Troubleshooting - -### Common Issues - -1. **Service Worker Not Registering**: - - ```javascript - // Check in browser console - navigator.serviceWorker.getRegistrations().then((registrations) => { - console.log("Active registrations:", registrations); - }); - ``` - -2. **Push Subscription Fails**: - - Verify VAPID keys are correct - - Check browser permissions - - Ensure HTTPS is used - -3. **Notifications Not Appearing**: - - Check browser notification settings - - Verify notification permission is granted - - Check if "Do Not Disturb" is enabled - -4. **CORS Issues**: - - Ensure your API allows requests from your frontend domain - - Check CORS headers include your domain - -### Debug Mode - -Enable debug logging: - -```typescript -// Add to your service or component -const DEBUG = process.env.REACT_APP_DEBUG_PUSH === "true"; - -if (DEBUG) { - console.log("Push notification debug info:", { - isSupported: isPushSupported(), - permission: Notification.permission, - serviceWorkerReady: "serviceWorker" in navigator, - }); -} -``` - -## ๐Ÿ”’ Security Best Practices - -1. **Validate Permissions**: Always check notification permission before attempting to subscribe -2. **Error Handling**: Implement proper error boundaries and user feedback -3. **Token Management**: Securely store and refresh authentication tokens -4. **Content Security Policy**: Configure CSP headers to allow service worker execution -5. **VAPID Keys**: Never expose private VAPID keys in frontend code - -## ๐Ÿ“ˆ Analytics and Monitoring - -Track push notification metrics: - -```typescript -// Example analytics tracking -const trackPushEvent = (event: string, data?: any) => { - // Replace with your analytics service - if (window.gtag) { - window.gtag("event", event, { - event_category: "push_notifications", - ...data, - }); - } -}; - -// Usage in components -const { subscribe } = usePushNotificationContext(); - -const handleSubscribe = async () => { - try { - await subscribe(); - trackPushEvent("push_subscription_success"); - } catch (error) { - trackPushEvent("push_subscription_error", { error: error.message }); - } -}; -``` - -## ๐Ÿš€ Performance Optimization - -1. **Lazy Loading**: Load push notification components only when needed -2. **Service Worker Caching**: Cache API responses for better offline experience -3. **Bundle Splitting**: Separate push notification code into its own chunk -4. **Error Boundaries**: Prevent push notification errors from crashing the app - -## ๐Ÿ“‹ Checklist for Production - -- [ ] HTTPS enabled on production domain -- [ ] VAPID keys generated and configured -- [ ] Service worker file accessible at `/sw.js` -- [ ] Icons and assets optimized and available -- [ ] Error handling implemented -- [ ] Browser compatibility tested -- [ ] Permission request UX implemented -- [ ] Analytics tracking configured -- [ ] Backup notification strategy (email fallback) - -## ๐Ÿ†˜ Support and Resources - -- [Web Push API Documentation](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) -- [Service Workers Guide](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) -- [Notification API](https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API) -- [VAPID Specification](https://tools.ietf.org/html/rfc8292) - ---- - -## ๐ŸŽ‰ You're Ready! - -This complete React implementation provides: - -- โœ… Type-safe TypeScript integration -- โœ… Modern React hooks and context -- โœ… Comprehensive error handling -- โœ… Production-ready components -- โœ… Excellent user experience -- โœ… Full browser compatibility - -Your users will now receive instant push notifications for new emails, even when your web app is closed! ๐Ÿš€ diff --git a/WEB_PUSH_SETUP.md b/WEB_PUSH_SETUP.md deleted file mode 100644 index 0361cdd..0000000 --- a/WEB_PUSH_SETUP.md +++ /dev/null @@ -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 - - - - -``` - -## 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 diff --git a/src/modules/ai-assistant/DTO/write-email-assistance.dto.ts b/src/modules/ai-assistant/DTO/write-email-assistance.dto.ts index ecbe62e..5bb0563 100644 --- a/src/modules/ai-assistant/DTO/write-email-assistance.dto.ts +++ b/src/modules/ai-assistant/DTO/write-email-assistance.dto.ts @@ -11,24 +11,3 @@ export class WriteEmailAssistanceDto { }) 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[]; -} diff --git a/src/modules/ai-assistant/services/ai-assistant.service.ts b/src/modules/ai-assistant/services/ai-assistant.service.ts index 26f06ff..2416e51 100644 --- a/src/modules/ai-assistant/services/ai-assistant.service.ts +++ b/src/modules/ai-assistant/services/ai-assistant.service.ts @@ -34,7 +34,7 @@ export class AiAssistantService { async assistEmailWriting(writeAssistanceDto: WriteEmailAssistanceDto, userId: string) { const assistance = await this.generateEmailWritingAssistance(writeAssistanceDto, userId); - return assistance; + return { assistance }; } //************************************************* */ diff --git a/src/modules/email/services/email.service.ts b/src/modules/email/services/email.service.ts index 5340f2c..c16d541 100644 --- a/src/modules/email/services/email.service.ts +++ b/src/modules/email/services/email.service.ts @@ -116,10 +116,6 @@ export class EmailService { wildDuckQuery.query = 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.to) wildDuckQuery.to = query.to; diff --git a/src/modules/notifications/handlers/new-email.handler.ts b/src/modules/notifications/handlers/new-email.handler.ts index 3554a7a..03d7622 100644 --- a/src/modules/notifications/handlers/new-email.handler.ts +++ b/src/modules/notifications/handlers/new-email.handler.ts @@ -2,7 +2,6 @@ import { EntityManager } from "@mikro-orm/postgresql"; import { Injectable, Logger } from "@nestjs/common"; import { BaseNotificationHandler } from "./base-notification.handler"; -import { PushNotificationHandler } from "./push-notification.handler"; import { INewEmailNotificationData } from "../interfaces/ISendNotificationData"; import { NotificationsService } from "../services/notifications.service"; @@ -10,21 +9,13 @@ import { NotificationsService } from "../services/notifications.service"; export class NewEmailHandler extends BaseNotificationHandler { protected readonly logger = new Logger(NewEmailHandler.name); - constructor( - private readonly notificationsService: NotificationsService, - private readonly pushNotificationHandler: PushNotificationHandler, - ) { + constructor(private readonly notificationsService: NotificationsService) { super(); } protected async handle(recipientId: string, data: INewEmailNotificationData, em: EntityManager): Promise { try { - // Create in-app notification 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}`); } catch (error) { this.logger.error(`Error processing new email notifications for user ${recipientId}:`, error); diff --git a/src/modules/notifications/handlers/push-notification.handler.ts b/src/modules/notifications/handlers/push-notification.handler.ts deleted file mode 100644 index ee519ba..0000000 --- a/src/modules/notifications/handlers/push-notification.handler.ts +++ /dev/null @@ -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 { - protected readonly logger = new Logger(PushNotificationHandler.name); - - constructor(private readonly pushNotificationService: PushNotificationService) { - super(); - } - - protected async handle(recipientId: string, data: INewEmailNotificationData, em: EntityManager): Promise { - 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"; - } -} diff --git a/src/modules/notifications/notifications.module.ts b/src/modules/notifications/notifications.module.ts index 4e9de95..724a742 100755 --- a/src/modules/notifications/notifications.module.ts +++ b/src/modules/notifications/notifications.module.ts @@ -8,12 +8,10 @@ import { Notification } from "./entities/notification.entity"; import { ChangePasswordHandler } from "./handlers/change-password.handler"; import { NewEmailHandler } from "./handlers/new-email.handler"; import { NotificationHandlerFactory } from "./handlers/notification-handler.factory"; -import { PushNotificationHandler } from "./handlers/push-notification.handler"; import { NotificationController } from "./notifications.controller"; import { NotificationProcessor } from "./queue/notification.processor"; import { NotificationQueue } from "./queue/notification.queue"; import { NotificationsService } from "./services/notifications.service"; -import { PushNotificationService } from "./services/push-notification.service"; import { pushNotificationsConfig } from "../../configs/push-notifications.config"; import { NotificationSetting } from "../settings/entities/notification-setting.entity"; import { SettingModule } from "../settings/settings.module"; @@ -44,15 +42,13 @@ import { UserLoginHandler } from "./handlers/user-login.handler"; NotificationsService, NotificationQueue, NotificationProcessor, - PushNotificationService, // notification handlers NotificationHandlerFactory, UserLoginHandler, NewEmailHandler, ChangePasswordHandler, - PushNotificationHandler, ], controllers: [NotificationController], - exports: [NotificationsService, NotificationQueue, PushNotificationService], + exports: [NotificationsService, NotificationQueue], }) export class NotificationModule {} diff --git a/src/modules/notifications/services/push-notification.service.ts b/src/modules/notifications/services/push-notification.service.ts deleted file mode 100644 index 6ba9e32..0000000 --- a/src/modules/notifications/services/push-notification.service.ts +++ /dev/null @@ -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("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 { - 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 { - 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 { - 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, - }; - } -} diff --git a/src/modules/users/DTO/push-notification.dto.ts b/src/modules/users/DTO/push-notification.dto.ts deleted file mode 100644 index 3ba2ef4..0000000 --- a/src/modules/users/DTO/push-notification.dto.ts +++ /dev/null @@ -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; -} diff --git a/src/modules/users/services/users.service.ts b/src/modules/users/services/users.service.ts index 1afac26..e5e14dd 100644 --- a/src/modules/users/services/users.service.ts +++ b/src/modules/users/services/users.service.ts @@ -1,16 +1,8 @@ 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 { - BusinessMessage, - CommonMessage, - DomainMessage, - MailServerMessage, - RoleMessage, - TemplateMessage, - UserMessage, -} from "../../../common/enums/message.enum"; +import { BusinessMessage, DomainMessage, MailServerMessage, RoleMessage, TemplateMessage, UserMessage } from "../../../common/enums/message.enum"; import { QUOTA_CONSTANTS } from "../../businesses/constant"; import { Business } from "../../businesses/entities/business.entity"; 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 { UpdateUserDto } from "../../mail-server/DTO/update-user.dto"; 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 { UserSettingsService } from "../../settings/services/user-settings.service"; import { Template } from "../../templates/entities/template.entity"; import { PasswordService } from "../../utils/services/password.service"; 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 { UpdateUserProfileDto } from "../DTO/update-user-profile.dto"; import { UserListQueryDto } from "../DTO/user-list-query.dto"; @@ -51,7 +36,6 @@ export class UsersService { private readonly domainAutomationService: DomainAutomationService, private readonly quotaSyncService: QuotaSyncService, private readonly userSettingsService: UserSettingsService, - private readonly pushNotificationService: PushNotificationService, ) {} 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}`); } - - // 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; - } - } } diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index dced7fa..470b616 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -2,12 +2,6 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseIntercepto import { ApiOperation, ApiResponse } from "@nestjs/swagger"; 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 { UpdateUserProfileDto } from "./DTO/update-user-profile.dto"; import { UserListQueryDto } from "./DTO/user-list-query.dto"; @@ -45,56 +39,6 @@ export class UsersController { 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() @UseInterceptors(BusinessInterceptor) @ApiOperation({ summary: "Create a new email user" })