diff --git a/components/adminComponents/AdminHeader.tsx b/components/adminComponents/AdminHeader.tsx index bd5b16c9..2b8ebabe 100644 --- a/components/adminComponents/AdminHeader.tsx +++ b/components/adminComponents/AdminHeader.tsx @@ -67,6 +67,14 @@ export default function AdminHeader() { Stats at a Glance )} + + Waitlist Check-in +
diff --git a/lib/types.d.ts b/lib/types.d.ts index f83d3e5f..41fb8b42 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -108,6 +108,7 @@ type Registration = { website?: string; resume?: string; companies: Companies[]; + waitlistNumber?: number; //claims: []; //Array of Strings will be used to id any claims (lunch, merch, etc.) made by user }; diff --git a/pages/admin/scan/index.tsx b/pages/admin/scan/index.tsx index db5d44be..6cf1cad7 100644 --- a/pages/admin/scan/index.tsx +++ b/pages/admin/scan/index.tsx @@ -17,6 +17,7 @@ const successStrings = { unexpectedError: 'Unexpected error...', notCheckedIn: "User hasn't checked in!", invalidFormat: 'Invalid hacker tag format...', + lateCheckInIneligible: 'User is not eligible for late check-in...', }; interface UserProfile extends Omit { @@ -109,6 +110,8 @@ export default function Admin() { return setSuccess(successStrings.alreadyClaimed); } else if (result.status === 403) { return setSuccess(successStrings.notCheckedIn); + } else if (result.status === 400) { + return setSuccess(successStrings.lateCheckInIneligible); } else if (result.status !== 200) { return setSuccess(successStrings.unexpectedError); } diff --git a/pages/admin/waitlist/index.tsx b/pages/admin/waitlist/index.tsx new file mode 100644 index 00000000..14982b3b --- /dev/null +++ b/pages/admin/waitlist/index.tsx @@ -0,0 +1,143 @@ +import Head from 'next/head'; +import AdminHeader from '../../../components/adminComponents/AdminHeader'; +import { useState } from 'react'; +import QRCodeReader from '../../../components/dashboardComponents/QRCodeReader'; +import { RequestHelper } from '../../../lib/request-helper'; +import { useAuthContext } from '../../../lib/user/AuthContext'; +import { isAuthorized } from '..'; + +const SCAN_STATUS = { + successful: 'Check-in successful...', + invalidUser: 'Invalid user...', + unexpectedError: 'Unexpected error...', + invalidFormat: 'Invalid hacker tag format...', +}; + +type ApiResponseType = { + statusCode: number; + msg: string; +}; + +type ApiRequestType = { + userId: string; +}; + +export default function WaitlistCheckinPage() { + const [scanStatus, setScanStatus] = useState(undefined); + const { user, isSignedIn } = useAuthContext(); + const [upperBoundValue, setUpperBoundValue] = useState(0); + + const handleUpdateLateCheckInUpperBound = async (value: number) => { + try { + const { data } = await RequestHelper.post<{ value: number }, ApiResponseType>( + '/api/waitlist/upperbound', + { + headers: { + Authorization: user.token, + 'Content-Type': 'application/json', + }, + }, + { + value, + }, + ); + if (data.statusCode !== 200) { + alert('Unexpected error...'); + console.error(data.msg); + } else { + alert(data.msg); + } + } catch (error) { + alert('Unexpected error...'); + console.error(error); + } + }; + + const handleScan = async (data: string) => { + if (!data.startsWith('hack:')) { + setScanStatus({ + statusCode: 400, + msg: SCAN_STATUS.invalidFormat, + }); + } + try { + const { data: resData } = await RequestHelper.post( + '/api/waitlist', + { + headers: { + Authorization: user.token, + 'Content-Type': 'application/json', + }, + }, + { + userId: data.replaceAll('hack:', ''), + }, + ); + setScanStatus(resData); + } catch (error) { + setScanStatus({ + statusCode: 500, + msg: SCAN_STATUS.unexpectedError, + }); + console.error(error); + } + }; + + if (!isSignedIn || !isAuthorized(user)) + return
Unauthorized
; + + return ( +
+ + HackPortal - Admin + + +
+ +
+
+
+
+

Set late check-in eligible upper bound:

+ setUpperBoundValue(parseInt(e.target.value))} + /> + +
+
+ {scanStatus === undefined ? ( + + ) : ( + <> +
+ {scanStatus.msg} +
+
+
setScanStatus(undefined)} + className="w-min-5 m-3 rounded-lg text-center text-lg font-black p-3 cursor-pointer hover:bg-green-300 border border-green-800 text-green-900" + > + Next Scan +
+
+ + )} +
+
+
+
+ ); +} diff --git a/pages/api/scan.tsx b/pages/api/scan.tsx index 7bafd54c..39e0e56b 100644 --- a/pages/api/scan.tsx +++ b/pages/api/scan.tsx @@ -13,6 +13,8 @@ const SCANTYPES_COLLECTION = '/scan-types'; // Used to dictate that user attempted to claim swag without checking in const ILLEGAL_SCAN_NAME = 'Illegal Scan'; +// Set this to false if application accept/reject feature is not needed. +const ENABLE_ACCEPT_REJECT_FEATURE = true; /** * * Check if a user has checked in into the event @@ -32,6 +34,27 @@ async function userAlreadyCheckedIn(scans: string[]) { return ok; } +async function checkLateCheckInEligible(userData: Registration) { + const lateCheckInManager = await db.collection('/miscellaneous').doc('lateCheckInManager').get(); + if (lateCheckInManager && lateCheckInManager.exists) { + return ( + userData.waitlistNumber !== undefined && + userData.waitlistNumber <= lateCheckInManager.data().allowedCheckInUpperBound + ); + } + // if no late check-in doc, assume that organizers does not want to use this feature. + return true; +} + +async function checkUserIsRejected(userId: string): Promise { + const snapshot = await db.collection('acceptreject').where('hackerId', '==', userId).get(); + if (snapshot.docs.length === 0) { + return true; + } + // if user is not accepted by the time they are being checked in, assume that they will be rejected + return snapshot.docs[0].data().status !== 'Accepted'; +} + /** * * Check if provided scan name corresponds to a check in scan-type @@ -91,6 +114,17 @@ async function handleScan(req: NextApiRequest, res: NextApiResponse) { const userCheckedIn = await userAlreadyCheckedIn(scans); const scanIsCheckInEvent = await checkIfScanIsCheckIn(bodyData.scan); + if (!userCheckedIn && scanIsCheckInEvent && ENABLE_ACCEPT_REJECT_FEATURE) { + // if user is reject and not eligible for late check-in yet, throw error + const userIsRejected = await checkUserIsRejected(snapshot.id); + const lateCheckInEligible = await checkLateCheckInEligible(snapshot.data() as Registration); + if (userIsRejected && !lateCheckInEligible) { + return res + .status(400) + .json({ code: 'non eligible', message: 'User is not eligible for late check-in yet...' }); + } + } + if (!userCheckedIn && !scanIsCheckInEvent) { scans.push({ name: ILLEGAL_SCAN_NAME, diff --git a/pages/api/waitlist/index.ts b/pages/api/waitlist/index.ts new file mode 100644 index 00000000..f7f47512 --- /dev/null +++ b/pages/api/waitlist/index.ts @@ -0,0 +1,117 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { userIsAuthorized } from '../../../lib/authorization/check-authorization'; +import initializeApi from '../../../lib/admin/init'; +import { firestore } from 'firebase-admin'; + +initializeApi(); +const db = firestore(); +const MAX_ASSIGN_ATTEMPT = 10; + +type FirebaseDocumentRefType = FirebaseFirestore.DocumentReference; + +async function getUserDocRef( + userId: string, +): Promise<{ userDocRef: FirebaseDocumentRefType; userData: any } | null> { + const snapshot = await db.collection('/registrations').doc(userId).get(); + if (snapshot && snapshot.exists) { + return { + userDocRef: snapshot.ref, + userData: snapshot.data(), + }; + } + return null; +} + +async function assignNewCheckInNumberToUser(userDocRef: FirebaseDocumentRefType): Promise { + const snapshot = await db.collection('/miscellaneous').doc('lateCheckInManager').get(); + if (snapshot && snapshot.exists) { + const { nextAvailableNumber } = snapshot.data(); + + // Attempt to assign id to user constant number of times. If unsuccessful, throw error and ask user to try again. + let assignSuccessful = false; + for (let iter = 0; iter < MAX_ASSIGN_ATTEMPT; iter++) { + try { + await userDocRef.update({ + waitlistNumber: nextAvailableNumber, + }); + assignSuccessful = true; + break; + } catch (error) { + console.error(error); + continue; + } + } + if (!assignSuccessful) { + throw new Error('Unsuccessfully assigned check-in number to user...'); + } + await snapshot.ref.update({ + nextAvailableNumber: nextAvailableNumber + 1, + }); + return nextAvailableNumber; + } else { + // First assignable number will be 1. + await snapshot.ref.set( + { + nextAvailableNumber: 1, + allowedCheckInUpperBound: 0, + }, + { merge: true }, + ); + return 1; + } +} + +function alreadyHasCheckInNumber(userData: any) { + return Object.hasOwn(userData, 'waitlistNumber'); +} + +async function handlePostRequest(req: NextApiRequest, res: NextApiResponse) { + const { headers } = req; + const userToken = headers['authorization']; + const isAuthorized = await userIsAuthorized(userToken, ['admin', 'super_admin']); + if (!isAuthorized) { + return res.status(403).json({ + statusCode: 403, + msg: 'Request is not authorized to perform admin functionality', + }); + } + try { + const fetchUserDocRefResult = await getUserDocRef(req.body.userId); + if (!fetchUserDocRefResult) { + return res.status(400).json({ + statusCode: 400, + msg: 'User not found...', + }); + } + const { userDocRef, userData } = fetchUserDocRefResult; + if (alreadyHasCheckInNumber(userData)) { + return res.status(400).json({ + statusCode: 400, + msg: 'User already had check-in number...', + }); + } + const checkInNumber = await assignNewCheckInNumberToUser(userDocRef); + return res.status(200).json({ + statusCode: 200, + msg: `User's check in number is ${checkInNumber}`, + }); + } catch (error) { + console.error(error); + return res.status(500).json({ + statusCode: 500, + msg: 'Unexpected error...', + }); + } +} + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req; + switch (method) { + case 'POST': { + return handlePostRequest(req, res); + } + default: { + return res.status(404).json({ msg: 'Route not found' }); + } + } +} diff --git a/pages/api/waitlist/upperbound.ts b/pages/api/waitlist/upperbound.ts new file mode 100644 index 00000000..2b884263 --- /dev/null +++ b/pages/api/waitlist/upperbound.ts @@ -0,0 +1,55 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { userIsAuthorized } from '../../../lib/authorization/check-authorization'; +import initializeApi from '../../../lib/admin/init'; +import { firestore } from 'firebase-admin'; + +initializeApi(); +const db = firestore(); + +async function handlePostRequest(req: NextApiRequest, res: NextApiResponse) { + const { headers } = req; + const userToken = headers['authorization']; + const isAuthorized = await userIsAuthorized(userToken, ['admin', 'super_admin']); + if (!isAuthorized) { + return res.status(403).json({ + statusCode: 403, + msg: 'Request is not authorized to perform admin functionality', + }); + } + try { + const snapshot = await db.collection('/miscellaneous').doc('lateCheckInManager').get(); + if (snapshot && snapshot.exists) { + await snapshot.ref.update({ + allowedCheckInUpperBound: req.body.value, + }); + } else { + await snapshot.ref.set({ + allowedCheckInUpperBound: req.body.value, + version: 1, + nextAvailableNumber: 1, + }); + } + return res.status(200).json({ + statusCode: 200, + msg: 'Sucessful', + }); + } catch (error) { + console.error(error); + return res.status(500).json({ + statusCode: 500, + msg: 'Unexpected error...', + }); + } +} + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req; + switch (method) { + case 'POST': { + return handlePostRequest(req, res); + } + default: { + return res.status(404).json({ msg: 'Route not found' }); + } + } +}