Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Ignore all node_modules folders
node_modules
/.vscode/launch.json
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,18 +2,20 @@
"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",
"mongo-dot-notation": "^3.1.0",
"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 @@ -28,10 +30,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
78 changes: 77 additions & 1 deletion backend/src/api/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Router } from 'express';
import { mongo } from 'mongoose';
import { BoardScope } from '../models/Board';
import { ProjectModel } from '../models/Project';
import { UserModel } from '../models/User';
import { Role, UserModel } from '../models/User';
import dalBoard from '../repository/dalBoard';
import dalProject from '../repository/dalProject';
import {
Expand All @@ -12,6 +12,14 @@ import {
import { BoardType } from '../models/Board';
import { ApplicationError } from '../errors/base.errors';
import { addUserToProject } from '../utils/project.helpers';
import {
createUserIfNecessary,
getOrCreateUser,
getParamMap,
getRole,
} from '../utils/auth';
import dalUser from '../repository/dalUser';
import { SCORE_DOMAIN } from '../constants';

const router = Router();

Expand Down Expand Up @@ -78,6 +86,74 @@ router.post('/:id', async (req, res) => {
res.status(200).json(updatedProject);
});

router.post('/score/link', async (req, res) => {
const { runId, code } = req.body;
const result: any = {
message: 'Code does not exist or has already been used',
};
const linkedRunId = Number(runId);

let project = await dalProject.getByConnectCode(code);
if (project) {
if (!isNaN(linkedRunId) && project.linkedRunId == 0) {
project = await dalProject.update(project.projectID, { linkedRunId });
result.code = project?.scoreJoinCode;
result.message =
'Successfully linked Run to an available CK Board project';
}
}

res.status(200).json(result);
});

router.post('/score/unlink', async (req, res) => {
const { runId, code } = req.body;
const result: any = {
message: 'Could not unlink due to invalid code or run id',
};
const linkedRunId = Number(runId);

let project = await dalProject.getByConnectCode(code);
if (project) {
if (!isNaN(linkedRunId) && project.linkedRunId == linkedRunId) {
project = await dalProject.update(project.projectID, { linkedRunId: 0 });
result.code = project?.scoreJoinCode;
result.message = 'Successfully unlinked Run from CK Board project';
}
}

res.status(200).json(result);
});

router.post('/score/addMember', async (req, res) => {
const { code, username, role } = req.body;
let project = await dalProject.getByConnectCode(code);
if (project && !project.membershipDisabled) {
const email = `${username}@${SCORE_DOMAIN}`;
const user = await createUserIfNecessary(email, username, getRole(role));
if (user?.role == Role.STUDENT) {
dalProject.addStudent(project.studentJoinCode, user.userID);
} else if (user?.role == Role.TEACHER) {
dalProject.addTeacher(project.teacherJoinCode, user.userID);
}
}
res.status(200).send();
});

router.post('/score/removeMember', async (req, res) => {
const { username, code } = req.body;
let project = await dalProject.getByConnectCode(code);
if (project) {
const user = await dalUser.findByUsername(username);
if (user?.role == Role.STUDENT) {
dalProject.removeStudent(project.studentJoinCode, user.userID);
} else if (user?.role == Role.TEACHER) {
dalProject.removeTeacher(project.teacherJoinCode, user.userID);
}
}
res.status(200).send();
});

router.get('/:id', async (req, res) => {
const id = req.params.id;

Expand Down
2 changes: 2 additions & 0 deletions backend/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,5 @@ export const DEFAULT_TAGS: Partial<TagModel>[] = [
QUESTION_TAG,
NEEDS_ATTENTION_TAG,
];

export const SCORE_DOMAIN = 'score.oise.utoronto.ca';
6 changes: 6 additions & 0 deletions backend/src/models/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ export class ProjectModel {
@prop({ required: true })
public teacherJoinCode!: string;

@prop({ required: false })
public scoreJoinCode!: string;

@prop({ required: false })
public linkedRunId!: number;

@prop({ required: true, type: () => PersonalBoardSetting })
public personalBoardSetting!: PersonalBoardSetting;

Expand Down
44 changes: 42 additions & 2 deletions backend/src/repository/dalProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,59 @@ export const getByJoinCode = async (code: string, role: Role) => {
}
};

export const getByConnectCode = async (code: string) => {
try {
return await Project.findOne({ scoreJoinCode: code });
} catch (err) {
throw new Error(JSON.stringify(err, null, ' '));
}
};

export const addStudent = async (code: string, userID: string) => {
const project = await Project.findOne({ studentJoinCode: code });
if (!project) {
throw new UnauthorizedError('Invalid Join Code!');
}
await project.updateOne({ $push: { members: userID } });
if (!project.members.includes(userID)) {
await project.updateOne({ $push: { members: userID } });
}

return project;
};

export const removeStudent = async (code: string, userID: string) => {
const project = await Project.findOne({ studentJoinCode: code });
if (!project) {
throw new UnauthorizedError('Invalid Join Code!');
}
if (project.members.includes(userID)) {
await project.updateOne({ $pull: { members: userID } });
}

return project;
``;
};

export const addTeacher = async (code: string, userID: string) => {
const project = await Project.findOne({ teacherJoinCode: code });
if (!project) {
throw new UnauthorizedError('Invalid Join Code!');
}
await project.updateOne({ $push: { teacherIDs: userID, members: userID } });
if (!project.members.includes(userID)) {
await project.updateOne({ $push: { teacherIDs: userID, members: userID } });
}

return project;
};

export const removeTeacher = async (code: string, userID: string) => {
const project = await Project.findOne({ teacherJoinCode: code });
if (!project) {
throw new UnauthorizedError('Invalid Join Code!');
}
if (project.members.includes(userID)) {
await project.updateOne({ $pull: { teacherIDs: userID, members: userID } });
}

return project;
};
Expand Down Expand Up @@ -112,9 +149,12 @@ const dalProject = {
getById,
getByUserId,
getByJoinCode,
getByConnectCode,
create,
addStudent,
removeStudent,
addTeacher,
removeTeacher,
update,
removeBoard,
remove,
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
Loading