mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-20 11:56:38 +00:00
add support message button in saas
This commit is contained in:
56
server/emails/templates/SupportEmail.tsx
Normal file
56
server/emails/templates/SupportEmail.tsx
Normal 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;
|
||||
@@ -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
|
||||
);
|
||||
);
|
||||
|
||||
1
server/private/routers/misc/index.ts
Normal file
1
server/private/routers/misc/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./sendSupportEmail";
|
||||
85
server/private/routers/misc/sendSupportEmail.ts
Normal file
85
server/private/routers/misc/sendSupportEmail.ts
Normal 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")
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user