Skip to content
Open
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Ignore all node_modules folders
node_modules
node_modules
dump.rdb
538 changes: 423 additions & 115 deletions backend/package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@
"dependencies": {
"@typegoose/typegoose": "^9.8.1",
"@types/bcrypt": "^5.0.0",
"axios": "^1.2.1",
"bcrypt": "^5.0.1",
"body-parser": "^1.20.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"crypto-js": "^4.1.1",
"dotenv": "^16.0.0",
"express": "^4.17.3",
"express-jwt": "^7.7.0",
"jsonwebtoken": "^8.5.1",
"mongoose": "^6.3.2",
"nodemon": "^2.0.16",
"prettier": "^2.6.2",
"redis": "^4.2.0",
"socket.io": "^4.4.1",
"uuid": "^8.3.2"
},
Expand All @@ -27,10 +29,12 @@
"version": "1.0.0",
"main": "server.js",
"devDependencies": {
"@types/cookie-parser": "^1.4.3",
"@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.13",
"@types/jsonwebtoken": "^8.5.8",
"@types/node": "^17.0.31",
"@types/redis": "^4.0.11",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.22.0",
"@typescript-eslint/parser": "^5.22.0",
Expand Down
49 changes: 41 additions & 8 deletions backend/src/api/auth.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { Router } from 'express';
import { sign } from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import dalUser from '../repository/dalUser';
import {
addHours,
generateHashedSsoPayload,
generateSsoPayload,
getJWTSecret,
getParamMap,
isAuthenticated,
isCorrectHashedSsoPayload,
isSsoEnabled,
isValidNonce,
logoutSCORE,
signInUserWithSso,
userToToken,
} from '../utils/auth';
import { UserModel } from '../models/User';
import { addToken, destroyToken, sign } from '../utils/jwt';

const router = Router();

Expand All @@ -37,9 +37,17 @@ router.post('/login', async (req, res) => {
}

const user = userToToken(foundUser);
const token = sign(user, getJWTSecret(), { expiresIn: '2h' });
const token = sign(user);
const expiresAt = addHours(2);

await addToken(foundUser.userID, token);

res.cookie('CK_SESSION', token, {
httpOnly: true,
domain: process.env.APP_DOMAIN || 'localhost',
expires: expiresAt,
secure: true,
});
res.status(200).send({ token, user, expiresAt });
});

Expand All @@ -52,12 +60,35 @@ router.post('/register', async (req, res) => {
const savedUser = await dalUser.create(body);

const user = userToToken(savedUser);
const token = sign(user, getJWTSecret(), { expiresIn: '2h' });
const token = sign(user);
const expiresAt = addHours(2);

await addToken(savedUser.userID, token);

res.cookie('CK_SESSION', token, {
httpOnly: true,
domain: process.env.APP_DOMAIN || 'localhost',
expires: expiresAt,
secure: true,
});
res.status(200).send({ token, user, expiresAt });
});

router.post('/logout', isAuthenticated, async (req, res) => {
if (!req.headers.authorization) {
return res.status(400).end('No authorization header found!');
}

const token = req.headers.authorization.replace('Bearer ', '');
await destroyToken(res.locals.user.userID, token);

if (req.query.score) {
await logoutSCORE(req);
}

res.status(200).end();
});

