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
8 changes: 8 additions & 0 deletions src/api/functions/tickets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type GetUserPurchasesInputs = {
dynamoClient: DynamoDBClient;
email: string;
logger: ValidLoggers;
productId?: string;
};

export type RawTicketEntry = {
Expand Down Expand Up @@ -36,6 +37,7 @@ export async function getUserTicketingPurchases({
dynamoClient,
email,
logger,
productId,
}: GetUserPurchasesInputs) {
const issuedTickets: TicketInfoEntry[] = [];
const ticketCommand = new QueryCommand({
Expand All @@ -44,7 +46,9 @@ export async function getUserTicketingPurchases({
KeyConditionExpression: "ticketholder_netid = :email",
ExpressionAttributeValues: {
":email": { S: email },
...(productId && { ":productId": { S: productId } }),
},
...(productId && { FilterExpression: "event_id = :productId" }),
});
let ticketResults;
try {
Expand Down Expand Up @@ -85,6 +89,7 @@ export async function getUserMerchPurchases({
dynamoClient,
email,
logger,
productId,
}: GetUserPurchasesInputs) {
const issuedTickets: TicketInfoEntry[] = [];
const merchCommand = new QueryCommand({
Expand All @@ -93,7 +98,9 @@ export async function getUserMerchPurchases({
KeyConditionExpression: "email = :email",
ExpressionAttributeValues: {
":email": { S: email },
...(productId && { ":productId": { S: productId } }),
},
...(productId && { FilterExpression: "item_id = :productId" }),
});
let ticketsResult;
try {
Expand Down Expand Up @@ -122,6 +129,7 @@ export async function getUserMerchPurchases({
email: item.email,
productId: item.item_id,
quantity: item.quantity,
size: item.size,
},
refunded: item.refunded,
fulfilled: item.fulfilled,
Expand Down
53 changes: 52 additions & 1 deletion src/api/functions/uin.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import {
DynamoDBClient,
PutItemCommand,
QueryCommand,
UpdateItemCommand,
} from "@aws-sdk/client-dynamodb";
import { marshall } from "@aws-sdk/util-dynamodb";
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
import { argon2id, hash } from "argon2";
import { genericConfig } from "common/config.js";
import {
BaseError,
DatabaseFetchError,
EntraFetchError,
InternalServerError,
UnauthenticatedError,
Expand Down Expand Up @@ -184,3 +186,52 @@ export async function saveHashedUserUin({
}),
);
}

export async function getUserIdByUin({
dynamoClient,
uin,
pepper,
}: {
dynamoClient: DynamoDBClient;
uin: string;
pepper: string;
}): Promise<{ id: string }> {
const uinHash = await getUinHash({
pepper,
uin,
});

const queryCommand = new QueryCommand({
TableName: genericConfig.UserInfoTable,
IndexName: "UinHashIndex",
KeyConditionExpression: "uinHash = :hash",
ExpressionAttributeValues: {
":hash": { S: uinHash },
},
});

const response = await dynamoClient.send(queryCommand);

if (!response || !response.Items) {
throw new DatabaseFetchError({
message: "Failed to retrieve user from database.",
});
}

if (response.Items.length === 0) {
throw new ValidationError({
message:
"Failed to find user in database. Please have the user run sync and try again.",
});
}

if (response.Items.length > 1) {
throw new ValidationError({
message:
"Multiple users tied to this UIN. This user probably had a NetID change. Please contact support.",
});
}

const data = unmarshall(response.Items[0]) as { id: string };
return data;
}
21 changes: 16 additions & 5 deletions src/api/routes/tickets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import {
getUserMerchPurchases,
getUserTicketingPurchases,
} from "api/functions/tickets.js";
import { illinoisUin } from "common/types/generic.js";
import { getUserIdByUin } from "api/functions/uin.js";

const postMerchSchema = z.object({
type: z.literal("merch"),
Expand Down Expand Up @@ -512,15 +514,18 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => {
});
},
);
fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().get(
"/purchases/:email",
fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().post(
"/getPurchasesByUser",
{
schema: withRoles(
[AppRoles.TICKETS_MANAGER, AppRoles.TICKETS_SCANNER],
withTags(["Tickets/Merchandise"], {
summary: "Get all purchases (merch and tickets) for a given user.",
params: z.object({
email: z.email(),
body: z.object({
productId: z.string().min(1).meta({
description: "The product ID currently being verified",
}),
uin: illinoisUin,
}),
response: {
200: {
Expand All @@ -540,18 +545,24 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => {
onRequest: fastify.authorizeFromSchema,
},
async (request, reply) => {
const userEmail = request.params.email;
const { id: userEmail } = await getUserIdByUin({
dynamoClient: fastify.dynamoClient,
uin: request.body.uin,
pepper: fastify.secretConfig.UIN_HASHING_SECRET_PEPPER,
});
try {
const [ticketsResult, merchResult] = await Promise.all([
getUserTicketingPurchases({
dynamoClient: UsEast1DynamoClient,
email: userEmail,
logger: request.log,
productId: request.body.productId,
}),
getUserMerchPurchases({
dynamoClient: UsEast1DynamoClient,
email: userEmail,
logger: request.log,
productId: request.body.productId,
}),
]);
await reply.send({ merch: merchResult, tickets: ticketsResult });
Expand Down
49 changes: 9 additions & 40 deletions src/api/routes/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
searchUserByUinRequest,
searchUserByUinResponse,
} from "common/types/user.js";
import { getUinHash } from "api/functions/uin.js";
import { getUinHash, getUserIdByUin } from "api/functions/uin.js";
import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
import { QueryCommand } from "@aws-sdk/client-dynamodb";
import { genericConfig } from "common/config.js";
Expand All @@ -30,11 +30,7 @@ const userRoute: FastifyPluginAsync = async (fastify, _options) => {
"/findUserByUin",
{
schema: withRoles(
[
AppRoles.VIEW_USER_INFO,
AppRoles.TICKETS_MANAGER,
AppRoles.TICKETS_SCANNER,
],
[AppRoles.VIEW_USER_INFO],
withTags(["Generic"], {
summary: "Find a user by UIN.",
body: searchUserByUinRequest,
Expand All @@ -53,40 +49,13 @@ const userRoute: FastifyPluginAsync = async (fastify, _options) => {
onRequest: fastify.authorizeFromSchema,
},
async (request, reply) => {
const uinHash = await getUinHash({
pepper: fastify.secretConfig.UIN_HASHING_SECRET_PEPPER,
uin: request.body.uin,
});
const queryCommand = new QueryCommand({
TableName: genericConfig.UserInfoTable,
IndexName: "UinHashIndex",
KeyConditionExpression: "uinHash = :hash",
ExpressionAttributeValues: {
":hash": { S: uinHash },
},
});
const response = await fastify.dynamoClient.send(queryCommand);
if (!response || !response.Items) {
throw new DatabaseFetchError({
message: "Failed to retrieve user from database.",
});
}
if (response.Items.length === 0) {
throw new ValidationError({
message:
"Failed to find user in database. Please have the user run sync and try again.",
});
}
if (response.Items.length > 1) {
throw new ValidationError({
message:
"Multiple users tied to this UIN. This user probably had a NetID change. Please contact support.",
});
}
const data = unmarshall(response.Items[0]) as { id: string };
return reply.send({
email: data.id,
});
return reply.send(
await getUserIdByUin({
dynamoClient: fastify.dynamoClient,
uin: request.body.uin,
pepper: fastify.secretConfig.UIN_HASHING_SECRET_PEPPER,
}),
);
},
);
};
Expand Down
13 changes: 13 additions & 0 deletions src/common/types/generic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ export const illinoisNetId = z
id: "IllinoisNetId",
});

export const illinoisUin = z
.string()
.length(9, { message: "UIN must be 9 characters." })
.regex(/^\d{9}$/i, {
message: "UIN is malformed.",
})
.meta({
description: "Valid Illinois UIN.",
example: "627838939",
id: "IllinoisUin",
});


export const OrgUniqueId = z.enum(Object.keys(Organizations)).meta({
description: "The unique org ID for a given ACM sub-organization. See https://github.com/acm-uiuc/js-shared/blob/main/src/orgs.ts#L15",
examples: ["A01", "C01"],
Expand Down
3 changes: 2 additions & 1 deletion src/common/types/user.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as z from "zod/v4";
import { illinoisUin } from "./generic.js";

export const searchUserByUinRequest = z.object({
uin: z.string().length(9)
uin: illinoisUin
});

export const searchUserByUinResponse = z.object({
Expand Down
Loading
Loading