Skip to content

Commit

Permalink
Merge branch 'main' into dev/jacob/weak-verify
Browse files Browse the repository at this point in the history
  • Loading branch information
AydanPirani authored Sep 18, 2024
2 parents 052bca0 + 11c496c commit 77c5ee5
Show file tree
Hide file tree
Showing 16 changed files with 224 additions and 40 deletions.
2 changes: 1 addition & 1 deletion src/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const TESTER_EVENT = {
isVisible: true,
eventId: "b",
attendanceCount: 0,
eventType: "B",
eventType: "SPEAKER",
};

describe("general app test", () => {
Expand Down
3 changes: 3 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export const Config = {

HASH_SALT_ROUNDS: 10,
VERIFY_EXP_TIME_MS: 300,
SPONSOR_ENTIRES_PER_PAGE: 60,

// QR Scanning
QR_HASH_ITERATIONS: 10000,
Expand All @@ -98,6 +99,8 @@ export const DeviceRedirects: Record<string, string> = {
dev: `${API_BASE}/auth/dev/`,
mobile: "reflectionsprojections://--/Login",
admin: "https://admin.reflectionsprojections.org/auth/",
// admin: "http://localhost:5173/auth/",
pwa: "localhost:8081/Login",
};

export const ses = new AWS.SES({
Expand Down
38 changes: 31 additions & 7 deletions src/services/attendee/attendee-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,13 +227,13 @@ attendeeRouter.get(
}
);

// Get attendee info via userId
attendeeRouter.get(
"/",
RoleChecker([Role.Enum.USER]),
"/id/:USERID",
RoleChecker([Role.Enum.STAFF, Role.Enum.ADMIN]),
async (req, res, next) => {
try {
const payload = res.locals.payload;
const userId = payload.userId;
const userId = req.params.USERID;

// Check if the user exists in the database
const user = await Database.ATTENDEE.findOne({ userId });
Expand All @@ -251,13 +251,30 @@ attendeeRouter.get(
}
);

attendeeRouter.get(
"/emails",
RoleChecker([Role.Enum.STAFF, Role.Enum.ADMIN]),
async (req, res, next) => {
try {
const projection = {
email: 1,
userId: 1,
};
const registrations = await Database.ATTENDEE.find({}, projection);

return res.status(StatusCodes.OK).json(registrations);
} catch (error) {
next(error);
}
}
);

attendeeRouter.post(
"/redeemMerch/:ITEM",
RoleChecker([]),
RoleChecker([Role.Enum.STAFF, Role.Enum.ADMIN]),
async (req, res, next) => {
try {
const payload = res.locals.payload;
const userId = payload.userId;
const userId = req.body.userId;
const merchItem = req.params.ITEM;

// Check if the user exists in the database
Expand All @@ -269,7 +286,14 @@ attendeeRouter.post(
.json({ error: "UserNotFound" });
}

if (!user) {
return res
.status(StatusCodes.NOT_FOUND)
.json({ error: "UserNotFound" });
}

if (
merchItem == "Tshirt" ||
merchItem == "Cap" ||
merchItem == "Tote" ||
merchItem == "Button"
Expand Down
14 changes: 14 additions & 0 deletions src/services/attendee/attendee-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,36 @@ export const AttendeeSchema = new Schema({
hasRedeemedMerch: {
type: new Schema(
{
Tshirt: { type: Boolean, default: false },
Button: { type: Boolean, default: false },
Tote: { type: Boolean, default: false },
Cap: { type: Boolean, default: false },
},
{ _id: false }
),
default: {
Tshirt: false,
Button: false,
Tote: false,
Cap: false,
},
},
isEligibleMerch: {
type: new Schema(
{
Tshirt: { type: Boolean, default: true },
Button: { type: Boolean, default: false },
Tote: { type: Boolean, default: false },
Cap: { type: Boolean, default: false },
},
{ _id: false }
),
default: {
Tshirt: true,
Button: false,
Tote: false,
Cap: false,
},
},

favorites: [{ type: String }],
Expand Down
2 changes: 0 additions & 2 deletions src/services/auth/auth-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ export const RoleSchema = new Schema(
{
userId: {
type: String,
required: true,
unique: true,
},
displayName: {
type: String,
Expand Down
4 changes: 2 additions & 2 deletions src/services/auth/auth-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ export function createGoogleStrategy(device: string) {
const isAdmin = email && Config.AUTH_ADMIN_WHITELIST.has(email);

Database.ROLES.findOneAndUpdate(
{ userId: userId },
{ email: email },
{
userId,
displayName,
email,
...(isAdmin && { $addToSet: { roles: Role.Enum.ADMIN } }),
},
{ upsert: true }
Expand Down
16 changes: 12 additions & 4 deletions src/services/auth/sponsor/sponsor-router.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Router } from "express";
import { Database } from "../../../database";
import { StatusCodes } from "http-status-codes";
import { sendEmail } from "../../ses/ses-utils";
import { sendHTMLEmail } from "../../ses/ses-utils";
import jsonwebtoken from "jsonwebtoken";
import { Config } from "../../../config";
import { Role } from "../../auth/auth-models";
import mustache from "mustache";
import templates from "../../../templates/templates";

import {
createSixDigitCode,
encryptSixDigitCode,
Expand Down Expand Up @@ -37,10 +40,15 @@ authSponsorRouter.post("/login", async (req, res, next) => {
},
{ upsert: true }
);
await sendEmail(

const emailBody = mustache.render(templates.SPONSOR_VERIFICATION, {
code: sixDigitCode,
});

await sendHTMLEmail(
email,
"R|P Sponsor Email Verification!",
`Here is your verification code: ${sixDigitCode}`
"R|P Resume Book Email Verification",
emailBody
);
return res.sendStatus(StatusCodes.CREATED);
} catch (error) {
Expand Down
104 changes: 101 additions & 3 deletions src/services/checkin/checkin-router.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import { Router } from "express";
import { StatusCodes } from "http-status-codes";
import { ScanValidator } from "./checkin-schema";
import {
ScanValidator,
MerchScanValidator,
EventValidator,
} from "./checkin-schema";
import RoleChecker from "../../middleware/role-checker";
import { Role } from "../auth/auth-models";
import { validateQrHash } from "./checkin-utils";
import { checkInUserToEvent } from "./checkin-utils";
import { Database } from "../../database";

const checkinRouter = Router();

checkinRouter.post(
"/scan/staff",
RoleChecker([Role.Enum.ADMIN]),
RoleChecker([Role.Enum.ADMIN, Role.Enum.STAFF]),
async (req, res, next) => {
try {
const { eventId, qrCode } = ScanValidator.parse(req.body);
Expand All @@ -23,7 +28,100 @@ checkinRouter.post(
.json({ error: "QR code has expired" });
}

await checkInUserToEvent(eventId, userId, true);
try {
await checkInUserToEvent(eventId, userId);
} catch (error: unknown) {
if (error instanceof Error && error.message == "IsDuplicate") {
return res
.status(StatusCodes.FORBIDDEN)
.json({ error: "IsDuplicate" });
}
return res.sendStatus(StatusCodes.INTERNAL_SERVER_ERROR);
}

return res.status(StatusCodes.OK).json(userId);
} catch (error) {
next(error);
}
}
);

checkinRouter.post(
"/event",
RoleChecker([Role.Enum.ADMIN, Role.Enum.STAFF]),
async (req, res, next) => {
try {
const { eventId, userId } = EventValidator.parse(req.body);

try {
await checkInUserToEvent(eventId, userId);
} catch (error: unknown) {
if (error instanceof Error && error.message == "IsDuplicate") {
return res
.status(StatusCodes.FORBIDDEN)
.json({ error: "IsDuplicate" });
}
return res.sendStatus(StatusCodes.INTERNAL_SERVER_ERROR);
}
return res.status(StatusCodes.OK).json(userId);
} catch (error) {
next(error);
}
}
);

checkinRouter.post(
"/scan/merch",
RoleChecker([Role.Enum.ADMIN, Role.Enum.STAFF]),
async (req, res, next) => {
try {
const { qrCode } = MerchScanValidator.parse(req.body);

const { userId, expTime } = validateQrHash(qrCode);

if (Date.now() / 1000 > expTime) {
return res
.status(StatusCodes.UNAUTHORIZED)
.json({ error: "QR code has expired" });
}

return res.status(StatusCodes.OK).json(userId);
} catch (error) {
next(error);
}
}
);

checkinRouter.post(
"/",
RoleChecker([Role.Enum.ADMIN, Role.Enum.STAFF]),
async (req, res, next) => {
try {
const { qrCode } = ScanValidator.parse(req.body);

const { userId, expTime } = validateQrHash(qrCode);

if (Date.now() / 1000 > expTime) {
return res
.status(StatusCodes.UNAUTHORIZED)
.json({ error: "QR code has expired" });
}

const attendee = await Database.ATTENDEE.findOne({ userId });
if (!attendee) {
return res
.status(StatusCodes.NOT_FOUND)
.json({ error: "UserNotFound" });
}
if (attendee.hasCheckedIn) {
return res
.status(StatusCodes.BAD_REQUEST)
.json({ error: "AlreadyCheckedIn" });
}
await Database.ATTENDEE.updateOne(
{ userId: userId },
{ $set: { hasCheckedIn: true } }
);

return res.status(StatusCodes.OK).json(userId);
} catch (error) {
Expand Down
11 changes: 10 additions & 1 deletion src/services/checkin/checkin-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,13 @@ const ScanValidator = z.object({
qrCode: z.string(),
});

export { ScanValidator };
const MerchScanValidator = z.object({
qrCode: z.string(),
});

const EventValidator = z.object({
eventId: z.string(),
userId: z.string(),
});

export { ScanValidator, MerchScanValidator, EventValidator };
31 changes: 20 additions & 11 deletions src/services/checkin/checkin-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Database } from "../../database";
import crypto from "crypto";
import { Config } from "../../config";
import { EventType } from "../events/events-schema";

export function getCurrentDay() {
const currDate = new Date();
Expand Down Expand Up @@ -34,7 +35,7 @@ async function checkForDuplicateAttendance(eventId: string, userId: string) {
]);

if (isRepeatEvent || isRepeatAttendee) {
throw new Error("Is Duplicate");
throw new Error("IsDuplicate");
}
}

Expand Down Expand Up @@ -74,6 +75,7 @@ async function assignPixelsToUser(userId: string, pixels: number) {
"isEligibleMerch.Cap": new_points >= 50,
"isEligibleMerch.Tote": new_points >= 35,
"isEligibleMerch.Button": new_points >= 20,
"isEligibleMerch.Tshirt": new_points >= 0,
};

await Database.ATTENDEE.findOneAndUpdate(
Expand All @@ -82,24 +84,31 @@ async function assignPixelsToUser(userId: string, pixels: number) {
);
}

export async function checkInUserToEvent(
eventId: string,
userId: string,
isCheckin: boolean = false
) {
async function markUserAsCheckedIn(userId: string) {
await Database.ATTENDEE.findOneAndUpdate(
{ userId },
{ $set: { hasCheckedIn: true } }
);
}

export async function checkInUserToEvent(eventId: string, userId: string) {
await checkEventAndAttendeeExist(eventId, userId);
await checkForDuplicateAttendance(eventId, userId);

if (!isCheckin) {
const event = await Database.EVENTS.findOne({ eventId });
if (!event) {
throw new Error("Event not found");
}

// check for checkin event, and for meals
if (event.eventType == EventType.Enum.CHECKIN) {
await markUserAsCheckedIn(userId);
} else if (event.eventType != EventType.Enum.MEALS) {
await updateAttendeePriority(userId);
}

await updateAttendanceRecords(eventId, userId);

const event = await Database.EVENTS.findOne({ eventId });
if (!event) {
throw new Error("Event not found");
}
await assignPixelsToUser(userId, event.points);
}

Expand Down
Loading

0 comments on commit 77c5ee5

Please sign in to comment.