Skip to content

Commit

Permalink
Add collaboration mode and some fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
vgimonnet-wt committed Feb 19, 2024
1 parent 9022fdf commit b4d3656
Show file tree
Hide file tree
Showing 79 changed files with 2,414 additions and 179 deletions.
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,25 @@
"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-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
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 = 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: false })
.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
77 changes: 77 additions & 0 deletions server/authentication/loginStudent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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 ||
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
53 changes: 50 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 @@ -220,6 +235,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 @@ -246,6 +265,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 || 0;
const languageCode = getQueryString(req.query.languageCode) || req.cookies?.['app-language'] || 'fr';
newProject.languageCode = languageCode;
const newQuestions: Question[] = [];
Expand Down Expand Up @@ -280,6 +300,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 +325,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 +365,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;
// 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
16 changes: 16 additions & 0 deletions server/controllers/questions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { JSONSchemaType } from 'ajv';
import type { FindConditions } from 'typeorm';
import { getManager, getRepository } from 'typeorm';

import { QuestionStatus } from '../../types/models/question.type';
import type { Title } from '../../types/models/title.type';
import { Question } from '../entities/question';
import { UserType } from '../entities/user';
Expand Down Expand Up @@ -173,6 +174,8 @@ type PutQuestionData = {
voiceOffBeginTime?: number;
soundUrl?: string | null;
soundVolume?: number | null;
feedback?: string | null;
status?: QuestionStatus | null;
};
const PUT_QUESTION_SCHEMA: JSONSchemaType<PutQuestionData> = {
type: 'object',
Expand Down Expand Up @@ -216,6 +219,14 @@ const PUT_QUESTION_SCHEMA: JSONSchemaType<PutQuestionData> = {
minimum: 0,
maximum: 200,
},
feedback: {
type: 'string',
nullable: true,
},
status: {
type: 'number',
nullable: true,
},
},
required: [],
additionalProperties: false,
Expand All @@ -241,6 +252,11 @@ questionController.put({ path: '/:id', userType: UserType.CLASS }, async (req, r
question.voiceOffBeginTime = data.voiceOffBeginTime || 0;
question.soundUrl = data.soundUrl !== undefined ? data.soundUrl : question.soundUrl;
question.soundVolume = data.soundVolume !== undefined ? data.soundVolume : question.soundVolume;
const dataStatus = data.status;
if (dataStatus !== undefined && dataStatus !== null) {
question.status = dataStatus;
question.feedback = [QuestionStatus.ONGOING, QuestionStatus.PREMOUNTING].includes(dataStatus) && data.feedback ? data.feedback : null;
}
await getRepository(Question).save(question);
res.sendJSON(question);
});
Expand Down
2 changes: 2 additions & 0 deletions server/controllers/scenarios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ const POST_SCENARIO_SCHEMA: JSONSchemaType<PostScenarioData> = {
type: 'object',
additionalProperties: {
type: 'string',
pattern: '([^\\s]*)',
},
required: [],
},
Expand Down Expand Up @@ -155,6 +156,7 @@ const PUT_SCENARIO_SCHEMA: JSONSchemaType<PutScenarioData> = {
type: 'object',
additionalProperties: {
type: 'string',
pattern: '([^\\s]*)',
},
required: [],
nullable: true,
Expand Down
Loading

0 comments on commit b4d3656

Please sign in to comment.