diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 460ae61..6e3dc27 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,3 +13,8 @@ jobs:
steps:
- uses: actions/checkout@v2
- run: cd client && yarn --frozen-lockfile && yarn lint
+ lint_server:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - run: cd server && yarn --frozen-lockfile && yarn lint
diff --git a/.husky/pre-commit b/.husky/pre-commit
index ea2e0e0..2a952ae 100755
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -3,3 +3,6 @@
cd client
npx lint-staged
+
+cd ../server
+yarn lint
\ No newline at end of file
diff --git a/README.md b/README.md
index 0fd17ca..270384e 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,8 @@ Note: Only the core files & directories are listed below
| |── routes
| | |── pollRoute.ts
| | └── userRoutes.ts
+| |── types
+| | └── pollController.types.ts
| |── redis.ts # Redis related configurations to setup the professor list
| |── server.ts
| └── socket.ts
@@ -205,7 +207,7 @@ utorid3
2. Setting up the server `.env` file (Placed in the root of your `server` folder)
```
-PPORT=3001
+PORT=3001
MONGODB_URL="mongodb://localhost:27018/quiz"
FRONTEND_URL="http://localhost:3000"
REDIS_URL="redis://default:password@localhost:6379"
diff --git a/client/src/components/ErrorAlert.tsx b/client/src/components/ErrorAlert.tsx
new file mode 100644
index 0000000..b116912
--- /dev/null
+++ b/client/src/components/ErrorAlert.tsx
@@ -0,0 +1,93 @@
+import React from "react";
+
+interface ErrorAlertProps {
+ title: string;
+ text?: string;
+ onClose: () => void;
+ enabled: boolean;
+ buttonText?: string;
+}
+
+export const ErrorAlert = ({
+ title,
+ text,
+ onClose,
+ enabled,
+ buttonText,
+}: ErrorAlertProps) => {
+ return (
+
+ {/* Background backdrop, show/hide based on modal state. */}
+
+ {/* Entering: "ease-out duration-300" */}
+ {/* From: "opacity-0" */}
+ {/* To: "opacity-100" */}
+ {/* Leaving: "ease-in duration-200" */}
+ {/* From: "opacity-100" */}
+ {/* To: "opacity-0" */}
+
+
+
+
+ {/* Modal panel, show/hide based on modal state. */}
+
+ {/* Entering: "ease-out duration-300" */}
+ {/* From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" */}
+ {/* To: "opacity-100 translate-y-0 sm:scale-100" */}
+ {/* Leaving: "ease-in duration-200" */}
+ {/* From: "opacity-100 translate-y-0 sm:scale-100" */}
+ {/* To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" */}
+
+
+
+
+ {/* Heroicon name: outline/exclamation-triangle */}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/client/src/components/PollOptionButton.tsx b/client/src/components/PollOptionButton.tsx
index 82b0b78..c14debe 100644
--- a/client/src/components/PollOptionButton.tsx
+++ b/client/src/components/PollOptionButton.tsx
@@ -18,11 +18,9 @@ export const PollOptionButton = ({
disabled={disabled}
onClick={onClick}
className={`m-2 py-2 px-40 inline-block ${
- selected ? "bg-hover" : "bg-primary"
- } ${
- disabled
- ? "cursor-not-allowed opacity-50"
- : "hover:bg-hover cursor-pointer"
+ selected ? "bg-selected" : "bg-primary"
+ } ${disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer"} ${
+ !selected && !disabled ? "hover:bg-hover" : ""
}`}
>
{name}
diff --git a/client/src/pages/VotePage.tsx b/client/src/pages/VotePage.tsx
index 5e5f4ae..be66990 100644
--- a/client/src/pages/VotePage.tsx
+++ b/client/src/pages/VotePage.tsx
@@ -9,33 +9,41 @@ import {
questionStarted,
} from "../constants/constants";
import { socket } from "../socket";
+import { ErrorAlert } from "../components/ErrorAlert";
export const VotePage = () => {
const history = useHistory();
const cookies = new Cookies();
const [pollCode] = useState(cookies.get(pollCodeCookie));
const [started, setStarted] = useState(false);
+ const [hasAllowedNotif, setAllowedNotif] = useState(false);
+ const [lastNotif, setLastNotif] = useState(null);
+
+ const [loading, setLoading] = useState(false);
+ const [timeoutCode, setTimeoutCode] = useState(0);
+ const [timeoutError, setTimeoutError] = useState(false);
const [errorCode, setErrorCode] = useState(0);
const [selectedOption, setSelectionOption] = useState("");
- const [isFocus, setFocus] = useState(true);
-
- const onBlur = () => {
- setFocus(false);
- };
const onFocus = () => {
- document.title = mcsPollVoting;
- setFocus(true);
+ if (document.visibilityState === "visible") {
+ document.title = mcsPollVoting;
+ }
};
useEffect(() => {
socket.emit("join", pollCode);
- window.addEventListener("blur", onBlur);
- window.addEventListener("focus", onFocus);
+ try {
+ Notification.requestPermission().then((permission) =>
+ setAllowedNotif(permission === "granted")
+ );
+ } catch (e) {
+ /* notifications probably not supported */
+ }
+ document.addEventListener("visibilitychange", onFocus);
return () => {
- window.removeEventListener("blur", onBlur);
- window.removeEventListener("focus", onFocus);
+ document.removeEventListener("visibilitychange", onFocus);
};
}, []);
@@ -57,8 +65,20 @@ export const VotePage = () => {
const audio = new Audio("/newQuestion.wav");
audio.play();
setSelectionOption("");
- if (!isFocus) {
+ if (document.visibilityState === "hidden") {
document.title = questionStarted;
+ if (hasAllowedNotif) {
+ /* Close old notification to prevent spam */
+ if (lastNotif != null) {
+ lastNotif.close();
+ }
+ /* send new notification */
+ const notification = new Notification("New Question Started!", {
+ icon: "/favicon.ico",
+ tag: "new-question",
+ });
+ setLastNotif(notification);
+ }
}
}
};
@@ -73,6 +93,12 @@ export const VotePage = () => {
const voteAckHandler = (data: any) => {
setSelectionOption(String.fromCharCode(data + 64));
+ if (lastNotif != null) {
+ lastNotif.close();
+ setLastNotif(null);
+ }
+ clearTimeout(timeoutCode);
+ setLoading(false);
};
socket.on("ack", voteAckHandler);
@@ -82,12 +108,21 @@ export const VotePage = () => {
socket.off("end", pollClosedHandler);
socket.off("ack", voteAckHandler);
};
- }, [errorCode, started, selectedOption]);
+ }, [errorCode, started, selectedOption, hasAllowedNotif, lastNotif]);
const pollButtonHandler = (selectedOption: string) => {
+ setLoading(true);
+ setTimeoutCode(
+ setTimeout(() => {
+ triggerTimeOutError();
+ setLoading(false);
+ }, 10000) as unknown as number
+ );
socket.emit("vote", (selectedOption.charCodeAt(0) % 65) + 1);
};
-
+ const triggerTimeOutError = () => {
+ setTimeoutError(true);
+ };
const optionButtons = () => {
const pollOptionButtons = [];
for (let i = 65; i < 70; i++) {
@@ -110,8 +145,50 @@ export const VotePage = () => {
) : (
-
-
{optionButtons()}
+
0 ? selectedOption : "None"
+ }`}
+ />
+
+
{optionButtons()}
+ {loading ? (
+
+ ) : null}
+
+ {
+ setTimeoutError(false);
+ }}
+ />
);
};
diff --git a/client/tailwind.config.js b/client/tailwind.config.js
index 52d469f..5abea22 100644
--- a/client/tailwind.config.js
+++ b/client/tailwind.config.js
@@ -17,6 +17,7 @@ module.exports = {
primary: "#00204E",
secondary: "#FFFFFF",
hover: "#00B5B5",
+ selected: "#0171B7",
background: "#F5F5F5",
}),
},
diff --git a/client/yarn.lock b/client/yarn.lock
index 10f0cc4..a8c1f23 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -1937,9 +1937,9 @@
integrity sha512-8MLkBIYQMuhRBQzGN9875bYsOhPnf/0rgXGo66S2FemHkhbn9qtsz9ywV1iCG+vbjigE4WUNVvw37Dx+L0qsPg==
"@types/node@^12.0.0":
- version "12.20.33"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.33.tgz#24927446e8b7669d10abacedd16077359678f436"
- integrity sha512-5XmYX2GECSa+CxMYaFsr2mrql71Q4EvHjKS+ox/SiwSdaASMoBIWE6UmZqFO+VX1jIcsYLStI4FFoB6V7FeIYw==
+ version "12.20.55"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240"
+ integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==
"@types/normalize-package-data@^2.4.0":
version "2.4.1"
@@ -10825,7 +10825,7 @@ source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, sourc
source-map@^0.5.0, source-map@^0.5.6:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
- integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
+ integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
source-map@^0.7.3, source-map@~0.7.2:
version "0.7.3"
diff --git a/server/.eslintignore b/server/.eslintignore
new file mode 100644
index 0000000..3881e74
--- /dev/null
+++ b/server/.eslintignore
@@ -0,0 +1,3 @@
+node_modules/
+dist/
+.idea
diff --git a/server/.eslintrc.json b/server/.eslintrc.json
new file mode 100644
index 0000000..09213d7
--- /dev/null
+++ b/server/.eslintrc.json
@@ -0,0 +1,25 @@
+{
+ "env": {
+ "node": true
+ },
+ "extends": [
+ "standard",
+ "eslint:recommended",
+ "plugin:@typescript-eslint/eslint-recommended",
+ "plugin:@typescript-eslint/recommended"],
+ "parser": "@typescript-eslint/parser",
+ "parserOptions": {
+ "ecmaVersion": 13,
+ "sourceType": "module"
+ },
+ "plugins": ["@typescript-eslint"],
+ "rules": {
+ "no-use-before-define": "off",
+ "no-unused-vars": "warn",
+ "no-console": "off",
+ "camelcase": "off",
+ "no-throw-literal": "warn"
+ },
+ "settings": {
+ }
+}
diff --git a/server/package.json b/server/package.json
index 33fdf3c..273b04f 100644
--- a/server/package.json
+++ b/server/package.json
@@ -8,7 +8,9 @@
"prestart": "tsc",
"start": "node .",
"dev": "nodemon --watch src -e ts,ejs --exec yarn run start",
- "build": "tsc --project ./"
+ "build": "tsc --project ./",
+ "lint": "eslint --ext .ts .",
+ "lint:fix": "eslint --ext .ts --fix ."
},
"repository": "git+https://github.com/hiimchrislim/QuizVotingSystem.git",
"author": "Chris, Akshit, Shubh, Ilir",
@@ -42,6 +44,7 @@
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.1",
+ "husky": "^8.0.1",
"nodemon": "^2.0.14",
"prettier": "^2.4.1",
"socket.io-client": "^4.3.2",
diff --git a/server/src/controllers/pollController.ts b/server/src/controllers/pollController.ts
index ad06951..440a2f5 100644
--- a/server/src/controllers/pollController.ts
+++ b/server/src/controllers/pollController.ts
@@ -1,76 +1,79 @@
-import { Poll } from "../db/schema";
-import { PollModel, StudentModel } from "../db/mogoose";
-import { io } from "../socket";
-import { client } from "../redis";
-import { customAlphabet } from "nanoid/async";
-import { pollResult } from "./socketController";
-const nanoid = customAlphabet("qwertyuiopasdfghjklzxcvbnm1234567890", 6);
+import { Poll } from '../db/schema'
+import { PollModel, StudentModel } from '../db/mogoose'
+import { io } from '../socket'
+import { client } from '../redis'
+import { customAlphabet } from 'nanoid/async'
+import { pollResult, getRoomById } from './socketController'
+import { AggregatedStudent } from '../types/pollController.types'
+import { UserType } from '../types/user.types'
+const nanoid = customAlphabet('QWERTYUIOPASDFGHJKLZXCVBNM', 6)
// set poll code expiry to 1 day
-const expiry = 60 * 60 * 24;
+const expiry = 60 * 60 * 24
-async function createPoll(poll: Poll) {
- if (!poll.hasOwnProperty("courseCode"))
- return { status: 400, data: { message: "courseCode is required" } };
- const promise = [nanoid(), PollModel.create(poll)] as const;
- const result = await Promise.all(promise);
- const pollId = result[1]["_id"].toString();
- const pollCode = result[0];
- await client.set(pollCode, pollId, { EX: expiry, NX: true });
+async function createPoll (poll: Poll) {
+ if (!poll.courseCode) { return { status: 400, data: { message: 'courseCode is required' } } }
+ const promise = [nanoid(), PollModel.create(poll)] as const
+ const result = await Promise.all(promise)
+ const pollId = result[1]._id.toString()
+ const pollCode = result[0]
+ await client.set(pollCode, pollId, { EX: expiry, NX: true })
- return { status: 201, data: { pollCode, pollId } };
+ return { status: 201, data: { pollCode, pollId } }
}
-async function changePollStatus(pollId: string, hasStarted: boolean) {
- if (typeof hasStarted !== "boolean")
+async function changePollStatus (pollId: string, hasStarted: boolean) {
+ if (typeof hasStarted !== 'boolean') {
return {
status: 400,
- data: { message: "hasStarted should be boolean" },
- };
- const currSequence = await client.get(pollId);
- console.log("currSequence", currSequence);
- let newSequence;
+ data: { message: 'hasStarted should be boolean' }
+ }
+ }
+ const currSequence = await client.get(pollId)
+ console.log('currSequence', currSequence)
+ let newSequence
// on every new start increment the sequence counter
if (hasStarted) {
- if (currSequence == null) newSequence = 0;
- else newSequence = parseInt(currSequence);
- if (newSequence < 0) newSequence *= -1;
- newSequence++;
- console.log("newSequence", newSequence);
+ if (currSequence == null) newSequence = 0
+ else newSequence = parseInt(currSequence)
+ if (newSequence < 0) newSequence *= -1
+ newSequence++
+ console.log('newSequence', newSequence)
await client.set(pollId, newSequence.toString(), {
- EX: expiry,
- });
- const result = await pollResult(pollId, newSequence);
- io.to(pollId).emit("result", result);
- }
- // for every stop make the current counter negative to indicate that it is not an active sequence
- else {
+ EX: expiry
+ })
+ const result = await pollResult(pollId, newSequence)
+ io.to(getRoomById(pollId, UserType.INSTRUCTOR)).emit('result', result)
+ } else {
+ // for every stop make the current counter negative to indicate that it is not an active sequence
if (currSequence != null) {
- newSequence = parseInt(currSequence) * -1;
+ newSequence = parseInt(currSequence) * -1
await client.set(pollId, newSequence.toString(), {
- EX: expiry,
- });
+ EX: expiry
+ })
}
}
- io.to(pollId).emit("pollStarted", hasStarted);
+ io.to(pollId).emit('pollStarted', hasStarted)
- return { status: 200, data: { message: "poll status successfully changed" } };
+ return { status: 200, data: { message: 'poll status successfully changed' } }
}
-async function getStudents(courseCode: string, startTime: Date, endTime: Date) {
+async function getStudents (courseCode: string, startTime: Date, endTime: Date) {
+ // TODO Already addressed in TODO bellow
+ // eslint-disable-next-line no-useless-catch
try {
- const pollDoc = await PollModel.find({ courseCode });
- let promises: Promise[] = [];
- let responses: any[] = [];
+ const pollDoc = await PollModel.find({ courseCode })
+ const promises: Promise[] = []
+ const responses: AggregatedStudent[] = []
pollDoc.forEach((element) => {
promises.push(
- StudentModel.aggregate([
+ StudentModel.aggregate([
{
$match: {
pollId: element._id.toString(),
- timestamp: { $gte: startTime, $lte: endTime },
- },
+ timestamp: { $gte: startTime, $lte: endTime }
+ }
},
{
$project: {
@@ -81,57 +84,54 @@ async function getStudents(courseCode: string, startTime: Date, endTime: Date) {
utorid: 1,
timestamp: {
$dateToString: {
- date: "$timestamp",
- timezone: "America/Toronto"
- },
+ date: '$timestamp',
+ timezone: 'America/Toronto'
+ }
},
pollName: element.name,
description: element.description,
- answer: 1,
- },
- },
+ answer: 1
+ }
+ }
]).then((data) => {
data.forEach((val) => {
- responses.push(val);
- });
+ responses.push(val)
+ })
})
- );
- });
- await Promise.all(promises);
- console.log(responses);
- return { responses };
+ )
+ })
+ await Promise.all(promises)
+ console.log(responses)
+ return { responses }
} catch (err) {
/**
* TODO: Add error handler
*/
- throw err;
+ throw err
}
}
-async function getPollStatus(pollId: any) {
- if (pollId === null || pollId === undefined || typeof pollId !== "string")
- return { status: 400, data: { message: "Invalid poll Id" } };
- const result = await client.get(pollId);
- const pollStarted = result === null ? false : parseInt(result) > 0;
- return { status: 200, data: { pollStarted } };
+async function getPollStatus (pollId: string) {
+ if (pollId.trim().length === 0) { return { status: 400, data: { message: 'Invalid poll Id' } } }
+ const result = await client.get(pollId)
+ const pollStarted = result === null ? false : parseInt(result) > 0
+ return { status: 200, data: { pollStarted } }
}
-async function getResult(pollId: any) {
- if (pollId === null || pollId === undefined || typeof pollId !== "string")
- return { status: 400, data: { message: "Invalid poll Id" } };
- const currSequence = await client.get(pollId);
- const result = await pollResult(pollId, parseInt(currSequence));
- return { status: 200, data: { ...result } };
+async function getResult (pollId: string) {
+ if (pollId.trim().length === 0) { return { status: 400, data: { message: 'Invalid poll Id' } } }
+ const currSequence = await client.get(pollId)
+ const result = await pollResult(pollId, parseInt(currSequence))
+ return { status: 200, data: { ...result } }
}
-async function endForever(pollCode: string) {
- if (pollCode === null || pollCode === undefined)
- return { status: 400, data: { message: "Invalid poll code" } };
- const pollId = await client.get(pollCode);
- await Promise.all([client.del(pollCode), client.del(pollId)]);
- io.to(pollId).emit("end", true);
- io.of("/").in(pollId).disconnectSockets();
- return { status: 200, data: { message: "Poll closed" } };
+async function endForever (pollCode: string) {
+ if (pollCode === null || pollCode === undefined) { return { status: 400, data: { message: 'Invalid poll code' } } }
+ const pollId = await client.get(pollCode)
+ await Promise.all([client.del(pollCode), client.del(pollId)])
+ io.to(pollId).emit('end', true)
+ io.of('/').in(pollId).disconnectSockets()
+ return { status: 200, data: { message: 'Poll closed' } }
}
export {
@@ -140,5 +140,5 @@ export {
getStudents,
getPollStatus,
getResult,
- endForever,
-};
+ endForever
+}
diff --git a/server/src/controllers/socketController.ts b/server/src/controllers/socketController.ts
index ad7b023..915b347 100644
--- a/server/src/controllers/socketController.ts
+++ b/server/src/controllers/socketController.ts
@@ -1,75 +1,78 @@
-import { StudentModel } from "../db/mogoose";
-import { io } from "../socket";
-import { Socket } from "socket.io";
-import { client } from "../redis";
+import { StudentModel } from '../db/mogoose'
+import { io } from '../socket'
+import { Socket } from 'socket.io'
+import { client } from '../redis'
+import { UserType } from '../types/user.types'
-async function join(socket: Socket, pollCode: string) {
+function getRoomById (pollId: string, userType?: UserType): string {
+ if (userType) { return userType + '-' + pollId } else { return pollId.toString() }
+}
+
+async function join (socket: Socket, pollCode: string) {
try {
- console.log(`join: ${socket.id}`);
- if (pollCode === null || pollCode === undefined)
- throw { code: 1, message: "Invalid poll code" };
- const pollId = await client.get(pollCode);
- console.log(pollId);
- if (pollId === null) throw { code: 1, message: "Invalid poll code" };
+ console.log(`join: ${socket.id}`)
+ if (pollCode === null || pollCode === undefined) { throw { code: 1, message: 'Invalid poll code' } }
+ const pollId = await client.get(pollCode)
+ console.log(pollId)
+ if (pollId === null) throw { code: 1, message: 'Invalid poll code' }
// ensure that socket is connected to 1 room (other than the default room)
socket.rooms.forEach((room) => {
- if (room !== socket.id) socket.leave(room);
- });
+ if (room !== socket.id) socket.leave(room)
+ })
- const currSequence = await client.get(pollId);
+ const currSequence = await client.get(pollId)
const hasStarted =
- currSequence == null ? false : parseInt(currSequence) > 0;
- console.log("Has Started", hasStarted);
- socket.join(pollId);
- socket.data["pollId"] = pollId;
- io.to(socket.id).emit("pollStarted", hasStarted);
+ currSequence == null ? false : parseInt(currSequence) > 0
+ console.log('Has Started', hasStarted)
+ socket.join(getRoomById(pollId))
+ // allow targeting specific user types
+ socket.join(getRoomById(pollId, socket.data.userType))
+ socket.data.pollId = pollId
+ io.to(socket.id).emit('pollStarted', hasStarted)
} catch (err) {
- console.log(err);
- io.to(socket.id).emit("error", err);
+ console.log(err)
+ io.to(socket.id).emit('error', err)
}
}
-async function pollResult(pollId: string, sequence: number) {
+async function pollResult (pollId: string, sequence: number) {
try {
const result = await StudentModel.aggregate([
{ $match: { pollId, sequence } },
{
$facet: {
- result: [{ $group: { _id: "$answer", count: { $sum: 1 } } }],
- totalVotes: [{ $count: "totalVotes" }],
- },
- },
+ result: [{ $group: { _id: '$answer', count: { $sum: 1 } } }],
+ totalVotes: [{ $count: 'totalVotes' }]
+ }
+ }
]).then((data: any): any => {
return {
result: data[0].result,
totalVotes:
- data[0].totalVotes.length > 0 ? data[0].totalVotes[0].totalVotes : 0,
- };
- });
- console.log(result);
- return result;
+ data[0].totalVotes.length > 0 ? data[0].totalVotes[0].totalVotes : 0
+ }
+ })
+ console.log(result)
+ return result
} catch (err) {
- console.log(err);
+ console.log(err)
}
}
-async function vote(socket: Socket, answer: number, utorid: string) {
+async function vote (socket: Socket, answer: number, utorid: string) {
try {
- console.log(`vote: ${socket.id}`);
- let pollId = socket.data.pollId;
- if (pollId === null || pollId === undefined)
- throw { code: 1, message: "haven't joined any room" };
- const currSequence = await client.get(pollId);
- console.log(currSequence);
+ console.log(`vote: ${socket.id}`)
+ const pollId = socket.data.pollId
+ if (pollId === null || pollId === undefined) { throw { code: 1, message: "haven't joined any room" } }
+ const currSequence = await client.get(pollId)
+ console.log(currSequence)
if (currSequence === null || parseInt(currSequence) < 0) {
- throw { code: 2, message: "Poll not live yet" };
+ throw { code: 2, message: 'Poll not live yet' }
}
- if (utorid === undefined || utorid === null)
- throw { code: 1, message: "Invalid utorid" };
- if (answer === undefined || answer === null)
- throw { code: 2, message: "Invalid answer" };
+ if (utorid === undefined || utorid === null) { throw { code: 1, message: 'Invalid utorid' } }
+ if (answer === undefined || answer === null) { throw { code: 2, message: 'Invalid answer' } }
await StudentModel.updateOne(
{
@@ -81,19 +84,19 @@ async function vote(socket: Socket, answer: number, utorid: string) {
utorid,
sequence: parseInt(currSequence),
answer,
- timestamp: new Date(),
+ timestamp: new Date()
},
{ upsert: true }
- );
+ )
pollResult(pollId, parseInt(currSequence)).then((data) => {
- io.to(pollId).emit("result", data);
- });
- io.to(socket.id).emit("ack", answer);
- return;
+ io.to(getRoomById(pollId, UserType.INSTRUCTOR)).emit('result', data)
+ })
+ io.to(socket.id).emit('ack', answer)
+ return
} catch (err) {
- console.log(err);
- io.to(socket.id).emit("error", err);
+ console.log(err)
+ io.to(socket.id).emit('error', err)
}
}
-export { vote, join, pollResult };
+export { vote, join, pollResult, getRoomById }
diff --git a/server/src/controllers/userController.ts b/server/src/controllers/userController.ts
index 82d4388..63cba24 100644
--- a/server/src/controllers/userController.ts
+++ b/server/src/controllers/userController.ts
@@ -1,11 +1,14 @@
-import { client } from "../redis";
+import { UserType } from '../types/user.types'
+import { client } from '../redis'
-async function getUser(utorid: any) {
- if (utorid === null || utorid === undefined || typeof utorid !== "string")
- return { status: 400, data: { message: "Invalid utorid" } };
- let userType = await client.get(utorid);
- if (userType == null) userType = "student";
- return { status: 200, data: { userType } };
+async function getUser (utorid: string): Promise<{ status: number; data: {
+ message?: string,
+ userType?: UserType
+}}> {
+ if (utorid.trim().length === 0) { return { status: 400, data: { message: 'Invalid utorid' } } }
+ let userType = await client.get(utorid) as UserType
+ if (userType == null) userType = UserType.STUDENT
+ return { status: 200, data: { userType } }
}
-export { getUser };
+export { getUser }
diff --git a/server/src/db/mogoose.ts b/server/src/db/mogoose.ts
index ead7cf7..3b759fa 100644
--- a/server/src/db/mogoose.ts
+++ b/server/src/db/mogoose.ts
@@ -1,16 +1,19 @@
-import { model, connect, connection } from "mongoose";
+import { model, connect, connection } from 'mongoose'
import {
PollDocument,
pollSchema,
StudentDocument,
- studentSchema,
-} from "./schema";
+ studentSchema
+} from './schema'
-export const PollModel = model("Poll", pollSchema);
-export const StudentModel = model("Student", studentSchema);
+export const PollModel = model('Poll', pollSchema)
+export const StudentModel = model('Student', studentSchema)
-connect(process.env.MONGODB_URL).catch((err) => {
- throw err;
-});
+export async function connectMongo () {
+ await connect(process.env.MONGODB_URL)
+ db.on('open', () => {
+ console.log('Connected to mongo')
+ })
+}
-export const db = connection;
+export const db = connection
diff --git a/server/src/db/schema.ts b/server/src/db/schema.ts
index 4f36ea4..2a9d99f 100644
--- a/server/src/db/schema.ts
+++ b/server/src/db/schema.ts
@@ -1,4 +1,4 @@
-import { Schema, Document, Types } from "mongoose";
+import { Schema, Document, Types } from 'mongoose'
/**
* student subdocument. Used as nested objects in PollResults schema
@@ -18,25 +18,25 @@ export interface StudentDocument extends Student, Document {}
export const studentSchema = new Schema({
utorid: {
type: String,
- required: true,
+ required: true
},
answer: {
type: Number,
- required: true,
+ required: true
},
timestamp: {
type: Date,
- required: true,
+ required: true
},
sequence: {
type: Number,
- required: true,
+ required: true
},
pollId: {
type: String,
- required: true,
- },
-});
+ required: true
+ }
+})
/**
* Poll schema. Represents how a single poll will look like
@@ -64,7 +64,7 @@ export const pollSchema = new Schema({
description: String,
courseCode: { type: String, required: true },
created: { type: Date, required: true },
- options: Number,
-});
+ options: Number
+})
-export const ObjectId = Types.ObjectId;
+export const ObjectId = Types.ObjectId
diff --git a/server/src/redis.ts b/server/src/redis.ts
index 3bf6d41..982a9d5 100644
--- a/server/src/redis.ts
+++ b/server/src/redis.ts
@@ -1,30 +1,32 @@
-import { createClient } from "redis";
-import fs from "fs";
-import path from "path";
-import readline from "readline";
+import { createClient } from 'redis'
+import fs from 'fs'
+import path from 'path'
+import readline from 'readline'
+import { UserType } from './types/user.types'
+
const client = createClient({
- url: process.env.REDIS_URL,
-});
+ url: process.env.REDIS_URL
+})
-(async () => {
- client.on("error", (err) => console.log("Redis Client Error", err));
+async function connectRedis () {
+ client.on('error', (err) => console.log('Redis Client Error', err))
- await client.connect();
- console.log("connected to redis");
+ await client.connect()
+ console.log('connected to redis')
const readstream = fs.createReadStream(
path.join(__dirname, process.env.WHITELIST)
- );
+ )
const rl = readline.createInterface({
input: readstream,
- crlfDelay: Infinity,
- });
+ crlfDelay: Infinity
+ })
for await (const line of rl) {
- console.log(line.trim());
- client.set(line.trim(), "instructor");
+ console.log(line.trim())
+ client.set(line.trim(), UserType.INSTRUCTOR)
}
-})();
+}
-export { client };
+export { client, connectRedis }
diff --git a/server/src/routes/pollRoute.ts b/server/src/routes/pollRoute.ts
index 7a58775..bebd377 100644
--- a/server/src/routes/pollRoute.ts
+++ b/server/src/routes/pollRoute.ts
@@ -1,92 +1,98 @@
-import { Router } from "express";
+import { Router } from 'express'
import {
changePollStatus,
createPoll,
getPollStatus,
getResult,
getStudents,
- endForever,
-} from "../controllers/pollController";
+ endForever
+} from '../controllers/pollController'
-const pollRouter = Router();
+const pollRouter = Router()
-pollRouter.post("/", async (req, res) => {
- const { name, description, courseCode, options } = req.body;
+pollRouter.post('/', async (req, res) => {
+ const { name, description, courseCode, options } = req.body
try {
const poll = {
name,
description,
courseCode: courseCode.toUpperCase(),
options,
- created: new Date(),
- };
- const result = await createPoll(poll);
- return res.status(result.status).send(result.data);
+ created: new Date()
+ }
+ const result = await createPoll(poll)
+ return res.status(result.status).send(result.data)
} catch (err) {
- console.log(err);
- return res.status(500).send({ message: "Internal Server Error" });
+ console.log(err)
+ return res.status(500).send({ message: 'Internal Server Error' })
}
-});
+})
-pollRouter.patch("/:pollId", async (req, res) => {
- const { pollId } = req.params;
- const { hasStarted } = req.body;
+pollRouter.patch('/:pollId', async (req, res) => {
+ const { pollId } = req.params
+ const { hasStarted } = req.body
try {
- const result = await changePollStatus(pollId, hasStarted);
- return res.status(result.status).send(result.data);
+ const result = await changePollStatus(pollId, hasStarted)
+ return res.status(result.status).send(result.data)
} catch (err) {
- console.log(err);
- return res.status(500).send({ message: "Internal Server Error" });
+ console.log(err)
+ return res.status(500).send({ message: 'Internal Server Error' })
}
-});
+})
-pollRouter.get("/students", async (req, res) => {
- const courseCode = req.query.courseCode as string;
- const startTime = req.query.startTime as string;
- const endTime = req.query.endTime as string;
- //const { startTime, endTime } = req.body;
- console.log(req.query);
- console.log(startTime, endTime);
+pollRouter.get('/students', async (req, res) => {
+ const courseCode = req.query.courseCode as string
+ const startTime = req.query.startTime as string
+ const endTime = req.query.endTime as string
+ // const { startTime, endTime } = req.body;
+ console.log(req.query)
+ console.log(startTime, endTime)
try {
const result = await getStudents(
courseCode.toUpperCase(),
new Date(startTime),
new Date(endTime)
- );
- return res.status(200).send(result);
+ )
+ return res.status(200).send(result)
} catch (err) {
- return res.status(500).send({ message: "Failed to find students" });
+ return res.status(500).send({ message: 'Failed to find students' })
}
-});
+})
-pollRouter.get("/status", async (req, res) => {
- const { pollId } = req.query;
+pollRouter.get('/status', async (req, res) => {
+ const { pollId } = req.query
try {
- const result = await getPollStatus(pollId);
- return res.status(result.status).send(result.data);
+ if (typeof pollId !== 'string') {
+ return res.status(400).send({ message: 'Invalid utorid' })
+ }
+ const result = await getPollStatus(pollId)
+ return res.status(result.status).send(result.data)
} catch (err) {
- return res.status(500).send({ message: "Internal Server Error" });
+ return res.status(500).send({ message: 'Internal Server Error' })
}
-});
+})
-pollRouter.get("/result", async (req, res) => {
- const { pollId } = req.query;
+pollRouter.get('/result', async (req, res) => {
+ const { pollId } = req.query
try {
- const result = await getResult(pollId);
- return res.status(result.status).send(result.data);
+ if (typeof pollId !== 'string') {
+ return res.status(400).send({ message: 'Invalid utorid' })
+ }
+ const result = await getResult(pollId)
+ return res.status(result.status).send(result.data)
} catch (err) {
- return res.status(500).send({ message: "Internal Server Error" });
+ return res.status(500).send({ message: 'Internal Server Error' })
}
-});
+})
-pollRouter.patch("/end/:pollCode", async (req, res) => {
- const { pollCode } = req.params;
+pollRouter.patch('/end/:pollCode', async (req, res) => {
+ const { pollCode } = req.params
try {
- const result = await endForever(pollCode);
- return res.status(result.status).send(result.data);
+ const result = await endForever(pollCode)
+ return res.status(result.status).send(result.data)
} catch (err) {
- return res.status(500).send({ message: "Internal Server Error" });
+ return res.status(500).send({ message: 'Internal Server Error' })
}
-});
+})
-export default pollRouter;
+export default pollRouter
diff --git a/server/src/routes/userRoutes.ts b/server/src/routes/userRoutes.ts
index 0057905..34f88cf 100644
--- a/server/src/routes/userRoutes.ts
+++ b/server/src/routes/userRoutes.ts
@@ -1,16 +1,19 @@
-import { Router } from "express";
-import { getUser } from "../controllers/userController";
+import { Router } from 'express'
+import { getUser } from '../controllers/userController'
-const userRouter = Router();
+const userRouter = Router()
-userRouter.get("/", async (req, res) => {
+userRouter.get('/', async (req, res) => {
try {
- const result = await getUser(req.headers.utorid);
- return res.status(result.status).send(result.data);
+ if (typeof req.headers.utorid !== 'string') {
+ return res.status(400).send({ message: 'Invalid utorid' })
+ }
+ const result = await getUser(req.headers.utorid)
+ return res.status(result.status).send(result.data)
} catch (err) {
- console.log(err);
- return res.status(500).send({ message: "Internal Server Error" });
+ console.log(err)
+ return res.status(500).send({ message: 'Internal Server Error' })
}
-});
+})
-export default userRouter;
+export default userRouter
diff --git a/server/src/server.ts b/server/src/server.ts
index 5314d71..a499672 100644
--- a/server/src/server.ts
+++ b/server/src/server.ts
@@ -1,62 +1,71 @@
-"use strict";
+'use strict'
-import * as dotenv from "dotenv";
-dotenv.config();
-import express from "express";
-import cors from "cors";
-import cookieParser from "cookie-parser";
-import { io } from "./socket";
-import pollRouter from "./routes/pollRoute";
-import { db } from "./db/mogoose";
-import userRouter from "./routes/userRoutes";
-import { getUser } from "./controllers/userController";
-db.on("open", () => {
- console.log("Connected to mongo");
-});
+import * as dotenv from 'dotenv'
+import express from 'express'
+import cors from 'cors'
+import cookieParser from 'cookie-parser'
+import { io } from './socket'
+import pollRouter from './routes/pollRoute'
+import { connectMongo } from './db/mogoose'
+import userRouter from './routes/userRoutes'
+import { getUser } from './controllers/userController'
+import { UserType } from './types/user.types'
+import { connectRedis } from './redis'
+dotenv.config()
// starting the express server
-const app = express();
-const port = process.env.PORT || 5000;
+const app = express()
+const port = process.env.PORT || 5000
// parse cookies and body and enable cors
app.use(
cors({
origin: process.env.FRONTEND_URL,
credentials: true,
- methods: "GET,POST,DELETE,PATCH",
+ methods: 'GET,POST,DELETE,PATCH'
})
-);
-app.use(express.urlencoded({ extended: true }));
-app.use(express.json());
-app.use(cookieParser());
-app.use("/user", userRouter);
+)
+app.use(express.urlencoded({ extended: true }))
+app.use(express.json())
+app.use(cookieParser())
+app.use('/user', userRouter)
app.use(async (req, res, next) => {
try {
- const userType = await getUser(req.headers.utorid);
- if (userType.data.userType === "instructor") next();
- else next(new Error("Forbidden User"));
+ if (typeof req.headers.utorid !== 'string') {
+ return next(new Error('Invalid utorid'))
+ }
+ const userType = await getUser(req.headers.utorid)
+ if (userType.data.userType === UserType.INSTRUCTOR) next()
+ else next(new Error('Forbidden User'))
} catch (err) {
- next(new Error("Forbidden User"));
+ next(new Error('Forbidden User'))
}
-});
-app.use("/poll", pollRouter);
+})
+app.use('/poll', pollRouter)
const server = app.listen(port, () => {
- console.log("Listening on http://localhost:" + port);
- io.attach(server);
+ console.log('Listening on http://localhost:' + port)
+ connectRedis()
+ connectMongo()
+ io.attach(server)
io.use(async (socket, next) => {
try {
- if (socket.handshake.headers.utorid != undefined) {
- socket.data["utorid"] = socket.handshake.headers.utorid;
- next();
- return;
- } else if (socket.data.utorid != undefined) {
- next();
- return;
+ if (socket.handshake.headers.utorid) {
+ socket.data.utorid = socket.handshake.headers.utorid
+ next()
+ return
+ } else if (socket.data.utorid) {
+ next()
+ return
}
- next(new Error("Not Authorized"));
+ next(new Error('Not Authorized'))
} catch (err) {
- next(new Error("Not Authorized"));
+ next(new Error('Not Authorized'))
}
- });
-});
+ })
+ io.use(async (socket, next) => {
+ const userType = await getUser(socket.data.utorid)
+ socket.data.userType = userType.data.userType
+ next()
+ })
+})
diff --git a/server/src/socket.ts b/server/src/socket.ts
index 09dac3a..781e940 100644
--- a/server/src/socket.ts
+++ b/server/src/socket.ts
@@ -1,42 +1,42 @@
// socket setup
-import { Server, Socket } from "socket.io";
-import { vote, join } from "./controllers/socketController";
-import { RateLimiterMemory } from "rate-limiter-flexible";
+import { Server, Socket } from 'socket.io'
+import { vote, join } from './controllers/socketController'
+import { RateLimiterMemory } from 'rate-limiter-flexible'
const rateLimiter = new RateLimiterMemory({
points: 5, // 5 points connections
- duration: 3, // per second
-});
+ duration: 3 // per second
+})
const io = new Server({
- path: "/socket.io",
- cors: { origin: process.env.FRONTEND_URL, credentials: true },
-});
+ path: '/socket.io',
+ cors: { origin: process.env.FRONTEND_URL, credentials: true }
+})
// log the socket id when client socket connects for the first time
-io.on("connection", (socket: Socket) => {
- console.log(`connect: ${socket.id}`);
+io.on('connection', (socket: Socket) => {
+ console.log(`connect: ${socket.id}`)
// let the socket join rooms once connected
- socket.on("join", async (pollCode: string) => {
+ socket.on('join', async (pollCode: string) => {
try {
- await rateLimiter.consume(socket.data.utorid);
- await join(socket, pollCode);
+ await rateLimiter.consume(socket.data.utorid)
+ await join(socket, pollCode)
// let the socket vote in the connected room
- socket.on("vote", async (answer: number) => {
+ socket.on('vote', async (answer: number) => {
try {
- await rateLimiter.consume(socket.data.utorid);
- await vote(socket, answer, socket.data.utorid);
+ await rateLimiter.consume(socket.data.utorid)
+ await vote(socket, answer, socket.data.utorid)
} catch (err) {
- socket.emit("error", { code: 2, message: "retry again later" });
+ socket.emit('error', { code: 2, message: 'retry again later' })
}
- });
+ })
} catch (err) {
- socket.emit("error", { code: 2, message: "retry again later" });
+ socket.emit('error', { code: 2, message: 'retry again later' })
}
- });
-});
+ })
+})
-export { io };
+export { io }
diff --git a/server/src/types/pollController.types.ts b/server/src/types/pollController.types.ts
new file mode 100644
index 0000000..3f885ea
--- /dev/null
+++ b/server/src/types/pollController.types.ts
@@ -0,0 +1,10 @@
+export type AggregatedStudent = {
+ pollId: string,
+ courseCode: string,
+ sequence: number,
+ utorid: string,
+ timestamp: string,
+ pollName: string,
+ description: string,
+ answer: number
+}
diff --git a/server/src/types/user.types.ts b/server/src/types/user.types.ts
new file mode 100644
index 0000000..5339d8f
--- /dev/null
+++ b/server/src/types/user.types.ts
@@ -0,0 +1,4 @@
+export enum UserType {
+ INSTRUCTOR = 'instructor',
+ STUDENT = 'student'
+}
diff --git a/server/yarn.lock b/server/yarn.lock
index 877a989..9e3ae79 100644
--- a/server/yarn.lock
+++ b/server/yarn.lock
@@ -1434,6 +1434,11 @@ http-errors@~1.7.2:
statuses ">= 1.5.0 < 2"
toidentifier "1.0.0"
+husky@^8.0.1:
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.1.tgz#511cb3e57de3e3190514ae49ed50f6bc3f50b3e9"
+ integrity sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==
+
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"