Skip to content

Commit

Permalink
feat: add validation function and oauth signin
Browse files Browse the repository at this point in the history
  • Loading branch information
BoonHianLim committed Mar 2, 2024
1 parent dfa1e52 commit bf0839b
Show file tree
Hide file tree
Showing 19 changed files with 402 additions and 14 deletions.
8 changes: 7 additions & 1 deletion apps/challenges/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,10 @@ 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=
3 changes: 3 additions & 0 deletions apps/challenges/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"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",
"cron": "^3.1.6",
Expand All @@ -15,6 +16,7 @@
"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 +27,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
55 changes: 55 additions & 0 deletions apps/challenges/src/controllers/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import asyncHandler from "express-async-handler";
import { Request, Response } from "express";
import { z } from "zod";
import AuthService from "../service/authService";


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

try {
const resp = await AuthService.oauthSignIn(redirectURL);
res.status(resp.status).json({ resp });
} catch (error) {
res.status(500).json({ message: 'Internal Server Error' });
}
});

const authCallback = asyncHandler(async (req: Request, res: Response) => {
let codeString: string;
let nextString: string | undefined;
let append = '#status=auth_failure';

try {
console.log(req.query);
const { code, next } = req.query;

if (!code) {
throw new Error('Invalid code');
}

codeString = z.string().parse(code);
nextString = z.string().optional().parse(next);

const { accessToken, refreshToken } = await AuthService.oauthCallback(codeString, nextString);
append = `#access_token=${accessToken}&refresh_token=${refreshToken}&status=auth_success`;

} catch (err) {
console.error(err);
} finally {
if (nextString) {
res.redirect(303, nextString + append);
} else {
res.status(append === '#status=auth_failure' ? 400 : 200).json({ message: append });
}
}

});


const AuthController = {
oauthSignIn,
authCallback,
}

export { AuthController as default };
2 changes: 1 addition & 1 deletion 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
6 changes: 6 additions & 0 deletions apps/challenges/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ 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";
dotenv.config({ path: "../.env"});

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

const app: Express = express();
const port = process.env.PORT || 3000;
Expand All @@ -31,7 +34,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
21 changes: 21 additions & 0 deletions apps/challenges/src/middleware/jwtMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// jwt middleware for express
import jwt from "jsonwebtoken";
import { Request, Response, NextFunction } from "express";

const jwtMiddleware = (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];

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

jwt.verify(token, process.env.JWT_SECRET || "", (err, user) => {
if (err) {
return res.sendStatus(403);
}
next();
});
}

export default jwtMiddleware;
5 changes: 0 additions & 5 deletions apps/challenges/src/model/response.ts

This file was deleted.

10 changes: 9 additions & 1 deletion apps/challenges/src/repo/userRepo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ const getUserByID = async (id: mongoose.Types.ObjectId) => {
return user;
}

const getUserByEmail = async (email: string) => {
const user = await User.findOne({
email: email
});
return user;
}

