Skip to content

Commit

Permalink
Mobile Wallet Membership Pass (#44)
Browse files Browse the repository at this point in the history
* tooling updates

* update gitignore

* functionality

* fix route integration?

* fix?

* weird hack around build process

* fix lockfile

* add tests

* mock something

* testing

* fix ses mock

* add one more live test

* use moment-timezone build
  • Loading branch information
devksingh4 authored Jan 29, 2025
1 parent ba0d6b1 commit 437fad4
Show file tree
Hide file tree
Showing 29 changed files with 1,356 additions and 43 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,4 @@ __pycache__
/playwright-report/
/blob-report/
/playwright/.cache/
dist_devel/
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@ clean:
rm -rf src/ui/node_modules/
rm -rf dist/
rm -rf dist_ui/
rm -rf dist_devel/

build: src/ cloudformation/ docs/
yarn -D
VITE_BUILD_HASH=$(GIT_HASH) yarn build
cp -r src/api/resources/ dist/api/resources
sam build --template-file cloudformation/main.yml

local:
Expand Down
19 changes: 18 additions & 1 deletion cloudformation/iam.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Parameters:
LambdaFunctionName:
Type: String
AllowedPattern: ^[a-zA-Z0-9]+[a-zA-Z0-9-]+[a-zA-Z0-9]+$
SesEmailDomain:
Type: String
Resources:
ApiLambdaIAMRole:
Type: AWS::IAM::Role
Expand All @@ -24,6 +26,21 @@ Resources:
Service:
- lambda.amazonaws.com
Policies:
- PolicyDocument:
Version: '2012-10-17'
Statement:
- Action:
- ses:SendEmail
- ses:SendRawEmail
Effect: Allow
Resource: "*"
Condition:
StringEquals:
ses:FromAddress: !Sub "membership@${SesEmailDomain}"
ForAllValues:StringLike:
ses:Recipients:
- "*@illinois.edu"
PolicyName: ses-membership
- PolicyDocument:
Version: '2012-10-17'
Statement:
Expand Down Expand Up @@ -85,4 +102,4 @@ Outputs:
Value:
Fn::GetAtt:
- ApiLambdaIAMRole
- Arn
- Arn
25 changes: 4 additions & 21 deletions cloudformation/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ Mappings:
General:
dev:
LogRetentionDays: 7
SesDomain: "aws.qa.acmuiuc.org"
prod:
LogRetentionDays: 365
SesDomain: "acm.illinois.edu"
ApiGwConfig:
dev:
ApiCertificateArn: arn:aws:acm:us-east-1:427040638965:certificate/63ccdf0b-d2b5-44f0-b589-eceffb935c23
Expand Down Expand Up @@ -71,6 +73,7 @@ Resources:
Parameters:
RunEnvironment: !Ref RunEnvironment
LambdaFunctionName: !Sub ${ApplicationPrefix}-lambda
SesEmailDomain: !FindInMap [General, !Ref RunEnvironment, SesDomain]

AppLogGroups:
Type: AWS::Serverless::Application
Expand Down Expand Up @@ -120,29 +123,9 @@ Resources:
Type: AWS::Serverless::Function
DependsOn:
- AppLogGroups
Metadata:
BuildMethod: esbuild
BuildProperties:
Format: esm
Minify: true
OutExtension:
- .js=.mjs
Target: "es2022"
Sourcemap: false
EntryPoints:
- api/lambda.js
External:
- aws-sdk
Banner:
- js=import path from 'path';
import { fileURLToPath } from 'url';
import { createRequire as topLevelCreateRequire } from 'module';
const require = topLevelCreateRequire(import.meta.url);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Properties:
Architectures: [arm64]
CodeUri: ../dist
CodeUri: ../dist/lambda
AutoPublishAlias: live
Runtime: nodejs22.x
Description: !Sub "${ApplicationFriendlyName} API Lambda"
Expand Down
2 changes: 1 addition & 1 deletion generate_jwt.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const payload = {
groups: ["0"],
idp: "https://login.microsoftonline.com",
ipaddr: "192.168.1.1",
name: "John Doe",
name: "Doe, John",
oid: "00000000-0000-0000-0000-000000000000",
rh: "rh-value",
scp: "user_impersonation",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"scripts": {
"build": "yarn workspaces run build && yarn lockfile-manage",
"dev": "concurrently --names 'api,ui' 'yarn workspace infra-core-api run dev' 'yarn workspace infra-core-ui run dev'",
"lockfile-manage": "synp --with-workspace --source-file yarn.lock && cp package-lock.json dist/ && cp src/api/package.json dist/ && rm package-lock.json",
"lockfile-manage": "synp --with-workspace --source-file yarn.lock && cp package-lock.json dist/lambda/ && cp src/api/package.lambda.json dist/lambda/package.json && rm package-lock.json",
"prettier": "yarn workspaces run prettier && prettier --check tests/**/*.ts",
"prettier:write": "yarn workspaces run prettier:write && prettier --write tests/**/*.ts",
"lint": "yarn workspaces run lint",
Expand Down
39 changes: 39 additions & 0 deletions src/api/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import esbuild from "esbuild";
import { resolve } from "path";

esbuild
.build({
entryPoints: ["api/lambda.js"], // Entry file
bundle: true,
format: "esm",
minify: true,
outdir: "../../dist/lambda/",
outExtension: { ".js": ".mjs" },
loader: {
".png": "file",
".pkpass": "file",
".json": "file",
}, // File loaders
target: "es2022", // Target ES2022
sourcemap: false,
platform: "node",
external: ["aws-sdk", "moment-timezone", "passkit-generator", "fastify"],
alias: {
'moment-timezone': resolve(process.cwd(), '../../node_modules/moment-timezone/builds/moment-timezone-with-data-10-year-range.js')
},
banner: {
js: `
import path from 'path';
import { fileURLToPath } from 'url';
import { createRequire as topLevelCreateRequire } from 'module';
const require = topLevelCreateRequire(import.meta.url);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
`.trim(),
}, // Banner for compatibility with CommonJS
})
.then(() => console.log("Build completed successfully!"))
.catch((error) => {
console.error("Build failed:", error);
process.exit(1);
});
47 changes: 47 additions & 0 deletions src/api/esbuild.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { build, context } from 'esbuild';
import { readFileSync } from 'fs';
import { resolve } from 'path';

const isWatching = !!process.argv.includes('--watch')
const nodePackage = JSON.parse(readFileSync(resolve(process.cwd(), 'package.json'), 'utf8'));

const buildOptions = {
entryPoints: [resolve(process.cwd(), 'index.ts')],
outfile: resolve(process.cwd(), '../', '../', 'dist_devel', 'index.js'),
bundle: true,
platform: 'node',
format: 'esm',
external: [
Object.keys(nodePackage.dependencies ?? {}),
Object.keys(nodePackage.peerDependencies ?? {}),
Object.keys(nodePackage.devDependencies ?? {}),
].flat(),
loader: {
'.png': 'file', // Add this line to specify a loader for .png files
},
alias: {
'moment-timezone': resolve(process.cwd(), '../../node_modules/moment-timezone/builds/moment-timezone-with-data-10-year-range.js')
},
banner: {
js: `
import path from 'path';
import { fileURLToPath } from 'url';
import { createRequire as topLevelCreateRequire } from 'module';
const require = topLevelCreateRequire(import.meta.url);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
`.trim(),
}, // Banner for compatibility with CommonJS
};

if (isWatching) {
context(buildOptions).then(ctx => {
if (isWatching) {
ctx.watch();
} else {
ctx.rebuild();
}
});
} else {
build(buildOptions)
}
46 changes: 46 additions & 0 deletions src/api/functions/entraId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "../../common/config.js";
import {
BaseError,

Check warning on line 9 in src/api/functions/entraId.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

'BaseError' is defined but never used. Allowed unused vars must match /^_/u
EntraFetchError,
EntraGroupError,
EntraInvitationError,
InternalServerError,
Expand All @@ -19,6 +20,7 @@ import {
EntraInvitationResponse,
} from "../../common/types/iam.js";
import { FastifyInstance } from "fastify";
import { UserProfileDataBase } from "common/types/msGraphApi.js";

function validateGroupId(groupId: string): boolean {
const groupIdPattern = /^[a-zA-Z0-9-]+$/; // Adjust the pattern as needed
Expand Down Expand Up @@ -351,3 +353,47 @@ export async function listGroupMembers(
});
}
}

/**
* Retrieves the profile of a user from Entra ID.
* @param token - Entra ID token authorized to perform this action.
* @param userId - The user ID to fetch the profile for.
* @throws {EntraUserError} If fetching the user profile fails.
* @returns {Promise<UserProfileDataBase>} The user's profile information.
*/
export async function getUserProfile(
token: string,
email: string,
): Promise<UserProfileDataBase> {
const userId = await resolveEmailToOid(token, email);
try {
const url = `https://graph.microsoft.com/v1.0/users/${userId}?$select=userPrincipalName,givenName,surname,displayName,otherMails,mail`;
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
});

if (!response.ok) {
const errorData = (await response.json()) as {
error?: { message?: string };
};
throw new EntraFetchError({
message: errorData?.error?.message ?? response.statusText,
email,
});
}
return (await response.json()) as UserProfileDataBase;
} catch (error) {
if (error instanceof EntraFetchError) {
throw error;
}

throw new EntraFetchError({
message: error instanceof Error ? error.message : String(error),
email,
});
}
}
18 changes: 18 additions & 0 deletions src/api/functions/membership.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { FastifyBaseLogger, FastifyInstance } from "fastify";

Check warning on line 1 in src/api/functions/membership.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

'FastifyInstance' is defined but never used. Allowed unused vars must match /^_/u

export async function checkPaidMembership(
endpoint: string,
log: FastifyBaseLogger,
netId: string,
) {
const membershipApiPayload = (await (
await fetch(`${endpoint}?netId=${netId}`)
).json()) as { netId: string; isPaidMember: boolean };
log.trace(`Got Membership API Payload for ${netId}: ${membershipApiPayload}`);
try {
return membershipApiPayload["isPaidMember"];
} catch (e: any) {

Check warning on line 14 in src/api/functions/membership.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

Unexpected any. Specify a different type
log.error(`Failed to get response from membership API: ${e.toString()}`);
throw e;
}
}
Loading

0 comments on commit 437fad4

Please sign in to comment.