Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stripe Link creation support in manage.acm #48

Merged
merged 16 commits into from
Feb 6, 2025
Merged
2 changes: 2 additions & 0 deletions .github/workflows/deploy-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ jobs:
run: make dev_health_check
- name: Run live testing
run: make test_live_integration
env:
JWT_KEY: ${{ secrets.JWT_KEY }}
- name: Run E2E testing
run: make test_e2e
env:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/deploy-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ jobs:
python-version: 3.11
- name: Run live testing
run: make test_live_integration
env:
JWT_KEY: ${{ secrets.JWT_KEY }}
- name: Run E2E testing
run: make test_e2e
env:
Expand Down
2 changes: 2 additions & 0 deletions cloudformation/iam.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ Resources:
- !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-iam-userroles/*
- !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-iam-grouproles
- !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-iam-grouproles/*
- !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-stripe-links
- !Sub arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/infra-core-api-stripe-links/*

PolicyName: lambda-dynamo
Outputs:
Expand Down
28 changes: 28 additions & 0 deletions cloudformation/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,34 @@ Resources:
Projection:
ProjectionType: ALL

StripeLinksTable:
Type: 'AWS::DynamoDB::Table'
DeletionPolicy: "Retain"
UpdateReplacePolicy: "Retain"
Properties:
BillingMode: 'PAY_PER_REQUEST'
TableName: infra-core-api-stripe-links
DeletionProtectionEnabled: true
PointInTimeRecoverySpecification:
PointInTimeRecoveryEnabled: false
AttributeDefinitions:
- AttributeName: userId
AttributeType: S
- AttributeName: linkId
AttributeType: S
KeySchema:
- AttributeName: userId
KeyType: "HASH"
- AttributeName: linkId
KeyType: "RANGE"
GlobalSecondaryIndexes:
- IndexName: LinkIdIndex
KeySchema:
- AttributeName: linkId
KeyType: "HASH"
Projection:
ProjectionType: "ALL"

CacheRecordsTable:
Type: 'AWS::DynamoDB::Table'
DeletionPolicy: "Retain"
Expand Down
58 changes: 29 additions & 29 deletions generate_jwt.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
import jwt from 'jsonwebtoken';
import jwt from "jsonwebtoken";
import * as dotenv from "dotenv";
dotenv.config();

const username = process.env.JWTGEN_USERNAME || 'infra@acm.illinois.edu'
const username = process.env.JWTGEN_USERNAME || "infra@acm.illinois.edu"
const payload = {
aud: "custom_jwt",
iss: "custom_jwt",
iat: Math.floor(Date.now() / 1000),
nbf: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (3600 * 24), // Token expires after 24 hour
acr: "1",
aio: "AXQAi/8TAAAA",
amr: ["pwd"],
appid: "your-app-id",
appidacr: "1",
email: username,
groups: ["0"],
idp: "https://login.microsoftonline.com",
ipaddr: "192.168.1.1",
name: "Doe, John",
oid: "00000000-0000-0000-0000-000000000000",
rh: "rh-value",
scp: "user_impersonation",
sub: "subject",
tid: "tenant-id",
unique_name: username,
uti: "uti-value",
ver: "1.0"
aud: "custom_jwt",
iss: "custom_jwt",
iat: Math.floor(Date.now() / 1000),
nbf: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600 * 24, // Token expires after 24 hour
acr: "1",
aio: "AXQAi/8TAAAA",
amr: ["pwd"],
appid: "your-app-id",
appidacr: "1",
email: username,
groups: ["0"],
idp: "https://login.microsoftonline.com",
ipaddr: "192.168.1.1",
name: "Doe, John",
oid: "00000000-0000-0000-0000-000000000000",
rh: "rh-value",
scp: "user_impersonation",
sub: "subject",
tid: "tenant-id",
unique_name: username,
uti: "uti-value",
ver: "1.0",
};

const secretKey = process.env.JwtSigningKey;
const token = jwt.sign(payload, secretKey, { algorithm: 'HS256' });
console.log(`USERNAME=${username}`)
console.log('=====================')
console.log(token)
const token = jwt.sign(payload, secretKey, { algorithm: "HS256" });
console.log(`USERNAME=${username}`);
console.log("=====================");
console.log(token);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,4 @@
"resolutions": {
"pdfjs-dist": "^4.8.69"
}
}
}
6 changes: 1 addition & 5 deletions src/api/functions/authorization.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
DynamoDBClient,
GetItemCommand,
QueryCommand,
} from "@aws-sdk/client-dynamodb";
import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb";
import { unmarshall } from "@aws-sdk/util-dynamodb";
import { genericConfig } from "../../common/config.js";
import { DatabaseFetchError } from "../../common/errors/index.js";
Expand Down
2 changes: 0 additions & 2 deletions src/api/functions/entraId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
officersGroupTestingId,
} from "../../common/config.js";
import {
BaseError,
EntraFetchError,
EntraGroupError,
EntraInvitationError,
Expand All @@ -19,7 +18,6 @@ import {
EntraGroupActions,
EntraInvitationResponse,
} from "../../common/types/iam.js";
import { FastifyInstance } from "fastify";
import { UserProfileDataBase } from "common/types/msGraphApi.js";
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
Expand Down
10 changes: 8 additions & 2 deletions src/api/functions/membership.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FastifyBaseLogger, FastifyInstance } from "fastify";
import { FastifyBaseLogger } from "fastify";

export async function checkPaidMembership(
endpoint: string,
Expand All @@ -11,7 +11,13 @@ export async function checkPaidMembership(
log.trace(`Got Membership API Payload for ${netId}: ${membershipApiPayload}`);
try {
return membershipApiPayload["isPaidMember"];
} catch (e: any) {
} catch (e: unknown) {
if (!(e instanceof Error)) {
log.error(
"Failed to get response from membership API (unknown error type.)",
);
throw e;
}
log.error(`Failed to get response from membership API: ${e.toString()}`);
throw e;
}
Expand Down
3 changes: 0 additions & 3 deletions src/api/functions/mobileWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@ import { getSecretValue } from "../plugins/auth.js";
import {
ConfigType,
genericConfig,
GenericConfigType,
SecretConfig,
} from "../../common/config.js";
import {
InternalServerError,
UnauthorizedError,
} from "../../common/errors/index.js";
import { FastifyInstance, FastifyRequest } from "fastify";
// these make sure that esbuild includes the files
import icon from "../resources/MembershipPass.pkpass/icon.png";
import logo from "../resources/MembershipPass.pkpass/logo.png";
import strip from "../resources/MembershipPass.pkpass/strip.png";
Expand Down
55 changes: 55 additions & 0 deletions src/api/functions/stripe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Stripe from "stripe";

export type StripeLinkCreateParams = {
invoiceId: string;
invoiceAmountUsd: number;
contactName: string;
contactEmail: string;
createdBy: string;
stripeApiKey: string;
};

/**
* Create a Stripe payment link for an invoice. Note that invoiceAmountUsd MUST IN CENTS!!
* @param {StripeLinkCreateParams} options
* @returns {string} A stripe link that can be used to pay the invoice
*/
export const createStripeLink = async ({
invoiceId,
invoiceAmountUsd,
contactName,
contactEmail,
createdBy,
stripeApiKey,
}: StripeLinkCreateParams): Promise<{
linkId: string;
priceId: string;
productId: string;
url: string;
}> => {
const stripe = new Stripe(stripeApiKey);
const description = `Created for ${contactName} (${contactEmail}) by ${createdBy}.`;
const product = await stripe.products.create({
name: `Payment for Invoice: ${invoiceId}`,
description,
});
const price = await stripe.prices.create({
currency: "usd",
unit_amount: invoiceAmountUsd,
product: product.id,
});
const paymentLink = await stripe.paymentLinks.create({
line_items: [
{
price: price.id,
quantity: 1,
},
],
});
return {
url: paymentLink.url,
linkId: paymentLink.id,
productId: product.id,
priceId: price.id,
};
};
2 changes: 2 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import NodeCache from "node-cache";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
import mobileWalletRoute from "./routes/mobileWallet.js";
import stripeRoutes from "./routes/stripe.js";

