trying to integrate TS

This commit is contained in:
Gabriel Matte 2024-11-09 18:36:24 -05:00
parent 4cca066751
commit 55fe6003c6
36 changed files with 1979 additions and 1382 deletions

25
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,25 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug backend",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/server/app.js",
"cwd":"${workspaceFolder}/server/"
},
{
"type": "msedge",
"request": "launch",
"name": "Debug frontend",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/client/"
}
]
}

6
package-lock.json generated Normal file
View file

@ -0,0 +1,6 @@
{
"name": "EvalueTonSavoir",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

1
server/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
auth_config.json

View file

@ -1,100 +0,0 @@
// Import API
const express = require("express");
const http = require("http");
const dotenv = require('dotenv');
// Import Sockets
const { setupWebsocket } = require("./socket/socket");
const { Server } = require("socket.io");
// instantiate the db
const db = require('./config/db.js');
// instantiate the models
const quiz = require('./models/quiz.js');
const quizModel = new quiz(db);
const folders = require('./models/folders.js');
const foldersModel = new folders(db, quizModel);
const users = require('./models/users.js');
const userModel = new users(db, foldersModel);
const images = require('./models/images.js');
const imageModel = new images(db);
// instantiate the controllers
const usersController = require('./controllers/users.js');
const usersControllerInstance = new usersController(userModel);
const foldersController = require('./controllers/folders.js');
const foldersControllerInstance = new foldersController(foldersModel);
const quizController = require('./controllers/quiz.js');
const quizControllerInstance = new quizController(quizModel, foldersModel);
const imagesController = require('./controllers/images.js');
const imagesControllerInstance = new imagesController(imageModel);
// export the controllers
module.exports.users = usersControllerInstance;
module.exports.folders = foldersControllerInstance;
module.exports.quizzes = quizControllerInstance;
module.exports.images = imagesControllerInstance;
//import routers (instantiate controllers as side effect)
const userRouter = require('./routers/users.js');
const folderRouter = require('./routers/folders.js');
const quizRouter = require('./routers/quiz.js');
const imagesRouter = require('./routers/images.js');
// Setup environment
dotenv.config();
const errorHandler = require("./middleware/errorHandler.js");
// Start app
const app = express();
const cors = require("cors");
const bodyParser = require('body-parser');
const configureServer = (httpServer, isDev) => {
return new Server(httpServer, {
path: "/socket.io",
cors: {
origin: "*",
methods: ["GET", "POST"],
credentials: true,
},
secure: !isDev, // true for https, false for http
});
};
// Start sockets (depending on the dev or prod environment)
let server = http.createServer(app);
let isDev = process.env.NODE_ENV === 'development';
console.log(`Environnement: ${process.env.NODE_ENV} (${isDev ? 'dev' : 'prod'})`);
const io = configureServer(server);
setupWebsocket(io);
app.use(cors());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
// Create routes
app.use('/api/user', userRouter);
app.use('/api/folder', folderRouter);
app.use('/api/quiz', quizRouter);
app.use('/api/image', imagesRouter);
app.use(errorHandler);
// Start server
async function start() {
const port = process.env.PORT || 4400;
// Check DB connection
await db.connect();
db.getConnection();
console.log(`Connexion MongoDB établie`);
server.listen(port, () => {
console.log(`Serveur écoutant sur le port ${port}`);
});
}
start();

136
server/app.ts Normal file
View file

@ -0,0 +1,136 @@
import express, { Application } from 'express';
import http from 'http';
import dotenv from 'dotenv';
import cors from 'cors';
import bodyParser from 'body-parser';
import { ServerOptions, Server as SocketIOServer } from 'socket.io';
import { GlideClient } from '@valkey/valkey-glide';
// Set app defaults
const environment: string = process.env.NODE_ENV ?? "production";
const isDev: boolean = environment === "development";
// Import Sockets
import setupWebsocket from "./socket/socket";
// Import Database
import db from './config/db';
// Import Models
import Quiz from './models/quiz';
import Folders from './models/folders';
import Users from './models/users';
import Images from './models/images';
// Instantiate models
const quizModel = new Quiz(db);
const foldersModel = new Folders(db, quizModel);
const userModel = new Users(db, foldersModel);
const imageModel = new Images(db);
// Initialize cache
const valkey = await GlideClient.createClient({
addresses: [{
host: process.env.VALKEY_HOST ?? 'localhost',
port: Number(process.env.VALKEY_PORT) ?? 6379
}]
});
// Import Controllers
import UsersController from './controllers/users';
import FoldersController from './controllers/folders';
import QuizController from './controllers/quiz';
import ImagesController from './controllers/images';
import { RoomManager as RoomsController } from './controllers/rooms';
// Instantiate Controllers
const usersControllerInstance = new UsersController(userModel);
const foldersControllerInstance = new FoldersController(foldersModel);
const quizControllerInstance = new QuizController(quizModel, foldersModel);
const imagesControllerInstance = new ImagesController(imageModel);
// Initialize valkey before creating rooms controller
const roomsControllerInstance = new RoomsController({}, valkey);
// Export Controllers
export const controllers = {
users: usersControllerInstance,
folders: foldersControllerInstance,
quizzes: quizControllerInstance,
images: imagesControllerInstance,
rooms: roomsControllerInstance
};
// Import Routers
import userRouter from './routers/users';
import folderRouter from './routers/folders';
import quizRouter from './routers/quiz';
import imagesRouter from './routers/images';
// Setup environment
dotenv.config();
import errorHandler from "./middleware/errorHandler";
// Start app
const app: Application = express();
const configureServer = (httpServer: http.Server, isDev: boolean): SocketIOServer => {
isDev
const options: Partial<ServerOptions> = {
path: "/socket.io",
cors: {
origin: "*", // Not secure -- to
methods: ["GET", "POST"],
credentials: true,
},
transports: ['websocket', 'polling'],
};
return new SocketIOServer(httpServer, options);
};
// Create HTTP server
const server: http.Server = http.createServer(app);
console.log(`Environment: ${process.env.NODE_ENV} (${isDev ? 'dev' : 'prod'})`);
const io: SocketIOServer = configureServer(server, isDev);
setupWebsocket(io);
// Middleware
app.use(cors());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
// Routes
app.use('/api/user', userRouter);
app.use('/api/folder', folderRouter);
app.use('/api/quiz', quizRouter);
app.use('/api/image', imagesRouter);
// Error handling
app.use(errorHandler);
// Server startup function
const start = async (): Promise<void> => {
const port: number = Number(process.env.PORT) || 4400;
try {
// Check DB connection
await db.connect();
db.getConnection();
console.log('MongoDB connection established');
server.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
};
start();

View file

@ -1,28 +0,0 @@
const { MongoClient } = require('mongodb');
const dotenv = require('dotenv')
dotenv.config();
class DBConnection {
constructor() {
this.mongoURI = process.env.MONGO_URI;
this.databaseName = process.env.MONGO_DATABASE;
this.connection = null;
}
async connect() {
const client = new MongoClient(this.mongoURI);
this.connection = await client.connect();
}
getConnection() {
if (!this.connection) {
throw new Error('Connexion MongoDB non établie');
}
return this.connection.db(this.databaseName);
}
}
const instance = new DBConnection();
module.exports = instance;

25
server/config/db.ts Normal file
View file

@ -0,0 +1,25 @@
import { MongoClient } from 'mongodb';
import dotenv from 'dotenv'
dotenv.config();
export class DBConnection {
mongoURI:string = process.env.MONGO_URI ?? "localhost:27017"
databaseName:string = process.env.MONGO_DATABASE ?? "mangodb"
connection:MongoClient | undefined = undefined;
async connect() {
const client = new MongoClient(this.mongoURI!);
this.connection = await client.connect();
}
getConnection() {
if (!this.connection) {
throw new Error('Connexion MongoDB non établie');
}
return this.connection.db(this.databaseName);
}
}
export default new DBConnection();

View file

@ -1,22 +1,19 @@
const nodemailer = require('nodemailer'); import nodemailer from 'nodemailer';
const dotenv = require('dotenv'); import dotenv from 'dotenv';
dotenv.config(); dotenv.config();
class Emailer { class Emailer {
senderEmail = process.env.SENDER_EMAIL;
constructor() { psw = process.env.EMAIL_PSW;
this.senderEmail = process.env.SENDER_EMAIL; transporter = nodemailer.createTransport({
this.psw = process.env.EMAIL_PSW; service: process.env.EMAIL_SERVICE,
this.transporter = nodemailer.createTransport({ auth: {
service: process.env.EMAIL_SERVICE, user: this.senderEmail,
auth: { pass: this.psw
user: this.senderEmail, }
pass: this.psw });
}
});
}
registerConfirmation(email) { registerConfirmation(email) {
this.transporter.sendMail({ this.transporter.sendMail({
from: this.senderEmail, from: this.senderEmail,
@ -46,4 +43,4 @@ class Emailer {
} }
module.exports = new Emailer(); export default new Emailer()

View file

@ -1,133 +0,0 @@
exports.UNAUTHORIZED_NO_TOKEN_GIVEN = {
message: 'Accès refusé. Aucun jeton fourni.',
code: 401
}
exports.UNAUTHORIZED_INVALID_TOKEN = {
message: 'Accès refusé. Jeton invalide.',
code: 401
}
exports.MISSING_REQUIRED_PARAMETER = {
message: 'Paramètre requis manquant.',
code: 400
}
exports.USER_ALREADY_EXISTS = {
message: 'L\'utilisateur existe déjà.',
code: 400
}
exports.LOGIN_CREDENTIALS_ERROR = {
message: 'L\'email et le mot de passe ne correspondent pas.',
code: 400
}
exports.GENERATE_PASSWORD_ERROR = {
message: 'Une erreur s\'est produite lors de la création d\'un nouveau mot de passe.',
code: 400
}
exports.UPDATE_PASSWORD_ERROR = {
message: 'Une erreur s\'est produite lors de la mise à jours du mot de passe.',
code: 400
}
exports.DELETE_USER_ERROR = {
message: 'Une erreur s\'est produite lors de supression de l\'utilisateur.',
code: 400
}
exports.IMAGE_NOT_FOUND = {
message: 'Nous n\'avons pas trouvé l\'image.',
code: 404
}
exports.QUIZ_NOT_FOUND = {
message: 'Aucun quiz portant cet identifiant n\'a été trouvé.',
code: 404
}
exports.QUIZ_ALREADY_EXISTS = {
message: 'Le quiz existe déja.',
code: 400
}
exports.UPDATE_QUIZ_ERROR = {
message: 'Une erreur s\'est produite lors de la mise à jours du quiz.',
code: 400
}
exports.DELETE_QUIZ_ERROR = {
message: 'Une erreur s\'est produite lors de la supression du quiz.',
code: 400
}
exports.GETTING_QUIZ_ERROR = {
message: 'Une erreur s\'est produite lors de la récupération du quiz.',
code: 400
}
exports.MOVING_QUIZ_ERROR = {
message: 'Une erreur s\'est produite lors du déplacement du quiz.',
code: 400
}
exports.DUPLICATE_QUIZ_ERROR = {
message: 'Une erreur s\'est produite lors de la duplication du quiz.',
code: 400
}
exports.COPY_QUIZ_ERROR = {
message: 'Une erreur s\'est produite lors de la copie du quiz.',
code: 400
}
exports.FOLDER_NOT_FOUND = {
message: 'Aucun dossier portant cet identifiant n\'a été trouvé.',
code: 404
}
exports.FOLDER_ALREADY_EXISTS = {
message: 'Le dossier existe déja.',
code: 400
}
exports.UPDATE_FOLDER_ERROR = {
message: 'Une erreur s\'est produite lors de la mise à jours du dossier.',
code: 400
}
exports.DELETE_FOLDER_ERROR = {
message: 'Une erreur s\'est produite lors de la supression du dossier.',
code: 400
}
exports.GETTING_FOLDER_ERROR = {
message: 'Une erreur s\'est produite lors de la récupération du dossier.',
code: 400
}
exports.MOVING_FOLDER_ERROR = {
message: 'Une erreur s\'est produite lors du déplacement du dossier.',
code: 400
}
exports.DUPLICATE_FOLDER_ERROR = {
message: 'Une erreur s\'est produite lors de la duplication du dossier.',
code: 400
}
exports.COPY_FOLDER_ERROR = {
message: 'Une erreur s\'est produite lors de la copie du dossier.',
code: 400
}
exports.NOT_IMPLEMENTED = {
message: 'Route not implemented yet!',
code: 400
}
// static ok(res, results) {200
// static badRequest(res, message) {400
// static unauthorized(res, message) {401
// static notFound(res, message) {404
// static serverError(res, message) {505

View file

@ -0,0 +1,129 @@
import type { AppErrorInfos } from "../middleware/AppError";
import { HttpStatusCode } from "../utils/http-status-codes";
export const UNAUTHORIZED_NO_TOKEN_GIVEN:AppErrorInfos = {
message: 'Accès refusé. Aucun jeton fourni.',
statusCode: HttpStatusCode.UNAUTHORIZED
};
export const UNAUTHORIZED_INVALID_TOKEN:AppErrorInfos = {
message: 'Accès refusé. Jeton invalide.',
statusCode: HttpStatusCode.UNAUTHORIZED
}
export const MISSING_REQUIRED_PARAMETER:AppErrorInfos = {
message: 'Paramètre requis manquant.',
statusCode: HttpStatusCode.BAD_REQUEST
}
export const USER_ALREADY_EXISTS:AppErrorInfos = {
message: 'L\'utilisateur existe déjà.',
statusCode: HttpStatusCode.BAD_REQUEST
}
export const USER_NOT_FOUND:AppErrorInfos = {
message: 'L\'utilisateur n\'existe pas.',
statusCode: HttpStatusCode.BAD_REQUEST
}
export const USER_CONTROLLER_NOT_INITIALIZED:AppErrorInfos = {
message: "Le controlleur d'utilisateur n'est pas initialisé",
statusCode: HttpStatusCode.INTERNAL_SERVER_ERROR
}
export const LOGIN_CREDENTIALS_ERROR:AppErrorInfos = {
message: 'L\'email et le mot de passe ne correspondent pas.',
statusCode: HttpStatusCode.BAD_REQUEST
}
export const GENERATE_PASSWORD_ERROR:AppErrorInfos = {
message: 'Une erreur s\'est produite lors de la création d\'un nouveau mot de passe.',
statusCode: HttpStatusCode.BAD_REQUEST
}
export const UPDATE_PASSWORD_ERROR:AppErrorInfos = {
message: 'Une erreur s\'est produite lors de la mise à jours du mot de passe.',
statusCode: HttpStatusCode.BAD_REQUEST
}
export const DELETE_USER_ERROR:AppErrorInfos = {
message: 'Une erreur s\'est produite lors de supression de l\'utilisateur.',
statusCode: HttpStatusCode.BAD_REQUEST
}
export const IMAGE_NOT_FOUND:AppErrorInfos = {
message: 'Nous n\'avons pas trouvé l\'image.',
statusCode: HttpStatusCode.NOT_FOUND
}
export const QUIZ_NOT_FOUND:AppErrorInfos = {
message: 'Aucun quiz portant cet identifiant n\'a été trouvé.',
statusCode: HttpStatusCode.NOT_FOUND
}
export const QUIZ_ALREADY_EXISTS:AppErrorInfos = {
message: 'Le quiz existe déja.',
statusCode: HttpStatusCode.BAD_REQUEST
}
export const UPDATE_QUIZ_ERROR:AppErrorInfos = {
message: 'Une erreur s\'est produite lors de la mise à jours du quiz.',
statusCode: HttpStatusCode.BAD_REQUEST
}
export const DELETE_QUIZ_ERROR:AppErrorInfos = {
message: 'Une erreur s\'est produite lors de la supression du quiz.',
statusCode: HttpStatusCode.BAD_REQUEST
}
export const GETTING_QUIZ_ERROR:AppErrorInfos = {
message: 'Une erreur s\'est produite lors de la récupération du quiz.',
statusCode: HttpStatusCode.BAD_REQUEST
}
export const MOVING_QUIZ_ERROR:AppErrorInfos = {
message: 'Une erreur s\'est produite lors du déplacement du quiz.',
statusCode: HttpStatusCode.BAD_REQUEST
}
export const DUPLICATE_QUIZ_ERROR:AppErrorInfos = {
message: 'Une erreur s\'est produite lors de la duplication du quiz.',
statusCode: HttpStatusCode.BAD_REQUEST
}
export const COPY_QUIZ_ERROR:AppErrorInfos = {
message: 'Une erreur s\'est produite lors de la copie du quiz.',
statusCode: HttpStatusCode.BAD_REQUEST
}
export const FOLDER_NOT_FOUND:AppErrorInfos = {
message: 'Aucun dossier portant cet identifiant n\'a été trouvé.',
statusCode: HttpStatusCode.NOT_FOUND
}
export const FOLDER_ALREADY_EXISTS:AppErrorInfos = {
message: 'Le dossier existe déja.',
statusCode: HttpStatusCode.BAD_REQUEST
}
export const UPDATE_FOLDER_ERROR:AppErrorInfos = {
message: 'Une erreur s\'est produite lors de la mise à jours du dossier.',
statusCode: HttpStatusCode.BAD_REQUEST
}
export const DELETE_FOLDER_ERROR:AppErrorInfos = {
message: 'Une erreur s\'est produite lors de la supression du dossier.',
statusCode: HttpStatusCode.BAD_REQUEST
}
export const GETTING_FOLDER_ERROR:AppErrorInfos = {
message: 'Une erreur s\'est produite lors de la récupération du dossier.',
statusCode: HttpStatusCode.BAD_REQUEST
}
export const MOVING_FOLDER_ERROR:AppErrorInfos = {
message: 'Une erreur s\'est produite lors du déplacement du dossier.',
statusCode: HttpStatusCode.BAD_REQUEST
}
export const DUPLICATE_FOLDER_ERROR:AppErrorInfos = {
message: 'Une erreur s\'est produite lors de la duplication du dossier.',
statusCode: HttpStatusCode.BAD_REQUEST
}
export const COPY_FOLDER_ERROR:AppErrorInfos = {
message: 'Une erreur s\'est produite lors de la copie du dossier.',
statusCode: HttpStatusCode.BAD_REQUEST
}
export const NOT_IMPLEMENTED:AppErrorInfos = {
message: 'Route not implemented yet!',
statusCode: HttpStatusCode.BAD_REQUEST
}
// static ok(res, results) {200
// static badRequest(res, message) {HttpStatusCode.BAD_REQUEST
// static unauthorized(res, message) {HttpStatusCode.UNAUTHORIZED
// static notFound(res, message) {HttpStatusCode.NOT_FOUND
// static serverError(res, message) {505

View file

@ -1,9 +1,11 @@
//controller //controller
const AppError = require('../middleware/AppError.js'); import AppError from '../middleware/AppError.js';
const { MISSING_REQUIRED_PARAMETER, NOT_IMPLEMENTED, FOLDER_NOT_FOUND, FOLDER_ALREADY_EXISTS, GETTING_FOLDER_ERROR, DELETE_FOLDER_ERROR, UPDATE_FOLDER_ERROR, MOVING_FOLDER_ERROR, DUPLICATE_FOLDER_ERROR, COPY_FOLDER_ERROR } = require('../constants/errorCodes'); import { MISSING_REQUIRED_PARAMETER, NOT_IMPLEMENTED, FOLDER_NOT_FOUND, FOLDER_ALREADY_EXISTS, GETTING_FOLDER_ERROR, DELETE_FOLDER_ERROR, UPDATE_FOLDER_ERROR, MOVING_FOLDER_ERROR, DUPLICATE_FOLDER_ERROR, COPY_FOLDER_ERROR } from '../constants/errorCodes';
import Folders from '../models/folders.js';
// controllers must use arrow functions to bind 'this' to the class instance in order to access class properties as callbacks in Express // controllers must use arrow functions to bind 'this' to the class instance in order to access class properties as callbacks in Express
class FoldersController { class FoldersController {
folders:Folders
constructor(foldersModel) { constructor(foldersModel) {
this.folders = foldersModel; this.folders = foldersModel;
@ -258,6 +260,4 @@ class FoldersController {
} }
export default FoldersController
module.exports = FoldersController;

View file

@ -1,7 +1,9 @@
const AppError = require('../middleware/AppError.js'); import AppError from '../middleware/AppError.js';
const { MISSING_REQUIRED_PARAMETER, IMAGE_NOT_FOUND } = require('../constants/errorCodes'); import { MISSING_REQUIRED_PARAMETER, IMAGE_NOT_FOUND } from '../constants/errorCodes.js';
import Images from '../models/images.js';
class ImagesController { class ImagesController {
images:Images
constructor(imagesModel) { constructor(imagesModel) {
this.images = imagesModel; this.images = imagesModel;
@ -52,4 +54,4 @@ class ImagesController {
} }
module.exports = ImagesController; export default ImagesController

View file

@ -1,9 +1,13 @@
const emailer = require('../config/email.js'); import emailer from '../config/email.js';
const AppError = require('../middleware/AppError.js'); import AppError from'../middleware/AppError.js';
const { MISSING_REQUIRED_PARAMETER, NOT_IMPLEMENTED, QUIZ_NOT_FOUND, FOLDER_NOT_FOUND, QUIZ_ALREADY_EXISTS, GETTING_QUIZ_ERROR, DELETE_QUIZ_ERROR, UPDATE_QUIZ_ERROR, MOVING_QUIZ_ERROR, DUPLICATE_QUIZ_ERROR, COPY_QUIZ_ERROR } = require('../constants/errorCodes'); import { MISSING_REQUIRED_PARAMETER, NOT_IMPLEMENTED, QUIZ_NOT_FOUND, FOLDER_NOT_FOUND, QUIZ_ALREADY_EXISTS, GETTING_QUIZ_ERROR, DELETE_QUIZ_ERROR, UPDATE_QUIZ_ERROR, MOVING_QUIZ_ERROR, DUPLICATE_QUIZ_ERROR, COPY_QUIZ_ERROR } from'../constants/errorCodes.js';
import Folders from '../models/folders.js';
import Quiz from '../models/quiz.js';
class QuizController { class QuizController {
folders:Folders
quizzes:Quiz
constructor(quizModel, foldersModel) { constructor(quizModel, foldersModel) {
this.folders = foldersModel; this.folders = foldersModel;
@ -207,7 +211,7 @@ class QuizController {
} }
// Call the method from the Quiz model to delete quizzes by folder ID // Call the method from the Quiz model to delete quizzes by folder ID
await Quiz.deleteQuizzesByFolderId(folderId); await this.quizzes.deleteQuizzesByFolderId(folderId);
return res.status(200).json({ return res.status(200).json({
message: 'Quizzes deleted successfully.' message: 'Quizzes deleted successfully.'
@ -314,4 +318,4 @@ class QuizController {
} }
module.exports = QuizController; export default QuizController

View file

@ -1,17 +1,16 @@
import { GlideClient, GlideClientConfiguration } from '@valkey/valkey-glide'; import { GlideClient } from '@valkey/valkey-glide';
import { import {
RoomInfo, RoomInfo,
RoomOptions, RoomOptions,
ProviderType, ProviderType,
ProviderConfig ProviderConfig
} from '../../types/room'; } from '../../types/room';
import { BaseRoomProvider } from './providers/base-provider'; import { BaseRoomProvider } from '../roomsProviders/base-provider';
import { ClusterRoomProvider } from './providers/cluster-provider'; import { ClusterRoomProvider } from '../roomsProviders/cluster-provider';
import { DockerRoomProvider } from './providers/docker-provider'; //import { DockerRoomProvider } from '../roomsProviders/docker-provider';
import { KubernetesRoomProvider } from './providers/kubernetes-provider'; //import { KubernetesRoomProvider } from '../roomsProviders/kubernetes-provider';
interface RoomManagerOptions { interface RoomManagerOptions {
valkeyConfig?: GlideClientConfiguration;
provider?: ProviderType; provider?: ProviderType;
providerOptions?: ProviderConfig; providerOptions?: ProviderConfig;
} }
@ -20,8 +19,8 @@ export class RoomManager {
private valkey: GlideClient; private valkey: GlideClient;
private provider: BaseRoomProvider<RoomInfo>; private provider: BaseRoomProvider<RoomInfo>;
constructor(options: RoomManagerOptions = {}) { constructor(options: RoomManagerOptions = {}, valkeyClient:GlideClient) {
this.valkey = new GlideClient(); this.valkey = valkeyClient;
this.provider = this.createProvider( this.provider = this.createProvider(
options.provider || process.env.ROOM_PROVIDER as ProviderType || 'cluster', options.provider || process.env.ROOM_PROVIDER as ProviderType || 'cluster',
options.providerOptions options.providerOptions
@ -36,11 +35,13 @@ export class RoomManager {
): BaseRoomProvider<RoomInfo> { ): BaseRoomProvider<RoomInfo> {
switch (type) { switch (type) {
case 'cluster': case 'cluster':
return new ClusterRoomProvider(this.redis, options); return new ClusterRoomProvider(this.valkey, options);
/*
case 'docker': case 'docker':
return new DockerRoomProvider(this.redis, options); return new DockerRoomProvider(this.redis, options);
case 'kubernetes': case 'kubernetes':
return new KubernetesRoomProvider(this.redis, options); return new KubernetesRoomProvider(this.redis, options);
*/
default: default:
throw new Error(`Unknown provider type: ${type}`); throw new Error(`Unknown provider type: ${type}`);
} }
@ -72,4 +73,6 @@ export class RoomManager {
private generateRoomId(): string { private generateRoomId(): string {
return `room-${Math.random().toString(36).substr(2, 9)}`; return `room-${Math.random().toString(36).substr(2, 9)}`;
} }
} }
module.exports = RoomManager;

View file

@ -1,11 +1,13 @@
const emailer = require('../config/email.js'); import emailer from'../config/email.js';
const jwt = require('../middleware/jwtToken.js'); import jwt from'../middleware/jwtToken.js';
const AppError = require('../middleware/AppError.js'); import AppError from'../middleware/AppError.js';
const { MISSING_REQUIRED_PARAMETER, LOGIN_CREDENTIALS_ERROR, GENERATE_PASSWORD_ERROR, UPDATE_PASSWORD_ERROR, DELETE_USER_ERROR } = require('../constants/errorCodes'); import { MISSING_REQUIRED_PARAMETER, LOGIN_CREDENTIALS_ERROR, GENERATE_PASSWORD_ERROR, UPDATE_PASSWORD_ERROR, DELETE_USER_ERROR, USER_NOT_FOUND,USER_CONTROLLER_NOT_INITIALIZED } from'../constants/errorCodes';
import Users from '../models/users.js'
// controllers must use arrow functions to bind 'this' to the class instance in order to access class properties as callbacks in Express // controllers must use arrow functions to bind 'this' to the class instance in order to access class properties as callbacks in Express
class UsersController { class UsersController {
users:Users
constructor(userModel) { constructor(userModel) {
this.users = userModel; this.users = userModel;
@ -20,7 +22,7 @@ class UsersController {
} }
if (!this.users) { if (!this.users) {
throw new AppError('Users model not found'); throw new AppError(USER_NOT_FOUND);
} }
await this.users.register(email, password); await this.users.register(email, password);
@ -43,7 +45,7 @@ class UsersController {
} }
if (!this) { if (!this) {
throw new AppError('UsersController not initialized'); throw new AppError(USER_CONTROLLER_NOT_INITIALIZED);
} }
const user = await this.users.login(email, password); const user = await this.users.login(email, password);
@ -143,4 +145,4 @@ class UsersController {
} }
} }
module.exports = UsersController; export default UsersController;

View file

@ -1,8 +0,0 @@
class AppError extends Error {
constructor (errorCode) {
super(errorCode.message)
this.statusCode = errorCode.code;
}
}
module.exports = AppError;

View file

@ -0,0 +1,14 @@
import { HttpStatusCode } from "../utils/http-status-codes"
export type AppErrorInfos = {
message:string
statusCode:number|HttpStatusCode
}
export default class AppError extends Error {
statusCode:number
constructor (infos:AppErrorInfos) {
super(infos.message)
this.statusCode = infos.statusCode
}
}

View file

@ -1,5 +1,6 @@
const AppError = require("./AppError"); import AppError from './AppError'
const fs = require('fs'); import fs from 'fs';
import { HttpStatusCode } from '../utils/http-status-codes';
const errorHandler = (error, req, res, next) => { const errorHandler = (error, req, res, next) => {
console.log("ERROR", error); console.log("ERROR", error);
@ -12,7 +13,7 @@ const errorHandler = (error, req, res, next) => {
} }
logError(error.stack); logError(error.stack);
return res.status(505).send("Oups! We screwed up big time. ┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻"); return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR).send("Oups! We screwed up big time. ┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻");
} }
const logError = (error) => { const logError = (error) => {
@ -21,4 +22,4 @@ const logError = (error) => {
log_file.write(time + '\n' + error + '\n\n'); log_file.write(time + '\n' + error + '\n\n');
} }
module.exports = errorHandler; export default errorHandler

View file

@ -1,7 +1,7 @@
const jwt = require('jsonwebtoken') import jwt from 'jsonwebtoken'
const dotenv = require('dotenv') import dotenv from 'dotenv'
const AppError = require('./AppError.js'); import AppError from './AppError.ts';
const { UNAUTHORIZED_NO_TOKEN_GIVEN, UNAUTHORIZED_INVALID_TOKEN } = require('../constants/errorCodes'); import { UNAUTHORIZED_NO_TOKEN_GIVEN, UNAUTHORIZED_INVALID_TOKEN } from '../constants/errorCodes';
dotenv.config(); dotenv.config();
@ -34,4 +34,4 @@ class Token {
} }
} }
module.exports = new Token(); export default new Token()

View file

@ -1,8 +1,14 @@
//model //model
const ObjectId = require('mongodb').ObjectId; import type { DBConnection } from '../config/db';
const { generateUniqueTitle } = require('./utils'); import type {Quiz} from './quiz'
import {ObjectId} from 'mongodb'
import { generateUniqueTitle } from '../utils/models-utils';
class Folders { class Folders {
db: DBConnection
quizModel:Quiz
constructor(db, quizModel) { constructor(db, quizModel) {
this.db = db; this.db = db;
this.quizModel = quizModel; this.quizModel = quizModel;
@ -57,7 +63,7 @@ class Folders {
const folder = await foldersCollection.findOne({ _id: ObjectId.createFromHexString(folderId) }); const folder = await foldersCollection.findOne({ _id: ObjectId.createFromHexString(folderId) });
return folder.userId; return folder?.userId;
} }
// finds all quizzes in a folder // finds all quizzes in a folder
@ -151,7 +157,8 @@ class Folders {
async copy(folderId, userId) { async copy(folderId, userId) {
const sourceFolder = await this.getFolderWithContent(folderId); const sourceFolder = await this.getFolderWithContent(folderId);
const newFolderId = await this.create(sourceFolder.title, userId);
const newFolderId = await this.create(sourceFolder[0].title, userId);
if (!newFolderId) { if (!newFolderId) {
throw new Error('Failed to create a new folder.'); throw new Error('Failed to create a new folder.');
} }
@ -192,4 +199,4 @@ class Folders {
} }
module.exports = Folders; export default Folders

View file

@ -1,15 +1,16 @@
//const db = require('../config/db.js') import type {DBConnection} from '../config/db.js'
const { ObjectId } = require('mongodb'); import { ObjectId } from 'mongodb';
class Images { class Images {
db:DBConnection
constructor(db) { constructor(db) {
this.db = db; this.db = db;
} }
async upload(file, userId) { async upload(file, userId) {
await db.connect() await this.db.connect()
const conn = db.getConnection(); const conn = this.db.getConnection();
const imagesCollection = conn.collection('images'); const imagesCollection = conn.collection('images');
@ -27,8 +28,8 @@ class Images {
} }
async get(id) { async get(id) {
await db.connect() await this.db.connect()
const conn = db.getConnection(); const conn = this.db.getConnection();
const imagesCollection = conn.collection('images'); const imagesCollection = conn.collection('images');
@ -45,4 +46,4 @@ class Images {
} }
module.exports = Images; export default Images

View file

@ -1,7 +1,10 @@
const { ObjectId } = require('mongodb'); import type { DBConnection } from '../config/db';
const { generateUniqueTitle } = require('./utils'); import { ObjectId } from 'mongodb';
import { generateUniqueTitle } from '../utils/models-utils'
class Quiz { export class Quiz {
db: DBConnection
constructor(db) { constructor(db) {
// console.log("Quiz constructor: db", db) // console.log("Quiz constructor: db", db)
@ -44,7 +47,7 @@ class Quiz {
const quiz = await quizCollection.findOne({ _id: ObjectId.createFromHexString(quizId) }); const quiz = await quizCollection.findOne({ _id: ObjectId.createFromHexString(quizId) });
return quiz.userId; return quiz?.userId;
} }
async getContent(quizId) { async getContent(quizId) {
@ -151,5 +154,4 @@ class Quiz {
} }
} }
export default Quiz;
module.exports = Quiz;

View file

@ -1,9 +1,14 @@
//user //user
const bcrypt = require('bcrypt'); import bcrypt from 'bcrypt';
const AppError = require('../middleware/AppError.js'); import AppError from '../middleware/AppError.js'
const { USER_ALREADY_EXISTS } = require('../constants/errorCodes'); import { USER_ALREADY_EXISTS } from '../constants/errorCodes.js';
import type { DBConnection } from '../config/db.js';
import Folders from './folders.js';
class Users { class Users {
db:DBConnection
folders:Folders
constructor(db, foldersModel) { constructor(db, foldersModel) {
// console.log("Users constructor: db", db) // console.log("Users constructor: db", db)
this.db = db; this.db = db;
@ -122,4 +127,4 @@ class Users {
} }
module.exports = Users; export default Users

2404
server/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,17 +3,18 @@
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "app.js", "main": "app.js",
"lockfileVersion": 2,
"type": "module",
"scripts": { "scripts": {
"build": "webpack --config webpack.config.js", "build": "tsc",
"start": "node app.js", "start": "node dist/app.js",
"dev": "cross-env NODE_ENV=development nodemon app.js", "dev": "ts-node app.ts",
"test": "jest --colors" "watch": "tsc -w"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/express": "^5.0.0",
"@valkey/valkey-glide": "^1.1.0", "@valkey/valkey-glide": "^1.1.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"cors": "^2.8.5", "cors": "^2.8.5",
@ -27,12 +28,19 @@
"socket.io-client": "^4.7.2" "socket.io-client": "^4.7.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.8.4", "@types/body-parser": "^1.19.5",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/multer": "^1.4.12",
"@types/node": "^22.9.0",
"@types/socket.io": "^3.0.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-mock": "^29.7.0", "jest-mock": "^29.7.0",
"nodemon": "^3.0.1", "nodemon": "^3.0.1",
"supertest": "^6.3.4" "supertest": "^6.3.4",
"ts-node": "^10.9.2",
"typescript": "^5.6.3"
}, },
"engines": { "engines": {
"node": "18.x" "node": "18.x"

View file

@ -1,7 +1,9 @@
const express = require('express'); import express, { Response, Request } from "express";
import jwt from '../middleware/jwtToken.js';
import {controllers} from '../app.js'
const folders = controllers.folders
const router = express.Router(); const router = express.Router();
const jwt = require('../middleware/jwtToken.js');
const folders = require('../app.js').folders;
router.post("/create", jwt.authenticate, folders.create); router.post("/create", jwt.authenticate, folders.create);
router.get("/getUserFolders", jwt.authenticate, folders.getUserFolders); router.get("/getUserFolders", jwt.authenticate, folders.getUserFolders);
@ -14,7 +16,7 @@ router.post("/duplicate", jwt.authenticate, folders.duplicate);
router.post("/copy/:folderId", jwt.authenticate, folders.copy); router.post("/copy/:folderId", jwt.authenticate, folders.copy);
module.exports = router; export default router
// export also folders (the controller) // export also folders (the controller)
module.exports.folders = folders; module.exports.folders = folders;

View file

@ -1,15 +1,16 @@
const express = require('express'); import express, { Response, Request } from "express";
const router = express.Router(); import jwt from '../middleware/jwtToken.js';
const images = require('../app.js').images; import {controllers} from '../app.js'
import multer from 'multer';
const jwt = require('../middleware/jwtToken.js'); const images = controllers.images
const router = express.Router();
// For getting the image out of the form data // For getting the image out of the form data
const multer = require('multer');
const storage = multer.memoryStorage(); const storage = multer.memoryStorage();
const upload = multer({ storage: storage }); const upload = multer({ storage: storage });
router.post("/upload", jwt.authenticate, upload.single('image'), images.upload); router.post("/upload", jwt.authenticate, upload.single('image'), images.upload);
router.get("/get/:id", images.get); router.get("/get/:id", images.get);
module.exports = router; export default router

View file

@ -1,7 +1,9 @@
const express = require('express'); import express, { Response, Request } from "express";
import jwt from '../middleware/jwtToken.js';
import {controllers} from '../app.js'
const quizzes = controllers.quizzes
const router = express.Router(); const router = express.Router();
const quizzes = require('../app.js').quizzes;
const jwt = require('../middleware/jwtToken.js');
if (!quizzes) { if (!quizzes) {
console.error("quizzes is not defined"); console.error("quizzes is not defined");
@ -19,4 +21,4 @@ router.put("/Share", jwt.authenticate, quizzes.share);
router.get("/getShare/:quizId", jwt.authenticate, quizzes.getShare); router.get("/getShare/:quizId", jwt.authenticate, quizzes.getShare);
router.post("/receiveShare", jwt.authenticate, quizzes.receiveShare); router.post("/receiveShare", jwt.authenticate, quizzes.receiveShare);
module.exports = router; export default router

View file

@ -1 +1,13 @@
import {express} from 'express' import express, { Response, Request } from "express";
import jwt from '../middleware/jwtToken.js';
import {controllers} from '../app.js'
const roomsController = controllers.rooms
const router = express.Router();
router.get("/", jwt.authenticate, async(req:Request,res:Response)=>{
const data = await roomsController.listRooms();
res.json(data)
});
export default router

View file

@ -1,7 +1,9 @@
const express = require('express'); import express, { Response, Request } from "express";
import jwt from '../middleware/jwtToken.js';
import {controllers} from '../app.js'
const users = controllers.users
const router = express.Router(); const router = express.Router();
const users = require('../app.js').users;
const jwt = require('../middleware/jwtToken.js');
router.post("/register", users.register); router.post("/register", users.register);
router.post("/login", users.login); router.post("/login", users.login);
@ -9,4 +11,4 @@ router.post("/reset-password", users.resetPassword);
router.post("/change-password", jwt.authenticate, users.changePassword); router.post("/change-password", jwt.authenticate, users.changePassword);
router.post("/delete-user", jwt.authenticate, users.delete); router.post("/delete-user", jwt.authenticate, users.delete);
module.exports = router; export default router

View file

@ -1,7 +1,9 @@
import { Server as SocketIOServer } from 'socket.io';
const MAX_USERS_PER_ROOM = 60; const MAX_USERS_PER_ROOM = 60;
const MAX_TOTAL_CONNECTIONS = 2000; const MAX_TOTAL_CONNECTIONS = 2000;
const setupWebsocket = (io) => { const setupWebsocket = (io: SocketIOServer) => {
let totalConnections = 0; let totalConnections = 0;
io.on("connection", (socket) => { io.on("connection", (socket) => {
@ -46,7 +48,7 @@ const setupWebsocket = (io) => {
socket.on("join-room", ({ enteredRoomName, username }) => { socket.on("join-room", ({ enteredRoomName, username }) => {
if (io.sockets.adapter.rooms.has(enteredRoomName)) { if (io.sockets.adapter.rooms.has(enteredRoomName)) {
const clientsInRoom = const clientsInRoom =
io.sockets.adapter.rooms.get(enteredRoomName).size; io.sockets.adapter.rooms.get(enteredRoomName)?.size ?? 0;
if (clientsInRoom <= MAX_USERS_PER_ROOM) { if (clientsInRoom <= MAX_USERS_PER_ROOM) {
const newStudent = { const newStudent = {
@ -122,4 +124,4 @@ const setupWebsocket = (io) => {
}; };
}; };
module.exports = { setupWebsocket }; export default setupWebsocket

29
server/tsconfig.json Normal file
View file

@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "es2022",
"module": "es2022",
"lib": ["es2017", "esnext.asynciterable"],
"skipLibCheck": true,
"sourceMap": true,
"outDir": "./dist",
"moduleResolution": "node",
"removeComments": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
"baseUrl": ".",
"rootDir": "."
},
"exclude": ["node_modules"],
"include": ["./**/*.ts"]
}

View file

@ -0,0 +1,41 @@
export enum HttpStatusCode {
// 1xx Informational
CONTINUE = 100,
SWITCHING_PROTOCOLS = 101,
PROCESSING = 102,
// 2xx Success
OK = 200,
CREATED = 201,
ACCEPTED = 202,
NON_AUTHORITATIVE_INFORMATION = 203,
NO_CONTENT = 204,
RESET_CONTENT = 205,
PARTIAL_CONTENT = 206,
// 3xx Redirection
MULTIPLE_CHOICES = 300,
MOVED_PERMANENTLY = 301,
FOUND = 302,
SEE_OTHER = 303,
NOT_MODIFIED = 304,
USE_PROXY = 305,
TEMPORARY_REDIRECT = 307,
PERMANENT_REDIRECT = 308,
// 4xx Client errors
BAD_REQUEST = 400,
UNAUTHORIZED = 401,
PAYMENT_REQUIRED = 402,
FORBIDDEN = 403,
NOT_FOUND = 404,
METHOD_NOT_ALLOWED = 405,
NOT_ACCEPTABLE = 406,
// 5xx Client errors
INTERNAL_SERVER_ERROR = 500,
NOT_IMPLEMENTED = 501,
BAD_GATEWAY = 502,
SERVICE_UNAVAILABLE = 503,
GATEWAY_TIMEOUT = 504,
}

View file

@ -1,5 +1,7 @@
// utils.js // utils.js
async function generateUniqueTitle(baseTitle, existsCallback) { export async function generateUniqueTitle(baseTitle:string, existsCallback:CallableFunction) {
console.log(`generateUniqueTitle(${baseTitle})`); console.log(`generateUniqueTitle(${baseTitle})`);
let newTitle = baseTitle; let newTitle = baseTitle;
let counter = 1; let counter = 1;
@ -12,7 +14,7 @@ async function generateUniqueTitle(baseTitle, existsCallback) {
} }
// If the base title does not end with a parentheses expression, start with "(1)" // If the base title does not end with a parentheses expression, start with "(1)"
if (!match[2]) { if (match != null && match[2] != null) {
newTitle = `${baseTitle} (${counter})`; newTitle = `${baseTitle} (${counter})`;
} else { } else {
// else increment the counter in the parentheses expression as a first try // else increment the counter in the parentheses expression as a first try
@ -28,8 +30,4 @@ async function generateUniqueTitle(baseTitle, existsCallback) {
} }
return newTitle; return newTitle;
} }
module.exports = {
generateUniqueTitle
};

View file

View file

@ -1,9 +1,4 @@
export interface ValkeyBasicConfig{ export interface ValkeyBasicConfig{
host:string, host:string,
port:number port:number
}
const valkeyDefaults : ValkeyBasicConfig = {
host:'localhost',
port:6379
} }