Skip to content

Commit ea6c9e3

Browse files
committed
add ability to get the issued tickets given the right role
1 parent c385558 commit ea6c9e3

File tree

7 files changed

+173
-29
lines changed

7 files changed

+173
-29
lines changed

src/config.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const environmentConfig: EnvironmentConfigType = {
5454
"1": [], // Dummy Group for development only
5555
},
5656
UserRoleMapping: {
57-
"[email protected]": [AppRoles.TICKET_SCANNER],
57+
"[email protected]": [AppRoles.TICKETS_SCANNER],
5858
},
5959
AzureRoleMapping: { AutonomousWriters: [AppRoles.EVENTS_MANAGER] },
6060
ValidCorsOrigins: [
@@ -77,11 +77,17 @@ const environmentConfig: EnvironmentConfigType = {
7777
], // Exec
7878
},
7979
UserRoleMapping: {
80-
"[email protected]": [AppRoles.TICKET_SCANNER],
81-
"[email protected]": [AppRoles.TICKET_SCANNER],
82-
"[email protected]": [AppRoles.TICKET_SCANNER],
83-
"[email protected]": [AppRoles.TICKET_SCANNER],
84-
"[email protected]": [AppRoles.TICKET_SCANNER],
80+
"[email protected]": [AppRoles.TICKETS_SCANNER],
81+
"[email protected]": [AppRoles.TICKETS_SCANNER],
82+
"[email protected]": [AppRoles.TICKETS_SCANNER],
83+
84+
AppRoles.TICKETS_SCANNER,
85+
AppRoles.TICKETS_MANAGER,
86+
],
87+
88+
AppRoles.TICKETS_SCANNER,
89+
AppRoles.TICKETS_MANAGER,
90+
],
8591
},
8692
AzureRoleMapping: { AutonomousWriters: [AppRoles.EVENTS_MANAGER] },
8793
ValidCorsOrigins: [

src/errors/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,14 @@ export class TicketNotValidError extends BaseError<"TicketNotValidError"> {
157157
});
158158
}
159159
}
160+
161+
export class NotSupportedError extends BaseError<"NotSupportedError"> {
162+
constructor({ message }: { message?: string }) {
163+
super({
164+
name: "NotSupportedError",
165+
id: 110,
166+
message: message || "This operation is not supported.",
167+
httpStatusCode: 400,
168+
});
169+
}
170+
}

