diff --git a/src/app.test.ts b/src/app.test.ts index 5458b8d..3544407 100644 --- a/src/app.test.ts +++ b/src/app.test.ts @@ -15,7 +15,7 @@ const TESTER_EVENT = { isVisible: true, eventId: "b", attendanceCount: 0, - eventType: "B", + eventType: "SPEAKER", }; describe("general app test", () => { diff --git a/src/config.ts b/src/config.ts index 9d80333..ad3736f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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, @@ -98,6 +99,8 @@ export const DeviceRedirects: Record = { 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({ diff --git a/src/services/attendee/attendee-router.ts b/src/services/attendee/attendee-router.ts index 97e424e..d194195 100644 --- a/src/services/attendee/attendee-router.ts +++ b/src/services/attendee/attendee-router.ts @@ -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 }); @@ -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 @@ -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" diff --git a/src/services/attendee/attendee-schema.ts b/src/services/attendee/attendee-schema.ts index b8a0fa0..bc9bdb2 100644 --- a/src/services/attendee/attendee-schema.ts +++ b/src/services/attendee/attendee-schema.ts @@ -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 }], diff --git a/src/services/auth/auth-schema.ts b/src/services/auth/auth-schema.ts index 0980814..e3324ab 100644 --- a/src/services/auth/auth-schema.ts +++ b/src/services/auth/auth-schema.ts @@ -18,8 +18,6 @@ export const RoleSchema = new Schema( { userId: { type: String, - required: true, - unique: true, }, displayName: { type: String, diff --git a/src/services/auth/auth-utils.ts b/src/services/auth/auth-utils.ts index 38575d2..f3a9763 100644 --- a/src/services/auth/auth-utils.ts +++ b/src/services/auth/auth-utils.ts @@ -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 } diff --git a/src/services/auth/sponsor/sponsor-router.ts b/src/services/auth/sponsor/sponsor-router.ts index e1d2190..af5d907 100644 --- a/src/services/auth/sponsor/sponsor-router.ts +++ b/src/services/auth/sponsor/sponsor-router.ts @@ -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, @@ -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) { diff --git a/src/services/checkin/checkin-router.ts b/src/services/checkin/checkin-router.ts index 9248a0c..40da2a0 100644 --- a/src/services/checkin/checkin-router.ts +++ b/src/services/checkin/checkin-router.ts @@ -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); @@ -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) { diff --git a/src/services/checkin/checkin-schema.ts b/src/services/checkin/checkin-schema.ts index c0b7c0a..c3307c0 100644 --- a/src/services/checkin/checkin-schema.ts +++ b/src/services/checkin/checkin-schema.ts @@ -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 }; diff --git a/src/services/checkin/checkin-utils.ts b/src/services/checkin/checkin-utils.ts index f40ab85..0f5df66 100644 --- a/src/services/checkin/checkin-utils.ts +++ b/src/services/checkin/checkin-utils.ts @@ -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(); @@ -34,7 +35,7 @@ async function checkForDuplicateAttendance(eventId: string, userId: string) { ]); if (isRepeatEvent || isRepeatAttendee) { - throw new Error("Is Duplicate"); + throw new Error("IsDuplicate"); } } @@ -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( @@ -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); } diff --git a/src/services/events/events-router.ts b/src/services/events/events-router.ts index 0e5fac1..92ab39f 100644 --- a/src/services/events/events-router.ts +++ b/src/services/events/events-router.ts @@ -27,7 +27,7 @@ eventsRouter.get( try { const event = await Database.EVENTS.findOne({ startTime: { $gte: currentTime }, - isVisible: isUser ? { $eq: true } : {}, + ...(isUser && { isVisible: true }), }).sort({ startTime: 1 }); if (event) { @@ -50,7 +50,10 @@ eventsRouter.get("/", RoleChecker([], true), async (req, res, next) => { var filterFunction; try { - var unfiltered_events = await Database.EVENTS.find(); + var unfiltered_events = await Database.EVENTS.find().sort({ + startTime: 1, + endTime: -1, + }); if (isStaff(payload) || isAdmin(payload)) { filterFunction = (x: any) => internalEventView.parse(x); @@ -145,7 +148,7 @@ eventsRouter.put( // Delete event eventsRouter.delete( "/:EVENTID", - RoleChecker([Role.Enum.STAFF]), + RoleChecker([Role.Enum.ADMIN]), async (req, res, next) => { const eventId = req.params.EVENTID; try { diff --git a/src/services/events/events-schema.ts b/src/services/events/events-schema.ts index 7f4d1d8..9b8cc40 100644 --- a/src/services/events/events-schema.ts +++ b/src/services/events/events-schema.ts @@ -2,7 +2,14 @@ import { Schema } from "mongoose"; import { z } from "zod"; import { v4 as uuidv4 } from "uuid"; -export const EventType = z.enum(["A", "B", "C"]); +export const EventType = z.enum([ + "SPEAKER", + "CORPORATE", + "SPECIAL", + "PARTNERS", + "MEALS", + "CHECKIN", +]); export const externalEventView = z.object({ eventId: z.coerce.string().default(() => uuidv4()), diff --git a/src/services/registration/registration-router.ts b/src/services/registration/registration-router.ts index dd539d1..c93cc50 100644 --- a/src/services/registration/registration-router.ts +++ b/src/services/registration/registration-router.ts @@ -155,7 +155,7 @@ registrationRouter.get("/", RoleChecker([]), async (req, res, next) => { } }); -registrationRouter.get( +registrationRouter.post( "/filter/pagecount", RoleChecker([Role.Enum.ADMIN, Role.Enum.CORPORATE]), async (req, res, next) => { @@ -190,7 +190,10 @@ registrationRouter.get( ); return res.status(StatusCodes.OK).json({ - pagecount: Math.floor((registrants.length + 99) / 100), + pagecount: Math.floor( + (registrants.length + Config.SPONSOR_ENTIRES_PER_PAGE - 1) / + Config.SPONSOR_ENTIRES_PER_PAGE + ), }); } catch (error) { next(error); @@ -235,7 +238,10 @@ registrationRouter.post( const registrants = await Database.REGISTRATION.find( query, projection, - { skip: 100 * (page - 1), limit: 100 } + { + skip: Config.SPONSOR_ENTIRES_PER_PAGE * (page - 1), + limit: Config.SPONSOR_ENTIRES_PER_PAGE, + } ); return res.status(StatusCodes.OK).json({ registrants, page }); diff --git a/src/services/s3/s3-router.ts b/src/services/s3/s3-router.ts index d43e4e6..0e2e993 100644 --- a/src/services/s3/s3-router.ts +++ b/src/services/s3/s3-router.ts @@ -65,7 +65,7 @@ s3Router.get( } ); -s3Router.get( +s3Router.post( "/download/batch/", RoleChecker([Role.Enum.STAFF, Role.Enum.CORPORATE], false), s3ClientMiddleware, diff --git a/src/services/stats/stats-router.ts b/src/services/stats/stats-router.ts index abe78df..66e72db 100644 --- a/src/services/stats/stats-router.ts +++ b/src/services/stats/stats-router.ts @@ -3,6 +3,7 @@ import { StatusCodes } from "http-status-codes"; import { Database } from "../../database"; import RoleChecker from "../../middleware/role-checker"; import { Role } from "../auth/auth-models"; +import { getCurrentDay } from "../checkin/checkin-utils"; const statsRouter = Router(); @@ -52,8 +53,11 @@ statsRouter.get( RoleChecker([Role.enum.STAFF], false), async (req, res, next) => { try { + const day = getCurrentDay(); + const dayField = `hasPriority.${day}`; const attendees = await Database.ATTENDEE.find({ hasCheckedIn: true, + [dayField]: true, }); return res.status(StatusCodes.OK).json({ count: attendees.length }); diff --git a/src/templates/templates.ts b/src/templates/templates.ts index f41e2ca..ac6cd57 100644 --- a/src/templates/templates.ts +++ b/src/templates/templates.ts @@ -73,8 +73,9 @@ const templates = {
-

Here is your verification code:

+

Here is your SponsorRP verification code:

{{code}}
+

Note that this verification code will expire approximately 10 minutes from now.

`,