Skip to content

Commit

Permalink
Merge branch 'main' into dev/jacob/s3-flexible
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobc2700 authored Jul 15, 2024
2 parents 41bae16 + 1308987 commit efdf434
Show file tree
Hide file tree
Showing 13 changed files with 412 additions and 257 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"crypto": "^1.0.1",
"datetime": "^0.0.3",
"dotenv": "^16.4.5",
"express": "^4.19.1",
"express-rate-limit": "^6.0.0",
Expand Down
10 changes: 4 additions & 6 deletions scripts/install_dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
cd /home/ubuntu/rp-api
sudo yarn

sudo pm2 describe appname 2>&1 /dev/null

sudo pm2 describe RP_API 2>&1 /dev/null
RUNNING=$?


if [ "${RUNNING}" -eq 0 ]; then
sudo pm2 start build/app.js --name RP_API -i 2 --wait-ready --listen-timeout 10000
if [ "${RUNNING}" -eq 1 ]; then
sudo pm2 start build/src/app.js --name RP_API -i 2 --wait-ready --listen-timeout 10000
fi;



2 changes: 2 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import bodyParser from "body-parser";
import errorHandler from "./middleware/error-handler";

import attendeeRouter from "./services/attendee/attendee-router";
import checkinRouter from "./services/checkin/checkin-router";
import authRouter from "./services/auth/auth-router";
import eventsRouter from "./services/events/events-router";
import notificationsRouter from "./services/notifications/notifications-router";
Expand Down Expand Up @@ -38,6 +39,7 @@ app.use("/", bodyParser.json());
// API routes
app.use("/attendee", databaseMiddleware, attendeeRouter);
app.use("/auth", databaseMiddleware, authRouter);
app.use("/checkin", databaseMiddleware, checkinRouter);
app.use("/events", databaseMiddleware, eventsRouter);
app.use("/notifications", databaseMiddleware, notificationsRouter);
app.use("/registration", databaseMiddleware, registrationRouter);
Expand Down
18 changes: 0 additions & 18 deletions src/services/attendee/attendee-utils.ts

This file was deleted.

36 changes: 36 additions & 0 deletions src/services/checkin/checkin-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Router } from "express";
import { StatusCodes } from "http-status-codes";
import { ScanValidator } 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";

const checkinRouter = Router();