src/roles.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ export type RunEnvironment = (typeof runEnvironments)[number];
44
export enum AppRoles {
55
EVENTS_MANAGER = "manage:events",
66
SSO_INVITE_USER = "invite:sso",
7-
TICKET_SCANNER = "scan:tickets",
7+
TICKETS_SCANNER = "scan:tickets",
8+
TICKETS_MANAGER = "manage:tickets",
89
}
910
export const allAppRoles = Object.values(AppRoles).filter(
1011
(value) => typeof value === "string",

src/routes/tickets.ts

Lines changed: 102 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { FastifyPluginAsync } from "fastify";
22
import { z } from "zod";
3-
import { DynamoDBClient, UpdateItemCommand } from "@aws-sdk/client-dynamodb";
3+
import {
4+
DynamoDBClient,
5+
QueryCommand,
6+
UpdateItemCommand,
7+
} from "@aws-sdk/client-dynamodb";
48
import { genericConfig } from "../config.js";
59
import {
610
BaseError,
711
DatabaseFetchError,
12+
NotFoundError,
13+
NotSupportedError,
814
TicketNotFoundError,
915
TicketNotValidError,
1016
UnauthenticatedError,
@@ -35,12 +41,20 @@ const purchaseSchema = z.object({
3541

3642
type PurchaseData = z.infer<typeof purchaseSchema>;
3743

38-
const responseJsonSchema = zodToJsonSchema(
44+
const ticketEntryZod = z.object({
45+
valid: z.boolean(),
46+
type: z.enum(["merch", "ticket"]),
47+
ticketId: z.string().min(1),
48+
purchaserData: purchaseSchema,
49+
});
50+
51+
type TicketEntry = z.infer<typeof ticketEntryZod>;
52+
53+
const responseJsonSchema = zodToJsonSchema(ticketEntryZod);
54+
55+
const getTicketsResponseJsonSchema = zodToJsonSchema(
3956
z.object({
40-
valid: z.boolean(),
41-
type: z.enum(["merch", "ticket"]),
42-
ticketId: z.string().min(1),
43-
purchaserData: purchaseSchema,
57+
tickets: z.array(ticketEntryZod),
4458
}),
4559
);
4660

@@ -52,7 +66,79 @@ const dynamoClient = new DynamoDBClient({
5266
region: genericConfig.AwsRegion,
5367
});
5468

69+
type TicketsGetRequest = {
70+
Params: { id: string };
71+
Querystring: { type: string };
72+
Body: undefined;
73+
};
74+
5575
const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => {
76+
fastify.get<TicketsGetRequest>(
77+
"/:eventId",
78+
{
79+
schema: {
80+
querystring: {
81+
type: "object", // Add this to specify it's an object schema
82+
properties: {
83+
// Add this to define the properties
84+
type: {
85+
type: "string",
86+
enum: ["merch", "ticket"],
87+
},
88+
},
89+
},
90+
response: {
91+
200: getTicketsResponseJsonSchema,
92+
},
93+
},
94+
onRequest: async (request, reply) => {
95+
await fastify.authorize(request, reply, [AppRoles.TICKETS_MANAGER]);
96+
},
97+
},
98+
async (request, reply) => {
99+
const eventId = (request.params as Record<string, string>).eventId;
100+
const eventType = request.query?.type;
101+
const issuedTickets: TicketEntry[] = [];
102+
switch (eventType) {
103+
case "merch":
104+
const command = new QueryCommand({
105+
TableName: genericConfig.MerchStorePurchasesTableName,
106+
IndexName: "ItemIdIndexAll",
107+
KeyConditionExpression: "item_id = :itemId",
108+
ExpressionAttributeValues: {
109+
":itemId": { S: eventId },
110+
},
111+
});
112+
const response = await dynamoClient.send(command);
113+
if (!response.Items) {
114+
throw new NotFoundError({
115+
endpointName: `/api/v1/tickets/${eventId}`,
116+
});
117+
}
118+
for (const item of response.Items) {
119+
const unmarshalled = unmarshall(item);
120+
issuedTickets.push({
121+
type: "merch",
122+
valid: true,
123+
ticketId: unmarshalled["stripe_pi"],
124+
purchaserData: {
125+
email: unmarshalled["email"],
126+
productId: eventId,
127+
quantity: unmarshalled["quantity"],
128+
size: unmarshalled["size"],
129+
},
130+
});
131+
}
132+
break;
133+
default:
134+
throw new NotSupportedError({
135+
message: `Retrieving tickets currently only supported on type "merch"!`,
136+
});
137+
}
138+
const response = { tickets: issuedTickets };
139+
return reply.send(response);
140+
},
141+
);
56142
fastify.post<{ Body: VerifyPostRequest }>(
57143
"/checkIn",
58144
{
@@ -63,14 +149,16 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => {
63149
await fastify.zodValidateBody(request, reply, postSchema);
64150
},
65151
onRequest: async (request, reply) => {
66-
await fastify.authorize(request, reply, [AppRoles.TICKET_SCANNER]);
152+
await fastify.authorize(request, reply, [AppRoles.TICKETS_SCANNER]);
67153
},
68154
},
69155
async (request, reply) => {
70156
let command: UpdateItemCommand;
71157
let ticketId: string;
72158
if (!request.username) {
73-
throw new UnauthenticatedError({ message: "Could not find username." });
159+
throw new UnauthenticatedError({
160+
message: "Could not find username.",
161+
});
74162
}
75163
switch (request.body.type) {
76164
case "merch":
@@ -110,7 +198,9 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => {
110198
});
111199
break;
112200
default:
113-
throw new ValidationError({ message: `Unknown verification type!` });
201+
throw new ValidationError({
202+
message: `Unknown verification type!`,
203+
});
114204
}
115205
let purchaserData: PurchaseData;
116206
try {
@@ -200,7 +290,9 @@ const ticketsPlugin: FastifyPluginAsync = async (fastify, _options) => {
200290
});
201291
break;
202292
default:
203-
throw new ValidationError({ message: `Unknown verification type!` });
293+
throw new ValidationError({
294+
message: `Unknown verification type!`,
295+
});
204296
}
205297
await dynamoClient.send(command);
206298
reply.send(response);

tests/unit/auth.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,6 @@ test("Test user-specific role grants", async () => {
8080
const jsonBody = await response.json();
8181
expect(jsonBody).toEqual({
8282
username: "[email protected]",
83-
roles: [AppRoles.TICKET_SCANNER],
83+
roles: [AppRoles.TICKETS_SCANNER],
8484
});
8585
});

tests/unit/mockMerchPurchases.testdata.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const fulfilledMerchItem1 = {
1010
BOOL: true,
1111
},
1212
item_id: {
13-
S: "sigpwny_fallctf_2022_shirt",
13+
S: "2024_fa_barcrawl",
1414
},
1515
quantity: {
1616
N: "1",

tests/unit/tickets.test.ts

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { afterAll, expect, test, beforeEach, vi, describe } from "vitest";
2-
import { DynamoDBClient, UpdateItemCommand } from "@aws-sdk/client-dynamodb";
2+
import {
3+
DynamoDBClient,
4+
QueryCommand,
5+
UpdateItemCommand,
6+
} from "@aws-sdk/client-dynamodb";
37
import { mockClient } from "aws-sdk-client-mock";
48
import init from "../../src/index.js";
5-
import { EventGetResponse } from "../../src/routes/events.js";
69
import { secretObject } from "./secret.testdata.js";
710
import {
11+
dynamoTableData,
812
fulfilledMerchItem1,
913
refundedMerchItem,
1014
unfulfilledMerchItem1,
@@ -39,7 +43,7 @@ describe("Test ticket purchase verification", async () => {
3943
ticketId:
4044
"9d98e1e3c2138c93dd5a284239eddfa9c3037a0862972cd0f51ee1b54257a37e",
4145
});
42-
const responseDataJson = response.body as EventGetResponse;
46+
const responseDataJson = response.body;
4347
expect(response.statusCode).toEqual(200);
4448
expect(responseDataJson).toEqual({
4549
valid: true,
@@ -69,7 +73,7 @@ describe("Test ticket purchase verification", async () => {
6973
ticketId:
7074
"9d98e1e3c2138c93dd5a284239eddfa9c3037a0862972cd0f51ee1b54257a37e",
7175
});
72-
const responseDataJson = response.body as EventGetResponse;
76+
const responseDataJson = response.body;
7377
expect(response.statusCode).toEqual(200);
7478
expect(responseDataJson).toEqual({
7579
valid: true,
@@ -99,7 +103,7 @@ describe("Test ticket purchase verification", async () => {
99103
100104
stripePi: "pi_6T9QvUwR2IOj4CyF35DsXK7P",
101105
});
102-
const responseDataJson = response.body as EventGetResponse;
106+
const responseDataJson = response.body;
103107
expect(response.statusCode).toEqual(400);
104108
expect(responseDataJson).toEqual({
105109
error: true,
@@ -125,7 +129,7 @@ describe("Test ticket purchase verification", async () => {
125129
ticketId:
126130
"975b4470cf37d7cf20fd404a711513fd1d1e68259ded27f10727d1384961843d",
127131
});
128-
const responseDataJson = response.body as EventGetResponse;
132+
const responseDataJson = response.body;
129133
expect(response.statusCode).toEqual(400);
130134
expect(responseDataJson).toEqual({
131135
error: true,
@@ -153,7 +157,7 @@ describe("Test merch purchase verification", async () => {
153157
154158
stripePi: "pi_8J4NrYdA3S7cW8Ty92FnGJ6L",
155159
});
156-
const responseDataJson = response.body as EventGetResponse;
160+
const responseDataJson = response.body;
157161
expect(response.statusCode).toEqual(200);
158162
expect(responseDataJson).toEqual({
159163
valid: true,
@@ -183,7 +187,7 @@ describe("Test merch purchase verification", async () => {
183187
ticketId:
184188
"975b4470cf37d7cf20fd404a711513fd1d1e68259ded27f10727d1384961843d",
185189
});
186-
const responseDataJson = response.body as EventGetResponse;
190+
const responseDataJson = response.body;
187191
expect(response.statusCode).toEqual(400);
188192
expect(responseDataJson).toEqual({
189193
error: true,
@@ -208,7 +212,7 @@ describe("Test merch purchase verification", async () => {
208212
209213
stripePi: "pi_6T9QvUwR2IOj4CyF35DsXK7P",
210214
});
211-
const responseDataJson = response.body as EventGetResponse;
215+
const responseDataJson = response.body;
212216
expect(response.statusCode).toEqual(400);
213217
expect(responseDataJson).toEqual({
214218
error: true,
@@ -233,7 +237,7 @@ describe("Test merch purchase verification", async () => {
233237
234238
stripePi: "pi_3Q5GewDiGOXU9RuS16txRR5D",
235239
});
236-
const responseDataJson = response.body as EventGetResponse;
240+
const responseDataJson = response.body;
237241
expect(response.statusCode).toEqual(400);
238242
expect(responseDataJson).toEqual({
239243
error: true,
@@ -244,6 +248,36 @@ describe("Test merch purchase verification", async () => {
244248
});
245249
});
246250

251+
describe("Test getting all issued tickets", async () => {
252+
test("Happy path: get all tickets", async () => {
253+
ddbMock
254+
.on(QueryCommand)
255+
.resolvesOnce({ Items: dynamoTableData })
256+
.resolvesOnce({});
257+
258+
const testJwt = createJwt();
259+
await app.ready();
260+
const response = await supertest(app.server)
261+
.get("/api/v1/tickets/2024_fa_barcrawl?type=merch")
262+
.set("authorization", `Bearer ${testJwt}`);
263+
const responseDataJson = response.body;
264+
expect(response.statusCode).toEqual(200);
265+
expect(responseDataJson.tickets).toHaveLength(4);
266+
});
267+
test("Sad path: fail on type 'ticket'", async () => {
268+
ddbMock.on(QueryCommand).rejects();
269+
270+
const testJwt = createJwt();
271+
await app.ready();
272+
const response = await supertest(app.server)
273+
.get("/api/v1/tickets/2024_fa_barcrawl?type=ticket")
274+
.set("authorization", `Bearer ${testJwt}`);
275+
const responseDataJson = response.body;
276+
expect(response.statusCode).toEqual(400);
277+
expect(responseDataJson.id).toEqual(110);
278+
});
279+
});
280+
247281
afterAll(async () => {
248282
await app.close();
249283
vi.useRealTimers();

0 commit comments

Comments
 (0)