Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implemented late check-in system #269

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
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
8 changes: 8 additions & 0 deletions components/adminComponents/AdminHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ export default function AdminHeader() {
Stats at a Glance
</NavLink>
)}
<NavLink
href="/admin/waitlist"
exact={true}
activeOptions={'border-b-4 border-primaryDark text-complementaryDark'}
className="mx-4 py-2"
>
Waitlist Check-in
</NavLink>
</div>
</header>
<div className="mt-4 md:hidden ">
Expand Down
1 change: 1 addition & 0 deletions lib/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

Expand Down
3 changes: 3 additions & 0 deletions pages/admin/scan/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Registration, 'user'> {
Expand Down Expand Up @@ -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);
}
Expand Down
143 changes: 143 additions & 0 deletions pages/admin/waitlist/index.tsx
Original file line number Diff line number Diff line change
@@ -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<ApiResponseType | undefined>(undefined);
const { user, isSignedIn } = useAuthContext();
const [upperBoundValue, setUpperBoundValue] = useState<number>(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<ApiRequestType, ApiResponseType>(
'/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 <div className="text-2xl font-black text-center">Unauthorized</div>;

return (
<div className="flex flex-col flex-grow">
<Head>
<title>HackPortal - Admin</title>
<meta name="description" content="HackPortal's Admin Page" />
</Head>
<section className="p-4">
<AdminHeader />
</section>
<div className="flex flex-col justify-center">
<div className="my-6 mx-auto">
<div className="flex gap-x-4 mb-10 items-center">
<h1 className="text-lg">Set late check-in eligible upper bound: </h1>
<input
type="number"
name="lateCheckInUpperBound"
onChange={(e) => setUpperBoundValue(parseInt(e.target.value))}
/>
<button
className="rounded-lg border border-green-900 p-3 text-green-900 hover:bg-green-400"
onClick={async () => {
await handleUpdateLateCheckInUpperBound(upperBoundValue);
}}
>
Update
</button>
</div>
<div className="flex flex-col gap-y-4">
{scanStatus === undefined ? (
<QRCodeReader width={200} height={200} callback={handleScan} />
) : (
<>
<div
className={`text-center text-3xl ${
scanStatus.statusCode < 400 ? 'font-black' : 'text-red-600'
}`}
>
{scanStatus.msg}
</div>
<div className="flex gap-x-5 mx-auto">
<div
onClick={() => 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
</div>
</div>
</>
)}
</div>
</div>
</div>
</div>
);
}
34 changes: 34 additions & 0 deletions pages/api/scan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<boolean> {
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
Expand Down Expand Up @@ -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,
Expand Down
117 changes: 117 additions & 0 deletions pages/api/waitlist/index.ts
Original file line number Diff line number Diff line change
@@ -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<FirebaseFirestore.DocumentData>;

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<number> {
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' });
}
}
}
Loading