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
1 change: 1 addition & 0 deletions .test.env
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ GOOGLE_OAUTH_ID="123456789.apps.googleusercontent.com"
GOOGLE_OAUTH_SECRET="123456789"

JWT_SECRET="123456789"
EVENT_SECRET="123456789"

# System administrators
SYSTEM_ADMINS="admin"
Expand Down
1 change: 1 addition & 0 deletions src/common/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const Config = {
GOOGLE_OAUTH_SECRET: requireEnv("GOOGLE_OAUTH_SECRET"),

JWT_SECRET: requireEnv("JWT_SECRET"),
EVENT_SECRET: requireEnv("EVENT_SECRET"),

SYSTEM_ADMIN_LIST: requireEnv("SYSTEM_ADMINS").split(","),

Expand Down
13 changes: 13 additions & 0 deletions src/services/event/event-lib.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { FilterQuery } from "mongoose";
import { Role } from "../auth/auth-schemas";
import { Event } from "./event-schemas";
import Config from "../../common/config";
import { createHash } from "crypto";

/**
* Returns a filter query to filter out events that the roles specified cannot access
Expand All @@ -16,3 +18,14 @@ export function restrictEventsByRoles(roles: Role[]): FilterQuery<Event> {
return { isPrivate: false, isStaff: false, isPro: false };
}
}

/**
* Returns an event QR code generated from an event id, intended to make event qr codes private from event id
* @param eventId The event id to generate a qr for
* @returns The qr code
*/
export function getEventQRCode(eventId: string): string {
const data = `${eventId}:${Config.EVENT_SECRET}`;
const hash = createHash("sha256").update(data).digest();
return `${eventId}:${hash.toString("base64")}`;
}
18 changes: 18 additions & 0 deletions src/services/event/event-router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { TESTER, getAsAttendee, getAsStaff, putAsAttendee, putAsStaff } from "..
import { UserFollowing, UserInfo } from "../user/user-schemas";
import { EventSchema } from "./event-schemas";
import { z } from "zod";
import { getEventQRCode } from "./event-lib";

const TESTER_EVENT_FOLLOWERS = {
eventId: "other-event",
Expand Down Expand Up @@ -374,3 +375,20 @@ describe("PUT /event/update-attendance/:id/", () => {
expect(count).toBe(1);
});
});

describe("GET /event/:id/qr/", () => {
it("returns qr code for an event", async () => {
const response = await getAsStaff(`/event/${TESTER_EVENT_1.eventId}/qr/`).expect(StatusCode.SuccessOK);

const data = JSON.parse(response.text);
expect(data).toMatchObject({
qrCode: getEventQRCode(TESTER_EVENT_1.eventId),
});
});

it("fails for an attendee", async () => {
const response = await getAsAttendee(`/event/${TESTER_EVENT_1.eventId}/qr/`).expect(StatusCode.ClientErrorForbidden);

expect(JSON.parse(response.text)).toHaveProperty("error", "Forbidden");
});
});
40 changes: 39 additions & 1 deletion src/services/event/event-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ import {
EventAttendeesSchema,
EventAttendeesInfoSchema,
EventAttendanceSchema,
EventQRCodeResponseSchema,
} from "./event-schemas";
import { EventIdSchema, SuccessResponseSchema, UserIdSchema } from "../../common/schemas";
import { z } from "zod";
import Models from "../../common/models";
import { tryGetAuthenticatedUser } from "../../common/auth";
import { restrictEventsByRoles } from "./event-lib";
import { getEventQRCode, restrictEventsByRoles } from "./event-lib";
import Config from "../../common/config";
import crypto from "crypto";

Expand Down Expand Up @@ -480,4 +481,41 @@ eventsRouter.put(
},
);