const createUser = async (
name: string,
email: string,
Expand All @@ -23,7 +30,8 @@ const createUser = async (

const UserRepo = {
getUserByID,
createUser
getUserByEmail,
createUser,
}

export { UserRepo as default };
10 changes: 10 additions & 0 deletions apps/challenges/src/routes/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Express from "express";
import AuthController from "../controllers/auth";

const router = Express.Router();

router.post("/oauth/signin", AuthController.oauthSignIn);
router.get("/oauth/callback", AuthController.authCallback);
// router.get("/:userID/rankings", SeasonController.getUserAllSeasonRankings);

export { router as default };
4 changes: 1 addition & 3 deletions apps/challenges/src/routes/user.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import Express from "express";
import UserController from "../controllers/user";
import SeasonController from "../controllers/season";

const router = Express.Router();

router.post("/", UserController.createUser);
// router.get("/:userID/rankings", SeasonController.getUserAllSeasonRankings);
router.get("/:userID", UserController.createUser);

export { router as default };
90 changes: 90 additions & 0 deletions apps/challenges/src/service/authService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { SupabaseService } from '../utils/supabase';

import { jwtDecode } from "jwt-decode";
import UserService from "../service/userService";
import { isValidEmail, getEmailPrefix } from "../utils/validator";
import supabase from "../utils/supabase";
import { z } from 'zod';
import TokenService from './tokenService';
import { GeneralResp, OauthcallbackResp } from '../types/types';

const oauthSignIn = async (
url: string
) => {
let redirectURL: string;
try {
redirectURL = z.string().url().parse(url);
} catch (err) {
return {
status: 400,
message: 'Invalid redirectURL',
data: null,
} as GeneralResp;
}

try {
const resp = await SupabaseService.signInWithAzure(redirectURL);
return {
status: 302,
message: 'waiting for response from azure',
data: resp,
} as GeneralResp;
} catch (err) {
console.log("error in createUser with azure: ", err)
return {
status: 500,
message: 'Internal Server Error',
data: null,
} as GeneralResp;
}
}

const oauthCallback = async (
code: string,
next: string | undefined,
): Promise<OauthcallbackResp> => {
// verify the query parameters
const resp = await supabase.auth.exchangeCodeForSession(code);

const supabaseToken = resp.data.session?.access_token;

if (resp.error || !supabaseToken) {
throw new Error("Failed to exchange code for session.");
}

const decodedJWTToken = jwtDecode(supabaseToken);
const decodedJWTObj = decodedJWTToken as {
aud: string;
exp: number;
iat: number;
iss: string;
sub: string;
email: string;
role: string;
session_id: string;
}
console.log(decodedJWTObj);

let user = await UserService.getUserByEmail(decodedJWTObj.email);

if (!user) {
const email = isValidEmail.parse(decodedJWTObj.email);
const userName = getEmailPrefix(email);
user = await UserService.createUser(userName, decodedJWTObj.email);
}

const accessToken = await TokenService.generateAccessToken(user._id.toString(), user.email);
const refreshToken = await TokenService.generateRefreshToken(user._id.toString(), user.email);

return {
accessToken: accessToken,
refreshToken: refreshToken,
}
}

const AuthService = {
oauthSignIn,
oauthCallback,
}

export { AuthService as default };
2 changes: 1 addition & 1 deletion apps/challenges/src/service/questionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import mongoose from 'mongoose';
import QuestionRepo from '../repo/questionRepo';
import { CreateQuestionReq } from '../model/question';
import ValidationService from './validationService';
import { GeneralResp } from '../model/response';
import { GeneralResp } from '../types/types';

const getQuestionByID = async(
questionID: string,
Expand Down
2 changes: 1 addition & 1 deletion apps/challenges/src/service/submissionService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CreateSubmissionReq, SubmissionModel } from "../model/submission";
import SubmissionRepo from "../repo/submissionRepo";
import { GeneralResp } from "../types/types";
import QuestionService from "./questionService";
import { GeneralResp } from "../model/response";
import ValidationService from "./validationService";

const createSubmission = async (
Expand Down
32 changes: 32 additions & 0 deletions apps/challenges/src/service/tokenService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import jwt from "jsonwebtoken";

const minuteInSeconds = 60;

const generateAccessToken = async (
id: string,
email: string
) => {
const secret = process.env.JWT_SECRET || "";
const token = jwt.sign({ id, email }, secret, {
expiresIn: 10 * minuteInSeconds
});
return token;
}

const generateRefreshToken = async (
id: string,
email: string
) => {
const secret = process.env.JWT_SECRET || "";
const token = jwt.sign({ id, email }, secret, {
expiresIn: "7d"
});
return token;
}

const TokenService = {
generateAccessToken,
generateRefreshToken,
}

export { TokenService as default };
13 changes: 12 additions & 1 deletion apps/challenges/src/service/userService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import mongoose from 'mongoose';
import UserRepo from '../repo/userRepo';
import { z } from 'zod';
import { isValidEmail } from '../utils/validator';

const getUserByID = async (id: string) => {
if (!mongoose.isValidObjectId(id)) {
Expand All @@ -10,6 +12,14 @@ const getUserByID = async (id: string) => {
return user;
}

const getUserByEmail = async (email: string) => {
const _email = isValidEmail.parse(email);

const user = await UserRepo.getUserByEmail(_email);

return user;
}

const createUser = async (
name: string,
email: string,
Expand All @@ -20,7 +30,8 @@ const createUser = async (

const UserService = {
getUserByID,
createUser
getUserByEmail,
createUser,
}

export { UserService as default };
Loading

0 comments on commit bf0839b

Please sign in to comment.