mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat(email): add Send Email page and integrate with navigation
This commit is contained in:
@@ -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={<SettingsDataRetention />}
|
||||
/>
|
||||
|
||||
<PageRoute
|
||||
path={RouteMap[PageMap.SEND_EMAIL]?.toString() || ""}
|
||||
element={<SendEmail />}
|
||||
/>
|
||||
</Routes>
|
||||
</MasterPage>
|
||||
);
|
||||
|
||||
@@ -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 <NavBar items={navItems} />;
|
||||
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 <NavBar items={navItems} moreMenuItems={moreMenuItems} />;
|
||||
};
|
||||
|
||||
export default DashboardNavbar;
|
||||
|
||||
282
App/FeatureSet/AdminDashboard/src/Pages/SendEmail/Index.tsx
Normal file
282
App/FeatureSet/AdminDashboard/src/Pages/SendEmail/Index.tsx
Normal file
@@ -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<boolean>(false);
|
||||
const [isSendingAll, setIsSendingAll] = useState<boolean>(false);
|
||||
const [testError, setTestError] = useState<string>("");
|
||||
const [testSuccess, setTestSuccess] = useState<string>("");
|
||||
const [sendAllError, setSendAllError] = useState<string>("");
|
||||
const [sendAllSuccess, setSendAllSuccess] = useState<string>("");
|
||||
const [showConfirmModal, setShowConfirmModal] = useState<boolean>(false);
|
||||
const [pendingSubject, setPendingSubject] = useState<string>("");
|
||||
const [pendingMessage, setPendingMessage] = useState<string>("");
|
||||
|
||||
const sendToAllUsers: () => Promise<void> = async (): Promise<void> => {
|
||||
setIsSendingAll(true);
|
||||
setSendAllError("");
|
||||
setSendAllSuccess("");
|
||||
|
||||
try {
|
||||
const response: HTTPResponse<JSONObject> | 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 (
|
||||
<Page
|
||||
title={"Send Announcement Email"}
|
||||
breadcrumbLinks={[
|
||||
{
|
||||
title: "Admin Dashboard",
|
||||
to: RouteUtil.populateRouteParams(RouteMap[PageMap.HOME] as Route),
|
||||
},
|
||||
{
|
||||
title: "Send Email",
|
||||
to: RouteUtil.populateRouteParams(
|
||||
RouteMap[PageMap.SEND_EMAIL] as Route,
|
||||
),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Card
|
||||
title="Send Test Email"
|
||||
description="Send a test announcement email to a single email address to preview how it looks before sending to all users."
|
||||
>
|
||||
{testSuccess ? (
|
||||
<Alert
|
||||
type={AlertType.SUCCESS}
|
||||
title={testSuccess}
|
||||
className="mb-4"
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<BasicForm
|
||||
id="send-test-email-form"
|
||||
name="Send Test Announcement Email"
|
||||
isLoading={isSendingTest}
|
||||
error={testError || ""}
|
||||
submitButtonText="Send Test Email"
|
||||
maxPrimaryButtonWidth={true}
|
||||
initialValues={{
|
||||
subject: "",
|
||||
message: "",
|
||||
testEmail: "",
|
||||
}}
|
||||
fields={[
|
||||
{
|
||||
field: {
|
||||
subject: true,
|
||||
},
|
||||
title: "Subject",
|
||||
description: "The subject line of the announcement email.",
|
||||
placeholder: "Enter email subject",
|
||||
required: true,
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
message: true,
|
||||
},
|
||||
title: "Message",
|
||||
description:
|
||||
"The body of the announcement email. This will be displayed in a branded OneUptime email template.",
|
||||
placeholder: "Enter your announcement message here...",
|
||||
required: true,
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
testEmail: true,
|
||||
},
|
||||
title: "Test Email Address",
|
||||
description:
|
||||
"The email address where the test email will be sent.",
|
||||
placeholder: "test@example.com",
|
||||
required: true,
|
||||
fieldType: FormFieldSchemaType.Email,
|
||||
disableSpellCheck: true,
|
||||
},
|
||||
]}
|
||||
onSubmit={async (
|
||||
values: JSONObject,
|
||||
onSubmitSuccessful?: () => 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<JSONObject> | 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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="Send Email to All Users"
|
||||
description="Send an announcement email to all registered users. Please send a test email first to verify the content."
|
||||
>
|
||||
{sendAllSuccess ? (
|
||||
<Alert
|
||||
type={AlertType.SUCCESS}
|
||||
title={sendAllSuccess}
|
||||
className="mb-4"
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
<BasicForm
|
||||
id="send-all-email-form"
|
||||
name="Send Announcement to All Users"
|
||||
isLoading={isSendingAll}
|
||||
error={sendAllError || ""}
|
||||
submitButtonText="Send to All Users"
|
||||
maxPrimaryButtonWidth={true}
|
||||
initialValues={{
|
||||
subject: "",
|
||||
message: "",
|
||||
}}
|
||||
fields={[
|
||||
{
|
||||
field: {
|
||||
subject: true,
|
||||
},
|
||||
title: "Subject",
|
||||
description: "The subject line of the announcement email.",
|
||||
placeholder: "Enter email subject",
|
||||
required: true,
|
||||
fieldType: FormFieldSchemaType.Text,
|
||||
},
|
||||
{
|
||||
field: {
|
||||
message: true,
|
||||
},
|
||||
title: "Message",
|
||||
description:
|
||||
"The body of the announcement email. This will be sent to all registered users.",
|
||||
placeholder: "Enter your announcement message here...",
|
||||
required: true,
|
||||
fieldType: FormFieldSchemaType.LongText,
|
||||
},
|
||||
]}
|
||||
onSubmit={async (values: JSONObject) => {
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{showConfirmModal ? (
|
||||
<ConfirmModal
|
||||
title="Confirm Send to All Users"
|
||||
description="Are you sure you want to send this announcement email to all registered users? This action cannot be undone."
|
||||
submitButtonText="Yes, Send to All Users"
|
||||
onSubmit={async () => {
|
||||
setShowConfirmModal(false);
|
||||
await sendToAllUsers();
|
||||
}}
|
||||
onClose={() => {
|
||||
setShowConfirmModal(false);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default SendEmail;
|
||||
@@ -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;
|
||||
|
||||
@@ -39,6 +39,8 @@ const RouteMap: Dictionary<Route> = {
|
||||
[PageMap.SETTINGS_DATA_RETENTION]: new Route(
|
||||
`/admin/settings/data-retention`,
|
||||
),
|
||||
|
||||
[PageMap.SEND_EMAIL]: new Route(`/admin/send-email`),
|
||||
};
|
||||
|
||||
export class RouteUtil {
|
||||
|
||||
135
App/FeatureSet/Notification/API/BroadcastEmail.ts
Normal file
135
App/FeatureSet/Notification/API/BroadcastEmail.ts
Normal file
@@ -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<User> = 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;
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user