checkinRouter.post(
"/scan/staff",
RoleChecker([Role.Enum.ADMIN]),
async (req, res, next) => {
try {
const { eventId, qrCode } = ScanValidator.parse(req.body);
console.log("Event ID:", eventId);

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

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

await checkInUserToEvent(eventId, userId, true);

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

export default checkinRouter;
8 changes: 8 additions & 0 deletions src/services/checkin/checkin-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { z } from "zod";

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

export { ScanValidator };
107 changes: 107 additions & 0 deletions src/services/checkin/checkin-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Database } from "../../database";
import crypto from "crypto";
import { Config } from "../../config";

function getCurrentDay() {
const currDate = new Date();
const dayString = new Intl.DateTimeFormat("en-US", {
timeZone: "America/Chicago",
weekday: "short",
}).format(currDate);
return dayString;
}

async function checkEventAndAttendeeExist(eventId: string, userId: string) {
const [event, attendee] = await Promise.all([
Database.EVENTS.exists({ eventId }),
Database.ATTENDEE.exists({ userId }),
]);

if (!event || !attendee) {
throw new Error("Event or Attendee not found");
}

return Promise.resolve();
}

async function checkForDuplicateAttendance(eventId: string, userId: string) {
const [isRepeatEvent, isRepeatAttendee] = await Promise.all([
Database.EVENTS_ATTENDANCE.exists({ eventId, attendees: userId }),
Database.ATTENDEE_ATTENDANCE.exists({
userId,
eventsAttended: eventId,
}),
]);

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

// Update attendee priority for the current day
async function updateAttendeePriority(userId: string) {
const day = getCurrentDay();
await Database.ATTENDEE.findOneAndUpdate(
{ userId },
{ $set: { [`hasPriority.${day}`]: true } }
);
}

async function updateAttendanceRecords(eventId: string, userId: string) {
await Promise.all([
Database.EVENTS_ATTENDANCE.findOneAndUpdate(
{ eventId },
{ $addToSet: { attendees: userId } },
{ new: true, upsert: true }
),
Database.ATTENDEE_ATTENDANCE.findOneAndUpdate(
{ userId },
{ $addToSet: { eventsAttended: eventId } },
{ new: true, upsert: true }
),
]);
}

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

if (!isCheckin) {
await updateAttendeePriority(userId);
}

await updateAttendanceRecords(eventId, userId);
}

export function generateQrHash(userId: string, expTime: number) {
let hashStr = userId + "#" + expTime;
const hashIterations = Config.QR_HASH_ITERATIONS;
const hashSecret = Config.QR_HASH_SECRET;

const hmac = crypto.createHmac("sha256", hashSecret);
hashStr = hmac.update(hashStr).digest("hex");

for (let i = 0; i < hashIterations; i++) {
const hash = crypto.createHash("sha256");
hashStr = hash.update(hashSecret + "#" + hashStr).digest("hex");
}

return `${hashStr}#${expTime}#${userId}`;
}

export function validateQrHash(qrCode: string) {
const parts = qrCode.split("#");
const userId = parts[2];
const expTime = parseInt(parts[1]);
const generatedHash = generateQrHash(userId, expTime);

if (generatedHash.split("#")[0] !== parts[0]) {
throw new Error("Invalid QR code");
}

return { userId, expTime };
}
24 changes: 0 additions & 24 deletions src/services/events/events-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { Router } from "express";
import { StatusCodes } from "http-status-codes";
import { publicEventValidator, privateEventValidator } from "./events-schema";
import { Database } from "../../database";
import { checkInUserToEvent } from "./events-utils";
import RoleChecker from "../../middleware/role-checker";
import { Role } from "../auth/auth-models";
import { isAdmin, isStaff } from "../auth/auth-utils";
Expand Down Expand Up @@ -154,27 +153,4 @@ eventsRouter.delete(
}
);

eventsRouter.post(
"/check-in",
RoleChecker([Role.Enum.STAFF], true),
async (req, res, next) => {
// add RoleChecker for staff
try {
const { eventId, userId } = req.body;
const result = await checkInUserToEvent(eventId, userId);
if (result.success) {
return res
.status(StatusCodes.OK)
.json({ message: "Check-in successful" });
} else {
return res
.status(StatusCodes.NOT_FOUND)
.json({ error: result.message });
}
} catch (error) {
next(error);
}
}
);

export default eventsRouter;
35 changes: 0 additions & 35 deletions src/services/events/events-utils.ts

This file was deleted.

3 changes: 3 additions & 0 deletions src/services/s3/s3-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ s3Router.get(
s3,
BucketName.RP_2024_RESUMES
);

return res.status(StatusCodes.OK).send({ url, fields });
} catch (error) {
next(error);
Expand All @@ -50,6 +51,7 @@ s3Router.get(
s3,
BucketName.RP_2024_RESUMES
);

Check failure on line 54 in src/services/s3/s3-router.ts

View workflow job for this annotation

GitHub Actions / lint

Delete `··········`
return res.status(StatusCodes.OK).send({ url: downloadUrl });
} catch (error) {
next(error);
Expand All @@ -71,6 +73,7 @@ s3Router.get(
s3,
BucketName.RP_2024_RESUMES
);

return res.status(StatusCodes.OK).send({ url: downloadUrl });
} catch (error) {
next(error);
Expand Down
7 changes: 7 additions & 0 deletions src/services/s3/s3-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from "zod";

const BatchResumeDownloadValidator = z.object({
userIds: z.string().array(),
});

export default BatchResumeDownloadValidator;
2 changes: 2 additions & 0 deletions src/services/s3/s3-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ export async function postResumeUrl(
return { url, fields };
}


Check failure on line 27 in src/services/s3/s3-utils.ts

View workflow job for this annotation

GitHub Actions / lint

Delete `⏎`
export async function getResumeUrl(
userId: string,
client: S3,
bucketName: BucketName
) {
const command = new GetObjectCommand({
Bucket: bucketName,
Bucket: Config.S3_BUCKET_NAME,

Check failure on line 35 in src/services/s3/s3-utils.ts

View workflow job for this annotation

GitHub Actions / build

An object literal cannot have multiple properties with the same name.

Check failure on line 35 in src/services/s3/s3-utils.ts

View workflow job for this annotation

GitHub Actions / build

Property 'S3_BUCKET_NAME' does not exist on type '{ DEFAULT_APP_PORT: number; ALLOWED_CORS_ORIGIN_PATTERNS: RegExp[]; ENV: "PRODUCTION" | "DEVELOPMENT" | "TESTING"; DATABASE_USERNAME: string; DATABASE_PASSWORD: string; DATABASE_HOST: string; ... 12 more ...; QR_HASH_SECRET: string; }'.
Key: `${userId}.pdf`,
});

Expand Down
Loading

0 comments on commit efdf434

Please sign in to comment.