Skip to content

Commit

Permalink
Feat/challenges question specific validation (#150)
Browse files Browse the repository at this point in the history
* feat: add getSeasonQuestion function

* fix: return correct itemcount and pagecount when generating pagination metadata

* test: add test file for pagination and getSeasonQuestion

* fix: modify some function to use cont-serv-repo architecture

* fix: add get season question endpoint

* feat: add ability to do custom validation

* chore: remove unused ranking model

* feat: add validation function and oauth signin

* feat: add jwt middleware

* fix: add oauth sign in
  • Loading branch information
BoonHianLim authored Mar 23, 2024
1 parent f9bac61 commit 72df095
Show file tree
Hide file tree
Showing 33 changed files with 898 additions and 157 deletions.
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

0 comments on commit 72df095

Please sign in to comment.