Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
57 changes: 48 additions & 9 deletions .github/workflows/deploy-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- main

jobs:
test-unit:
test:
runs-on: ubuntu-latest
name: Run Unit Tests
steps:
Expand All @@ -20,7 +20,7 @@
uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'yarn'
cache: "yarn"

- name: Restore Yarn Cache
uses: actions/cache@v4
Expand All @@ -30,15 +30,48 @@
restore-keys: |
yarn-modules-${{ runner.os }}-

- name: Set up Python 3.11 for testing
uses: actions/setup-python@v5
with:
python-version: 3.11

- name: Run unit testing
run: make test_unit

build:
runs-on: ubuntu-latest
name: Build Application
steps:
- uses: actions/checkout@v4
env:
HUSKY: "0"

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22.x
cache: "yarn"

- name: Restore Yarn Cache
uses: actions/cache@v4
with:
path: node_modules
key: yarn-modules-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-dev
restore-keys: |
yarn-modules-${{ runner.os }}-

- name: Run build
run: make build
env:
HUSKY: "0"
VITE_RUN_ENVIRONMENT: dev

- name: Upload Build files
uses: actions/upload-artifact@v4
with:
include-hidden-files: true
name: build
path: |
.aws-sam/
dist/
dist_ui/

deploy-test-dev:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
runs-on: ubuntu-latest
permissions:
id-token: write
Expand All @@ -49,7 +82,8 @@
environment: "AWS DEV"
name: Deploy to DEV and Run Tests
needs:
- test-unit
- test
- build
steps:
- uses: actions/checkout@v4
env:
Expand All @@ -59,7 +93,7 @@
uses: actions/setup-node@v4
with:
node-version: 22.x
cache: 'yarn'
cache: "yarn"

- name: Restore Yarn Cache
uses: actions/cache@v4
Expand All @@ -69,6 +103,11 @@
restore-keys: |
yarn-modules-${{ runner.os }}-

- name: Download Build files
uses: actions/download-artifact@v4
with:
name: build

- uses: aws-actions/setup-sam@v2
with:
use-installer: true
Expand Down
64 changes: 57 additions & 7 deletions .github/workflows/deploy-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,70 @@
branches:
- main
jobs:
test-unit:
test:
runs-on: ubuntu-latest
name: Run Unit Tests
steps:
- uses: actions/checkout@v4
env:
HUSKY: "0"

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22.x
cache: "yarn"

- name: Restore Yarn Cache
uses: actions/cache@v4
with:
path: node_modules
key: yarn-modules-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-dev
restore-keys: |
yarn-modules-${{ runner.os }}-

- name: Run unit testing
run: make test_unit

build:
runs-on: ubuntu-latest
name: Build Application
steps:
- uses: actions/checkout@v4
env:
HUSKY: "0"
- name: Set up Python 3.11 for testing
uses: actions/setup-python@v5

- name: Set up Node
uses: actions/setup-node@v4
with:
python-version: 3.11
- name: Run unit testing
run: make test_unit
node-version: 22.x
cache: "yarn"

- name: Restore Yarn Cache
uses: actions/cache@v4
with:
path: node_modules
key: yarn-modules-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-prod
restore-keys: |
yarn-modules-${{ runner.os }}-

- name: Run build
run: make build
env:
HUSKY: "0"
VITE_RUN_ENVIRONMENT: prod

- name: Upload Build files
uses: actions/upload-artifact@v4
with:
include-hidden-files: true
name: build-prod
path: |
.aws-sam/
dist/
dist_ui/

deploy-prod:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
runs-on: ubuntu-latest
name: Deploy to Prod and Run Health Check
concurrency:
Expand All @@ -35,7 +80,8 @@
id-token: write
contents: read
needs:
- test-unit
- test
- build
environment: "AWS PROD"
steps:
- name: Set up Node for testing
Expand All @@ -53,6 +99,10 @@
uses: actions/setup-python@v5
with:
python-version: 3.11
- name: Download Build files
uses: actions/download-artifact@v4
with:
name: build-prod
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::298118738376:role/GitHubActionsRole
Expand Down
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ clean:
rm -rf dist/
rm -rf dist_ui/
rm -rf dist_devel/
rm -rf coverage/