dotenv.config();

Expand Down Expand Up @@ -113,6 +114,7 @@ async function init() {
api.register(iamRoutes, { prefix: "/iam" });
api.register(ticketsPlugin, { prefix: "/tickets" });
api.register(mobileWalletRoute, { prefix: "/mobileWallet" });
api.register(stripeRoutes, { prefix: "/stripe" });
if (app.runEnvironment === "dev") {
api.register(vendingPlugin, { prefix: "/vending" });
}
Expand Down
1 change: 1 addition & 0 deletions src/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"passkit-generator": "^3.3.1",
"pino": "^9.6.0",
"pluralize": "^8.0.0",
"stripe": "^17.6.0",
"uuid": "^11.0.5",
"zod": "^3.23.8",
"zod-to-json-schema": "^3.23.2",
Expand Down
2 changes: 1 addition & 1 deletion src/api/plugins/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
} from "../../common/errors/index.js";
import { genericConfig, SecretConfig } from "../../common/config.js";
import { getGroupRoles, getUserRoles } from "../functions/authorization.js";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";

function intersection<T>(setA: Set<T>, setB: Set<T>): Set<T> {
const _intersection = new Set<T>();
Expand Down Expand Up @@ -237,6 +236,7 @@ const authPlugin: FastifyPluginAsync = async (fastify, _options) => {
});
}
request.log.info(`authenticated request from ${request.username} `);
request.userRoles = userRoles;
return userRoles;
},
);
Expand Down
2 changes: 2 additions & 0 deletions src/api/resources/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