router.post('/multiple', async (req, res) => {
const ids = req.body;
const users = await dalUser.findByUserIDs(ids);
Expand All @@ -71,12 +102,14 @@ router.get('/is-sso-enabled', async (req, res) => {
});

router.get('/sso/handshake', async (req, res) => {
const scoreSsoEndpoint = process.env.SCORE_SSO_ENDPOINT;
const payload = await generateSsoPayload();
const hashedPayload = generateHashedSsoPayload(payload);
if (!scoreSsoEndpoint) {
const ssoEndpoint = process.env.SCORE_SSO_ENDPOINT;
const scoreAddress = process.env.SCORE_SERVER_ADDRESS || 'http://localhost';
if (!ssoEndpoint) {
throw new Error('No SCORE SSO endpoint environment variable defined!');
}
const scoreSsoEndpoint = `${scoreAddress + ssoEndpoint}`;
const payload = await generateSsoPayload();
const hashedPayload = generateHashedSsoPayload(payload);
res.status(200).send({
scoreSsoEndpoint: scoreSsoEndpoint,
sig: hashedPayload,
Expand Down
26 changes: 25 additions & 1 deletion backend/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import express from 'express';
import http from 'http';
import bodyParser from 'body-parser';
import cookieParser from 'cookie-parser';
import cors from 'cors';
import mongoose from 'mongoose';
import dotenv from 'dotenv';
Expand All @@ -18,20 +19,43 @@ import trace from './api/trace';
import groups from './api/groups';
import todoItems from './api/todoItem';
import { isAuthenticated } from './utils/auth';
import redis from './utils/redis';
dotenv.config();

const port = process.env.PORT || 8001;
const ckAddr = process.env.CKBOARD_SERVER_ADDRESS || 'http://localhost:4201';
const scoreAddr = process.env.SCORE_SERVER_ADDRESS || 'http://localhost';
const dbUsername = process.env.DB_USER;
const dbPassword = process.env.DB_PASSWORD;
const dbUrl = process.env.DB_URL;
const dbName = process.env.DB_NAME;
const dbURI = `mongodb+srv://${dbUsername}:${dbPassword}@${dbUrl}.mongodb.net/${dbName}?retryWrites=true&w=majority`;

const app = express();
app.use(cors());
app.use(cookieParser());
app.use(
cors({
credentials: true,
origin: (origin, callback) => {
if (!origin) return callback(null, true);

if (origin != ckAddr && origin != scoreAddr) {
const msg = `This site ${origin} does not have an access. Only specific domains are allowed to access it.`;
return callback(new Error(msg), false);
}

return callback(null, true);
},
})
);
app.use(bodyParser.json());
const server = http.createServer(app);

(async () => {
await redis.connect();
return redis;
})();

const socket = Socket.Instance;
socket.init();

Expand Down
3 changes: 2 additions & 1 deletion backend/src/socket/events/post.events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@ class PostCommentRemove {
): Promise<object> {
const comment = input.eventData;
const commentAmount = await dalComment.getAmountByPost(comment.postID);
await postTrace.commentRemove(input, this.type);
if (input.trace.allowTracing)
await postTrace.commentRemove(input, this.type);

WorkflowManager.Instance.updateTask(
comment.userID,
Expand Down
43 changes: 33 additions & 10 deletions backend/src/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextFunction, Request, Response } from 'express';
import { sign, verify } from 'jsonwebtoken';
import axios from 'axios';
import { Role, UserModel } from '../models/User';
import { v4 as uuidv4 } from 'uuid';
import hmacSHA256 from 'crypto-js/hmac-sha256';
Expand All @@ -11,6 +11,7 @@ import { ProjectModel } from '../models/Project';
import { NotFoundError } from '../errors/client.errors';
import { addUserToProject } from './project.helpers';
import { ApplicationError } from '../errors/base.errors';
import { addToken, checkToken, sign, verify } from './jwt';

export interface Token {
email: string;
Expand All @@ -19,11 +20,6 @@ export interface Token {
role: string;
}

export const addHours = (numOfHours: number, date = new Date()) => {
date.setTime(date.getTime() + numOfHours * 60 * 60 * 1000);
return date;
};

export const getJWTSecret = (): string => {
const secret = process.env.JWT_SECRET;

Expand All @@ -34,6 +30,11 @@ export const getJWTSecret = (): string => {
return secret;
};

export const addHours = (numOfHours: number, date = new Date()) => {
date.setTime(date.getTime() + numOfHours * 60 * 60 * 1000);
return date;
};

export const userToToken = (user: UserModel): Token => {
return {
email: user.email,
Expand All @@ -54,11 +55,15 @@ export const isAuthenticated = async (
}

const token = req.headers.authorization.replace('Bearer ', '');
res.locals.user = verify(token, getJWTSecret()) as Token;
res.locals.user = verify(token);

const cachedToken = await checkToken(res.locals.user.userID, token);
if (cachedToken == null || cachedToken == 'invalid' || cachedToken == 'nil')
return res.status(401).end('Invalid token!');

next();
} catch (e) {
return res.status(403).end('Unable to authenticate!');
return res.status(401).end('Unable to authenticate!');
}
};

Expand Down Expand Up @@ -216,14 +221,32 @@ export const signInUserWithSso = async (
return res.status(500).end('Internal Server Error');
}
}
const sessionToken = generateSessionToken(userModel);
const sessionToken = await generateSessionToken(userModel);
sessionToken.redirectUrl = redirectUrl;
await addToken(sessionToken.user.userID, sessionToken.token);

res.cookie('CK_SESSION', sessionToken.token, {
httpOnly: true,
domain: process.env.APP_DOMAIN || 'localhost',
expires: sessionToken.expiresAt,
secure: true,
});
return res.status(200).send(sessionToken);
};

export const generateSessionToken = (userModel: UserModel): any => {
const user = userToToken(userModel);
const token = sign(user, getJWTSecret(), { expiresIn: '2h' });
const token = sign(user);
const expiresAt = addHours(2);
return { token, user, expiresAt };
};

export const logoutSCORE = async (req: Request) => {
const scoreAddress = process.env.SCORE_SERVER_ADDRESS || 'http://localhost';
return await axios.get(
`${scoreAddress + process.env.SCORE_LOGOUT_ENDPOINT}`,
{
headers: { Cookie: `SESSION=${req.cookies['SESSION']};` },
}
);
};
39 changes: 39 additions & 0 deletions backend/src/utils/jwt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import redis from './redis';
import jwt from 'jsonwebtoken';
import { Token } from './auth';

export const sign = (payload: Token, date = new Date()) => {
return jwt.sign(payload, 'secret', {
expiresIn: date.setTime(date.getTime() + 2 * 60 * 60 * 1000),
});
};

export const verify = (token: string): Token => {
return jwt.verify(token, 'secret') as Token;
};

export const addToken = async (
id: string,
token: string,
date = new Date()
) => {
const key = `${id}_${token}`;
const check = await redis.EXISTS(key); // check if key exists in cache
if (check == 1) return;

await redis.SET(key, 'valid'); // set key value to be 'valid'
await redis.EXPIREAT(key, date.setTime(date.getTime() + 2 * 60 * 60 * 1000)); // set expiry date for the key in the cache
return;
};

export const checkToken = async (id: string, token: string) => {
const key = `${id}_${token}`;
const status = redis.GET(key); // get the token from the cache and return its value
return status;
};

export const destroyToken = async (id: string, token: string) => {
const key = `${id}_${token}`;
await redis.DEL(key); // deletes token from cache
return;
};
35 changes: 35 additions & 0 deletions backend/src/utils/redis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createClient } from 'redis';

class Redis {
client: any;
connected: boolean;

constructor() {
this.client = null;
this.connected = false;
}

getConnection() {
if (this.connected) return this.client;

this.client = createClient();
console.log(this.client);

this.client.on('connect', () => {
console.log('Client connected to Redis...');
});
this.client.on('ready', () => {
console.log('Redis ready to use');
});
this.client.on('error', (err: string) => {
console.error('Redis Client', err);
});
this.client.on('end', () => {
console.log('Redis disconnected successfully');
});

return this.client;
}
}

export default new Redis().getConnection();
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<button
mat-icon-button
[matMenuTriggerFor]="addMenu"
*ngIf="user && user.role == Role.TEACHER"
*ngIf="user && user.role === Role.TEACHER"
matTooltip="Create Project or Board"
>
<mat-icon>add</mat-icon>
Expand All @@ -17,7 +17,7 @@
<button
mat-menu-item
(click)="openCreateBoardDialog()"
*ngIf="yourProjects.length > 0"
*ngIf="yourProjects && yourProjects.length > 0"
>
Create Board
</button>
Expand Down
Loading