add support message button in saas

This commit is contained in:
miloschwartz
2025-10-27 21:55:34 -07:00
parent 9e5c9d9c34
commit 8b4722b1c9
7 changed files with 476 additions and 15 deletions

View File

@@ -0,0 +1,56 @@
import React from "react";
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
import { themeColors } from "./lib/theme";
import {
EmailContainer,
EmailGreeting,
EmailLetterHead,
EmailText
} from "./components/Email";
interface SupportEmailProps {
email: string;
username: string;
subject: string;
body: string;
}
export const SupportEmail = ({
username,
email,
body,
subject
}: SupportEmailProps) => {
const previewText = subject;
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind config={themeColors}>
<Body className="font-sans bg-gray-50">
<EmailContainer>
<EmailLetterHead />
<EmailGreeting>Hi support,</EmailGreeting>
<EmailText>
You have received a new support request from{" "}
<strong>{username}</strong> ({email}).
</EmailText>
<EmailText>
<strong>Subject:</strong> {subject}
</EmailText>
<EmailText>
<strong>Message:</strong> {body}
</EmailText>
</EmailContainer>
</Body>
</Tailwind>
</Html>
);
};
export default SupportEmail;

View File

@@ -22,6 +22,7 @@ import * as auth from "#private/routers/auth";
import * as license from "#private/routers/license";
import * as generateLicense from "./generatedLicense";
import * as logs from "#private/routers/auditLogs";
import * as misc from "#private/routers/misc";
import {
verifyOrgAccess,
@@ -74,7 +75,7 @@ authenticated.put(
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.createIdp),
logActionAudit(ActionsEnum.createIdp),
orgIdp.createOrgOidcIdp,
orgIdp.createOrgOidcIdp
);
authenticated.post(
@@ -84,7 +85,7 @@ authenticated.post(
verifyIdpAccess,
verifyUserHasAction(ActionsEnum.updateIdp),
logActionAudit(ActionsEnum.updateIdp),
orgIdp.updateOrgOidcIdp,
orgIdp.updateOrgOidcIdp
);
authenticated.delete(
@@ -94,7 +95,7 @@ authenticated.delete(
verifyIdpAccess,
verifyUserHasAction(ActionsEnum.deleteIdp),
logActionAudit(ActionsEnum.deleteIdp),
orgIdp.deleteOrgIdp,
orgIdp.deleteOrgIdp
);
authenticated.get(
@@ -132,7 +133,7 @@ authenticated.post(
verifyCertificateAccess,
verifyUserHasAction(ActionsEnum.restartCertificate),
logActionAudit(ActionsEnum.restartCertificate),
certificates.restartCertificate,
certificates.restartCertificate
);
if (build === "saas") {
@@ -158,7 +159,7 @@ if (build === "saas") {
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.billing),
logActionAudit(ActionsEnum.billing),
billing.createCheckoutSession,
billing.createCheckoutSession
);
authenticated.post(
@@ -166,7 +167,7 @@ if (build === "saas") {
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.billing),
logActionAudit(ActionsEnum.billing),
billing.createPortalSession,
billing.createPortalSession
);
authenticated.get(
@@ -194,6 +195,24 @@ if (build === "saas") {
verifyOrgAccess,
generateLicense.generateNewLicense
);
authenticated.post(
"/send-support-request",
rateLimit({
windowMs: 15 * 60 * 1000,
max: 3,
keyGenerator: (req) =>
`sendSupportRequest:${req.user?.userId || ipKeyGenerator(req.ip || "")}`,
handler: (req, res, next) => {
const message = `You can only send 3 support requests every 15 minutes. Please try again later.`;
return next(
createHttpError(HttpCode.TOO_MANY_REQUESTS, message)
);
},
store: createStore()
}),
misc.sendSupportEmail
);
}
authenticated.get(
@@ -214,7 +233,7 @@ authenticated.put(
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.createRemoteExitNode),
logActionAudit(ActionsEnum.createRemoteExitNode),
remoteExitNode.createRemoteExitNode,
remoteExitNode.createRemoteExitNode
);
authenticated.get(
@@ -249,7 +268,7 @@ authenticated.delete(
verifyRemoteExitNodeAccess,
verifyUserHasAction(ActionsEnum.deleteRemoteExitNode),
logActionAudit(ActionsEnum.deleteRemoteExitNode),
remoteExitNode.deleteRemoteExitNode,
remoteExitNode.deleteRemoteExitNode
);
authenticated.put(
@@ -258,7 +277,7 @@ authenticated.put(
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.createLoginPage),
logActionAudit(ActionsEnum.createLoginPage),
loginPage.createLoginPage,
loginPage.createLoginPage
);
authenticated.post(
@@ -268,7 +287,7 @@ authenticated.post(
verifyLoginPageAccess,
verifyUserHasAction(ActionsEnum.updateLoginPage),
logActionAudit(ActionsEnum.updateLoginPage),
loginPage.updateLoginPage,
loginPage.updateLoginPage
);
authenticated.delete(
@@ -278,7 +297,7 @@ authenticated.delete(
verifyLoginPageAccess,
verifyUserHasAction(ActionsEnum.deleteLoginPage),
logActionAudit(ActionsEnum.deleteLoginPage),
loginPage.deleteLoginPage,
loginPage.deleteLoginPage
);
authenticated.get(
@@ -353,7 +372,7 @@ authenticated.get(
verifyValidSubscription,
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.exportLogs),
logs.queryActionAuditLogs
logs.queryActionAuditLogs
);
authenticated.get(
@@ -372,7 +391,7 @@ authenticated.get(
verifyValidSubscription,
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.exportLogs),
logs.queryAccessAuditLogs
logs.queryAccessAuditLogs
);
authenticated.get(
@@ -383,4 +402,4 @@ authenticated.get(
verifyUserHasAction(ActionsEnum.exportLogs),
logActionAudit(ActionsEnum.exportLogs),
logs.exportAccessAuditLogs
);
);

View File

@@ -0,0 +1 @@
export * from "./sendSupportEmail";

View File

@@ -0,0 +1,85 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { Request, Response, NextFunction } from "express";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { response as sendResponse } from "@server/lib/response";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import { sendEmail } from "@server/emails";
import SupportEmail from "@server/emails/templates/SupportEmail";
import config from "@server/lib/config";
const bodySchema = z
.object({
body: z.string().min(1),
subject: z.string().min(1).max(255)
})
.strict();
export async function sendSupportEmail(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { body, subject } = parsedBody.data;
const user = req.user!;
try {
await sendEmail(
SupportEmail({
username: user.username,
email: user.email || "Unknown",
subject,
body
}),
{
name: req.user?.email || "Support User",
to: "support@pangolin.net",
from: req.user?.email || config.getNoReplyEmail(),
subject: `Support Request: ${subject}`
}
);
return sendResponse(res, {
data: {},
success: true,
error: false,
message: "Sent support email successfully",
status: HttpCode.OK
});
} catch (e) {
logger.error(e);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, `${e}`)
);
}
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}