build: src/ cloudformation/ docs/
yarn -D
Expand All @@ -65,14 +66,14 @@ build: src/ cloudformation/ docs/
local:
VITE_BUILD_HASH=$(GIT_HASH) yarn run dev

deploy_prod: check_account_prod build
deploy_prod: check_account_prod
@echo "Deploying CloudFormation stack..."
sam deploy $(common_params) --parameter-overrides $(run_env)=prod $(set_application_prefix)=$(application_key) $(set_application_name)="$(application_name)" S3BucketPrefix="$(s3_bucket_prefix)"
@echo "Syncing S3 bucket..."
aws s3 sync $(dist_ui_directory_root) s3://$(ui_s3_bucket)/ --delete
make invalidate_cloudfront

deploy_dev: check_account_dev build
deploy_dev: check_account_dev
@echo "Deploying CloudFormation stack..."
sam deploy $(common_params) --parameter-overrides $(run_env)=dev $(set_application_prefix)=$(application_key) $(set_application_name)="$(application_name)" S3BucketPrefix="$(s3_bucket_prefix)"
@echo "Syncing S3 bucket..."
Expand Down
1 change: 1 addition & 0 deletions src/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"fastify-plugin": "^5.0.1",
"fastify-raw-body": "^5.0.0",
"fastify-zod-openapi": "^4.1.1",
"handlebars": "^4.7.8",
"ical-generator": "^8.1.1",
"ioredis": "^5.6.1",
"jsonwebtoken": "^9.0.2",
Expand Down
64 changes: 53 additions & 11 deletions src/api/routes/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
QueryCommand,
ScanCommand,
TransactWriteItemsCommand,
UpdateItemCommand,
} from "@aws-sdk/client-dynamodb";
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
import { withRoles, withTags } from "api/components/index.js";
Expand Down Expand Up @@ -190,16 +191,16 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
if (!request.rawBody) {
throw new ValidationError({ message: "Could not get raw body." });
}
const secretApiConfig =
(await getSecretValue(
fastify.secretsManagerClient,
genericConfig.ConfigSecretName,
)) || {};
try {
const sig = request.headers["stripe-signature"];
if (!sig || typeof sig !== "string") {
throw new Error("Missing or invalid Stripe signature");
}
const secretApiConfig =
(await getSecretValue(
fastify.secretsManagerClient,
genericConfig.ConfigSecretName,
)) || {};
if (!secretApiConfig) {
throw new InternalServerError({
message: "Could not connect to Stripe.",
Expand Down Expand Up @@ -259,13 +260,17 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
const unmarshalledEntry = unmarshall(response.Items[0]) as {
userId: string;
invoiceId: string;
amount: number;
priceId: string;
productId: string;
};
if (!unmarshalledEntry.userId || !unmarshalledEntry.invoiceId) {
return reply.status(200).send({
handled: false,
requestId: request.id,
});
}
const paidInFull = paymentAmount === unmarshalledEntry.amount;
const withCurrency = new Intl.NumberFormat("en-US", {
style: "currency",
currency: paymentCurrency.toUpperCase(),
Expand All @@ -274,8 +279,10 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
.map((val) => val.value)
.join("");
request.log.info(
`Registered payment of ${withCurrency} by ${name} (${email}) for payment link ${paymentLinkId} invoice ID ${unmarshalledEntry.invoiceId}).`,
`Registered payment of ${withCurrency} by ${name} (${email}) for payment link ${paymentLinkId} invoice ID ${unmarshalledEntry.invoiceId}). Invoice was paid ${paidInFull ? "in full." : "partially."}`,
);
// Notify link owner of payment
let queueId;
if (unmarshalledEntry.userId.includes("@")) {
request.log.info(
`Sending email to ${unmarshalledEntry.userId}...`,
Expand All @@ -290,7 +297,7 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
payload: {
to: [unmarshalledEntry.userId],
subject: `Payment Recieved for Invoice ${unmarshalledEntry.invoiceId}`,
content: `Received payment of ${withCurrency} by ${name} (${email}) for Invoice ${unmarshalledEntry.invoiceId}. Please contact [email protected] with any questions.`,
content: `ACM @ UIUC has received ${paidInFull ? "full" : "partial"} payment for Invoice ${unmarshalledEntry.invoiceId} (${withCurrency} by ${name}, ${email}).\n\nPlease contact Officer Board with any questions.`,
},
};
if (!fastify.sqsClient) {
Expand All @@ -304,15 +311,50 @@ const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
MessageBody: JSON.stringify(sqsPayload),
}),
);
return reply.status(200).send({
handled: true,
requestId: request.id,
queueId: result.MessageId,
queueId = result.MessageId || "";
}
// If full payment is done, disable the link
if (paidInFull) {
request.log.debug("Paid in full, disabling link.");
const logStatement = buildAuditLogTransactPut({
entry: {
module: Modules.STRIPE,
actor: eventId,
target: `Link ${paymentLinkId} | Invoice ${unmarshalledEntry.invoiceId}`,
message:
"Disabled Stripe payment link as payment was made in full.",
},
});
const dynamoCommand = new TransactWriteItemsCommand({
TransactItems: [
logStatement,
{
Update: {
TableName: genericConfig.StripeLinksDynamoTableName,
Key: {
userId: { S: unmarshalledEntry.userId },
linkId: { S: paymentLinkId },
},
UpdateExpression: "SET active = :new_val",
ConditionExpression: "active = :old_val",
ExpressionAttributeValues: {
":new_val": { BOOL: false },
":old_val": { BOOL: true },
},
},
},
],
});
await deactivateStripeLink({
stripeApiKey: secretApiConfig.stripe_secret_key as string,
linkId: paymentLinkId,
});
await fastify.dynamoClient.send(dynamoCommand);
}
return reply.status(200).send({
handled: true,
requestId: request.id,
queueId: queueId || "",
});
}
return reply
Expand Down
20 changes: 18 additions & 2 deletions src/api/sqs/handlers/emailNotifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ import { SendEmailCommand, SESClient } from "@aws-sdk/client-ses";
import { genericConfig } from "common/config.js";
import { createAuditLogEntry } from "api/functions/auditLog.js";
import { Modules } from "common/modules.js";
import Handlebars from "handlebars";
import emailTemplate from "./templates/notification.js";

Handlebars.registerHelper("nl2br", (text) => {
let nl2br = `${text}`.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, "$1<br>$2");
nl2br = `<p>${nl2br.replace(/<br>/g, "</p><p>")}</p>`;
return new Handlebars.SafeString(nl2br);
});

const compiledTemplate = Handlebars.compile(emailTemplate);

const stripHtml = (html: string): string => {
return html
Expand All @@ -17,7 +27,8 @@ export const emailNotificationsHandler: SQSHandlerFunction<
AvailableSQSFunctions.EmailNotifications
> = async (payload, metadata, logger) => {
const { to, cc, bcc, content, subject } = payload;
const senderEmail = `ACM @ UIUC <notifications@${currentEnvironmentConfig.EmailDomain}>`;
const senderEmailAddress = `notifications@${currentEnvironmentConfig.EmailDomain}`;
const senderEmail = `ACM @ UIUC <${senderEmailAddress}>`;
logger.info("Constructing email...");
const command = new SendEmailCommand({
Source: senderEmail,
Expand All @@ -33,7 +44,12 @@ export const emailNotificationsHandler: SQSHandlerFunction<
},
Body: {
Html: {
Data: content,
Data: compiledTemplate({
...payload,
id: metadata.reqId,
from: senderEmailAddress,
currentYear: new Date().getFullYear(),
}),
Charset: "UTF-8",
},
Text: {
Expand Down
Loading
Loading