declare module "*.png" {
const value: string;
export default value;
Expand Down
16 changes: 11 additions & 5 deletions src/api/routes/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
DatabaseFetchError,
DatabaseInsertError,
DiscordEventError,
NotFoundError,
ValidationError,
} from "../../common/errors/index.js";
import { randomUUID } from "crypto";
Expand Down Expand Up @@ -85,7 +86,7 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => {
"/:id?",
{
schema: {
response: { 200: responseJsonSchema },
response: { 201: responseJsonSchema },
},
preValidation: async (request, reply) => {
await fastify.zodValidateBody(request, reply, postRequestSchema);
Expand Down Expand Up @@ -168,7 +169,7 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => {
}
throw new DiscordEventError({});
}
reply.send({
reply.status(201).send({
id: entryUUID,
resource: `/api/v1/events/${entryUUID}`,
});
Expand Down Expand Up @@ -211,10 +212,15 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => {
);
const items = response.Items?.map((item) => unmarshall(item));
if (items?.length !== 1) {
throw new Error("Event not found");
throw new NotFoundError({
endpointName: request.url,
});
}
reply.send(items[0]);
} catch (e: unknown) {
if (e instanceof BaseError) {
throw e;
}
if (e instanceof Error) {
request.log.error("Failed to get from DynamoDB: " + e.toString());
}
Expand All @@ -233,7 +239,7 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => {
"/:id",
{
schema: {
response: { 200: responseJsonSchema },
response: { 201: responseJsonSchema },
},
onRequest: async (request, reply) => {
await fastify.authorize(request, reply, [AppRoles.EVENTS_MANAGER]);
Expand All @@ -254,7 +260,7 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => {
true,
request.log,
);
reply.send({
reply.status(201).send({
id,
resource: `/api/v1/events/${id}`,
});
Expand Down
10 changes: 3 additions & 7 deletions src/api/routes/iam.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FastifyPluginAsync } from "fastify";
import { allAppRoles, AppRoles } from "../../common/roles.js";
import { AppRoles } from "../../common/roles.js";
import { zodToJsonSchema } from "zod-to-json-schema";
import {
addToTenant,
Expand All @@ -16,13 +16,9 @@ import {
InternalServerError,
NotFoundError,
} from "../../common/errors/index.js";
import {
DynamoDBClient,
GetItemCommand,
PutItemCommand,
} from "@aws-sdk/client-dynamodb";
import { PutItemCommand } from "@aws-sdk/client-dynamodb";
import { genericConfig } from "../../common/config.js";
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
import { marshall } from "@aws-sdk/util-dynamodb";
import {
InviteUserPostRequest,
invitePostRequestSchema,
Expand Down
1 change: 0 additions & 1 deletion src/api/routes/ics.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { FastifyPluginAsync } from "fastify";
import {
DynamoDBClient,
QueryCommand,
QueryCommandInput,
ScanCommand,
Expand Down
Loading
Loading