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/challenges question specific validation #150

Merged
merged 11 commits into from
Mar 23, 2024
Merged
12 changes: 11 additions & 1 deletion apps/challenges/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,14 @@ MONGO_URI=mongodb://localhost:27017/
MONGO_PORT=27017
MONGO_DATABSE_NAME=challenges

DO_RANKING_CALCULATION=true
BASE_URL=http://localhost:3001
PORT=3001
DO_RANKING_CALCULATION=true

// fill in the following for supabase (used in register and login)
SUPABASE_URL=
SUPABASE_ANON_KEY=

// set the secret to something (hey dont peek these are secrets! ba..baka!)
JWT_SECRET=abc
COOKIE_SECRET=abc
3 changes: 2 additions & 1 deletion apps/challenges/mock/mongo-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ db.createCollection("seasons");
db.createCollection("rankings");
db.createCollection("questions");
db.createCollection("submissions");
db.createCollection("users");
db.createCollection("users");
db.createCollection("tokens");
4 changes: 4 additions & 0 deletions apps/challenges/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
"dependencies": {
"@babel/preset-env": "^7.23.8",
"@babel/preset-typescript": "^7.23.3",
"@supabase/supabase-js": "^2.39.7",
"@types/express": "^4.17.21",
"babel-jest": "^29.7.0",
"cookie-parser": "^1.4.6",
"cron": "^3.1.6",
"cross-env": "^7.0.3",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-async-handler": "^1.1.4",
"jest": "^29.7.0",
"jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0",
"prettier": "^2.8.1",
"supertest": "^6.3.4",
"ts-jest": "^29.1.1",
Expand All @@ -25,6 +28,7 @@
"devDependencies": {
"@types/express": "^4.17.21",
"@types/jest": "^29.5.11",
"@types/jsonwebtoken": "^9.0.6",
"@types/node": "^20.10.6",
"@types/supertest": "^6.0.2",
"jest": "^29.7.0",
Expand Down
39 changes: 39 additions & 0 deletions apps/challenges/src/controllers/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import asyncHandler from "express-async-handler";
import { Request, Response } from "express";
import { z } from "zod";
import AuthService from "../service/authService";
import { accessTokenMaxAgeSeconds, refreshCookieMaxAgeSeconds, secondInMilliseconds } from "../model/constants";


const oauthSignIn = asyncHandler(async (req: Request, res: Response) => {
const { access_token } = req.body;

try {
const { accessToken, refreshToken, createNewUser } = await AuthService.oauthSignIn(access_token);
res.status(createNewUser ? 200 : 201).json({
"access_token": accessToken,
"refresh_token": refreshToken
})
} catch (error) {
console.log("AuthService.oauthSignIn", error);
res.status(500).json({ message: 'Internal Server Error' });
}
});

const refreshToken = asyncHandler(async (req: Request, res: Response) => {
try {
const userID = req.params.userID;
const token = await AuthService.refreshToken(userID);
res.status(200).json(token);
} catch (err) {
console.error(err);
res.status(500).json({ message: "Internal Server Error" })
}
})

const AuthController = {
oauthSignIn,
refreshToken,
}

export { AuthController as default };
36 changes: 19 additions & 17 deletions apps/challenges/src/controllers/questionaire.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { Request, Response } from "express";
const asyncHandler = require('express-async-handler');
import Question from '../model/question';
import Question, { CreateQuestionReq } from '../model/question';
import Submission from '../model/submission';
import Season from "../model/season";
import { isValidObjectId } from "../utils/db";
import QuestionService from "../service/questionService";
import { isValidCreateQuestionRequest } from "../utils/validator";
import { z } from "zod";

// @desc Get questions
// @route GET /api/question
Expand Down Expand Up @@ -35,7 +38,7 @@ const getQuestion = asyncHandler(async (req: Request, res: Response) => {
}

try {
const question = await Question.findById(questionId);
const question = await QuestionService.getQuestionByID(questionId);

if (!question) {
return res.status(404).json({ message: 'Question not found' });
Expand All @@ -54,21 +57,20 @@ const getQuestion = asyncHandler(async (req: Request, res: Response) => {
// @access Private
const setQuestion = asyncHandler(async (req: Request, res: Response) => {
try {
const question = await Question.create({
question_no: req.body.question_no,
question_title: req.body.question_title,
question_desc: req.body.question_desc,
question_date: req.body.question_date,
seasonID: req.body.season_id,
expiry: req.body.expiry,
points: req.body.points,
answer: req.body.answer,
});

res.status(201).json(question);
} catch (error) {
if ((error as Error).name === 'ValidationError') {
return res.status(400).json({ message: (error as Error).message });
const question = isValidCreateQuestionRequest.parse(req.body);

const createQuestionReq: CreateQuestionReq = {
...question,
question_date: new Date(question.question_date),
expiry: new Date(question.expiry),
}

const resp = await QuestionService.createQuestion(createQuestionReq);

res.status(resp.status).json(resp);
} catch (err) {
if (err instanceof z.ZodError) {
return res.status(400).json({ message: (err as Error).message });
}

res.status(500).json({ message: 'Internal Server Error' });
Expand Down
74 changes: 7 additions & 67 deletions apps/challenges/src/controllers/submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import mongoose from 'mongoose';
// @access Public
const getSubmissions = asyncHandler(async (req: Request, res: Response) => {
const submissions = await Submission.find({})
res.status(200).json(submissions)
res.status(200).json(submissions)
})

// @desc Get submission
Expand Down Expand Up @@ -43,96 +43,36 @@ const getSubmission = asyncHandler(async (req: Request, res: Response) => {
})

// @desc Set submission
// @route POST /api/submission
// @route POST /api/submission/
// @access Private

const setSubmission = asyncHandler(async (req: Request, res: Response) => {
const questionId = req.body.question;
const questionID = req.body.question;

if (!isValidObjectId(questionId)) {
if (!isValidObjectId(questionID)) {
return res.status(400).json({ message: 'Invalid question ID' });
}

try {
const submission = isValidCreateSubmissionRequest.parse(req.body);
const createSubmissionReq = {
question: new mongoose.Types.ObjectId(submission.question),
user: new mongoose.Types.ObjectId(submission.user),
question: new mongoose.Types.ObjectId(submission.question),
answer: submission.answer
};

await SubmissionService.createSubmission(createSubmissionReq);

res.status(201).json({ message: 'Answer submitted' });

} catch (error) {
res.status(500).json({ message: 'Internal Server Error' });
}
})

// @desc Update submission
// @route PUT /api/submission/:id
// @access Private
const updateSubmission = asyncHandler(async (req: Request, res: Response) => {
const submissionId = req.params.id;

if (!isValidObjectId(submissionId)) {
return res.status(400).json({ message: 'Invalid submission ID' });
}

try {
const submission = await Submission.findById(submissionId);

if (!submission) {
return res.status(404).json({ message: 'Submission not found' });
}

const updatedSubmission = await Submission.findByIdAndUpdate(submissionId, req.body, { new: true });
const resp = await SubmissionService.createSubmission(createSubmissionReq);

// Re-evaluate the points awarded

res.status(resp.status).json(resp);


res.status(200).json(updatedSubmission);
} catch (error) {
console.error(error);
res.status(500).json({ message: 'Internal Server Error' });
}
})

// @desc Delete submission
// @route DELETE /api/submission/:id
// @access Private
const deleteSubmission = asyncHandler(async (req: Request, res: Response) => {
const submissionId = req.params.id;

if (!isValidObjectId(submissionId)) {
return res.status(400).json({ message: 'Invalid submission ID' });
}

try {
const submission = await Submission.findById(submissionId);

if (!submission) {
return res.status(404).json({ message: 'Submission not found' });
}

await submission.remove()

res.status(200).json({message: 'Submission deleted'});
} catch (error) {
console.error(error);
res.status(500).json({ message: 'Internal Server Error' });
}
})


const SubmissionController = {
getSubmissions,
getSubmission,
setSubmission,
updateSubmission,
deleteSubmission
};

export { SubmissionController as default };
28 changes: 21 additions & 7 deletions apps/challenges/src/controllers/user.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
import UserService from "../service/userService";
import asyncHandler from "express-async-handler";
import { Request, Response } from "express";
import { StatusCodeError } from "../types/types";

const createUser = asyncHandler(async (req: Request, res: Response) => {
const { name, email } = req.body;
const getUser = asyncHandler(async (req: Request, res: Response) => {
const { userID } = req.params;

try {
const user = await UserService.createUser(name, email);
res.status(201).json(user);
} catch (error) {
res.status(500).json({ message: 'Internal Server Error' });
const user = await UserService.getUserByID(userID);
res.status(200).json(user);
} catch (err) {
if (err instanceof StatusCodeError) {
res.status(err.status).json({ message: err.message });
} else {
res.status(500).json({ message: "Internal Server Error" })
}

}
});

const checkTokens = asyncHandler(async (req: Request, res: Response) => {
const token = req.signedCookies;

console.log(token);

res.status(200);
});
const UserController = {
createUser
getUser,
checkTokens,
}

export { UserController as default };
11 changes: 10 additions & 1 deletion apps/challenges/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@ import SeasonRouter from "./routes/seasons";
import QuestionaireRouter from "./routes/questionaire";
import SubmissionRouter from "./routes/submission";
import UserRouter from "./routes/user";
import AuthRouter from "./routes/auth";
import { connectDB } from "./config/db";
import { CronJob } from "cron";
import { rankingCalculation } from "./tasks/rankingCalculation";
import { SupabaseService } from "./utils/supabase";
import cookieParser from "cookie-parser";
dotenv.config({ path: "../.env"});

// Database
connectDB();
SupabaseService.initClient();

const app: Express = express();
const port = process.env.PORT || 3000;

const cookieSecret = process.env.COOKIE_SECRET || "";
// Middleware
app.use(express.json());
app.use(function(req, res, next) {
Expand All @@ -23,6 +27,8 @@ app.use(function(req, res, next) {
res.header("Access-Control-Allow-Headers", "*");
next();
});
app.use(cookieParser(cookieSecret));

// Routes
app.get("/ping", (req: Request, res: Response) => {
res.status(200).json({ message: "pong" });
Expand All @@ -31,7 +37,10 @@ app.use("/api/seasons", SeasonRouter);
app.use('/api/question', QuestionaireRouter);
app.use('/api/submission', SubmissionRouter);
app.use('/api/user', UserRouter);
app.use('/api/auth', AuthRouter);

// the check is needed for testing
// refer to https://stackoverflow.com/a/63299022
if (process.env.NODE_ENV !== 'test') {
app.listen(port, () => {
console.log(`[server]: Server is running at http://localhost:${port}`);
Expand Down
25 changes: 25 additions & 0 deletions apps/challenges/src/middleware/jwtMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// jwt middleware for express
import jwt from "jsonwebtoken";
import { Request, Response, NextFunction } from "express";

const jwtMiddleware = (req: Request, res: Response, next: NextFunction) => {
const token = req.signedCookies.access_token;

if (token == null) {
return res.sendStatus(401);
}

jwt.verify(token, process.env.JWT_SECRET || "", (err, tokenContent: any) => {
if (err) {
return res.sendStatus(401);
}

req.params.userID = tokenContent.id;
req.params.email = tokenContent.email;


next();
});
}

export default jwtMiddleware;
Loading
Loading