From 8063e97645fde1995da9a7a291961cb918ead797 Mon Sep 17 00:00:00 2001 From: Ammar Date: Wed, 9 Nov 2022 16:21:49 -0500 Subject: [PATCH 01/10] Add redis with jwt --- backend/package-lock.json | 187 +++++++++++++++++++++- backend/package.json | 4 +- backend/src/api/auth.ts | 17 +- backend/src/server.ts | 2 +- backend/src/utils/auth.ts | 52 +++++- frontend/src/app/services/user.service.ts | 14 +- 6 files changed, 260 insertions(+), 16 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 41d946d9..db111810 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -18,10 +18,11 @@ "dotenv": "^16.0.0", "express": "^4.17.3", "express-jwt": "^7.7.0", - "jsonwebtoken": "^8.5.1", + "jwt-redis": "^7.0.3", "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" }, @@ -30,6 +31,7 @@ "@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", @@ -254,6 +256,59 @@ "node": ">= 8" } }, + "node_modules/@redis/bloom": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.0.2.tgz", + "integrity": "sha512-EBw7Ag1hPgFzdznK2PBblc1kdlj5B5Cw3XwI9/oG7tSn85/HKy3X9xHy/8tm/eNXJYHLXHJL/pkwBpFMVVefkw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.2.0.tgz", + "integrity": "sha512-a8Nlw5fv2EIAFJxTDSSDVUT7yfBGpZO96ybZXzQpgkyLg/dxtQ1uiwTc0EGfzg1mrPjZokeBSEGTbGXekqTNOg==", + "dependencies": { + "cluster-key-slot": "1.1.0", + "generic-pool": "3.8.2", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/graph": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.0.1.tgz", + "integrity": "sha512-oDE4myMCJOCVKYMygEMWuriBgqlS5FqdWerikMoJxzmmTUErnTRRgmIDa2VcgytACZMFqpAOWDzops4DOlnkfQ==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.3.tgz", + "integrity": "sha512-4X0Qv0BzD9Zlb0edkUoau5c1bInWSICqXAGrpwEltkncUwcxJIGEcVryZhLgb0p/3PkKaLIWkjhHRtLe9yiA7Q==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.0.6.tgz", + "integrity": "sha512-pP+ZQRis5P21SD6fjyCeLcQdps+LuTzp2wdUbzxEmNhleighDDTD5ck8+cYof+WLec4csZX7ks+BuoMw0RaZrA==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.3.tgz", + "integrity": "sha512-OFp0q4SGrTH0Mruf6oFsHGea58u8vS/iI5+NpYdicaM+7BgqBZH8FFvNZ8rYYLrUO/QRqMq72NpXmxLVNcdmjA==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, "node_modules/@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -438,6 +493,16 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true }, + "node_modules/@types/redis": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@types/redis/-/redis-4.0.11.tgz", + "integrity": "sha512-bI+gth8La8Wg/QCR1+V1fhrL9+LZUSWfcqpOj2Kc80ZQ4ffbdL173vQd5wovmoV9i071FU9oP2g6etLuEwb6Rg==", + "deprecated": "This is a stub types definition. redis provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "redis": "*" + } + }, "node_modules/@types/serve-static": { "version": "1.13.10", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", @@ -1364,6 +1429,14 @@ "mimic-response": "^1.0.0" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz", + "integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2277,6 +2350,14 @@ "node": ">=10" } }, + "node_modules/generic-pool": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.8.2.tgz", + "integrity": "sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==", + "engines": { + "node": ">= 4" + } + }, "node_modules/get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -2814,6 +2895,17 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/jwt-redis": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/jwt-redis/-/jwt-redis-7.0.3.tgz", + "integrity": "sha512-FEpX10Y6FBd1n7Ty0F9nPecrdDn0MSG2YWezap/xImwh+1sg6Du3pJcCuYd1ZMe8SdRST+Z4xtFbse1lPwT+Tg==", + "dependencies": { + "jsonwebtoken": "^8.5.1" + }, + "peerDependencies": { + "redis": "^4.0.6" + } + }, "node_modules/kareem": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.5.tgz", @@ -3682,6 +3774,19 @@ "node": ">=8.10.0" } }, + "node_modules/redis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.2.0.tgz", + "integrity": "sha512-bCR0gKVhIXFg8zCQjXEANzgI01DDixtPZgIUZHBCmwqixnu+MK3Tb2yqGjh+HCLASQVVgApiwhNkv+FoedZOGQ==", + "dependencies": { + "@redis/bloom": "1.0.2", + "@redis/client": "1.2.0", + "@redis/graph": "1.0.1", + "@redis/json": "1.0.3", + "@redis/search": "1.0.6", + "@redis/time-series": "1.0.3" + } + }, "node_modules/reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", @@ -4882,6 +4987,46 @@ "fastq": "^1.6.0" } }, + "@redis/bloom": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.0.2.tgz", + "integrity": "sha512-EBw7Ag1hPgFzdznK2PBblc1kdlj5B5Cw3XwI9/oG7tSn85/HKy3X9xHy/8tm/eNXJYHLXHJL/pkwBpFMVVefkw==", + "requires": {} + }, + "@redis/client": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.2.0.tgz", + "integrity": "sha512-a8Nlw5fv2EIAFJxTDSSDVUT7yfBGpZO96ybZXzQpgkyLg/dxtQ1uiwTc0EGfzg1mrPjZokeBSEGTbGXekqTNOg==", + "requires": { + "cluster-key-slot": "1.1.0", + "generic-pool": "3.8.2", + "yallist": "4.0.0" + } + }, + "@redis/graph": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.0.1.tgz", + "integrity": "sha512-oDE4myMCJOCVKYMygEMWuriBgqlS5FqdWerikMoJxzmmTUErnTRRgmIDa2VcgytACZMFqpAOWDzops4DOlnkfQ==", + "requires": {} + }, + "@redis/json": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.3.tgz", + "integrity": "sha512-4X0Qv0BzD9Zlb0edkUoau5c1bInWSICqXAGrpwEltkncUwcxJIGEcVryZhLgb0p/3PkKaLIWkjhHRtLe9yiA7Q==", + "requires": {} + }, + "@redis/search": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.0.6.tgz", + "integrity": "sha512-pP+ZQRis5P21SD6fjyCeLcQdps+LuTzp2wdUbzxEmNhleighDDTD5ck8+cYof+WLec4csZX7ks+BuoMw0RaZrA==", + "requires": {} + }, + "@redis/time-series": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.3.tgz", + "integrity": "sha512-OFp0q4SGrTH0Mruf6oFsHGea58u8vS/iI5+NpYdicaM+7BgqBZH8FFvNZ8rYYLrUO/QRqMq72NpXmxLVNcdmjA==", + "requires": {} + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -5050,6 +5195,15 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true }, + "@types/redis": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@types/redis/-/redis-4.0.11.tgz", + "integrity": "sha512-bI+gth8La8Wg/QCR1+V1fhrL9+LZUSWfcqpOj2Kc80ZQ4ffbdL173vQd5wovmoV9i071FU9oP2g6etLuEwb6Rg==", + "dev": true, + "requires": { + "redis": "*" + } + }, "@types/serve-static": { "version": "1.13.10", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", @@ -5677,6 +5831,11 @@ "mimic-response": "^1.0.0" } }, + "cluster-key-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz", + "integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==" + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -6395,6 +6554,11 @@ "wide-align": "^1.1.2" } }, + "generic-pool": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.8.2.tgz", + "integrity": "sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==" + }, "get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -6787,6 +6951,14 @@ "safe-buffer": "^5.0.1" } }, + "jwt-redis": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/jwt-redis/-/jwt-redis-7.0.3.tgz", + "integrity": "sha512-FEpX10Y6FBd1n7Ty0F9nPecrdDn0MSG2YWezap/xImwh+1sg6Du3pJcCuYd1ZMe8SdRST+Z4xtFbse1lPwT+Tg==", + "requires": { + "jsonwebtoken": "^8.5.1" + } + }, "kareem": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.5.tgz", @@ -7428,6 +7600,19 @@ "picomatch": "^2.2.1" } }, + "redis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.2.0.tgz", + "integrity": "sha512-bCR0gKVhIXFg8zCQjXEANzgI01DDixtPZgIUZHBCmwqixnu+MK3Tb2yqGjh+HCLASQVVgApiwhNkv+FoedZOGQ==", + "requires": { + "@redis/bloom": "1.0.2", + "@redis/client": "1.2.0", + "@redis/graph": "1.0.1", + "@redis/json": "1.0.3", + "@redis/search": "1.0.6", + "@redis/time-series": "1.0.3" + } + }, "reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", diff --git a/backend/package.json b/backend/package.json index b3f3efba..205a0637 100644 --- a/backend/package.json +++ b/backend/package.json @@ -9,10 +9,11 @@ "dotenv": "^16.0.0", "express": "^4.17.3", "express-jwt": "^7.7.0", - "jsonwebtoken": "^8.5.1", + "jwt-redis": "^7.0.3", "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" }, @@ -31,6 +32,7 @@ "@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", diff --git a/backend/src/api/auth.ts b/backend/src/api/auth.ts index 7af086db..0c19d708 100644 --- a/backend/src/api/auth.ts +++ b/backend/src/api/auth.ts @@ -1,5 +1,4 @@ import { Router } from 'express'; -import { sign } from 'jsonwebtoken'; import bcrypt from 'bcrypt'; import dalUser from '../repository/dalUser'; import { @@ -12,6 +11,7 @@ import { isCorrectHashedSsoPayload, isSsoEnabled, isValidNonce, + JWT, signInUserWithSso, userToToken, } from '../utils/auth'; @@ -37,7 +37,7 @@ router.post('/login', async (req, res) => { } const user = userToToken(foundUser); - const token = sign(user, getJWTSecret(), { expiresIn: '2h' }); + const token = await JWT.Instance.sign(foundUser); const expiresAt = addHours(2); res.status(200).send({ token, user, expiresAt }); @@ -52,12 +52,23 @@ 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 = await JWT.Instance.sign(savedUser); const expiresAt = addHours(2); 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 JWT.Instance.destroy(token); + + res.status(200).send({ token }); +}); + router.post('/multiple', async (req, res) => { const ids = req.body; const users = await dalUser.findByUserIDs(ids); diff --git a/backend/src/server.ts b/backend/src/server.ts index d17e4a74..24bb135d 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -16,7 +16,7 @@ import workflows from './api/workflows'; import auth from './api/auth'; import trace from './api/trace'; import groups from './api/groups'; -import { isAuthenticated } from './utils/auth'; +import { isAuthenticated, JWT } from './utils/auth'; dotenv.config(); const port = process.env.PORT || 8001; diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index 4683ce26..be6c6036 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -1,5 +1,5 @@ import { NextFunction, Request, Response } from 'express'; -import { sign, verify } from 'jsonwebtoken'; +import JWTR from 'jwt-redis'; import { Role, UserModel } from '../models/User'; import { v4 as uuidv4 } from 'uuid'; import hmacSHA256 from 'crypto-js/hmac-sha256'; @@ -9,6 +9,9 @@ import dalNonce from '../repository/dalNonce'; import dalProject from '../repository/dalProject'; import { ProjectModel } from '../models/Project'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const redis = require('redis'); + export interface Token { email: string; username: string; @@ -16,6 +19,41 @@ export interface Token { role: string; } +export class JWT { + private static _instance: JWT; + + private _jwtr: JWTR | null = null; + + private constructor() { + const redisClient = redis.createClient(); + redisClient.on('connect', () => { + this._jwtr = new JWTR(redisClient); + }); + } + + public async sign(userModel: UserModel) { + const user = userToToken(userModel); + const token = await this._jwtr?.sign(user, getJWTSecret(), { + expiresIn: '2h', + }); + const expiresAt = addHours(2); + + return { user, token, expiresAt }; + } + + public async verify(token: string) { + return (await this._jwtr?.verify(token, getJWTSecret())) as Token; + } + + public async destroy(token: string) { + return await this._jwtr?.destroy(token); + } + + public static get Instance() { + return this._instance || (this._instance = new this()); + } +} + export const addHours = (numOfHours: number, date = new Date()) => { date.setTime(date.getTime() + numOfHours * 60 * 60 * 1000); return date; @@ -51,7 +89,7 @@ export const isAuthenticated = async ( } const token = req.headers.authorization.replace('Bearer ', ''); - res.locals.user = verify(token, getJWTSecret()) as Token; + res.locals.user = (await JWT.Instance.verify(token)) as Token; next(); } catch (e) { @@ -159,7 +197,7 @@ export const createUser = async ( } }; -export const getRole = (role: string = ''): Role => { +export const getRole = (role = ''): Role => { if (role.toUpperCase() === Role.TEACHER) { return Role.TEACHER; } else { @@ -207,14 +245,16 @@ export const signInUserWithSso = async ( if (projectID != null) { await joinProjectIfNecessary(userModel, projectID); } - const sessionToken = generateSessionToken(userModel); + const sessionToken = await generateSessionToken(userModel); sessionToken.redirectUrl = redirectUrl; return res.status(200).send(sessionToken); }; -export const generateSessionToken = (userModel: UserModel): any => { +export const generateSessionToken = async ( + userModel: UserModel +): Promise => { const user = userToToken(userModel); - const token = sign(user, getJWTSecret(), { expiresIn: '2h' }); + const token = await JWT.Instance.sign(userModel); const expiresAt = addHours(2); return { token, user, expiresAt }; }; diff --git a/frontend/src/app/services/user.service.ts b/frontend/src/app/services/user.service.ts index 873bcd86..c511e847 100644 --- a/frontend/src/app/services/user.service.ts +++ b/frontend/src/app/services/user.service.ts @@ -77,10 +77,16 @@ export class UserService { }); } - logout() { - localStorage.removeItem('user'); - localStorage.removeItem('access_token'); - localStorage.removeItem('expires_at'); + async logout(): Promise { + return this.http + .post('auth/logout', {}) + .toPromise() + .then(() => { + localStorage.removeItem('user'); + localStorage.removeItem('access_token'); + localStorage.removeItem('expires_at'); + return true; + }); } update(id: string, user: Partial) { From ed9da7a8924e4570d4f7f809da47c88d99b5491e Mon Sep 17 00:00:00 2001 From: Ammar Date: Wed, 23 Nov 2022 20:10:13 -0500 Subject: [PATCH 02/10] Implement JWT caching via redis --- .gitignore | 3 +- backend/package-lock.json | 85 ++++--------------- backend/package.json | 3 +- backend/src/api/auth.ts | 13 +-- backend/src/server.ts | 6 ++ backend/src/utils/auth.ts | 64 +++----------- backend/src/utils/jwt.ts | 39 +++++++++ backend/src/utils/redis.ts | 35 ++++++++ .../dashboard/dashboard.component.html | 4 +- .../app/components/login/login.component.ts | 10 ++- frontend/src/app/services/user.service.ts | 28 +++--- frontend/src/app/utils/interceptor.ts | 18 +++- 12 files changed, 160 insertions(+), 148 deletions(-) create mode 100644 backend/src/utils/jwt.ts create mode 100644 backend/src/utils/redis.ts diff --git a/.gitignore b/.gitignore index 6922cfc4..5a0793bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ # Ignore all node_modules folders -node_modules \ No newline at end of file +node_modules +dump.rdb diff --git a/backend/package-lock.json b/backend/package-lock.json index db111810..514c42db 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -17,8 +17,7 @@ "crypto-js": "^4.1.1", "dotenv": "^16.0.0", "express": "^4.17.3", - "express-jwt": "^7.7.0", - "jwt-redis": "^7.0.3", + "jsonwebtoken": "^8.5.1", "mongoose": "^6.3.2", "nodemon": "^2.0.16", "prettier": "^2.6.2", @@ -1250,7 +1249,7 @@ "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, "node_modules/buffer-from": { "version": "1.1.2", @@ -2107,23 +2106,6 @@ "node": ">= 0.10.0" } }, - "node_modules/express-jwt": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-7.7.0.tgz", - "integrity": "sha512-4W1N/wD1buyg5F1/IFi4hWMRwerHd1wl17PhfBrNnzMAb1MbAVDJiSoNByVrz2+NGThChIC+vuNaDwVLprkQzQ==", - "dependencies": { - "express-unless": "^1.0.0", - "jsonwebtoken": "^8.5.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/express-unless": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-1.0.0.tgz", - "integrity": "sha512-zXSSClWBPfcSYjg0hcQNompkFN/MxQQ53eyrzm9BYgik2ut2I7PxAf2foVqBRMYCwWaZx/aWodi+uk76npdSAw==" - }, "node_modules/express/node_modules/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", @@ -2895,17 +2877,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/jwt-redis": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/jwt-redis/-/jwt-redis-7.0.3.tgz", - "integrity": "sha512-FEpX10Y6FBd1n7Ty0F9nPecrdDn0MSG2YWezap/xImwh+1sg6Du3pJcCuYd1ZMe8SdRST+Z4xtFbse1lPwT+Tg==", - "dependencies": { - "jsonwebtoken": "^8.5.1" - }, - "peerDependencies": { - "redis": "^4.0.6" - } - }, "node_modules/kareem": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.5.tgz", @@ -2951,32 +2922,32 @@ "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" }, "node_modules/lodash.isnumber": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" }, "node_modules/lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -2987,7 +2958,7 @@ "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, "node_modules/loglevel": { "version": "1.8.0", @@ -5707,7 +5678,7 @@ "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, "buffer-from": { "version": "1.1.2", @@ -6386,20 +6357,6 @@ } } }, - "express-jwt": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-7.7.0.tgz", - "integrity": "sha512-4W1N/wD1buyg5F1/IFi4hWMRwerHd1wl17PhfBrNnzMAb1MbAVDJiSoNByVrz2+NGThChIC+vuNaDwVLprkQzQ==", - "requires": { - "express-unless": "^1.0.0", - "jsonwebtoken": "^8.5.1" - } - }, - "express-unless": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-1.0.0.tgz", - "integrity": "sha512-zXSSClWBPfcSYjg0hcQNompkFN/MxQQ53eyrzm9BYgik2ut2I7PxAf2foVqBRMYCwWaZx/aWodi+uk76npdSAw==" - }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6951,14 +6908,6 @@ "safe-buffer": "^5.0.1" } }, - "jwt-redis": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/jwt-redis/-/jwt-redis-7.0.3.tgz", - "integrity": "sha512-FEpX10Y6FBd1n7Ty0F9nPecrdDn0MSG2YWezap/xImwh+1sg6Du3pJcCuYd1ZMe8SdRST+Z4xtFbse1lPwT+Tg==", - "requires": { - "jsonwebtoken": "^8.5.1" - } - }, "kareem": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.5.tgz", @@ -6998,32 +6947,32 @@ "lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" }, "lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" }, "lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" }, "lodash.isnumber": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" }, "lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" }, "lodash.merge": { "version": "4.6.2", @@ -7034,7 +6983,7 @@ "lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, "loglevel": { "version": "1.8.0", diff --git a/backend/package.json b/backend/package.json index 205a0637..86201645 100644 --- a/backend/package.json +++ b/backend/package.json @@ -8,8 +8,7 @@ "crypto-js": "^4.1.1", "dotenv": "^16.0.0", "express": "^4.17.3", - "express-jwt": "^7.7.0", - "jwt-redis": "^7.0.3", + "jsonwebtoken": "^8.5.1", "mongoose": "^6.3.2", "nodemon": "^2.0.16", "prettier": "^2.6.2", diff --git a/backend/src/api/auth.ts b/backend/src/api/auth.ts index 0c19d708..22e4279e 100644 --- a/backend/src/api/auth.ts +++ b/backend/src/api/auth.ts @@ -5,17 +5,16 @@ import { addHours, generateHashedSsoPayload, generateSsoPayload, - getJWTSecret, getParamMap, isAuthenticated, isCorrectHashedSsoPayload, isSsoEnabled, isValidNonce, - JWT, signInUserWithSso, userToToken, } from '../utils/auth'; import { UserModel } from '../models/User'; +import { addToken, destroyToken, sign } from '../utils/jwt'; const router = Router(); @@ -37,9 +36,10 @@ router.post('/login', async (req, res) => { } const user = userToToken(foundUser); - const token = await JWT.Instance.sign(foundUser); + const token = sign(user); const expiresAt = addHours(2); + await addToken(foundUser.userID, token); res.status(200).send({ token, user, expiresAt }); }); @@ -52,9 +52,10 @@ router.post('/register', async (req, res) => { const savedUser = await dalUser.create(body); const user = userToToken(savedUser); - const token = await JWT.Instance.sign(savedUser); + const token = sign(user); const expiresAt = addHours(2); + await addToken(savedUser.userID, token); res.status(200).send({ token, user, expiresAt }); }); @@ -64,9 +65,9 @@ router.post('/logout', isAuthenticated, async (req, res) => { } const token = req.headers.authorization.replace('Bearer ', ''); - await JWT.Instance.destroy(token); + await destroyToken(res.locals.user.userID, token); - res.status(200).send({ token }); + res.status(200).end(); }); router.post('/multiple', async (req, res) => { diff --git a/backend/src/server.ts b/backend/src/server.ts index 2744b6a7..c685a38d 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -18,6 +18,7 @@ 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; @@ -32,6 +33,11 @@ app.use(cors()); app.use(bodyParser.json()); const server = http.createServer(app); +(async () => { + await redis.connect(); + return redis; +})(); + const socket = Socket.Instance; socket.init(); diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index 2e02a913..81c30cdb 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -1,5 +1,4 @@ import { NextFunction, Request, Response } from 'express'; -import JWTR from 'jwt-redis'; import { Role, UserModel } from '../models/User'; import { v4 as uuidv4 } from 'uuid'; import hmacSHA256 from 'crypto-js/hmac-sha256'; @@ -11,9 +10,7 @@ import { ProjectModel } from '../models/Project'; import { NotFoundError } from '../errors/client.errors'; import { addUserToProject } from './project.helpers'; import { ApplicationError } from '../errors/base.errors'; - -// eslint-disable-next-line @typescript-eslint/no-var-requires -const redis = require('redis'); +import { checkToken, sign, verify } from './jwt'; export interface Token { email: string; @@ -22,46 +19,6 @@ export interface Token { role: string; } -export class JWT { - private static _instance: JWT; - - private _jwtr: JWTR | null = null; - - private constructor() { - const redisClient = redis.createClient(); - redisClient.on('connect', () => { - this._jwtr = new JWTR(redisClient); - }); - } - - public async sign(userModel: UserModel) { - const user = userToToken(userModel); - const token = await this._jwtr?.sign(user, getJWTSecret(), { - expiresIn: '2h', - }); - const expiresAt = addHours(2); - - return { user, token, expiresAt }; - } - - public async verify(token: string) { - return (await this._jwtr?.verify(token, getJWTSecret())) as Token; - } - - public async destroy(token: string) { - return await this._jwtr?.destroy(token); - } - - public static get Instance() { - return this._instance || (this._instance = new this()); - } -} - -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; @@ -72,6 +29,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, @@ -92,11 +54,15 @@ export const isAuthenticated = async ( } const token = req.headers.authorization.replace('Bearer ', ''); - res.locals.user = (await JWT.Instance.verify(token)) 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!'); } }; @@ -259,11 +225,9 @@ export const signInUserWithSso = async ( return res.status(200).send(sessionToken); }; -export const generateSessionToken = async ( - userModel: UserModel -): Promise => { +export const generateSessionToken = (userModel: UserModel): any => { const user = userToToken(userModel); - const token = await JWT.Instance.sign(userModel); + const token = sign(userModel); const expiresAt = addHours(2); return { token, user, expiresAt }; }; diff --git a/backend/src/utils/jwt.ts b/backend/src/utils/jwt.ts new file mode 100644 index 00000000..1665d3d2 --- /dev/null +++ b/backend/src/utils/jwt.ts @@ -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; +}; diff --git a/backend/src/utils/redis.ts b/backend/src/utils/redis.ts new file mode 100644 index 00000000..045809ce --- /dev/null +++ b/backend/src/utils/redis.ts @@ -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(); diff --git a/frontend/src/app/components/dashboard/dashboard.component.html b/frontend/src/app/components/dashboard/dashboard.component.html index 75537536..7d6ef99e 100644 --- a/frontend/src/app/components/dashboard/dashboard.component.html +++ b/frontend/src/app/components/dashboard/dashboard.component.html @@ -5,7 +5,7 @@ diff --git a/frontend/src/app/components/login/login.component.ts b/frontend/src/app/components/login/login.component.ts index 14824830..2cb67b1c 100644 --- a/frontend/src/app/components/login/login.component.ts +++ b/frontend/src/app/components/login/login.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { UserService } from 'src/app/services/user.service'; import { MyErrorStateMatcher } from 'src/app/utils/ErrorStateMatcher'; @@ -8,7 +8,7 @@ import { MyErrorStateMatcher } from 'src/app/utils/ErrorStateMatcher'; templateUrl: './login.component.html', styleUrls: ['./login.component.scss'], }) -export class LoginComponent { +export class LoginComponent implements OnInit { email: string; password: string; @@ -16,11 +16,13 @@ export class LoginComponent { invalidCredentials = false; - constructor(private userService: UserService, private router: Router) { + constructor(private userService: UserService, private router: Router) {} + + ngOnInit(): void { if (this.userService.loggedIn) this.router.navigate(['/dashboard']); } - onLogin() { + onLogin(): void { this.userService .login(this.email, this.password) .then(async () => { diff --git a/frontend/src/app/services/user.service.ts b/frontend/src/app/services/user.service.ts index c511e847..718989b1 100644 --- a/frontend/src/app/services/user.service.ts +++ b/frontend/src/app/services/user.service.ts @@ -45,6 +45,16 @@ export class UserService { }); } + async logout(): Promise { + return this.http + .post('auth/logout', {}) + .toPromise() + .then(() => { + this.clearLocalStorage(); + return true; + }); + } + async isSsoEnabled(): Promise { return this.http .get('auth/is-sso-enabled') @@ -77,18 +87,6 @@ export class UserService { }); } - async logout(): Promise { - return this.http - .post('auth/logout', {}) - .toPromise() - .then(() => { - localStorage.removeItem('user'); - localStorage.removeItem('access_token'); - localStorage.removeItem('expires_at'); - return true; - }); - } - update(id: string, user: Partial) { return this.http.post('auth/' + id, user).toPromise(); } @@ -97,6 +95,12 @@ export class UserService { return this.http.delete('auth/' + id).toPromise(); } + clearLocalStorage(): void { + localStorage.removeItem('user'); + localStorage.removeItem('access_token'); + localStorage.removeItem('expires_at'); + } + public get loggedIn(): boolean { const token = localStorage.getItem('access_token'); const expiry = localStorage.getItem('expires_at'); diff --git a/frontend/src/app/utils/interceptor.ts b/frontend/src/app/utils/interceptor.ts index ac0a4c4a..19b568e8 100644 --- a/frontend/src/app/utils/interceptor.ts +++ b/frontend/src/app/utils/interceptor.ts @@ -4,13 +4,17 @@ import { HttpInterceptor, HttpHandler, HttpRequest, + HttpErrorResponse, + HttpResponse, } from '@angular/common/http'; -import { Observable } from 'rxjs'; +import { Observable, throwError } from 'rxjs'; +import { catchError } from 'rxjs/operators'; import { UserService } from '../services/user.service'; +import { Router } from '@angular/router'; @Injectable() export class APIInterceptor implements HttpInterceptor { - constructor(public auth: UserService) {} + constructor(public auth: UserService, private router: Router) {} intercept( req: HttpRequest, @@ -22,6 +26,14 @@ export class APIInterceptor implements HttpInterceptor { Authorization: `Bearer ${this.auth.token}`, }, }); - return next.handle(apiReq); + return next.handle(apiReq).pipe( + catchError(async (err) => { + if (err instanceof HttpErrorResponse && err.status == 401) { + this.auth.clearLocalStorage(); + this.router.navigate(['login']); + } + return err; + }) + ); } } From d045d26ed7af721b0570ee73d30dba9df293b8cc Mon Sep 17 00:00:00 2001 From: Ammar Date: Thu, 15 Dec 2022 20:00:04 -0500 Subject: [PATCH 03/10] Add token on SCORE login --- backend/package-lock.json | 252 ++++++++++++++++------ backend/package.json | 1 + backend/src/api/auth.ts | 15 +- backend/src/server.ts | 18 +- backend/src/utils/auth.ts | 21 +- frontend/src/app/services/user.service.ts | 6 +- 6 files changed, 238 insertions(+), 75 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 514c42db..dbb0e670 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@typegoose/typegoose": "^9.8.1", "@types/bcrypt": "^5.0.0", + "axios": "^1.2.1", "bcrypt": "^5.0.1", "body-parser": "^1.20.0", "cors": "^2.8.5", @@ -178,6 +179,25 @@ "node-pre-gyp": "bin/node-pre-gyp" } }, + "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -220,6 +240,25 @@ "node": ">=10" } }, + "node_modules/@mapbox/node-pre-gyp/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1030,6 +1069,21 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.1.tgz", + "integrity": "sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1460,6 +1514,17 @@ "color-support": "bin.js" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -1606,6 +1671,14 @@ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -2257,6 +2330,38 @@ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3272,44 +3377,6 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==" }, - "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/nodemon": { "version": "2.0.16", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.16.tgz", @@ -3585,6 +3652,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -4906,6 +4978,14 @@ "tar": "^6.1.11" }, "dependencies": { + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, "nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -4929,6 +5009,25 @@ "requires": { "lru-cache": "^6.0.0" } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } } } }, @@ -5533,6 +5632,21 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.1.tgz", + "integrity": "sha512-I88cFiGu9ryt/tfVEi4kX2SITsvDddTajXTOFmt2uK1ZVA8LytjtdeyefdQWEf5PU8w+4SSJDoYnggflB5tW4A==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -5825,6 +5939,14 @@ "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -5944,6 +6066,11 @@ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -6455,6 +6582,21 @@ "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", "dev": true }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -7210,35 +7352,6 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==" }, - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "requires": { - "whatwg-url": "^5.0.0" - }, - "dependencies": { - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - } - } - }, "nodemon": { "version": "2.0.16", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.16.tgz", @@ -7435,6 +7548,11 @@ "ipaddr.js": "1.9.1" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", diff --git a/backend/package.json b/backend/package.json index 86201645..1061b031 100644 --- a/backend/package.json +++ b/backend/package.json @@ -2,6 +2,7 @@ "dependencies": { "@typegoose/typegoose": "^9.8.1", "@types/bcrypt": "^5.0.0", + "axios": "^1.2.1", "bcrypt": "^5.0.1", "body-parser": "^1.20.0", "cors": "^2.8.5", diff --git a/backend/src/api/auth.ts b/backend/src/api/auth.ts index 22e4279e..3490d66d 100644 --- a/backend/src/api/auth.ts +++ b/backend/src/api/auth.ts @@ -10,6 +10,7 @@ import { isCorrectHashedSsoPayload, isSsoEnabled, isValidNonce, + logoutSCORE, signInUserWithSso, userToToken, } from '../utils/auth'; @@ -67,6 +68,10 @@ router.post('/logout', isAuthenticated, async (req, res) => { const token = req.headers.authorization.replace('Bearer ', ''); await destroyToken(res.locals.user.userID, token); + if (req.body.logoutSCORE) { + await logoutSCORE(req); + } + res.status(200).end(); }); @@ -83,12 +88,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, diff --git a/backend/src/server.ts b/backend/src/server.ts index c685a38d..fc9fbc67 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -22,6 +22,8 @@ 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; @@ -29,7 +31,21 @@ 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( + 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); diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index 81c30cdb..6fbde106 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -1,4 +1,5 @@ import { NextFunction, Request, Response } from 'express'; +import axios from 'axios'; import { Role, UserModel } from '../models/User'; import { v4 as uuidv4 } from 'uuid'; import hmacSHA256 from 'crypto-js/hmac-sha256'; @@ -10,7 +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 { checkToken, sign, verify } from './jwt'; +import { addToken, checkToken, sign, verify } from './jwt'; export interface Token { email: string; @@ -222,12 +223,28 @@ export const signInUserWithSso = async ( } const sessionToken = await generateSessionToken(userModel); sessionToken.redirectUrl = redirectUrl; + await addToken(sessionToken.user.userID, sessionToken.token); + return res.status(200).send(sessionToken); }; export const generateSessionToken = (userModel: UserModel): any => { const user = userToToken(userModel); - const token = sign(userModel); + const token = sign(user); const expiresAt = addHours(2); return { token, user, expiresAt }; }; + +export const logoutSCORE = async (req: Request) => { + const cookie = req.headers.cookie + ?.split(';') + .find((c) => c.startsWith('SESSION=')); + const scoreAddress = process.env.SCORE_SERVER_ADDRESS || 'http://localhost'; + + return await axios.get( + `${scoreAddress + process.env.SCORE_LOGOUT_ENDPOINT}`, + { + headers: { Cookie: `${cookie};` }, + } + ); +}; diff --git a/frontend/src/app/services/user.service.ts b/frontend/src/app/services/user.service.ts index 718989b1..874fdc77 100644 --- a/frontend/src/app/services/user.service.ts +++ b/frontend/src/app/services/user.service.ts @@ -47,7 +47,11 @@ export class UserService { async logout(): Promise { return this.http - .post('auth/logout', {}) + .post( + 'auth/logout', + { logoutSCORE: true }, + { withCredentials: true } + ) .toPromise() .then(() => { this.clearLocalStorage(); From dbba726efd06f2c25a8d8f60ed0dc35cc863f5a0 Mon Sep 17 00:00:00 2001 From: Ammar Date: Thu, 15 Dec 2022 20:13:15 -0500 Subject: [PATCH 04/10] Change body param to query param --- backend/src/api/auth.ts | 2 +- frontend/src/app/services/user.service.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/api/auth.ts b/backend/src/api/auth.ts index 3490d66d..0f38b788 100644 --- a/backend/src/api/auth.ts +++ b/backend/src/api/auth.ts @@ -68,7 +68,7 @@ router.post('/logout', isAuthenticated, async (req, res) => { const token = req.headers.authorization.replace('Bearer ', ''); await destroyToken(res.locals.user.userID, token); - if (req.body.logoutSCORE) { + if (req.query.score) { await logoutSCORE(req); } diff --git a/frontend/src/app/services/user.service.ts b/frontend/src/app/services/user.service.ts index 874fdc77..8e7cb35d 100644 --- a/frontend/src/app/services/user.service.ts +++ b/frontend/src/app/services/user.service.ts @@ -48,8 +48,8 @@ export class UserService { async logout(): Promise { return this.http .post( - 'auth/logout', - { logoutSCORE: true }, + 'auth/logout?score=true', + {}, { withCredentials: true } ) .toPromise() From 614d4a92d181c5ffcc75739b5bd9216eeed7c7c0 Mon Sep 17 00:00:00 2001 From: Ammar Date: Fri, 16 Dec 2022 19:08:08 -0500 Subject: [PATCH 05/10] Fix cookie parsing bug --- backend/src/utils/auth.ts | 2 +- frontend/src/app/components/toolbar/toolbar.component.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index 6fbde106..9f689f0c 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -238,7 +238,7 @@ export const generateSessionToken = (userModel: UserModel): any => { export const logoutSCORE = async (req: Request) => { const cookie = req.headers.cookie ?.split(';') - .find((c) => c.startsWith('SESSION=')); + .find((c) => c.trim().startsWith('SESSION=')); const scoreAddress = process.env.SCORE_SERVER_ADDRESS || 'http://localhost'; return await axios.get( diff --git a/frontend/src/app/components/toolbar/toolbar.component.ts b/frontend/src/app/components/toolbar/toolbar.component.ts index 2f484ba3..7632c9e8 100644 --- a/frontend/src/app/components/toolbar/toolbar.component.ts +++ b/frontend/src/app/components/toolbar/toolbar.component.ts @@ -19,8 +19,8 @@ export class ToolbarComponent implements OnInit { ngOnInit(): void {} - signOut(): void { - this.userService.logout(); + async signOut(): Promise { + await this.userService.logout(); this.router.navigate(['login']); } } From 50d9a6f0ebc842fad270b5207993913b96ce40a6 Mon Sep 17 00:00:00 2001 From: Ammar Date: Fri, 16 Dec 2022 19:20:32 -0500 Subject: [PATCH 06/10] Store cookie with access token for SCORE logout use --- backend/package-lock.json | 56 +++++++++++++++++++++++ backend/package.json | 2 + backend/src/api/auth.ts | 14 ++++++ backend/src/server.ts | 2 + backend/src/utils/auth.ts | 12 +++-- frontend/src/app/services/user.service.ts | 14 ++++-- 6 files changed, 90 insertions(+), 10 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index dbb0e670..e059f928 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -14,6 +14,7 @@ "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", @@ -27,6 +28,7 @@ "uuid": "^8.3.2" }, "devDependencies": { + "@types/cookie-parser": "^1.4.3", "@types/crypto-js": "^4.1.1", "@types/express": "^4.17.13", "@types/jsonwebtoken": "^8.5.8", @@ -459,6 +461,15 @@ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, + "node_modules/@types/cookie-parser": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz", + "integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/cors": { "version": "2.8.12", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", @@ -1583,6 +1594,26 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -5193,6 +5224,15 @@ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, + "@types/cookie-parser": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz", + "integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, "@types/cors": { "version": "2.8.12", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", @@ -5993,6 +6033,22 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" }, + "cookie-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz", + "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==", + "requires": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6" + }, + "dependencies": { + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + } + } + }, "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", diff --git a/backend/package.json b/backend/package.json index 1061b031..8ee18698 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,6 +5,7 @@ "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", @@ -28,6 +29,7 @@ "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", diff --git a/backend/src/api/auth.ts b/backend/src/api/auth.ts index 0f38b788..8c957f64 100644 --- a/backend/src/api/auth.ts +++ b/backend/src/api/auth.ts @@ -41,6 +41,13 @@ router.post('/login', async (req, res) => { 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 }); }); @@ -57,6 +64,13 @@ router.post('/register', async (req, res) => { 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 }); }); diff --git a/backend/src/server.ts b/backend/src/server.ts index fc9fbc67..8d41464f 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -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'; @@ -31,6 +32,7 @@ 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(cookieParser()); app.use( cors({ credentials: true, diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index 9f689f0c..1c2192ac 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -225,6 +225,12 @@ export const signInUserWithSso = async ( 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); }; @@ -236,15 +242,11 @@ export const generateSessionToken = (userModel: UserModel): any => { }; export const logoutSCORE = async (req: Request) => { - const cookie = req.headers.cookie - ?.split(';') - .find((c) => c.trim().startsWith('SESSION=')); const scoreAddress = process.env.SCORE_SERVER_ADDRESS || 'http://localhost'; - return await axios.get( `${scoreAddress + process.env.SCORE_LOGOUT_ENDPOINT}`, { - headers: { Cookie: `${cookie};` }, + headers: { Cookie: `SESSION=${req.cookies['SESSION']};` }, } ); }; diff --git a/frontend/src/app/services/user.service.ts b/frontend/src/app/services/user.service.ts index 8e7cb35d..952e432c 100644 --- a/frontend/src/app/services/user.service.ts +++ b/frontend/src/app/services/user.service.ts @@ -20,7 +20,7 @@ export class UserService { async register(user: User) { return this.http - .post('auth/register', user) + .post('auth/register', user, { withCredentials: true }) .toPromise() .then((result) => { localStorage.setItem('user', JSON.stringify(result.user)); @@ -32,10 +32,14 @@ export class UserService { async login(email: string, password: string): Promise { return this.http - .post('auth/login', { - email: email, - password: password, - }) + .post( + 'auth/login', + { + email: email, + password: password, + }, + { withCredentials: true } + ) .toPromise() .then((result) => { localStorage.setItem('user', JSON.stringify(result.user)); From 60702f725227a53f1d2b77ab8d1b587053ae934a Mon Sep 17 00:00:00 2001 From: Ammar Date: Sat, 17 Dec 2022 14:09:34 -0500 Subject: [PATCH 07/10] Add conditional for creating comment remove trace event --- backend/src/socket/events/post.events.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/socket/events/post.events.ts b/backend/src/socket/events/post.events.ts index 5328cf12..4866621a 100644 --- a/backend/src/socket/events/post.events.ts +++ b/backend/src/socket/events/post.events.ts @@ -178,7 +178,8 @@ class PostCommentRemove { ): Promise { 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, From 321c3da207c9550edf8396f85b6ef889d274d1cd Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 19 Dec 2022 15:48:37 -0500 Subject: [PATCH 08/10] Supply withCredentials param to sso request --- frontend/src/app/services/user.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/services/user.service.ts b/frontend/src/app/services/user.service.ts index 952e432c..f2efc1cd 100644 --- a/frontend/src/app/services/user.service.ts +++ b/frontend/src/app/services/user.service.ts @@ -84,7 +84,7 @@ export class UserService { async ssoLogin(sso: string | null, sig: string | null): Promise { return this.http - .get(`auth/sso/login/${sso}/${sig}`) + .get(`auth/sso/login/${sso}/${sig}`, { withCredentials: true }) .toPromise() .then((result: any) => { localStorage.setItem('user', JSON.stringify(result.user)); From f80c80ec85d77ecf841dab36d462988e39e343bc Mon Sep 17 00:00:00 2001 From: Victor Korir Date: Tue, 4 Apr 2023 22:12:10 -0700 Subject: [PATCH 09/10] issue 470 create SCORE-CK connect code --- backend/src/api/projects.ts | 27 +++++++++++++++++++ backend/src/models/Project.ts | 6 +++++ backend/src/repository/dalProject.ts | 9 +++++++ .../add-project-modal.component.html | 8 ++++++ .../add-project-modal.component.ts | 4 ++- ...project-configuration-modal.component.html | 25 ++++++++++------- frontend/src/app/models/project.ts | 2 ++ 7 files changed, 71 insertions(+), 10 deletions(-) diff --git a/backend/src/api/projects.ts b/backend/src/api/projects.ts index 5a3b55c8..20305af9 100644 --- a/backend/src/api/projects.ts +++ b/backend/src/api/projects.ts @@ -78,6 +78,33 @@ router.post('/:id', async (req, res) => { res.status(200).json(updatedProject); }); +router.post('/connect/:runId/:code', async (req, res) => { + const { runId, code } = req.params; + + let project = await dalProject.getByConnectCode(code); + const available = project != null && project.linkedRunId == 0; + const linkedRunId = Number(runId); + const response: any = { + code: null, + message: 'Code does not exist or has already been used', + }; + if (available && !isNaN(linkedRunId)) { + const id = project?.projectID; + if (!!id) { + project = await dalProject.update(id, { linkedRunId }); + if (!!project) { + response.message = + 'Successfully linked Run to an available CK Board project'; + response.code = project.scoreJoinCode; + } + } + } else if (!isNaN(linkedRunId) && linkedRunId == project?.linkedRunId) { + response.message = 'Run is already linked to this CK Board project'; + } + + res.status(200).json(response); +}); + router.get('/:id', async (req, res) => { const id = req.params.id; diff --git a/backend/src/models/Project.ts b/backend/src/models/Project.ts index 2e7b53ac..b8ae3605 100644 --- a/backend/src/models/Project.ts +++ b/backend/src/models/Project.ts @@ -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; diff --git a/backend/src/repository/dalProject.ts b/backend/src/repository/dalProject.ts index f5454758..c062cd83 100644 --- a/backend/src/repository/dalProject.ts +++ b/backend/src/repository/dalProject.ts @@ -34,6 +34,14 @@ 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) { @@ -112,6 +120,7 @@ const dalProject = { getById, getByUserId, getByJoinCode, + getByConnectCode, create, addStudent, addTeacher, diff --git a/frontend/src/app/components/add-project-modal/add-project-modal.component.html b/frontend/src/app/components/add-project-modal/add-project-modal.component.html index a6a4b943..34dcef06 100644 --- a/frontend/src/app/components/add-project-modal/add-project-modal.component.html +++ b/frontend/src/app/components/add-project-modal/add-project-modal.component.html @@ -45,6 +45,14 @@
Membership Settings:
+ +
+
SCORE Linking
+ + Connect to a SCORE Run (i.e., to add SCORE members and activities) + +
+
diff --git a/frontend/src/app/components/add-project-modal/add-project-modal.component.ts b/frontend/src/app/components/add-project-modal/add-project-modal.component.ts index e1e6785b..f352322e 100644 --- a/frontend/src/app/components/add-project-modal/add-project-modal.component.ts +++ b/frontend/src/app/components/add-project-modal/add-project-modal.component.ts @@ -4,7 +4,7 @@ import { fabric } from 'fabric'; import { PersonalBoardSetting } from 'src/app/models/project'; import { FileUploadService } from 'src/app/services/fileUpload.service'; import { UserService } from 'src/app/services/user.service'; -import { FabricUtils, ImageSettings } from 'src/app/utils/FabricUtils'; +import { FabricUtils } from 'src/app/utils/FabricUtils'; import { generateCode, generateUniqueID } from 'src/app/utils/Utils'; @Component({ @@ -20,6 +20,7 @@ export class AddProjectModalComponent implements OnInit { bgImage: null, }; membershipDisabledEditable = false; + linkToScore = false; constructor( public dialogRef: MatDialogRef, @@ -54,6 +55,7 @@ export class AddProjectModalComponent implements OnInit { boards: [], studentJoinCode: generateCode(5).toString(), teacherJoinCode: generateCode(5).toString(), + ...(this.linkToScore && { scoreJoinCode: generateCode(5).toString(), linkedRunId: 0 }), personalBoardSetting: this.personalBoardSetting, membershipDisabled: this.membershipDisabledEditable, }); diff --git a/frontend/src/app/components/project-configuration-modal/project-configuration-modal.component.html b/frontend/src/app/components/project-configuration-modal/project-configuration-modal.component.html index 48a20357..c5fe0025 100644 --- a/frontend/src/app/components/project-configuration-modal/project-configuration-modal.component.html +++ b/frontend/src/app/components/project-configuration-modal/project-configuration-modal.component.html @@ -17,17 +17,24 @@

Project Configuration

matTooltip="When activated, no users can join this project."> Restrict users from joining this project -
Invite Codes
+
{{ !!project.scoreJoinCode ? 'SCORE-CK Connect Code' : 'Invite Codes' }}
- + + + -
Current Members ({{members.length}})
diff --git a/frontend/src/app/models/project.ts b/frontend/src/app/models/project.ts index 7cf371fd..d8bc723a 100644 --- a/frontend/src/app/models/project.ts +++ b/frontend/src/app/models/project.ts @@ -13,6 +13,8 @@ export class Project { members: string[]; studentJoinCode: string; teacherJoinCode: string; + scoreJoinCode: string; + linkedRunId: number; personalBoardSetting: PersonalBoardSetting; membershipDisabled: boolean; } From 948f2c99ef236b1792ad420e625ddddef6957cba Mon Sep 17 00:00:00 2001 From: Victor Korir Date: Sat, 29 Apr 2023 15:00:34 -0700 Subject: [PATCH 10/10] Refactored API endpoints to link/unlink with SCORE connect code --- backend/src/api/projects.ts | 89 ++++++++++++++----- backend/src/constants.ts | 2 + backend/src/repository/dalProject.ts | 35 +++++++- backend/src/utils/auth.ts | 3 +- .../add-project-modal.component.ts | 5 +- 5 files changed, 110 insertions(+), 24 deletions(-) diff --git a/backend/src/api/projects.ts b/backend/src/api/projects.ts index 20305af9..50a69a6d 100644 --- a/backend/src/api/projects.ts +++ b/backend/src/api/projects.ts @@ -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 { @@ -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(); @@ -78,31 +86,72 @@ router.post('/:id', async (req, res) => { res.status(200).json(updatedProject); }); -router.post('/connect/:runId/:code', async (req, res) => { - const { runId, code } = req.params; +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); - const available = project != null && project.linkedRunId == 0; - const linkedRunId = Number(runId); - const response: any = { - code: null, - message: 'Code does not exist or has already been used', + 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', }; - if (available && !isNaN(linkedRunId)) { - const id = project?.projectID; - if (!!id) { - project = await dalProject.update(id, { linkedRunId }); - if (!!project) { - response.message = - 'Successfully linked Run to an available CK Board project'; - response.code = project.scoreJoinCode; - } + 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); } - } else if (!isNaN(linkedRunId) && linkedRunId == project?.linkedRunId) { - response.message = 'Run is already linked to this CK Board project'; } + res.status(200).send(); +}); - res.status(200).json(response); +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) => { diff --git a/backend/src/constants.ts b/backend/src/constants.ts index 0abaeda4..0ee4badd 100644 --- a/backend/src/constants.ts +++ b/backend/src/constants.ts @@ -79,3 +79,5 @@ export const DEFAULT_TAGS: Partial[] = [ QUESTION_TAG, NEEDS_ATTENTION_TAG, ]; + +export const SCORE_DOMAIN = 'score.oise.utoronto.ca'; diff --git a/backend/src/repository/dalProject.ts b/backend/src/repository/dalProject.ts index c062cd83..f04ec852 100644 --- a/backend/src/repository/dalProject.ts +++ b/backend/src/repository/dalProject.ts @@ -47,9 +47,24 @@ export const addStudent = async (code: string, userID: string) => { 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) => { @@ -57,7 +72,21 @@ export const addTeacher = async (code: string, userID: string) => { 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; }; @@ -123,7 +152,9 @@ const dalProject = { getByConnectCode, create, addStudent, + removeStudent, addTeacher, + removeTeacher, update, removeBoard, remove, diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index df281303..ff01b8f3 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -12,6 +12,7 @@ import { NotFoundError } from '../errors/client.errors'; import { addUserToProject } from './project.helpers'; import { ApplicationError } from '../errors/base.errors'; import { addToken, checkToken, sign, verify } from './jwt'; +import { SCORE_DOMAIN } from '../constants'; export interface Token { email: string; @@ -126,7 +127,7 @@ export const getOrCreateUser = async ( paramMap: Map ): Promise => { const username = paramMap.get('username') ?? ''; - const email = `${username}@score.oise.utoronto.ca`; + const email = `${username}@${SCORE_DOMAIN}`; const role = getRole(paramMap.get('role')); try { return await createUserIfNecessary(email, username, role); diff --git a/frontend/src/app/components/add-project-modal/add-project-modal.component.ts b/frontend/src/app/components/add-project-modal/add-project-modal.component.ts index f352322e..088ecea8 100644 --- a/frontend/src/app/components/add-project-modal/add-project-modal.component.ts +++ b/frontend/src/app/components/add-project-modal/add-project-modal.component.ts @@ -55,7 +55,10 @@ export class AddProjectModalComponent implements OnInit { boards: [], studentJoinCode: generateCode(5).toString(), teacherJoinCode: generateCode(5).toString(), - ...(this.linkToScore && { scoreJoinCode: generateCode(5).toString(), linkedRunId: 0 }), + ...(this.linkToScore && { + scoreJoinCode: generateCode(5).toString(), + linkedRunId: 0, + }), personalBoardSetting: this.personalBoardSetting, membershipDisabled: this.membershipDisabledEditable, });