Skip to content

Commit

Permalink
Merge pull request #31 from parlemonde/feat/collaboration-v1
Browse files Browse the repository at this point in the history
Add collaboration mode and some fixes
  • Loading branch information
DavidRobertAnsart authored Mar 26, 2024
2 parents 9022fdf + 58c46b8 commit 115a13b
Show file tree
Hide file tree
Showing 136 changed files with 3,510 additions and 527 deletions.
447 changes: 436 additions & 11 deletions .pnp.cjs

Large diffs are not rendered by default.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
"classnames": "^2.3.2",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"croppie": "^2.6.5",
"csrf": "^3.1.0",
"dotenv": "^16.3.1",
"express": "^4.18.2",
Expand All @@ -47,6 +46,7 @@
"helmet": "^7.0.0",
"json-stable-stringify": "^1.0.2",
"jsonwebtoken": "^9.0.1",
"merge-images": "^2.0.0",
"mime-types": "^2.1.35",
"mlt-xml": "^2.0.2",
"morgan": "^1.10.0",
Expand All @@ -63,19 +63,26 @@
"query-string": "^8.1.0",
"react": "^18.2.0",
"react-charts": "^2.0.0-beta.7",
"react-device-detect": "^2.2.3",
"react-dom": "^18.2.0",
"react-easy-crop": "^5.0.5",
"react-html5-camera-photo": "^1.5.11",
"react-qr-code": "^2.0.12",
"react-qr-reader": "^3.0.0-beta-1",
"react-query": "^3.39.3",
"resize-observer-polyfill": "^1.5.1",
"safe-buffer": "^5.2.1",
"sanitize-filename": "^1.6.3",
"sass": "^1.66.1",
"sharp": "^0.32.5",
"socket.io": "^4.7.4",
"socket.io-client": "^4.7.4",
"sortablejs": "1.15.0",
"typeorm": "^0.2.45",
"typescript": "5.1.6",
"uuid": "^9.0.0",
"waveform-data": "^4.3.0",
"webrtc-adapter": "^8.2.3",
"winston": "^3.10.0"
},
"devDependencies": {
Expand Down
Binary file added public/black.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/black_bg/black_1600x900.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/black_bg/black_1920x1080.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/black_bg/black_2560x1440.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/black_bg/black_3840x2160.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/black_bg/black_7680x4320.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 6 additions & 1 deletion server/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import express, { Router } from 'express';
import type { Request } from 'express';
import RateLimit from 'express-rate-limit';
import helmet from 'helmet';
import { Server } from 'http';
import morgan from 'morgan';
import next from 'next';
import path from 'path';
Expand All @@ -20,6 +21,7 @@ import { handleErrors } from './middlewares/handle-errors';
import { jsonify } from './middlewares/jsonify';
import { removeTrailingSlash } from './middlewares/remove-trailing-slash';
import { routes } from './routes';
import { startSocketServer } from './socket';
import { getLocales } from './translations/getLocales';
import { getDefaultDirectives } from './utils/get-default-directives';

Expand Down Expand Up @@ -132,7 +134,10 @@ async function startApp() {
if (port === false) {
logger.error(`Exiting. Invalid port to use: %s`, port);
} else {
const server = app.listen(port);
const server = new Server(app);
// --- Start socket server ---
startSocketServer(server);
server.listen(port);
server.on('error', onError);
server.on('listening', () => {
logger.info(`App listening on port ${port}!`);
Expand Down
2 changes: 2 additions & 0 deletions server/authentication/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Router } from 'express';

import { handleErrors } from '../middlewares/handle-errors';
import { login } from './login';
import { loginStudent } from './loginStudent';
import { logout } from './logout';
import { resetPassword, updatePassword, verifyEmail } from './password';
import { loginWithPlmSSO } from './plmSSO';
Expand All @@ -11,6 +12,7 @@ const authRouter = Router();
authRouter.post('/token', handleErrors(refreshToken));
authRouter.post('/token/reject', handleErrors(rejectAccessToken));
authRouter.post('/login', handleErrors(login));
authRouter.post('/login/student', handleErrors(loginStudent));
authRouter.post('/login/reset-password', handleErrors(resetPassword));
authRouter.post('/login/update-password', handleErrors(updatePassword));
authRouter.post('/login/verify-email', handleErrors(verifyEmail));
Expand Down
14 changes: 14 additions & 0 deletions server/authentication/lib/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,17 @@ export async function revokeRefreshToken(refreshToken: string): Promise<void> {
id: parseInt(refreshTokenID, 10) || 0,
});
}

export async function getStudentAccessToken(
projectId: number,
sequencyId: number,
teacherId: number,
): Promise<{
accessToken: string;
}> {
const accessToken = jwt.sign({ projectId, sequencyId, teacherId, isStudent: true }, APP_SECRET, { expiresIn: '4h' });

return {
accessToken,
};
}
15 changes: 10 additions & 5 deletions server/authentication/login.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { JSONSchemaType } from 'ajv';
import * as argon2 from 'argon2';
import type { Request, Response } from 'express';
import { getRepository } from 'typeorm';
import { getRepository, getConnection } from 'typeorm';

import { Project } from '../entities/project';
import { User } from '../entities/user';
import { ajv, sendInvalidDataError } from '../lib/json-schema-validator';
import { logger } from '../lib/logger';
Expand Down Expand Up @@ -59,10 +60,6 @@ export async function login(req: Request, res: Response): Promise<void> {
throw new AppError('forbidden', ['Unauthorized - Account blocked. Please reset password.'], 3);
}

if (user.accountRegistration === 10) {
throw new AppError('loginError', ['Unauthorized - Please use SSO.'], 5);
}

if (!isPasswordCorrect) {
user.accountRegistration += 1;
await getRepository(User).save(user);
Expand All @@ -72,6 +69,14 @@ export async function login(req: Request, res: Response): Promise<void> {
await getRepository(User).save(user);
}

// set collaboration mode to false on each user project
await getConnection()
.createQueryBuilder()
.update(Project)
.set({ isCollaborationActive: false, joinCode: null })
.where({ userId: user.id, isCollaborationActive: true })
.execute();

const { accessToken, refreshToken } = await getAccessToken(user.id, !!data.getRefreshToken);
res.cookie('access-token', accessToken, {
maxAge: 4 * 60 * 60000,
Expand Down
79 changes: 79 additions & 0 deletions server/authentication/loginStudent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { JSONSchemaType } from 'ajv';
import type { Request, Response } from 'express';
import { getRepository } from 'typeorm';

// import { QuestionStatus } from '../../types/models/question.type';
import { Project } from '../entities/project';
import { Question } from '../entities/question';
import { User } from '../entities/user';
import { ajv, sendInvalidDataError } from '../lib/json-schema-validator';
import { AppError } from '../middlewares/handle-errors';
import { ANONYMOUS_USER } from '../utils/anonymous-user';
import { getStudentAccessToken } from './lib/tokens';

const APP_SECRET: string = process.env.APP_SECRET || '';

// --- LOGIN ---
type LoginData = {
projectId: number;
sequencyId: number;
teacherId: number;
};
const LOGIN_SCHEMA: JSONSchemaType<LoginData> = {
type: 'object',
properties: {
projectId: { type: 'number' },
sequencyId: { type: 'number' },
teacherId: { type: 'number' },
},
required: ['projectId', 'sequencyId', 'teacherId'],
additionalProperties: false,
};
const loginValidator = ajv.compile(LOGIN_SCHEMA);
export async function loginStudent(req: Request, res: Response): Promise<void> {
if (APP_SECRET.length === 0 || !req.isCsrfValid) {
throw new AppError('forbidden');
}
const data = req.body;
if (!loginValidator(data)) {
sendInvalidDataError(loginValidator);
return;
}

const project = await getRepository(Project).findOne({ where: { id: data.projectId } });
const teacher = await getRepository(User).findOne({ where: { id: data.teacherId } });
const sequency = await getRepository(Question).findOne({
where: { id: data.sequencyId },
relations: ['project', 'project.user'],
});

// Check if data are correct before connect student as anonymous
if (
project === undefined ||
teacher === undefined ||
sequency === undefined ||
sequency.project === undefined ||
sequency.project.user === undefined ||
project.id !== sequency.project.id ||
teacher.id !== sequency.project.user.id ||
project.isCollaborationActive !== true
) {
throw new AppError('loginError', ['Unauthorized - Invalid QRCode.'], 0);
}

const { accessToken } = await getStudentAccessToken(project.id, sequency.id, teacher.id);
res.cookie('access-token', accessToken, {
maxAge: 4 * 60 * 60000,
expires: new Date(Date.now() + 4 * 60 * 60000),
httpOnly: true,
secure: true,
sameSite: 'strict',
});
res.sendJSON({
accessToken,
projectId: project.id,
sequencyId: sequency.id,
teacherId: teacher.id,
user: ANONYMOUS_USER,
});
}
4 changes: 0 additions & 4 deletions server/authentication/password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,6 @@ export async function updatePassword(req: Request, res: Response): Promise<void>
throw new AppError('loginError', ['Unauthorized - Invalid username.'], 0);
}

if (user.accountRegistration === 10) {
throw new AppError('loginError', ['Unauthorized - Please use SSO.'], 2);
}

// verify token
let isverifyTokenCorrect: boolean = false;
try {
Expand Down
62 changes: 59 additions & 3 deletions server/controllers/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import fs from 'fs-extra';
import http from 'http';
import * as path from 'path';
import sanitize from 'sanitize-filename';
import { getRepository } from 'typeorm';
import { getRepository, getConnection } from 'typeorm';
import { v4 as uuidv4 } from 'uuid';

import type { Title } from '../../types/models/title.type';
Expand Down Expand Up @@ -154,15 +154,30 @@ projectController.get({ path: '', userType: UserType.CLASS }, async (req, res) =
res.sendJSON(project);
});

projectController.get({ path: '/join/:joinCode(\\d+)' }, async (req, res, next) => {
const joinCode = parseInt(req.params.joinCode, 10) || 0;
const project: Project | undefined = await getRepository(Project).findOne({
where: { joinCode },
relations: ['questions'],
});
if (project === undefined) {
next();
return;
}

res.sendJSON(project);
});

projectController.get({ path: '/:id(\\d+)', userType: UserType.CLASS }, async (req, res, next) => {
if (!req.user) {
const user = req.user;
if (user === undefined) {
next();
return;
}
const id = parseInt(req.params.id, 10) || 0;
const includes = new Set((getQueryString(req.query.include) || '').split(','));
const project: Project | undefined = await getRepository(Project).findOne({
where: { id, userId: req.user.id },
where: { id, userId: user.teacherId ?? user.id },
relations: getRelations(includes),
});
if (project === undefined) {
Expand Down Expand Up @@ -191,6 +206,7 @@ type PostProjectData = {
soundUrl?: string | null;
soundVolume?: number | null;
musicBeginTime?: number;
videoJobId?: string | null;
};
const POST_PROJECT_SCHEMA: JSONSchemaType<PostProjectData> = {
type: 'object',
Expand Down Expand Up @@ -220,6 +236,10 @@ const POST_PROJECT_SCHEMA: JSONSchemaType<PostProjectData> = {
type: 'number',
nullable: true,
},
videoJobId: {
type: 'string',
nullable: true,
},
},
required: ['title', 'themeId', 'scenarioId', 'questions'],
additionalProperties: false,
Expand All @@ -232,6 +252,14 @@ projectController.post({ path: '/', userType: UserType.CLASS }, async (req, res,
return;
}

// set collaboration mode to false on each user project
await getConnection()
.createQueryBuilder()
.update(Project)
.set({ isCollaborationActive: false, joinCode: null })
.where({ userId: user.id, isCollaborationActive: true })
.execute();

const data = req.body;
if (!postProjectValidator(data)) {
sendInvalidDataError(postProjectValidator);
Expand All @@ -246,6 +274,7 @@ projectController.post({ path: '/', userType: UserType.CLASS }, async (req, res,
newProject.soundUrl = data.soundUrl || null;
newProject.soundVolume = data.soundVolume || null;
newProject.musicBeginTime = data.musicBeginTime || 0;
newProject.videoJobId = data.videoJobId || null;
const languageCode = getQueryString(req.query.languageCode) || req.cookies?.['app-language'] || 'fr';
newProject.languageCode = languageCode;
const newQuestions: Question[] = [];
Expand Down Expand Up @@ -280,6 +309,8 @@ type PutProjectData = {
soundUrl?: string | null;
soundVolume?: number | null;
musicBeginTime?: number;
isCollaborationActive?: boolean;
joinCode?: number;
};
const PUT_PROJECT_SCHEMA: JSONSchemaType<PutProjectData> = {
type: 'object',
Expand All @@ -303,9 +334,20 @@ const PUT_PROJECT_SCHEMA: JSONSchemaType<PutProjectData> = {
type: 'number',
nullable: true,
},
isCollaborationActive: {
type: 'boolean',
nullable: true,
},
joinCode: {
type: 'number',
nullable: true,
},
},
required: [],
additionalProperties: false,
dependentSchemas: {
isCollaborationActive: { required: ['joinCode'] },
},
};
const putProjectValidator = ajv.compile(PUT_PROJECT_SCHEMA);
projectController.put({ path: '/:id', userType: UserType.CLASS }, async (req, res, next) => {
Expand All @@ -332,6 +374,20 @@ projectController.put({ path: '/:id', userType: UserType.CLASS }, async (req, re
project.soundUrl = data.soundUrl !== undefined ? data.soundUrl : project.soundUrl;
project.soundVolume = data.soundVolume !== undefined ? data.soundVolume : project.soundVolume;
project.musicBeginTime = data.musicBeginTime ?? project.musicBeginTime;
if (data.isCollaborationActive === false) {
project.isCollaborationActive = false;
project.joinCode = null;
} else if (data.isCollaborationActive === true) {
project.isCollaborationActive = true;
project.joinCode = data.joinCode || null;
// set collaboration mode to false on each user project
await getConnection()
.createQueryBuilder()
.update(Project)
.set({ isCollaborationActive: false, joinCode: null })
.where({ userId: user.id, isCollaborationActive: true })
.execute();
}
await getRepository(Project).save(project);
res.sendJSON(project);
});
Expand Down
Loading

0 comments on commit 115a13b

Please sign in to comment.