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' });
+ }
+ }
+}