diff --git a/App/FeatureSet/AdminDashboard/src/App.tsx b/App/FeatureSet/AdminDashboard/src/App.tsx index 82da82651c..17e76498ef 100644 --- a/App/FeatureSet/AdminDashboard/src/App.tsx +++ b/App/FeatureSet/AdminDashboard/src/App.tsx @@ -12,6 +12,7 @@ import SettingsEmail from "./Pages/Settings/Email/Index"; import SettingsProbes from "./Pages/Settings/Probes/Index"; import SettingsAIAgents from "./Pages/Settings/AIAgents/Index"; import SettingsLlmProviders from "./Pages/Settings/LlmProviders/Index"; +import SendEmail from "./Pages/SendEmail/Index"; import Users from "./Pages/Users/Index"; import PageMap from "./Utils/PageMap"; import RouteMap from "./Utils/RouteMap"; @@ -149,6 +150,11 @@ const App: () => JSX.Element = () => { path={RouteMap[PageMap.SETTINGS_DATA_RETENTION]?.toString() || ""} element={} /> + + } + /> ); diff --git a/App/FeatureSet/AdminDashboard/src/Components/NavBar/NavBar.tsx b/App/FeatureSet/AdminDashboard/src/Components/NavBar/NavBar.tsx index d024c0eab3..4b5fb95507 100644 --- a/App/FeatureSet/AdminDashboard/src/Components/NavBar/NavBar.tsx +++ b/App/FeatureSet/AdminDashboard/src/Components/NavBar/NavBar.tsx @@ -2,7 +2,10 @@ import PageMap from "../../Utils/PageMap"; import RouteMap, { RouteUtil } from "../../Utils/RouteMap"; import Route from "Common/Types/API/Route"; import IconProp from "Common/Types/Icon/IconProp"; -import NavBar, { NavItem } from "Common/UI/Components/Navbar/NavBar"; +import NavBar, { + MoreMenuItem, + NavItem, +} from "Common/UI/Components/Navbar/NavBar"; import React, { FunctionComponent, ReactElement } from "react"; const DashboardNavbar: FunctionComponent = (): ReactElement => { @@ -28,7 +31,21 @@ const DashboardNavbar: FunctionComponent = (): ReactElement => { }, ]; - return ; + const moreMenuItems: MoreMenuItem[] = [ + { + title: "Send Email", + description: "Send announcement emails to all registered users.", + icon: IconProp.Email, + route: RouteUtil.populateRouteParams( + RouteMap[PageMap.SEND_EMAIL] as Route, + ), + activeRoute: RouteUtil.populateRouteParams( + RouteMap[PageMap.SEND_EMAIL] as Route, + ), + }, + ]; + + return ; }; export default DashboardNavbar; diff --git a/App/FeatureSet/AdminDashboard/src/Pages/SendEmail/Index.tsx b/App/FeatureSet/AdminDashboard/src/Pages/SendEmail/Index.tsx new file mode 100644 index 0000000000..169cde6ca4 --- /dev/null +++ b/App/FeatureSet/AdminDashboard/src/Pages/SendEmail/Index.tsx @@ -0,0 +1,282 @@ +import PageMap from "../../Utils/PageMap"; +import RouteMap, { RouteUtil } from "../../Utils/RouteMap"; +import Route from "Common/Types/API/Route"; +import React, { FunctionComponent, ReactElement, useState } from "react"; +import Page from "Common/UI/Components/Page/Page"; +import Card from "Common/UI/Components/Card/Card"; +import BasicForm from "Common/UI/Components/Forms/BasicForm"; +import Alert, { AlertType } from "Common/UI/Components/Alerts/Alert"; +import FormFieldSchemaType from "Common/UI/Components/Forms/Types/FormFieldSchemaType"; +import { JSONObject } from "Common/Types/JSON"; +import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse"; +import HTTPResponse from "Common/Types/API/HTTPResponse"; +import URL from "Common/Types/API/URL"; +import API from "Common/UI/Utils/API/API"; +import { APP_API_URL } from "Common/UI/Config"; +import ConfirmModal from "Common/UI/Components/Modal/ConfirmModal"; + +const SendEmail: FunctionComponent = (): ReactElement => { + const [isSendingTest, setIsSendingTest] = useState(false); + const [isSendingAll, setIsSendingAll] = useState(false); + const [testError, setTestError] = useState(""); + const [testSuccess, setTestSuccess] = useState(""); + const [sendAllError, setSendAllError] = useState(""); + const [sendAllSuccess, setSendAllSuccess] = useState(""); + const [showConfirmModal, setShowConfirmModal] = useState(false); + const [pendingSubject, setPendingSubject] = useState(""); + const [pendingMessage, setPendingMessage] = useState(""); + + const sendToAllUsers: () => Promise = async (): Promise => { + setIsSendingAll(true); + setSendAllError(""); + setSendAllSuccess(""); + + try { + const response: HTTPResponse | HTTPErrorResponse = + await API.post({ + url: URL.fromString(APP_API_URL.toString()).addRoute( + "/notification/broadcast-email/send-to-all-users", + ), + data: { + subject: pendingSubject, + message: pendingMessage, + }, + }); + + if (response instanceof HTTPErrorResponse) { + throw response; + } + + if (response.isFailure()) { + throw new Error("Failed to send emails."); + } + + const data: JSONObject = response.data as JSONObject; + setSendAllSuccess( + `Emails sent successfully. Total users: ${data["totalUsers"]}, Sent: ${data["sentCount"]}, Errors: ${data["errorCount"]}`, + ); + } catch (err) { + setSendAllError(API.getFriendlyMessage(err)); + } finally { + setIsSendingAll(false); + } + }; + + return ( + + + {testSuccess ? ( + + ) : ( + <> + )} + + void, + ) => { + const subject: string = String(values["subject"] || "").trim(); + const message: string = String(values["message"] || "").trim(); + const testEmail: string = String(values["testEmail"] || "").trim(); + + if (!subject || !message || !testEmail) { + setTestSuccess(""); + setTestError("Please fill in all fields."); + return; + } + + setIsSendingTest(true); + setTestError(""); + setTestSuccess(""); + + try { + const response: HTTPResponse | HTTPErrorResponse = + await API.post({ + url: URL.fromString(APP_API_URL.toString()).addRoute( + "/notification/broadcast-email/send-test", + ), + data: { + subject, + message, + testEmail, + }, + }); + + if (response instanceof HTTPErrorResponse) { + throw response; + } + + if (response.isFailure()) { + throw new Error("Failed to send test email."); + } + + setTestSuccess( + "Test email sent successfully. Please check your inbox.", + ); + + if (onSubmitSuccessful) { + onSubmitSuccessful(); + } + } catch (err) { + setTestError(API.getFriendlyMessage(err)); + } finally { + setIsSendingTest(false); + } + }} + /> + + + + {sendAllSuccess ? ( + + ) : ( + <> + )} + + { + const subject: string = String(values["subject"] || "").trim(); + const message: string = String(values["message"] || "").trim(); + + if (!subject || !message) { + setSendAllSuccess(""); + setSendAllError("Please fill in all fields."); + return; + } + + setPendingSubject(subject); + setPendingMessage(message); + setShowConfirmModal(true); + }} + /> + + + {showConfirmModal ? ( + { + setShowConfirmModal(false); + await sendToAllUsers(); + }} + onClose={() => { + setShowConfirmModal(false); + }} + /> + ) : ( + <> + )} + + ); +}; + +export default SendEmail; diff --git a/App/FeatureSet/AdminDashboard/src/Utils/PageMap.ts b/App/FeatureSet/AdminDashboard/src/Utils/PageMap.ts index 7b122e2418..0a99cc71bd 100644 --- a/App/FeatureSet/AdminDashboard/src/Utils/PageMap.ts +++ b/App/FeatureSet/AdminDashboard/src/Utils/PageMap.ts @@ -23,6 +23,8 @@ enum PageMap { SETTINGS_AUTHENTICATION = "SETTINGS_AUTHENTICATION", SETTINGS_API_KEY = "SETTINGS_API_KEY", SETTINGS_DATA_RETENTION = "SETTINGS_DATA_RETENTION", + + SEND_EMAIL = "SEND_EMAIL", } export default PageMap; diff --git a/App/FeatureSet/AdminDashboard/src/Utils/RouteMap.ts b/App/FeatureSet/AdminDashboard/src/Utils/RouteMap.ts index 0e7393c07a..e07e8765b4 100644 --- a/App/FeatureSet/AdminDashboard/src/Utils/RouteMap.ts +++ b/App/FeatureSet/AdminDashboard/src/Utils/RouteMap.ts @@ -39,6 +39,8 @@ const RouteMap: Dictionary = { [PageMap.SETTINGS_DATA_RETENTION]: new Route( `/admin/settings/data-retention`, ), + + [PageMap.SEND_EMAIL]: new Route(`/admin/send-email`), }; export class RouteUtil { diff --git a/App/FeatureSet/Notification/API/BroadcastEmail.ts b/App/FeatureSet/Notification/API/BroadcastEmail.ts new file mode 100644 index 0000000000..fa9373ea30 --- /dev/null +++ b/App/FeatureSet/Notification/API/BroadcastEmail.ts @@ -0,0 +1,135 @@ +import MailService from "../Services/MailService"; +import Email from "Common/Types/Email"; +import EmailMessage from "Common/Types/Email/EmailMessage"; +import EmailTemplateType from "Common/Types/Email/EmailTemplateType"; +import BadDataException from "Common/Types/Exception/BadDataException"; +import { JSONObject } from "Common/Types/JSON"; +import LIMIT_MAX from "Common/Types/Database/LimitMax"; +import ClusterKeyAuthorization from "Common/Server/Middleware/ClusterKeyAuthorization"; +import UserService from "Common/Server/Services/UserService"; +import Express, { + ExpressRequest, + ExpressResponse, + ExpressRouter, + NextFunction, +} from "Common/Server/Utils/Express"; +import Response from "Common/Server/Utils/Response"; +import logger from "Common/Server/Utils/Logger"; +import User from "Common/Models/DatabaseModels/User"; + +const router: ExpressRouter = Express.getRouter(); + +router.post( + "/send-test", + ClusterKeyAuthorization.isAuthorizedServiceMiddleware, + async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => { + try { + const body: JSONObject = req.body; + + const subject: string = body["subject"] as string; + const message: string = body["message"] as string; + const testEmail: string = body["testEmail"] as string; + + if (!subject) { + throw new BadDataException("Subject is required"); + } + + if (!message) { + throw new BadDataException("Message is required"); + } + + if (!testEmail) { + throw new BadDataException("Test email address is required"); + } + + const mail: EmailMessage = { + templateType: EmailTemplateType.SimpleMessage, + toEmail: new Email(testEmail), + subject: subject, + vars: { + subject: subject, + message: message, + }, + body: "", + }; + + await MailService.send(mail); + + return Response.sendEmptySuccessResponse(req, res); + } catch (err) { + return next(err); + } + }, +); + +router.post( + "/send-to-all-users", + ClusterKeyAuthorization.isAuthorizedServiceMiddleware, + async (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => { + try { + const body: JSONObject = req.body; + + const subject: string = body["subject"] as string; + const message: string = body["message"] as string; + + if (!subject) { + throw new BadDataException("Subject is required"); + } + + if (!message) { + throw new BadDataException("Message is required"); + } + + const users: Array = await UserService.findAllBy({ + query: {}, + select: { + email: true, + }, + skip: 0, + props: { + isRoot: true, + }, + }); + + let sentCount: number = 0; + let errorCount: number = 0; + + for (const user of users) { + if (!user.email) { + continue; + } + + try { + const mail: EmailMessage = { + templateType: EmailTemplateType.SimpleMessage, + toEmail: user.email, + subject: subject, + vars: { + subject: subject, + message: message, + }, + body: "", + }; + + await MailService.send(mail); + sentCount++; + } catch (err) { + errorCount++; + logger.error( + `Failed to send broadcast email to ${user.email.toString()}: ${err}`, + ); + } + } + + return Response.sendJsonObjectResponse(req, res, { + totalUsers: users.length, + sentCount: sentCount, + errorCount: errorCount, + }); + } catch (err) { + return next(err); + } + }, +); + +export default router; diff --git a/App/FeatureSet/Notification/Index.ts b/App/FeatureSet/Notification/Index.ts index c78ebdaebb..c4915d714d 100644 --- a/App/FeatureSet/Notification/Index.ts +++ b/App/FeatureSet/Notification/Index.ts @@ -1,3 +1,4 @@ +import BroadcastEmailAPI from "./API/BroadcastEmail"; import CallAPI from "./API/Call"; // API import MailAPI from "./API/Mail"; @@ -27,6 +28,10 @@ const NotificationFeatureSet: FeatureSet = { app.use([`/${APP_NAME}/smtp-config`, "/smtp-config"], SMTPConfigAPI); app.use([`/${APP_NAME}/phone-number`, "/phone-number"], PhoneNumberAPI); app.use([`/${APP_NAME}/incoming-call`, "/incoming-call"], IncomingCallAPI); + app.use( + [`/${APP_NAME}/broadcast-email`, "/broadcast-email"], + BroadcastEmailAPI, + ); }, };