eventsRouter.get(
"/:id/qr",
specification({
method: "get",
path: "/event/{id}/qr",
tag: Tag.EVENT,
role: Role.STAFF,
summary: "Gets qr code of an event",
parameters: z.object({
id: EventIdSchema,
}),
responses: {
[StatusCode.SuccessOK]: {
description: "The QR code",
schema: EventQRCodeResponseSchema,
},
[StatusCode.ClientErrorNotFound]: {
description: "Couldn't find the event specified.",
schema: EventNotFoundErrorSchema,
},
},
}),
async (req, res) => {
const { id: eventId } = req.params;

const event = await Models.Event.findOne({ eventId });

if (!event) {
return res.status(StatusCode.ClientErrorNotFound).send(EventNotFoundError);
}

return res.status(StatusCode.SuccessOK).send({
qrCode: getEventQRCode(eventId),
});
},
);

export default eventsRouter;
5 changes: 5 additions & 0 deletions src/services/event/event-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ export const LocationSchema = z
})
.openapi("Location");

export const EventQRCodeSchema = z.string().openapi("EventQRCode");
export const EventQRCodeResponseSchema = z.object({
qrCode: EventQRCodeSchema,
});

export const EventSchema = z
.object({
eventId: EventIdSchema,
Expand Down
9 changes: 5 additions & 4 deletions src/services/user/user-router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { UserAttendance, UserFollowing, UserInfo } from "./user-schemas";
import { Role } from "../auth/auth-schemas";
import { decryptQRCode } from "./user-lib";
import { fail } from "assert";
import { getEventQRCode } from "../event/event-lib";

const TESTER_USER = {
userId: TESTER.id,
Expand Down Expand Up @@ -342,7 +343,7 @@ describe("PUT /user/scan-event/", () => {
it("works for an attendee without existing attendance", async () => {
await Models.UserAttendance.deleteOne({ userId: TESTER.id });
const response = await putAsAttendee("/user/scan-event/")
.send({ eventId: TEST_EVENT.eventId })
.send({ eventId: getEventQRCode(TEST_EVENT.eventId) })
.expect(StatusCode.SuccessOK);

expect(JSON.parse(response.text)).toMatchObject({
Expand All @@ -365,15 +366,15 @@ describe("PUT /user/scan-event/", () => {
});

const duplicateResponse = await putAsAttendee("/user/scan-event/")
.send({ eventId: TEST_EVENT.eventId })
.send({ eventId: getEventQRCode(TEST_EVENT.eventId) })
.expect(StatusCode.ClientErrorBadRequest);

expect(JSON.parse(duplicateResponse.text)).toHaveProperty("error", "AlreadyCheckedIn");
});

it("works for an attendee and returns already checked in if already checked in", async () => {
const response = await putAsAttendee("/user/scan-event/")
.send({ eventId: TEST_EVENT.eventId })
.send({ eventId: getEventQRCode(TEST_EVENT.eventId) })
.expect(StatusCode.SuccessOK);

expect(JSON.parse(response.text)).toMatchObject({
Expand All @@ -396,7 +397,7 @@ describe("PUT /user/scan-event/", () => {
});

const duplicateResponse = await putAsAttendee("/user/scan-event/")
.send({ eventId: TEST_EVENT.eventId })
.send({ eventId: getEventQRCode(TEST_EVENT.eventId) })
.expect(StatusCode.ClientErrorBadRequest);

expect(JSON.parse(duplicateResponse.text)).toHaveProperty("error", "AlreadyCheckedIn");
Expand Down
10 changes: 9 additions & 1 deletion src/services/user/user-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import Models from "../../common/models";
import specification, { Tag } from "../../middleware/specification";
import { z } from "zod";
import { generateQRCode } from "./user-lib";
import { getEventQRCode } from "../event/event-lib";

const userRouter = Router();

Expand Down Expand Up @@ -292,8 +293,15 @@ userRouter.put(
}),
async (req, res) => {
const { id: userId } = getAuthenticatedUser(req);
const eventId = req.body.eventId;

// Ensure QR Code is valid & extract event id from it
const qrCode = req.body.eventId;
const eventId = qrCode.split(":")[0];
if (!eventId || getEventQRCode(eventId) != qrCode) {
return res.status(StatusCode.ClientErrorNotFound).json(EventNotFoundError);
}

// Then, check in
const result = await performCheckIn(eventId, userId);

if (!result.success) {
Expand Down