Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"pino": "^9.6.0",
"pluralize": "^8.0.0",
"qrcode": "^1.5.4",
"sanitize-html": "^2.17.0",
"stripe": "^18.0.0",
"uuid": "^11.1.0",
"zod": "^4.0.14",
Expand All @@ -67,9 +68,10 @@
"@tsconfig/node22": "^22.0.1",
"@types/aws-lambda": "^8.10.152",
"@types/qrcode": "^1.5.5",
"@types/sanitize-html": "^2.16.0",
"esbuild-copy-static-files": "^0.1.0",
"nodemon": "^3.1.10",
"pino-pretty": "^13.1.1",
"yaml": "^2.8.0"
}
}
}
5 changes: 3 additions & 2 deletions src/api/routes/apiKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import * as z from "zod/v4";
import { AvailableSQSFunctions, SQSPayload } from "common/types/sqsMessage.js";
import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
import { getAllUserEmails } from "common/utils.js";

const apiKeyRoute: FastifyPluginAsync = async (fastify, _options) => {
await fastify.register(rateLimiter, {
Expand Down Expand Up @@ -96,7 +97,7 @@ const apiKeyRoute: FastifyPluginAsync = async (fastify, _options) => {
reqId: request.id,
},
payload: {
to: [request.username!],
to: getAllUserEmails(request.username),
subject: "Important: API Key Created",
content: `
This email confirms that an API key for the Core API has been generated from your account.
Expand Down Expand Up @@ -203,7 +204,7 @@ If you did not create this API key, please secure your account and notify the AC
reqId: request.id,
},
payload: {
to: [request.username!],
to: getAllUserEmails(request.username),
subject: "Important: API Key Deleted",
content: `
This email confirms that an API key for the Core API has been deleted from your account.
Expand Down
5 changes: 3 additions & 2 deletions src/api/routes/iam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { AvailableSQSFunctions } from "common/types/sqsMessage.js";
import { SendMessageBatchCommand, SQSClient } from "@aws-sdk/client-sqs";
import { randomUUID } from "crypto";
import { getKey, setKey } from "api/functions/redisCache.js";
import { getAllUserEmails } from "common/utils.js";

const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
const getAuthorizedClients = async () => {
Expand Down Expand Up @@ -456,7 +457,7 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
reqId: request.id,
},
payload: {
to: [x],
to: getAllUserEmails(x),
subject: "You have been added to an access group",
content: `
Hello,
Expand All @@ -478,7 +479,7 @@ No action is required from you at this time.
reqId: request.id,
},
payload: {
to: [x],
to: getAllUserEmails(x),
subject: "You have been removed from an access group",
content: `
Hello,
Expand Down
3 changes: 2 additions & 1 deletion src/api/routes/roomRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { buildAuditLogTransactPut } from "api/functions/auditLog.js";
import { Modules } from "common/modules.js";
import {
generateProjectionParams,
getAllUserEmails,
getDefaultFilteringQuerystring,
nonEmptyCommaSeparatedStringSchema,
} from "common/utils.js";
Expand Down Expand Up @@ -142,7 +143,7 @@ const roomRequestRoutes: FastifyPluginAsync = async (fastify, _options) => {
reqId: request.id,
},
payload: {
to: [originalRequestor],
to: getAllUserEmails(originalRequestor),
subject: "Room Reservation Request Status Change",
content: `Your Room Reservation Request has been been moved to status "${formatStatus(request.body.status)}". Please visit the management portal for more details.`,
callToActionButton: {
Expand Down
7 changes: 4 additions & 3 deletions src/api/routes/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import rawbody from "fastify-raw-body";
import { AvailableSQSFunctions, SQSPayload } from "common/types/sqsMessage.js";
import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
import * as z from "zod/v4";
import { getAllUserEmails } from "common/utils.js";

const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
await fastify.register(rawbody, {
Expand Down Expand Up @@ -412,7 +413,7 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
reqId: request.id,
},
payload: {
to: [unmarshalledEntry.userId],
to: getAllUserEmails(unmarshalledEntry.userId),
subject: `Payment Failed for Invoice ${unmarshalledEntry.invoiceId}`,
content: `
A ${paidInFull ? "full" : "partial"} payment for Invoice ${unmarshalledEntry.invoiceId} (${withCurrency} paid by ${name}, ${email}) <b>has failed.</b>
Expand Down Expand Up @@ -565,7 +566,7 @@ Please ask the payee to try again, perhaps with a different payment method, or c
reqId: request.id,
},
payload: {
to: [unmarshalledEntry.userId],
to: getAllUserEmails(unmarshalledEntry.userId),
subject: `Payment Pending for Invoice ${unmarshalledEntry.invoiceId}`,
content: `
ACM @ UIUC has received intent of ${paidInFull ? "full" : "partial"} payment for Invoice ${unmarshalledEntry.invoiceId} (${withCurrency} paid by ${name}, ${email}).
Expand Down Expand Up @@ -610,7 +611,7 @@ Please contact Officer Board with any questions.
reqId: request.id,
},
payload: {
to: [unmarshalledEntry.userId],
to: getAllUserEmails(unmarshalledEntry.userId),
cc: [
notificationRecipients[fastify.runEnvironment]
.Treasurer,
Expand Down
28 changes: 17 additions & 11 deletions src/api/sqs/handlers/emailNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { Modules } from "common/modules.js";
import Handlebars from "handlebars";
import emailTemplate from "./templates/notification.js";
import sanitizeHtml from "sanitize-html";

Handlebars.registerHelper("nl2br", (text) => {
let nl2br = `${text}`.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, "$1<br>$2");
Expand All @@ -16,17 +17,23 @@
const compiledTemplate = Handlebars.compile(emailTemplate);

const stripHtml = (html: string): string => {
return html
.replace(/<[^>]*>/g, "") // Remove HTML tags
.replace(/&nbsp;/g, " ") // Replace non-breaking spaces
.replace(/\s+/g, " ") // Normalize whitespace
.trim();
return sanitizeHtml(
html
.replace(/<[^>]*>/g, "") // Remove HTML tags
.replace(/&nbsp;/g, " ") // Replace non-breaking spaces
.replace(/\s+/g, " ") // Normalize whitespace
.trim(),
);
};

export const emailNotificationsHandler: SQSHandlerFunction<
AvailableSQSFunctions.EmailNotifications
> = async (payload, metadata, logger) => {
const { to, cc, bcc, content, subject } = payload;
if (to.length + (cc || []).length + (bcc || []).length === 0) {
logger.warn("Found no message recipients. Exiting without calling SES.");
return;
}
const senderEmailAddress = `notifications@${currentEnvironmentConfig.EmailDomain}`;
const senderEmail = `ACM @ UIUC <${senderEmailAddress}>`;
logger.info("Constructing email...");
Expand Down Expand Up @@ -59,17 +66,16 @@
},
},
});
const logPromise = createAuditLogEntry({
const sesClient = new SESClient({ region: genericConfig.AwsRegion });
const response = await sesClient.send(command);
logger.info("Sent!");
await createAuditLogEntry({
entry: {
module: Modules.EMAIL_NOTIFICATION,
actor: metadata.initiator,
target: to.join(";"),
target: [...to, ...(bcc || []), ...(cc || [])].join(";"),
message: `Sent email notification with subject "${subject}".`,
},
});
const sesClient = new SESClient({ region: genericConfig.AwsRegion });
const response = await sesClient.send(command);
logger.info("Sent!");
await logPromise;
return response;
};
7 changes: 7 additions & 0 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,10 @@ export const getDefaultFilteringQuerystring = ({ defaultSelect }: GetDefaultFilt
})
};
};

export const getAllUserEmails = (username?: string) => {
if (!username) {
return [];
}
return [username.replace("@illinois.edu", "@acm.illinois.edu")]
}
Loading
Loading