From 048a590e74eac3447372d8abd66025ad355ebd7e Mon Sep 17 00:00:00 2001 From: Arthur Dodin Date: Mon, 27 Apr 2026 19:01:40 +0200 Subject: [PATCH 01/47] feat: unified upload system --- backend/server.ts | 2 +- .../src/controllers/im_export.controller.ts | 68 +++- backend/src/controllers/news.controller.ts | 105 ++++-- backend/src/middlewares/multer.middleware.ts | 85 +++-- backend/src/routes/im_export.routes.ts | 13 +- backend/src/routes/news.routes.ts | 6 +- backend/src/utils/responses.ts | 13 +- backend/src/utils/uploadDocuments.ts | 84 +++++ .../components/Admin/adminExportImport.tsx | 250 +------------ .../src/components/Admin/adminFileImport.tsx | 331 ++++++++++++++++++ frontend/src/components/Admin/adminNews.tsx | 153 ++++---- .../components/Plannings/planningSection.tsx | 4 +- .../components/WEI_SDI_Food/foodSection.tsx | 2 +- frontend/src/components/news/newsSection.tsx | 2 +- frontend/src/interfaces/import.interface.ts | 5 + .../services/requests/im_export.service.ts | 26 +- .../src/services/requests/news.service.ts | 31 +- 17 files changed, 779 insertions(+), 401 deletions(-) create mode 100644 backend/src/utils/uploadDocuments.ts create mode 100644 frontend/src/components/Admin/adminFileImport.tsx create mode 100644 frontend/src/interfaces/import.interface.ts diff --git a/backend/server.ts b/backend/server.ts index f6b425b..d691fef 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -62,7 +62,7 @@ async function startServer() { app.use('/api/discord', authenticateUser, discordRoutes); app.use('/api/tent', authenticateUser, tentRoutes); app.use('/api/bus', authenticateUser, busRoutes); - app.use("/api/uploads/imgnews", express.static(path.join(__dirname, "/uploads/imgnews"))); + app.use("/api/uploads/news", express.static(path.join(__dirname, "/uploads/news"))); app.use("/api/uploads/foodmenu", express.static(path.join(__dirname, "/uploads/foodmenu"))); app.use("/api/uploads/plannings", express.static(path.join(__dirname, "/uploads/plannings"))); app.use("/api/exports/bus", express.static(path.join(__dirname, "/exports/bus"))); diff --git a/backend/src/controllers/im_export.controller.ts b/backend/src/controllers/im_export.controller.ts index 3e5978f..60ae264 100644 --- a/backend/src/controllers/im_export.controller.ts +++ b/backend/src/controllers/im_export.controller.ts @@ -8,6 +8,7 @@ import * as team_service from "../services/team.service"; import * as user_service from '../services/user.service'; import { Error, Ok } from "../utils/responses"; import { spreadsheet_id } from "../utils/secret"; +import { getLatestUploadedDocument, isSafeUploadSegment, removeUploadedDocuments, toUploadedDocumentStatus } from "../utils/uploadDocuments"; export const exportAllDataToSheets = async (req: Request, res: Response) => { try { @@ -108,6 +109,7 @@ export const updateFoodMenu = async (req: Request, res: Response) => { const file = req.file; try { + // Supprimer l'ancien Menu si un nouveau est uploadé if (file) { const targetDir = path.join(__dirname, "../../foodmenu"); @@ -116,6 +118,7 @@ export const updateFoodMenu = async (req: Request, res: Response) => { fs.rmSync(targetDir, { recursive: true, force: true }); fs.mkdirSync(targetDir); } + } Ok(res, { msg: "Menu mis à jour avec succès" }); @@ -127,19 +130,7 @@ export const updateFoodMenu = async (req: Request, res: Response) => { }; export const updatePlannings = async (req: Request, res: Response) => { - const file = req.file; - try { - // Supprimer l'ancien Planning si un nouveau est uploadé - if (file) { - const targetDir = path.join(__dirname, "../../plannings"); - - if (fs.existsSync(targetDir)) { - fs.rmSync(targetDir, { recursive: true, force: true }); - fs.mkdirSync(targetDir); - } - } - Ok(res, { msg: "Planning mis à jour avec succès" }); return; } catch (err) { @@ -157,3 +148,56 @@ export const exportUsersCSV = async (req: Request, res: Response) => { Error(res, { msg: "Erreur lors de l'export CSV" }); } }; + +export const getUploadedDocumentStatus = async (req: Request, res: Response) => { + const { category, item } = req.params; + + if (!isSafeUploadSegment(category) || !isSafeUploadSegment(item)) { + Error(res, { msg: "Paramètres invalides" }); + return; + } + + try { + const latestDocument = await getLatestUploadedDocument(category, item); + + Ok(res, { + data: toUploadedDocumentStatus(category, latestDocument), + }); + } catch (err: any) { + if (err?.code === "ENOENT") { + Ok(res, { + data: toUploadedDocumentStatus(category, null), + }); + return; + } + + console.error(err); + Error(res, { msg: "Erreur lors de la vérification du document" }); + } +}; + +export const deleteDocument = async (req: Request, res: Response) => { + const { category, item } = req.params; + + if (!isSafeUploadSegment(category) || !isSafeUploadSegment(item)) { + Error(res, { msg: "Paramètres invalides" }); + return; + } + + try { + const deletedCount = await removeUploadedDocuments(category, item); + + if (deletedCount === 0) { + return Ok(res, { msg: "Aucun document à supprimer" }); + } + + Ok(res, {}); + } catch (err: any) { + if (err?.code === "ENOENT") { + Ok(res, {}); + return; + } + + Error(res, { msg: "Erreur lors de la vérification du document" }); + } +}; diff --git a/backend/src/controllers/news.controller.ts b/backend/src/controllers/news.controller.ts index a910932..0b97ac2 100644 --- a/backend/src/controllers/news.controller.ts +++ b/backend/src/controllers/news.controller.ts @@ -7,19 +7,72 @@ import * as user_service from '../services/user.service'; import * as template from "../utils/emailtemplates"; import { Error, Ok } from "../utils/responses"; +const toStoredUploadPath = (imageUrl: string) => { + if (!imageUrl) { + return null; + } + + let normalized = imageUrl.trim(); + if (!normalized) { + return null; + } + + // Accept absolute URLs and keep only the pathname part. + if (normalized.startsWith("http://") || normalized.startsWith("https://")) { + try { + normalized = new URL(normalized).pathname; + } catch { + return null; + } + } + + if (normalized.startsWith("/api/")) { + normalized = normalized.slice(4); + } + + if (!normalized.startsWith("/uploads/")) { + return null; + } + + return normalized; +}; + +const resolveStoredImagePath = (imageUrl: string) => { + const storedPath = toStoredUploadPath(imageUrl); + if (!storedPath) { + return null; + } + + return path.resolve(process.cwd(), storedPath.replace(/^\//, "")); +}; + +const deleteImageIfExists = (imageUrl: string) => { + const imagePath = resolveStoredImagePath(imageUrl); + if (!imagePath) { + return; + } + + if (fs.existsSync(imagePath)) { + fs.unlinkSync(imagePath); + } +}; + export const createNews = async (req: Request, res: Response) => { - const { title, description, type, published, target } = req.body; + const { title, description, type, published, target, image_url } = req.body; const file = req.file; try { - const image_url = file ? `/api/uploads/imgnews/${file.filename}` : undefined; + const resolvedImageUrl = file + ? `/uploads/news/${file.filename}` + : image_url; + const news = await news_service.createNews( title, description, type, - published, + published === true || published === "true", target, - image_url); + resolvedImageUrl); Ok(res, { msg: "Actu créée avec succès", data: news }); } catch (err) { console.error(err); @@ -103,41 +156,53 @@ export const deleteNews = async (req: Request, res: Response) => { try { const existing = await news_service.getNewsById(Number(newsId)); if (existing?.image_url) { - const imagePath = path.join(__dirname, "../../", existing.image_url); - if (fs.existsSync(imagePath)) { - fs.unlinkSync(imagePath); - } + deleteImageIfExists(existing.image_url); } + await news_service.deleteNews(Number(newsId)); Ok(res, { msg: "Actus supprimée avec succès !" }); - ; - } catch { + } catch (err) { + console.error(err); Error(res, { msg: "Erreur lors de la suppression de l'actus" }); } }; export const updateNews = async (req: Request, res: Response) => { - const { id, title, description, type, target } = req.body; + const { id, title, description, type, target, image_url } = req.body; const file = req.file; - const image_url = file ? `/api/uploads/imgnews/${file.filename}` : undefined; + const hasImageUrlField = Object.prototype.hasOwnProperty.call(req.body, "image_url"); + const resolvedImageUrl = file + ? `/uploads/news/${file.filename}` + : hasImageUrlField + ? (image_url ?? null) + : undefined; try { const existing = await news_service.getNewsById(Number(id)); if (!existing) { Error(res, { msg: "Actu introuvable" }); + return; } - // Supprimer l'ancienne image si une nouvelle est uploadée - if (file && existing.image_url) { - const oldPath = path.join(__dirname, "../../", existing.image_url); - if (fs.existsSync(oldPath)) { - fs.unlinkSync(oldPath); - } + const shouldReplaceImage = typeof resolvedImageUrl === "string"; + const shouldRemoveImage = resolvedImageUrl === null; + + // Supprimer l'ancienne image si elle est remplacée ou explicitement supprimée. + if (existing.image_url && ((shouldReplaceImage && existing.image_url !== resolvedImageUrl) || shouldRemoveImage)) { + deleteImageIfExists(existing.image_url); } - const updates: any = { title, description, type, target }; - if (image_url) updates.image_url = image_url; + const updates: { + title: string; + description: string; + type: string; + target: string; + image_url?: string | null; + } = { title, description, type, target }; + if (resolvedImageUrl !== undefined) { + updates.image_url = resolvedImageUrl; + } const updated = await news_service.updateNews(Number(id), updates); diff --git a/backend/src/middlewares/multer.middleware.ts b/backend/src/middlewares/multer.middleware.ts index 1ce4a37..6f4baac 100644 --- a/backend/src/middlewares/multer.middleware.ts +++ b/backend/src/middlewares/multer.middleware.ts @@ -3,13 +3,29 @@ import fs from "fs/promises"; import multer from "multer"; import path from "path"; import { Error } from "../utils/responses"; +import { isSafeUploadSegment, removeUploadedDocuments } from "../utils/uploadDocuments"; -export const createUploadMiddleware = ( - relativeUploadDir: string, - modifiedName: boolean = true -) => { - const uploadPath = path.resolve(process.cwd(), relativeUploadDir); +export enum MIMEType { + PDF = "application/pdf", + PNG = "image/png", + JPEG = "image/jpeg", +} +const acceptedMIMETypesByItem: Record> = { + foodmenu: { + menu: [MIMEType.PDF], + }, + news: {}, + plannings: { + tc: [MIMEType.PDF], + bacheloria: [MIMEType.PDF], + fise: [MIMEType.PDF], + fisea: [MIMEType.PDF], + master: [MIMEType.PDF], + }, +}; + +export const createUploadMiddleware = () => { // On stocke d'abord en mémoire pour vérifier le type réel const storage = multer.memoryStorage(); @@ -24,46 +40,73 @@ export const createUploadMiddleware = ( const verifyAndSave = async ( req: Request, res: Response, - next: NextFunction + next: NextFunction, ) => { try { if (!req.file) return Error(res, { msg: "Aucun fichier reçu" }); + + const category = req.params.category; + const item = req.params.item; + + if (!category || !item) { + return Error(res, { msg: "Catégorie ou item manquant" }); + } + + if (!isSafeUploadSegment(category) || !isSafeUploadSegment(item)) { + return Error(res, { msg: "Paramètres invalides" }); + } + const { originalname, buffer } = req.file; // Vérif du vrai type const { fileTypeFromBuffer } = await import("file-type"); const detected = await fileTypeFromBuffer(buffer); - console.log( - `[UPLOAD] Type détecté: ${detected?.mime || "inconnu"}` - ); - const isImage = detected?.mime?.startsWith("image/"); - const isPDF = detected?.mime === "application/pdf"; + const detectedMime = detected?.mime as MIMEType | undefined; + + const acceptedTypes = category === "news" ? [MIMEType.PNG, MIMEType.JPEG] : acceptedMIMETypesByItem[category]?.[item]; + - if (!isImage && !isPDF) { - Error(res, { msg: "Seules les images et les PDF sont autorisés" }); + + if (!acceptedTypes) { + return Error(res, { msg: "Catégorie ou item inconnu" }); + } + + const isAccepted = !!detectedMime && acceptedTypes.includes(detectedMime); + + if (!isAccepted) { + Error(res, { msg: "Type de fichier non autorisé" }); return; } + const uploadPath = path.resolve(process.cwd(), "uploads", category); + // Création dossier si nécessaire await fs.mkdir(uploadPath, { recursive: true }); + if (category === "news") { + if (!/^\d+$/.test(item)) { + return Error(res, { msg: "Le nom du fichier doit être composé uniquement de chiffres." }); + } + + const currentEntries = await fs.readdir(uploadPath); + if (currentEntries.length >= 100) { + return Error(res, { msg: "Le nombre maximal d'images d'actualités a été atteind." }); + } + } + + // Supprime tout document existant avec le même basename, quelle que soit l'extension. + await removeUploadedDocuments(category, item); + const ext = path.extname(originalname); - const baseName = path.basename(originalname, ext); - const timestamp = Date.now(); - const finalName = modifiedName - ? `${baseName}-${timestamp}${ext}` - : originalname; + const finalName = `${item}${ext}`; const finalPath = path.join(uploadPath, finalName); // Sauvegarde du fichier sur disque await fs.writeFile(finalPath, buffer); - // On rajoute le chemin pour les middlewares suivants - (req as any).savedFilePath = finalPath; - next(); } catch (err) { next(err); diff --git a/backend/src/routes/im_export.routes.ts b/backend/src/routes/im_export.routes.ts index 8d73cbe..38077cd 100644 --- a/backend/src/routes/im_export.routes.ts +++ b/backend/src/routes/im_export.routes.ts @@ -3,13 +3,18 @@ import * as imexportController from '../controllers/im_export.controller'; import { createUploadMiddleware } from "../middlewares/multer.middleware"; import { checkRole } from '../middlewares/user.middleware'; -const uploadFoodMenu = createUploadMiddleware("uploads/foodmenu/", false); -const uploadPlannings = createUploadMiddleware("uploads/plannings/", false); +const uploadMiddleware = createUploadMiddleware(); const imexportRouter = express.Router(); -imexportRouter.post('/admin/foodimport', checkRole("Admin", []), uploadFoodMenu.multerUpload.single("foodFile"), uploadFoodMenu.verifyAndSave, imexportController.updateFoodMenu); -imexportRouter.post('/admin/plannings', checkRole("Admin", []), uploadPlannings.multerUpload.single("planningFile"), uploadPlannings.verifyAndSave, imexportController.updatePlannings); +imexportRouter.post( + '/admin/import/:category/:item', checkRole("Admin", []), + uploadMiddleware.multerUpload.single("file"), + uploadMiddleware.verifyAndSave, + imexportController.updateFoodMenu); + imexportRouter.post('/admin/exportgsheet', checkRole("Admin", []), imexportController.exportAllDataToSheets); imexportRouter.get('/admin/exportbus', checkRole("Admin", []), imexportController.exportUsersCSV); +imexportRouter.get('/admin/document/:category/:item', checkRole("Admin", []), imexportController.getUploadedDocumentStatus); +imexportRouter.delete('/admin/document/:category/:item', checkRole("Admin", []), imexportController.deleteDocument); export default imexportRouter; diff --git a/backend/src/routes/news.routes.ts b/backend/src/routes/news.routes.ts index cc21fe2..7b74e4b 100644 --- a/backend/src/routes/news.routes.ts +++ b/backend/src/routes/news.routes.ts @@ -1,14 +1,12 @@ import express from "express"; import * as newsController from "../controllers/news.controller"; -import { createUploadMiddleware } from "../middlewares/multer.middleware"; import { checkRole } from "../middlewares/user.middleware"; -const uploadImgNews = createUploadMiddleware("uploads/imgnews/", true); const newsRouter = express.Router(); //Admin routes -newsRouter.post("/admin/createnews", checkRole("Admin", ["Communication"]), uploadImgNews.multerUpload.single("file"), uploadImgNews.verifyAndSave, newsController.createNews); -newsRouter.post("/admin/updatenews", checkRole("Admin", ["Communication"]), uploadImgNews.multerUpload.single("file"), uploadImgNews.verifyAndSave, newsController.updateNews); +newsRouter.post("/admin/createnews", checkRole("Admin", ["Communication"]), newsController.createNews); +newsRouter.post("/admin/updatenews", checkRole("Admin", ["Communication"]), newsController.updateNews); newsRouter.get("/admin/all", checkRole("Admin", ["Communication"]), newsController.listAllNews); newsRouter.post("/admin/publish", checkRole("Admin", ["Communication"]), newsController.publishNews); newsRouter.delete("/admin/deletenews", checkRole("Admin", ["Communication"]), newsController.deleteNews); diff --git a/backend/src/utils/responses.ts b/backend/src/utils/responses.ts index 1e6931b..acc1ff8 100644 --- a/backend/src/utils/responses.ts +++ b/backend/src/utils/responses.ts @@ -28,6 +28,12 @@ export const Unauthorized = (res: Response, details: { data?: any, msg?: string res.status(Code.UNAUTHORIZED).json(new HttpResponse(Code.UNAUTHORIZED, msg, details.data)); }; +export const Forbidden = (res: Response, details: { data?: any, msg?: string }) => { + const msg = details.msg || 'Forbidden'; + res.status(Code.FORBIDDEN).json(new HttpResponse(Code.FORBIDDEN, msg, details.data)); +}; + + export const Conflict = (res: Response, details: { data?: any, msg?: string }) => { const msg = details.msg || "Conflict"; res.status(Code.CONFLICT).json(new HttpResponse(Code.CONFLICT, msg, details.data)); @@ -40,11 +46,12 @@ export const Teapot = (res: Response, details: { data?: any, msg?: string }) => export enum Code { OK = 200, + CREATED = 201, ACCEPTED = 202, - NOT_FOUND = 404, BAD_REQUEST = 400, UNAUTHORIZED = 401, - CREATED = 201, + FORBIDDEN = 403, + NOT_FOUND = 404, CONFLICT = 409, IM_A_TEAPOT = 418, ISE = 500 @@ -76,6 +83,8 @@ export class HttpResponse { return 'INTERNAL_SERVER_ERROR'; case Code.UNAUTHORIZED: return 'UNAUTHORIZED'; + case Code.FORBIDDEN: + return 'FORBIDDEN'; case Code.CONFLICT: return 'CONFLICT'; case Code.IM_A_TEAPOT: diff --git a/backend/src/utils/uploadDocuments.ts b/backend/src/utils/uploadDocuments.ts new file mode 100644 index 0000000..91d61dd --- /dev/null +++ b/backend/src/utils/uploadDocuments.ts @@ -0,0 +1,84 @@ +import fs from "fs/promises"; +import path from "path"; + +export type UploadedDocument = { + name: string; + fullPath: string; + extension: string | null; + mtimeMs: number; +}; + +export type UploadedDocumentStatus = { + exists: boolean; + extension: string | null; + fileName: string | null; + relativePath: string | null; +}; + +export const isSafeUploadSegment = (value: string) => /^[a-zA-Z0-9_-]+$/.test(value); + +export const getUploadDirectory = (category: string) => path.resolve(process.cwd(), "uploads", category); + +export const findUploadedDocuments = async (category: string, item: string): Promise => { + const uploadsDir = getUploadDirectory(category); + + const entries = await fs.readdir(uploadsDir); + const candidates = entries.filter((name) => path.parse(name).name === item); + + return Promise.all( + candidates.map(async (name) => { + const fullPath = path.join(uploadsDir, name); + const stats = await fs.stat(fullPath); + + return { + name, + fullPath, + extension: path.extname(name).replace(/^\./, "").toLowerCase() || null, + mtimeMs: stats.mtimeMs, + }; + }) + ); +}; + +export const getLatestUploadedDocument = async ( + category: string, + item: string, +): Promise => { + const documents = await findUploadedDocuments(category, item); + + if (documents.length === 0) { + return null; + } + + documents.sort((a, b) => b.mtimeMs - a.mtimeMs); + return documents[0]; +}; + +export const removeUploadedDocuments = async (category: string, item: string): Promise => { + const documents = await findUploadedDocuments(category, item); + + await Promise.all(documents.map((document) => fs.unlink(document.fullPath))); + + return documents.length; +}; + +export const toUploadedDocumentStatus = ( + category: string, + document: UploadedDocument | null, +): UploadedDocumentStatus => { + if (!document) { + return { + exists: false, + extension: null, + fileName: null, + relativePath: null, + }; + } + + return { + exists: true, + extension: document.extension, + fileName: document.name, + relativePath: `/uploads/${category}/${document.name}`, + }; +}; diff --git a/frontend/src/components/Admin/adminExportImport.tsx b/frontend/src/components/Admin/adminExportImport.tsx index 883b8ab..009086b 100644 --- a/frontend/src/components/Admin/adminExportImport.tsx +++ b/frontend/src/components/Admin/adminExportImport.tsx @@ -1,9 +1,10 @@ -import { FileText } from "lucide-react"; -import { type ChangeEvent, useState } from "react"; +import { useState } from "react"; -import { exportBus, exportDb, importFoodMenu, importPlannings } from "../../services/requests/im_export.service"; +import { exportBus, exportDb } from "../../services/requests/im_export.service"; import { Button } from "../ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; +import { AdminFileImport } from "./adminFileImport"; + export const AdminExportConnect = () => { const [loading, setLoading] = useState<{ db: boolean; bus: boolean }>({ @@ -101,106 +102,16 @@ export const AdminExportConnect = () => { export const AdminImportFoodMenu = () => { - const [menu, setMenu] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [message, setMessage] = useState(""); - - const handleFileChange = (e: ChangeEvent) => { - setError(null); - setMessage(""); - if (e.target.files && e.target.files.length > 0) { - const selected = e.target.files[0]; - if (selected.type !== "application/pdf") { - setError("Seuls les fichiers PDF sont autorisés"); - setMenu(null); - } else { - setMenu(selected); - } - } - }; - - const handleImport = async () => { - if (!menu) { - setError("Veuillez sélectionner un fichier PDF avant d'importer."); - return; - } - - setLoading(true); - setError(null); - setMessage(""); - - try { - const formData = new FormData(); - formData.append("foodFile", menu); - - const response = await importFoodMenu(formData); - setMessage(response.message || "Importation réussie !"); - } catch (err) { - console.error("Erreur lors de l'importation du menu", err); - setError("Une erreur est survenue pendant l'importation."); - } finally { - setLoading(false); - } - }; return ( - Importer le menu au format PDF + Importer le menu - - {/* Rappel des règles de nommage */} -

- ⚠️ Le fichier doit être nommé FoodMenu.pdf
-

- -
- {/* Input fichier masqué */} - - - - - {/* Affiche le nom du fichier sélectionné */} - {menu && ( -
- - {menu.name} -
- )} - - {/* Bouton importer */} - -
- - {error && ( -

{error}

- )} - {message && ( -

- {message} -

- )} +
); @@ -208,154 +119,19 @@ export const AdminImportFoodMenu = () => { export const AdminImportPlannings = () => { - const [planning, setPlanning] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [message, setMessage] = useState(""); - const [selectedPlanning, setSelectedPlanning] = useState(""); - - const handleFileChange = (e: ChangeEvent) => { - setError(null); - setMessage(""); - if (e.target.files && e.target.files.length > 0) { - const selected = e.target.files[0]; - if (selected.type !== "application/pdf") { - setError("Seuls les fichiers PDF sont autorisés"); - setPlanning(null); - } else { - setPlanning(selected); - } - } - }; - - const handleImport = async (planningName: string) => { - if (!planning) { - setError("Veuillez sélectionner un fichier PDF avant d'importer."); - return; - } - - setLoading(true); - setError(null); - setMessage(""); - setSelectedPlanning(planningName); - - try { - const formData = new FormData(); - formData.append("planningFile", planning); - - const response = await importPlannings(formData); - setMessage(response.message || `Importation réussie pour ${planningName} !`); - } catch (err) { - console.error("Erreur lors de l'importation du planning", err); - setError("Une erreur est survenue pendant l'importation."); - } finally { - setLoading(false); - } - }; - return ( - Importer les plannings au format PDF + Importer les plannings - - - {/* Rappel des règles de nommage */} -

- ⚠️ Le fichier doit être nommé en minuscules, sans accents, au format
- filiere.pdf (ex: tc.pdf, bachelor.pdf) -

- -
- {/* Input fichier masqué */} - - - - - {/* Affiche le nom du fichier sélectionné */} - {planning && ( -
- - {planning.name} -
- )} - - {/* Différents boutons d'import */} -
- - - - - - - - - -
-
- - {/* Messages */} - {error && ( -

{error}

- )} - {message && ( -

- {message} -

- )} + + + + + +
); diff --git a/frontend/src/components/Admin/adminFileImport.tsx b/frontend/src/components/Admin/adminFileImport.tsx new file mode 100644 index 0000000..8274db5 --- /dev/null +++ b/frontend/src/components/Admin/adminFileImport.tsx @@ -0,0 +1,331 @@ +import { ExternalLink, FilePenLine, FilePlus, FileText, Trash } from "lucide-react"; +import { type ChangeEvent, useCallback, useEffect, useState } from "react"; +import Swal from "sweetalert2"; + +import { MIMEType } from "../../interfaces/import.interface"; +import { checkIfExistingDocument, deleteFile, importFile } from "../../services/requests/im_export.service"; +import { Card, CardContent } from "../ui/card"; + +type AdminFileImportProps = { + category: string; + item?: string; + acceptedTypes?: MIMEType[]; + title: string; + draft?: boolean; + draftInitialUrl?: string | null; + onDraftFileChange?: (file: File | null) => void; + onDraftDelete?: () => void; + onDraftSubmitReady?: (submit: (itemOverride?: string) => Promise) => void; +}; + +type ExistingFile = { + exists: boolean; + extension: string | null; + fileName: string | null; + relativePath: string | null; +}; + +export const AdminFileImport = ( + { + category, + item, + acceptedTypes = [MIMEType.PDF], + title, + draft = false, + draftInitialUrl, + onDraftFileChange, + onDraftDelete, + onDraftSubmitReady, + }: AdminFileImportProps, +) => { + const [existingFile, setExistingFile] = useState(null); + const [fileURL, setFileURL] = useState(null); + const [draftFile, setDraftFile] = useState(null); + const [draftPreviewURL, setDraftPreviewURL] = useState(null); + + const resolvePublicFileUrl = useCallback((url: string) => { + if (url.startsWith("http://") || url.startsWith("https://")) { + return url; + } + + const baseUrl = import.meta.env.VITE_API_URL as string; + if (url.startsWith("/api/") && baseUrl.endsWith("/api")) { + return `${baseUrl}${url.slice(4)}`; + } + + return `${baseUrl}${url}`; + }, []); + + const getExtensionFromPath = useCallback((url: string) => { + const cleanUrl = url.split("?")[0].split("#")[0]; + const fileName = cleanUrl.split("/").pop() ?? ""; + if (!fileName.includes(".")) { + return null; + } + + return fileName.split(".").pop()?.toLowerCase() ?? null; + }, []); + + const checkFileStatus = useCallback(async () => { + if (draft || !item) { + return; + } + + try { + const status = await checkIfExistingDocument(category, item); + setExistingFile(status); + + if (status.exists && status.relativePath) { + setFileURL(resolvePublicFileUrl(status.relativePath)); + } else { + setFileURL(null); + } + } catch { + setExistingFile(null); + setFileURL(null); + } + }, [category, draft, item, resolvePublicFileUrl]); + + useEffect(() => { + if (!draft || draftFile || draftPreviewURL) { + return; + } + + if (!draftInitialUrl) { + setExistingFile(null); + setFileURL(null); + return; + } + + const resolvedUrl = resolvePublicFileUrl(draftInitialUrl); + const extension = getExtensionFromPath(draftInitialUrl); + const fileName = draftInitialUrl.split("?")[0].split("#")[0].split("/").pop() ?? null; + + setExistingFile({ + exists: true, + extension, + fileName, + relativePath: null, + }); + setFileURL(resolvedUrl); + }, [draft, draftFile, draftInitialUrl, draftPreviewURL, getExtensionFromPath, resolvePublicFileUrl]); + + useEffect(() => { + if (!draft) { + void checkFileStatus(); + } + }, [checkFileStatus, draft]); + + const submitDraftUpload = useCallback(async (itemOverride?: string): Promise => { + if (!draft || !draftFile) { + return null; + } + + const targetItem = itemOverride ?? item; + + if (!targetItem) { + throw new Error("L'item est requis pour la soumission du brouillon."); + } + + const formData = new FormData(); + formData.append("file", draftFile); + + await importFile(formData, category, targetItem); + + const status = await checkIfExistingDocument(category, targetItem); + setExistingFile(status); + if (status.exists && status.relativePath) { + setFileURL(resolvePublicFileUrl(status.relativePath)); + } + + setDraftFile(null); + if (draftPreviewURL) { + URL.revokeObjectURL(draftPreviewURL); + setDraftPreviewURL(null); + } + onDraftFileChange?.(null); + + return status.relativePath ?? null; + }, [category, draft, draftFile, draftPreviewURL, item, onDraftFileChange, resolvePublicFileUrl]); + + useEffect(() => { + if (draft && onDraftSubmitReady) { + onDraftSubmitReady(submitDraftUpload); + } + }, [draft, onDraftSubmitReady, submitDraftUpload]); + + const inputId = `${category}-${item ?? "draft"}-fileInput`; + + const handleFileChange = async (e: ChangeEvent) => { + if (!e.target.files || e.target.files.length === 0) { + Swal.fire({ + icon: "error", + title: "Aucun fichier sélectionné" + }); + return; + } + + const selected = e.target.files[0]; + if (!selected || !acceptedTypes.includes(selected.type as MIMEType)) { + Swal.fire({ + icon: "error", + title: "Format incompatible", + text: "Le fichier sélectionné n'est pas autorisé pour ce champ." + }); + return; + } + + if (draft) { + const extension = selected.name.includes(".") + ? selected.name.split(".").pop()?.toLowerCase() ?? null + : null; + + if (draftPreviewURL) { + URL.revokeObjectURL(draftPreviewURL); + } + const localPreviewURL = URL.createObjectURL(selected); + + setDraftFile(selected); + setExistingFile({ + exists: true, + extension, + fileName: selected.name, + relativePath: null, + }); + setDraftPreviewURL(localPreviewURL); + setFileURL(localPreviewURL); + onDraftFileChange?.(selected); + return; + } + + if (!item) { + Swal.fire({ + icon: "error", + title: "Configuration invalide", + text: "L'item est requis pour l'upload direct.", + }); + return; + } + + try { + const formData = new FormData(); + formData.append("file", selected); + + const response = await importFile(formData, category, item); + await checkFileStatus(); + await Swal.fire("✅ Import réussi", response.message, "success"); + } catch (err) { + console.error("Erreur lors de l'importation du menu", err); + Swal.fire({ + icon: "error", + title: "Erreur", + text: "Erreur lors de l'importation du menu.", + }); + } + }; + + const handleDelete = async () => { + if (draft) { + if (draftPreviewURL) { + URL.revokeObjectURL(draftPreviewURL); + } + setDraftFile(null); + setDraftPreviewURL(null); + setExistingFile(null); + setFileURL(null); + onDraftFileChange?.(null); + onDraftDelete?.(); + return; + } + + if (!item) { + return; + } + + const confirm = await Swal.fire({ + title: `Supprimer le document ${title} ?`, + text: "Cette action est irreversible.", + icon: "warning", + showCancelButton: true, + confirmButtonColor: "#2563eb", + cancelButtonColor: "#d33", + confirmButtonText: "🚍 Oui", + cancelButtonText: "Annuler", + }); + + if (confirm.isConfirmed) { + try { + const response = await deleteFile(category, item); + await checkFileStatus(); + await Swal.fire("✅ Suppression réussie", response.message, "success"); + } catch (err) { + console.error("Erreur lors de la suppression du document", err); + Swal.fire({ + icon: "error", + title: "Erreur", + text: "Erreur lors de la suppression du document.", + }); + } + } + }; + + return ( + + +
+
+

{title}

+ +
+ {existingFile?.exists ? ( + <> + +

.{existingFile?.extension}

+ + ) : ( + + )} +
+
+
+ `${ext}`).join(",")} + onChange={handleFileChange} + className="hidden" + /> + + + + {(existingFile?.exists || !!draftPreviewURL) && ( + <> + {(draftPreviewURL || fileURL) && ( + + + + )} + + + + )} +
+
+
+
+ ); +}; diff --git a/frontend/src/components/Admin/adminNews.tsx b/frontend/src/components/Admin/adminNews.tsx index 4ce24f1..eab5435 100644 --- a/frontend/src/components/Admin/adminNews.tsx +++ b/frontend/src/components/Admin/adminNews.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; import Swal from "sweetalert2"; +import { MIMEType } from "../../interfaces/import.interface"; import { type News } from "../../interfaces/news.interface"; import { createNews, @@ -13,13 +14,14 @@ import { Button } from "../ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; import { Input } from "../ui/input"; import { Textarea } from "../ui/textarea"; +import { AdminFileImport } from "./adminFileImport"; export const AdminNews = () => { const [newsList, setNewsList] = useState([]); const [loading, setLoading] = useState(true); const [editingId, setEditingId] = useState(null); - const [selectedFile, setSelectedFile] = useState(null); - const [previewUrl, setPreviewUrl] = useState(null); + const [editingImageUrl, setEditingImageUrl] = useState(null); + const [submitDraftImage, setSubmitDraftImage] = useState<((itemOverride?: string) => Promise) | null>(null); const [formData, setFormData] = useState({ title: "", @@ -52,41 +54,78 @@ export const AdminNews = () => { setFormData({ ...formData, [e.target.name]: e.target.value }); }; - const handleImageChange = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (file) { - setSelectedFile(file); - setPreviewUrl(URL.createObjectURL(file)); + const getErrorMessage = (err: unknown, fallback: string) => { + if (typeof err === "object" && err !== null && "response" in err) { + const response = (err as { response?: { data?: { msg?: string } } }).response; + if (response?.data?.msg) { + return response.data.msg; + } } + + return fallback; }; - const handleCreateOrUpdate = async () => { - try { - const formDataToSend = new FormData(); + const resolveNewsImageUrl = (imageUrl: string) => { + if (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")) { + return imageUrl; + } - if (selectedFile) { - formDataToSend.append("file", selectedFile); - } + const baseUrl = import.meta.env.VITE_API_URL as string; + if (imageUrl.startsWith("/api/") && baseUrl.endsWith("/api")) { + return `${baseUrl}${imageUrl.slice(4)}`; + } + + return `${baseUrl}${imageUrl}`; + }; - formDataToSend.append("title", formData.title); - formDataToSend.append("description", formData.description); - formDataToSend.append("type", formData.type); - formDataToSend.append("published", String(formData.published)); - formDataToSend.append("target", formData.target); + const handleCreateOrUpdate = async () => { + try { + const payload = { + title: formData.title, + description: formData.description, + type: formData.type, + published: formData.published, + target: formData.target, + }; let response; if (editingId) { - formDataToSend.append("id", String(editingId)); - response = await updateNews(formDataToSend); + const uploadedImageUrl = submitDraftImage + ? await submitDraftImage(String(editingId)) + : null; + + const image_url = uploadedImageUrl + ? uploadedImageUrl + : editingImageUrl === null + ? null + : undefined; + + response = await updateNews({ + ...payload, + id: String(editingId), + image_url, + }); } else { - response = await createNews(formDataToSend); + response = await createNews(payload); + + const createdId = response?.data?.id; + if (createdId && submitDraftImage) { + const image_url = await submitDraftImage(String(createdId)); + if (image_url) { + await updateNews({ + ...payload, + id: String(createdId), + image_url, + }); + } + } } await Swal.fire("✅ Succès", response.message, "success"); resetForm(); fetchNews(); - } catch (err: any) { - Swal.fire("❌ Erreur", err.response?.data?.msg || "Erreur lors de la sauvegarde", "error"); + } catch (err: unknown) { + Swal.fire("❌ Erreur", getErrorMessage(err, "Erreur lors de la sauvegarde"), "error"); } }; @@ -137,8 +176,8 @@ export const AdminNews = () => { const response = await publishNews(news, sendEmail.isConfirmed); await Swal.fire("✅ Publiée", response.message, "success"); fetchNews(); - } catch (err: any) { - Swal.fire("❌ Erreur", err.response?.data?.msg || "Erreur lors de la publication", "error"); + } catch (err: unknown) { + Swal.fire("❌ Erreur", getErrorMessage(err, "Erreur lors de la publication"), "error"); } }; @@ -151,6 +190,7 @@ export const AdminNews = () => { target: news.target, }); setEditingId(news.id); + setEditingImageUrl(news.image_url ?? null); window.scrollTo({ top: 0, behavior: "smooth" }); }; @@ -162,14 +202,10 @@ export const AdminNews = () => { published: false, target: "Tous", }); - setSelectedFile(null); - setPreviewUrl(null); setEditingId(null); + setEditingImageUrl(null); }; - const handleRemoveImage = () => { - setPreviewUrl(null); - }; return ( @@ -194,48 +230,19 @@ export const AdminNews = () => { placeholder="Contenu de l'actu" /> - {/* Image upload amélioré */} -
- - - - - {selectedFile && ( -

{selectedFile.name}

- )} - - {previewUrl && ( -
-
- Aperçu -
- -
- )} -
+ { + setEditingImageUrl(null); + setSubmitDraftImage(null); + }} + onDraftSubmitReady={(submit) => setSubmitDraftImage(() => submit)} + /> ) => { - setValidationType(option?.value ?? null); - setSelectedTargetId(null); - }} - options={[ - { value: "user", label: "Utilisateur" }, - { value: "team", label: "Équipe" }, - { value: "faction", label: "Faction" }, - ]} - /> {validationType === "user" && ( + setSelectedUnvalidationTargetIds(options.map((o) => Number(o.value))) + } + options={group.recipients.map((recipient) => ({ + value: recipient.id, + label: recipient.label, + }))} + /> + +
+ + + + + +
+ +
+ )} ))} From 3544a1d394d4870e1a166d765cc2d1b83eebaf12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Poncin?= Date: Wed, 24 Jun 2026 18:49:26 +0200 Subject: [PATCH 11/47] feat: multiselection for validation + prevent validation of same target multiple times --- backend/src/services/challenge.service.ts | 26 ++- .../AdminChallenge/adminChalengeList.tsx | 27 +-- .../adminChallengeAddPointsForm.tsx | 2 +- .../AdminChallenge/adminChallengeEditor.tsx | 2 +- .../adminChallengeValidatedList.tsx | 166 +++++++++--------- 5 files changed, 122 insertions(+), 101 deletions(-) diff --git a/backend/src/services/challenge.service.ts b/backend/src/services/challenge.service.ts index 44d7ece..759d874 100644 --- a/backend/src/services/challenge.service.ts +++ b/backend/src/services/challenge.service.ts @@ -82,7 +82,29 @@ export const validateChallenge = async ({ throw new Error("Type de challenge non valide"); } - // 3. Créer l'objet de validation du challenge + // 3. Vérifier si ce challenge a déjà été validé pour cette cible + const targetColumn = + type === "user" + ? challengeValidationSchema.target_user_id + : type === "team" + ? challengeValidationSchema.target_team_id + : challengeValidationSchema.target_faction_id; + + const existingValidation = await db + .select() + .from(challengeValidationSchema) + .where( + and( + eq(challengeValidationSchema.challenge_id, challengeId), + eq(targetColumn, targetId) + ) + ); + + if (existingValidation.length > 0) { + throw new Error("Ce challenge a déjà été validé pour cette cible"); + } + + // 4. Créer l'objet de validation du challenge const newChallengeValidationPoints = { challenge_id: challengeId, validated_by_admin_id: validatedBy, @@ -94,7 +116,7 @@ export const validateChallenge = async ({ target_faction_id: target_faction_id, }; - // 4. Insérer la validation du challenge dans la base de données + // 5. Insérer la validation du challenge dans la base de données const inserted = await db.insert(challengeValidationSchema).values(newChallengeValidationPoints).returning(); return inserted[0]; diff --git a/frontend/src/components/Admin/AdminChallenge/adminChalengeList.tsx b/frontend/src/components/Admin/AdminChallenge/adminChalengeList.tsx index 27fe698..4395fb1 100644 --- a/frontend/src/components/Admin/AdminChallenge/adminChalengeList.tsx +++ b/frontend/src/components/Admin/AdminChallenge/adminChalengeList.tsx @@ -26,7 +26,7 @@ type ValidationTarget = "user" | "team" | "faction"; const AdminChallengeList = ({ challenges, refreshChallenges, onEdit, teams, factions, users }: Props) => { const [showValidationFormForId, setShowValidationFormForId] = useState(null); const [validationType, setValidationType] = useState(null); - const [selectedTargetId, setSelectedTargetId] = useState(null); + const [selectedTargetIds, setSelectedTargetIds] = useState([]); const [searchTerm, setSearchTerm] = useState(""); @@ -68,13 +68,14 @@ const AdminChallengeList = ({ challenges, refreshChallenges, onEdit, teams, fact } const handleValidate = async () => { - if (!showValidationFormForId || !validationType || !selectedTargetId) return; - + if (!showValidationFormForId || !validationType || selectedTargetIds.length === 0) return; + + for (const targetId of selectedTargetIds) { try { const res = await validateChallenge({ challengeId: showValidationFormForId, type: validationType, - targetId: selectedTargetId, + targetId: targetId, }); Swal.fire({ @@ -87,20 +88,21 @@ const AdminChallengeList = ({ challenges, refreshChallenges, onEdit, teams, fact setShowValidationFormForId(null); setValidationType(null); - setSelectedTargetId(null); + setSelectedTargetIds([]); refreshChallenges(); } catch (err) { console.error("Erreur lors de la validation du challenge", err); Swal.fire({ icon: "error", title: "Erreur ❌", - text: "Impossible de valider ce challenge. Réessaie plus tard.", + text: "Impossible de valider ce challenge. Réessaie plus tard. (cette erreur peut survenir si le challenge a déjà été validé pour cet utilisateur/équipe/faction)", }); } + } }; return ( - + 📜 Challenges @@ -165,8 +167,9 @@ const AdminChallengeList = ({ challenges, refreshChallenges, onEdit, teams, fact {validationType === "user" && ( setSelectedTargetId(Number(option?.value))} + onChange={(options) => setSelectedTargetIds(options.map((o) => Number(o.value)))} options={teams.map((t: Team) => ({ value: t.teamId, label: t.name, @@ -187,8 +191,9 @@ const AdminChallengeList = ({ challenges, refreshChallenges, onEdit, teams, fact {validationType === "faction" && ( - setSelectedUnvalidationTargetIds(options.map((o) => Number(o.value))) - } - options={group.recipients.map((recipient) => ({ - value: recipient.id, - label: recipient.label, - }))} - /> + {showUnvalidationFormForId === group.challenge_id && ( + + +

❌ Invalider le challenge

-
- + setCustomTitle(e.target.value)} + /> + )} {!isCustom ? ( ({ value: u.email, label: `${u.firstName} ${u.lastName}` }))} - onChange={(val) => setSendTo(val as any)} + onChange={(val) => setSendTo((val ?? []) as SelectOption[])} /> )}
); diff --git a/frontend/src/services/requests/email.service.ts b/frontend/src/services/requests/email.service.ts index 8a8f6bc..2d53f34 100644 --- a/frontend/src/services/requests/email.service.ts +++ b/frontend/src/services/requests/email.service.ts @@ -1,11 +1,28 @@ import api from "../api"; -export const sendEmail = async (payload: any) => { +type SendEmailPayload = { + subject: string; + templateName: string; + format?: 'html' | 'txt'; + permission: string | null; + sendTo: string[] | null; + title?: string; + content?: string; + html?: string; +}; + +type PreviewEmailPayload = { + templateName: string; + title?: string; + content?: string; +}; + +export const sendEmail = async (payload: SendEmailPayload) => { const response = await api.post('/email/admin/sendemail', { payload }); return response.data; }; -export const emailPreview = async (templateName: any) => { - const response = await api.post('email/admin/previewemail', { templateName }) +export const emailPreview = async (payload: PreviewEmailPayload) => { + const response = await api.post('/email/admin/previewemail', { payload }); return response.data.data } From ef62ebc0812478a6a5f5ddf7a3fa1939cc65c82c Mon Sep 17 00:00:00 2001 From: "Antoine D." Date: Thu, 25 Jun 2026 10:51:25 +0200 Subject: [PATCH 13/47] chore: add .env.exemple and backend dist in gitignore --- .gitignore | 1 + backend/.env.exemple | 12 ++++++++++++ frontend/.env.exemple | 5 +++++ 3 files changed, 18 insertions(+) create mode 100644 backend/.env.exemple create mode 100644 frontend/.env.exemple diff --git a/.gitignore b/.gitignore index 270d1c2..47046e0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ frontend/dist/ backend/node_modules/ backend/uploads/ backend/exports/ +backend/dist # Autres fichiers à ignorer .DS_Store diff --git a/backend/.env.exemple b/backend/.env.exemple new file mode 100644 index 0000000..b6b2162 --- /dev/null +++ b/backend/.env.exemple @@ -0,0 +1,12 @@ +DATABASE_URL=postgresql://admin:password@localhost:5432/integration-dev +OUTSIDE_DATABASE_URL=postgresql://admin:password@localhost:5432/integration-dev +POSTGRES_PASSWORD=password +POSTGRES_USER=admin +POSTGRES_DB=integration-dev +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 +SERVER_PORT=4001 +ZIMBRA_PASSWORD=codecode +SHOTGUN_PASSWORD=vodka +CAS_VALIDATE_URL= +CAS_LOGIN_URL= \ No newline at end of file diff --git a/frontend/.env.exemple b/frontend/.env.exemple new file mode 100644 index 0000000..4095081 --- /dev/null +++ b/frontend/.env.exemple @@ -0,0 +1,5 @@ +VITE_ROADBOOK_URL_FRENCH= +VITE_ROADBOOK_URL_ENGLISH= +VITE_SERVICE_URL=https://localhost:4000 +VITE_API_URL =http://localhost:4001/api +VITE_CAS_LOGIN_URL=https://auth.utt.fr/cas/login \ No newline at end of file From bf1234df48d059b6706d3810a7d7389ce58431fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Poncin?= Date: Thu, 25 Jun 2026 10:57:35 +0200 Subject: [PATCH 14/47] =?UTF-8?q?fix:=20"d=C3=A9valider=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/controllers/tent.controller.ts | 4 +-- backend/src/services/tent.service.ts | 2 +- backend/src/utils/emailtemplates.ts | 2 +- .../adminChallengeValidatedList.tsx | 26 +++++++++---------- frontend/src/components/Admin/adminTent.tsx | 6 ++--- .../src/services/requests/tent.service.ts | 2 +- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/backend/src/controllers/tent.controller.ts b/backend/src/controllers/tent.controller.ts index 5dd067b..2f87a81 100644 --- a/backend/src/controllers/tent.controller.ts +++ b/backend/src/controllers/tent.controller.ts @@ -90,7 +90,7 @@ export const toggleTentConfirmation = async (req: Request, res: Response) => { to: [user1.email, user2.email], subject: confirmed ? "🎉 Votre tente a été validée !" - : "⛺ Votre tente a été dévalidée", + : "⛺ Votre tente a été invalidée", text: "", // optionnel html: htmlEmail, }; @@ -101,7 +101,7 @@ export const toggleTentConfirmation = async (req: Request, res: Response) => { Ok(res, { msg: confirmed ? "Tente validée et email envoyé." - : "Tente dévalidée et email envoyé.", + : "Tente invalidée et email envoyé.", }); } catch (err: any) { console.error(err); diff --git a/backend/src/services/tent.service.ts b/backend/src/services/tent.service.ts index ed9b32b..5f02fce 100644 --- a/backend/src/services/tent.service.ts +++ b/backend/src/services/tent.service.ts @@ -131,5 +131,5 @@ export const toggleTentConfirmation = async ( ) ); - return { success: true, message: confirmed ? "Tente validée." : "Tente dévalidée." }; + return { success: true, message: confirmed ? "Tente validée." : "Tente invalidée." }; }; diff --git a/backend/src/utils/emailtemplates.ts b/backend/src/utils/emailtemplates.ts index 309068e..3d53f57 100644 --- a/backend/src/utils/emailtemplates.ts +++ b/backend/src/utils/emailtemplates.ts @@ -341,7 +341,7 @@ export const templateNotifyTentConfirmation = `

La tente entre {{user1}} et {{user2}} a été - {{#if confirmed}}validée{{else}}dévalidée{{/if}} + {{#if confirmed}}validée{{else}}invalidée{{/if}} .

diff --git a/frontend/src/components/Admin/AdminChallenge/adminChallengeValidatedList.tsx b/frontend/src/components/Admin/AdminChallenge/adminChallengeValidatedList.tsx index 7420b64..a136943 100644 --- a/frontend/src/components/Admin/AdminChallenge/adminChallengeValidatedList.tsx +++ b/frontend/src/components/Admin/AdminChallenge/adminChallengeValidatedList.tsx @@ -102,23 +102,23 @@ export const AdminValidatedChallengesList = ({ const handleUnvalidate = async ( ) => { const confirm = await Swal.fire({ - title: "Confirmer la dévalidation ?", + title: "Confirmer la invalidation ?", text: "Cette action retirera la validation du challenge.", icon: "warning", showCancelButton: true, confirmButtonColor: "#e3342f", cancelButtonColor: "#6b7280", - confirmButtonText: "Oui, dévalider", + confirmButtonText: "Oui, invalider", cancelButtonText: "Annuler", }); if (!confirm.isConfirmed) return; for (const targetId of selectedUnvalidationTargetIds) { - console.log("Dévalidation du challenge pour l'ID cible :", targetId); + console.log("Invalidation du challenge pour l'ID cible :", targetId); console.log(validatedChallenges); const challengeToUnvalidate = validatedChallenges.find( (c) => c.challenge_id === showUnvalidationFormForId ); - console.log("Challenge à dévalider :", challengeToUnvalidate); + console.log("Challenge à invalider :", challengeToUnvalidate); if (!challengeToUnvalidate) { Swal.fire({ icon: "error", title: "Erreur", text: "Challenge non trouvé." }); return; @@ -130,7 +130,7 @@ export const AdminValidatedChallengesList = ({ teamId: challengeToUnvalidate.target_team_id ?? 0, userId: challengeToUnvalidate.target_user_id ?? 0, }); - console.log("Réponse de la dévalidation :", res); + console.log("Réponse de la invalidation :", res); validatedChallenges = validatedChallenges.filter( (c) => c.challenge_id === showUnvalidationFormForId && @@ -139,14 +139,14 @@ export const AdminValidatedChallengesList = ({ c.target_faction_id !== targetId ); } catch { - Swal.fire({ icon: "error", title: "Erreur", text: "Impossible de dévalider ce challenge." }); + Swal.fire({ icon: "error", title: "Erreur", text: "Impossible de invalider ce challenge." }); } } Swal.fire({ icon: "success", - title: "Dévalidé", - text: "Les challenges ont été dévalidés.", + title: "Invalidé", + text: "Les challenges ont été invalidés.", timer: 1800, showConfirmButton: false, }); @@ -157,13 +157,13 @@ export const AdminValidatedChallengesList = ({ const handleUnvalidateAll = async () => { const confirm = await Swal.fire({ - title: "Confirmer la dévalidation ?", + title: "Confirmer la invalidation ?", text: "Cette action retirera la validation du challenge.", icon: "warning", showCancelButton: true, confirmButtonColor: "#e3342f", cancelButtonColor: "#6b7280", - confirmButtonText: "Oui, dévalider", + confirmButtonText: "Oui, invalider", cancelButtonText: "Annuler", }); @@ -185,7 +185,7 @@ export const AdminValidatedChallengesList = ({ Swal.fire({ icon: "error", title: "Erreur", - text: "Impossible de dévalider un ou plusieurs challenges.", + text: "Impossible de invalider un ou plusieurs challenges.", }); return; } @@ -193,8 +193,8 @@ export const AdminValidatedChallengesList = ({ Swal.fire({ icon: "success", - title: "Dévalidé", - text: "Les challenges ont été dévalidés.", + title: "Invalidé", + text: "Les challenges ont été invalidés.", timer: 1800, showConfirmButton: false, }); diff --git a/frontend/src/components/Admin/adminTent.tsx b/frontend/src/components/Admin/adminTent.tsx index 644492d..df4ecf6 100644 --- a/frontend/src/components/Admin/adminTent.tsx +++ b/frontend/src/components/Admin/adminTent.tsx @@ -41,7 +41,7 @@ export const TentAdmin = () => { }; const handleToggle = async (pair: TentPair) => { - const action = pair.confirmed ? "dévalider" : "valider"; + const action = pair.confirmed ? "invalider" : "valider"; const confirm = await Swal.fire({ title: '⚠️ Confirmation requise', @@ -73,7 +73,7 @@ export const TentAdmin = () => { Swal.fire( "Succès ✅", - `La tente a bien été ${pair.confirmed ? "dévalidée" : "validée"}.`, + `La tente a bien été ${pair.confirmed ? "invalidée" : "validée"}.`, "success" ); @@ -194,7 +194,7 @@ export const TentAdmin = () => { : "bg-green-600 hover:bg-green-700 text-white" } > - {pair.confirmed ? "❌ Dévalider" : "✅ Valider"} + {pair.confirmed ? "❌ Invalidée" : "✅ Validée"} diff --git a/frontend/src/services/requests/tent.service.ts b/frontend/src/services/requests/tent.service.ts index 945fe85..83709f8 100644 --- a/frontend/src/services/requests/tent.service.ts +++ b/frontend/src/services/requests/tent.service.ts @@ -24,7 +24,7 @@ export const getAllTentPairs = async () => { return response.data; }; -// Valider ou dévalider une tente (admin) +// Valider ou invalider une tente (admin) export const toggleTentConfirmation = async ( userId1: number, userId2: number, From b0037bab0c556b0300a8f677e1e5893e085cfa32 Mon Sep 17 00:00:00 2001 From: Arthur Dodin Date: Tue, 30 Jun 2026 00:57:31 +0200 Subject: [PATCH 15/47] feat(email): permanences reminders --- backend/.env.example | 23 +++ backend/server.ts | 3 + backend/src/controllers/auth.controller.ts | 11 +- backend/src/controllers/bus.controller.ts | 6 +- backend/src/controllers/email.controller.ts | 126 +---------------- backend/src/controllers/news.controller.ts | 5 +- .../src/controllers/permanence.controller.ts | 28 ++++ backend/src/controllers/tent.controller.ts | 6 +- backend/src/email/email.preview-data.ts | 36 +++++ backend/src/email/email.registry.ts | 81 +++++++++++ backend/src/email/email.renderer.ts | 25 ++++ .../templates}/attribution-bus.html | 0 .../email => email/templates}/custom.html | 0 .../email => email/templates}/notebook.html | 0 .../templates}/notify-news.html | 0 .../templates/notify-permanence-reminder.html | 78 +++++++++++ .../templates}/notify-tent-confirmation.html | 0 .../templates}/reset-password.html | 0 .../email => email/templates}/welcome.html | 0 backend/src/errors/permanence.error.ts | 7 + .../src/middlewares/automation.middleware.ts | 22 +++ backend/src/routes/automation.routes.ts | 11 ++ backend/src/services/email.service.ts | 25 ++-- backend/src/services/permanence.service.ts | 131 +++++++++++++++--- backend/src/utils/emailtemplates.ts | 41 ------ backend/src/utils/secret.ts | 1 + backend/tsconfig.json | 4 +- backend/types/email.d.ts | 36 +++++ backend/types/permanence.d.ts | 34 +++++ 29 files changed, 532 insertions(+), 208 deletions(-) create mode 100644 backend/.env.example create mode 100644 backend/src/email/email.preview-data.ts create mode 100644 backend/src/email/email.registry.ts create mode 100644 backend/src/email/email.renderer.ts rename backend/src/{templates/email => email/templates}/attribution-bus.html (100%) rename backend/src/{templates/email => email/templates}/custom.html (100%) rename backend/src/{templates/email => email/templates}/notebook.html (100%) rename backend/src/{templates/email => email/templates}/notify-news.html (100%) create mode 100644 backend/src/email/templates/notify-permanence-reminder.html rename backend/src/{templates/email => email/templates}/notify-tent-confirmation.html (100%) rename backend/src/{templates/email => email/templates}/reset-password.html (100%) rename backend/src/{templates/email => email/templates}/welcome.html (100%) create mode 100644 backend/src/errors/permanence.error.ts create mode 100644 backend/src/middlewares/automation.middleware.ts create mode 100644 backend/src/routes/automation.routes.ts delete mode 100644 backend/src/utils/emailtemplates.ts create mode 100644 backend/types/email.d.ts create mode 100644 backend/types/permanence.d.ts diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..e8a333d --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,23 @@ +DATABASE_URL=postgresql://admin:password@localhost:5432/integration-dev +OUTSIDE_DATABASE_URL=postgresql://admin:password@localhost:5432/integration-dev + +POSTGRES_PASSWORD=password +POSTGRES_USER=admin +POSTGRES_DB=integration-dev +POSTGRES_HOST=localhost +POSTGRES_PORT=5432 + +SERVER_PORT=4001 +SERVICE_URL=http://localhost:4001/ + +SHOTGUN_PASSWORD=vodka + +CAS_VALIDATE_URL= +CAS_LOGIN_URL= + +EMAIL_HOST= +EMAIL_USER= +EMAIL_PASSWORD= +EMAIL_FROM= + +AUTOMATION_TOKEN= diff --git a/backend/server.ts b/backend/server.ts index d1def34..02128c2 100644 --- a/backend/server.ts +++ b/backend/server.ts @@ -10,7 +10,9 @@ import { initUser } from './src/database/initdb/initUser'; import { initEvent } from './src/database/initdb/initevent'; import { initRoles } from './src/database/initdb/initrole'; import { authenticateUser } from './src/middlewares/auth.middleware'; +import { authenticateAutomation } from './src/middlewares/automation.middleware'; import authRoutes from './src/routes/auth.routes'; +import automationRoutes from './src/routes/automation.routes'; import busRoutes from './src/routes/bus.routes'; import challengeRoutes from './src/routes/challenge.routes'; import defaultRoute from './src/routes/default.routes'; @@ -48,6 +50,7 @@ async function startServer() { // Utilisation des routes d'authentification app.use('/api', defaultRoute) app.use('/api/auth', authRoutes); + app.use('/api/automation', authenticateAutomation, automationRoutes); app.use('/api/authadmin', authenticateUser, authRoutes); app.use('/api/role', authenticateUser, roleRoutes); app.use('/api/user', authenticateUser, userRoutes); diff --git a/backend/src/controllers/auth.controller.ts b/backend/src/controllers/auth.controller.ts index c71871c..2719570 100644 --- a/backend/src/controllers/auth.controller.ts +++ b/backend/src/controllers/auth.controller.ts @@ -2,16 +2,17 @@ import bcrypt from 'bcryptjs'; import bigInt from 'big-integer'; import { type Request, type Response } from 'express'; import { sign, verify } from 'jsonwebtoken'; +import type { EmailOptions } from '../../types/email'; +import { templateResetPassword } from '../email/email.registry'; +import { compileTemplate } from '../email/email.renderer'; import * as auth_service from '../services/auth.service'; import * as email_service from '../services/email.service'; import * as registration_service from '../services/registration.service'; import * as role_service from '../services/role.service'; import * as user_service from '../services/user.service'; -import * as template from '../utils/emailtemplates'; import { Error, Ok, Unauthorized } from '../utils/responses'; -import { jwtSecret, service_url } from '../utils/secret'; +import { email_from, jwtSecret, service_url } from '../utils/secret'; import { decodeToken } from '../utils/token'; -import { type EmailOptions } from './email.controller'; // Fonction de connexion export const login = async (req: Request, res: Response) => { @@ -166,7 +167,7 @@ export const requestPasswordUser = async (req: Request, res: Response) => { // Générer le contenu HTML du mail - const htmlEmail = template.compileTemplate({ resetLink: resetLink }, template.templateResetPassword); + const htmlEmail = compileTemplate({ resetLink: resetLink }, templateResetPassword); if (!htmlEmail) { Error(res, { msg: "Nom de template invalide" }); @@ -175,7 +176,7 @@ export const requestPasswordUser = async (req: Request, res: Response) => { // Préparer les options d'email const emailOptions: EmailOptions = { - from: "integration@utt.fr", + from: email_from, to: [user_email], cc: [], bcc: [], diff --git a/backend/src/controllers/bus.controller.ts b/backend/src/controllers/bus.controller.ts index 5b3a142..561b50c 100644 --- a/backend/src/controllers/bus.controller.ts +++ b/backend/src/controllers/bus.controller.ts @@ -1,8 +1,8 @@ import { type Request, type Response } from "express"; import * as bus_service from "../services/bus.service"; -import { sendEmail } from "../services/email.service"; +import { generateEmailHtml, sendEmail } from "../services/email.service"; import { Error, Ok } from "../utils/responses"; -import { generateEmailHtml } from "./email.controller"; +import { email_from } from "../utils/secret"; interface MulterRequest extends Request { file?: Express.Multer.File; @@ -23,7 +23,7 @@ export const sendBusAttributionEmails = async (req: Request, res: Response) => { }); const emailOptions = { - from: "integration@utt.fr", + from: email_from, to: [attr.email], cc: [], bcc: [], diff --git a/backend/src/controllers/email.controller.ts b/backend/src/controllers/email.controller.ts index 93780b5..02f8d95 100644 --- a/backend/src/controllers/email.controller.ts +++ b/backend/src/controllers/email.controller.ts @@ -1,133 +1,13 @@ import { type Request, type Response } from 'express'; -import { sendEmail } from '../services/email.service'; +import type { EmailOptions } from "../../types/email"; +import { defaultPreviewData } from '../email/email.preview-data'; +import { generateEmailHtml, sendEmail } from '../services/email.service'; import * as registration_service from '../services/registration.service'; import * as user_service from '../services/user.service'; -import * as template from '../utils/emailtemplates'; import { Error, Ok } from '../utils/responses'; import { email_from, service_url } from '../utils/secret'; import { getLatestUploadedDocument } from '../utils/uploadDocuments'; -export interface EmailOptions { - from: string; - to: string[]; - subject: string; - text?: string; - html: string; - cc: string[]; - bcc: string[]; -} - -type TemplateData = Record; - -type TemplateRenderer = { - fileName: string; - buildData: (data: TemplateData) => TemplateData; -}; - -const templateRenderers: Record = { - custom: { - fileName: 'custom.html', - buildData: (data) => { - const typedData = data as { title?: string; content?: string }; - return { - title: typedData.title ?? '', - content: typedData.content ?? '', - }; - }, - }, - templateNotebook: { - fileName: template.templateNotebook, - buildData: (data) => { - const typedData = data as { - notebook_fr?: string; - notebook_en?: string; - }; - return { - notebook_fr: typedData.notebook_fr, - notebook_en: typedData.notebook_en - }; - }, - }, - templateAttributionBus: { - fileName: template.templateAttributionBus, - buildData: (data) => { - const typedData = data as { bus?: string; time?: string }; - return { bus: typedData.bus, time: typedData.time }; - }, - }, - templateWelcome: { - fileName: template.templateWelcome, - buildData: (data) => { - const typedData = data as { token?: string }; - return { token: typedData.token }; - }, - }, - templateNotifyNews: { - fileName: template.templateNotifyNews, - buildData: (data) => { - const typedData = data as { title?: string }; - return { title: typedData.title }; - }, - }, - templateNotifyTentConfirmation: { - fileName: template.templateNotifyTentConfirmation, - buildData: (data) => { - const typedData = data as { user1?: string; user2?: string; confirmed?: boolean }; - return { - user1: typedData.user1, - user2: typedData.user2, - confirmed: typedData.confirmed, - }; - }, - }, - templateResetPassword: { - fileName: template.templateResetPassword, - buildData: (data) => { - const typedData = data as { resetLink?: string }; - return { resetLink: typedData.resetLink }; - }, - }, -}; - -// Fonction pour générer l'HTML à partir du template -export const generateEmailHtml = (templateName: string, data: TemplateData) => { - const renderer = templateRenderers[templateName]; - if (!renderer) { - return null; - } - - return template.compileTemplate(renderer.buildData(data), renderer.fileName); -}; - -const defaultPreviewData: Record = { - custom: { - title: 'Titre de démonstration', - content: 'Premier paragraphe.\nDeuxième ligne conservée.\n\nNouvel alinéa.', - }, - templateNotebook: { - notebook_fr: `${service_url}api/uploads/notebooks/fr.pdf`, - notebook_en: `${service_url}api/uploads/notebooks/en.pdf` - }, - templateAttributionBus: { - bus: 'bus', - time: '09h00', - }, - templateWelcome: { - token: 'preview-token', - }, - templateNotifyNews: { - title: 'Titre de démonstration', - }, - templateNotifyTentConfirmation: { - user1: 'Utilisateur 1', - user2: 'Utilisateur 2', - confirmed: true, - }, - templateResetPassword: { - resetLink: `${service_url}resetpassword?token=preview-token`, - }, -}; - // Fonction utilitaire pour récupérer les destinataires const getRecipients = async (permission: string | undefined, sendTo: string[] | undefined) => { if (permission) { diff --git a/backend/src/controllers/news.controller.ts b/backend/src/controllers/news.controller.ts index 4534bc1..3b40a52 100644 --- a/backend/src/controllers/news.controller.ts +++ b/backend/src/controllers/news.controller.ts @@ -2,10 +2,11 @@ import { type Request, type Response } from "express"; import fs from "fs"; import path from "path"; import * as email_service from "../services/email.service"; +import { generateEmailHtml } from "../services/email.service"; import * as news_service from "../services/news.service"; import * as user_service from '../services/user.service'; import { Error, Ok } from "../utils/responses"; -import { generateEmailHtml } from './email.controller'; +import { email_from } from "../utils/secret"; const toStoredUploadPath = (imageUrl: string) => { if (!imageUrl) { @@ -136,7 +137,7 @@ export const publishNews = async (req: Request, res: Response) => { } const email = { - from: "integration@utt.fr", + from: email_from, to: [], subject: `[INTEGRATION UTT] Nouvelle actu : ${news.title}`, html: html, diff --git a/backend/src/controllers/permanence.controller.ts b/backend/src/controllers/permanence.controller.ts index 6f89390..956fdae 100644 --- a/backend/src/controllers/permanence.controller.ts +++ b/backend/src/controllers/permanence.controller.ts @@ -371,3 +371,31 @@ export const claimMember = async (req: Request, res: Response) => { Error(res, { msg: "Erreur lors de la mise à jour du statut du membre" }); } }; + +export const sendHourlyNotificationToUsers = async (req: Request, res: Response) => { + + const notifications = await permanence_service.getHourlyNotifications(); + + if (notifications.length === 0) { + Ok(res, { msg: "Aucune notification horaire à envoyer." }); + return; + } + + permanence_service.sendNotifications(notifications); + + Ok(res, { msg: "Notifications horaires envoyées avec succès" }); +}; + +export const sendDailyNotificationToUsers = async (req: Request, res: Response) => { + + const notifications = await permanence_service.getDailyNotifications(); + + if (notifications.length === 0) { + Ok(res, { msg: "Aucune notification quotidienne à envoyer." }); + return; + } + + permanence_service.sendNotifications(notifications); + + Ok(res, { msg: "Notifications quotidiennes envoyées avec succès" }); +}; diff --git a/backend/src/controllers/tent.controller.ts b/backend/src/controllers/tent.controller.ts index 5dd067b..ed62e44 100644 --- a/backend/src/controllers/tent.controller.ts +++ b/backend/src/controllers/tent.controller.ts @@ -1,9 +1,9 @@ import { type Request, type Response } from "express"; -import { sendEmail } from "../services/email.service"; +import { generateEmailHtml, sendEmail } from "../services/email.service"; import * as tent_service from "../services/tent.service"; import { getUserById } from "../services/user.service"; import { Error, Ok } from "../utils/responses"; -import { generateEmailHtml } from "./email.controller"; +import { email_from } from "../utils/secret"; export const createTent = async (req: Request, res: Response) => { const { userId2 } = req.body; @@ -86,7 +86,7 @@ export const toggleTentConfirmation = async (req: Request, res: Response) => { // Options d'email const emailOptions = { - from: "integration@utt.fr", + from: email_from, to: [user1.email, user2.email], subject: confirmed ? "🎉 Votre tente a été validée !" diff --git a/backend/src/email/email.preview-data.ts b/backend/src/email/email.preview-data.ts new file mode 100644 index 0000000..f2742b1 --- /dev/null +++ b/backend/src/email/email.preview-data.ts @@ -0,0 +1,36 @@ +import type { TemplateData } from "../../types/email"; +import { service_url } from '../utils/secret'; + +export const defaultPreviewData: Record = { + custom: { + title: 'Titre de démonstration', + content: 'Premier paragraphe.\nDeuxième ligne conservée.\n\nNouvel alinéa.', + }, + templateNotebook: { + notebook_fr: `${service_url}api/uploads/notebooks/fr.pdf`, + notebook_en: `${service_url}api/uploads/notebooks/en.pdf` + }, + templateAttributionBus: { + bus: 'bus', + time: '09h00', + }, + templateWelcome: { + token: 'preview-token', + }, + templateNotifyNews: { + title: 'Titre de démonstration', + }, + templateNotifyTentConfirmation: { + user1: 'Utilisateur 1', + user2: 'Utilisateur 2', + confirmed: true, + }, + templateResetPassword: { + resetLink: `${service_url}resetpassword?token=preview-token`, + }, + templateNotifyPermanenceReminder: { + permanence: { + + } + } +}; diff --git a/backend/src/email/email.registry.ts b/backend/src/email/email.registry.ts new file mode 100644 index 0000000..a1f4972 --- /dev/null +++ b/backend/src/email/email.registry.ts @@ -0,0 +1,81 @@ +import type { PermanenceEmailData, TemplateRenderer } from "../../types/email"; + +export const templateResetPassword = 'reset-password.html'; +const templateNotebook = 'notebook.html'; +const templateAttributionBus = 'attribution-bus.html'; +const templateWelcome = 'welcome.html'; +const templateNotifyNews = 'notify-news.html'; +const templateNotifyTentConfirmation = 'notify-tent-confirmation.html'; +const templateNotifyPermanenceReminder = 'notify-permanence-reminder.html'; + +export const templateRenderers: Record = { + custom: { + fileName: 'custom.html', + buildData: (data) => { + const typedData = data as { title?: string; content?: string }; + return { + title: typedData.title ?? '', + content: typedData.content ?? '', + }; + }, + }, + templateNotebook: { + fileName: templateNotebook, + buildData: (data) => { + const typedData = data as { + notebook_fr?: string; + notebook_en?: string; + }; + return { + notebook_fr: typedData.notebook_fr, + notebook_en: typedData.notebook_en + }; + }, + }, + templateAttributionBus: { + fileName: templateAttributionBus, + buildData: (data) => { + const typedData = data as { bus?: string; time?: string }; + return { bus: typedData.bus, time: typedData.time }; + }, + }, + templateWelcome: { + fileName: templateWelcome, + buildData: (data) => { + const typedData = data as { token?: string }; + return { token: typedData.token }; + }, + }, + templateNotifyNews: { + fileName: templateNotifyNews, + buildData: (data) => { + const typedData = data as { title?: string }; + return { title: typedData.title }; + }, + }, + templateNotifyTentConfirmation: { + fileName: templateNotifyTentConfirmation, + buildData: (data) => { + const typedData = data as { user1?: string; user2?: string; confirmed?: boolean }; + return { + user1: typedData.user1, + user2: typedData.user2, + confirmed: typedData.confirmed, + }; + }, + }, + templateResetPassword: { + fileName: templateResetPassword, + buildData: (data) => { + const typedData = data as { resetLink?: string }; + return { resetLink: typedData.resetLink }; + }, + }, + templateNotifyPermanenceReminder: { + fileName: templateNotifyPermanenceReminder, + buildData: (data) => { + const typedData = data as PermanenceEmailData; + return typedData; + }, + } +}; diff --git a/backend/src/email/email.renderer.ts b/backend/src/email/email.renderer.ts new file mode 100644 index 0000000..ec3fef5 --- /dev/null +++ b/backend/src/email/email.renderer.ts @@ -0,0 +1,25 @@ +import fs from "fs"; +import Handlebars from "handlebars"; +import path from "path"; + +const templateCache = new Map(); + +const readTemplate = (templateFileName: string) => { + const cached = templateCache.get(templateFileName); + if (cached) return cached; + + const templatePath = path.join(path.resolve(__dirname, "./templates"), templateFileName); + + if (fs.existsSync(templatePath)) { + const content = fs.readFileSync(templatePath, "utf8"); + templateCache.set(templateFileName, content); + return content; + } + + throw new Error(`Template introuvable: ${templateFileName}`); +}; + +export const compileTemplate = (data: any, fileName: string) => { + const compiled = Handlebars.compile(readTemplate(fileName)); + return compiled(data); +}; diff --git a/backend/src/templates/email/attribution-bus.html b/backend/src/email/templates/attribution-bus.html similarity index 100% rename from backend/src/templates/email/attribution-bus.html rename to backend/src/email/templates/attribution-bus.html diff --git a/backend/src/templates/email/custom.html b/backend/src/email/templates/custom.html similarity index 100% rename from backend/src/templates/email/custom.html rename to backend/src/email/templates/custom.html diff --git a/backend/src/templates/email/notebook.html b/backend/src/email/templates/notebook.html similarity index 100% rename from backend/src/templates/email/notebook.html rename to backend/src/email/templates/notebook.html diff --git a/backend/src/templates/email/notify-news.html b/backend/src/email/templates/notify-news.html similarity index 100% rename from backend/src/templates/email/notify-news.html rename to backend/src/email/templates/notify-news.html diff --git a/backend/src/email/templates/notify-permanence-reminder.html b/backend/src/email/templates/notify-permanence-reminder.html new file mode 100644 index 0000000..3a3b6b9 --- /dev/null +++ b/backend/src/email/templates/notify-permanence-reminder.html @@ -0,0 +1,78 @@ + + + + + + Intégration UTT + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ Logo Intégration UTT +
+ INTEGRATION UTT +
+

Rappel de permanence

+ +

{{permName}}

+

Entre le {{permBeginDate}} à {{permBeginHour}} et le {{permEndDate}} à {{permEndHour}}

+

Point de rendez-vous: {{permLocation}}

+

Description de la permanence: {{permDescription}}

+

Pour le bon déroulement des permanences, nous vous demandons de vous présenter 15 minutes à l'avance sur les lieux.

+

Le ou la reponsable de permanence effectuera l'appel. Seules les permanences honorées seront comptabilisés pour le total de permanences des chefs et cheffes d'équipe.

+
+

Retrouve des informations utiles sur notre Instagram !

+
+ + Instagram + +
+

Cordialement,
L'équipe intégration UTT

+

Si vous avez des questions, n'hésitez pas à nous contacter.

+
+

Permanence reminder

+ +

{{permName}}

+

From the {{permBeginDate}} at {{permBeginHour}} to the {{permEndDate}} at {{permEndHour}}

+

Meeting point: {{permLocation}}

+

Description: {{permDescription}}

+

To ensure the smooth running of the permanence hours, we ask that you arrive 15 minutes early on the premises.

+
+

Find useful updates on our Instagram!

+
+ + Instagram + +
+

Regards,
The UTT's Integration Team

+

If you have any questions, feel free to contact us.

+
+
+ + diff --git a/backend/src/templates/email/notify-tent-confirmation.html b/backend/src/email/templates/notify-tent-confirmation.html similarity index 100% rename from backend/src/templates/email/notify-tent-confirmation.html rename to backend/src/email/templates/notify-tent-confirmation.html diff --git a/backend/src/templates/email/reset-password.html b/backend/src/email/templates/reset-password.html similarity index 100% rename from backend/src/templates/email/reset-password.html rename to backend/src/email/templates/reset-password.html diff --git a/backend/src/templates/email/welcome.html b/backend/src/email/templates/welcome.html similarity index 100% rename from backend/src/templates/email/welcome.html rename to backend/src/email/templates/welcome.html diff --git a/backend/src/errors/permanence.error.ts b/backend/src/errors/permanence.error.ts new file mode 100644 index 0000000..1092c0f --- /dev/null +++ b/backend/src/errors/permanence.error.ts @@ -0,0 +1,7 @@ +export class UnauthorizedError extends Error { } +export class AlreadyRegisteredError extends Error { } +export class PermanenceNotFoundError extends Error { } +export class PermanenceClosedError extends Error { } +export class PermanenceFullError extends Error { } +export class UnregisterDeadlineError extends Error { } +export class RegisterDeadlineError extends Error { } diff --git a/backend/src/middlewares/automation.middleware.ts b/backend/src/middlewares/automation.middleware.ts new file mode 100644 index 0000000..d3a41bf --- /dev/null +++ b/backend/src/middlewares/automation.middleware.ts @@ -0,0 +1,22 @@ +import { type NextFunction, type Request, type Response } from "express"; +import { Unauthorized } from "../utils/responses"; // Assurez-vous que cette fonction est bien définie +import { automation_token } from "../utils/secret"; + +export const authenticateAutomation = (req: Request, res: Response, next: NextFunction) => { + try { + + const { token } = req.body; + + if (!token) { + return Unauthorized(res, { msg: "Unauthorized: Missing or malformed token" }); + } + + if (token !== automation_token) { + return Unauthorized(res, { msg: "Unauthorized: Invalid token" }) + } + + next(); + } catch { + return Unauthorized(res, { msg: "Unauthorized: Invalid token" }); + } +}; diff --git a/backend/src/routes/automation.routes.ts b/backend/src/routes/automation.routes.ts new file mode 100644 index 0000000..6fdec9a --- /dev/null +++ b/backend/src/routes/automation.routes.ts @@ -0,0 +1,11 @@ +import express from "express"; +import * as permanenceController from "../controllers/permanence.controller"; + + +const automationRoutes = express.Router(); + +// Permanences routes +automationRoutes.post("/permanence/notification/hourly", permanenceController.sendHourlyNotificationToUsers); +automationRoutes.post("/permanence/notification/daily", permanenceController.sendDailyNotificationToUsers); + +export default automationRoutes; diff --git a/backend/src/services/email.service.ts b/backend/src/services/email.service.ts index 0d6d445..53f9abc 100644 --- a/backend/src/services/email.service.ts +++ b/backend/src/services/email.service.ts @@ -1,15 +1,22 @@ import nodemailer from 'nodemailer'; +import type { EmailOptions, TemplateData } from "../../types/email"; +import { templateRenderers } from "../email/email.registry"; +import { compileTemplate } from "../email/email.renderer"; import { email_from, email_host, email_password, email_user } from '../utils/secret'; -interface EmailOptions { - from: string; - to: string[]; - subject: string; - text?: string; - html?: string; - cc?: string[]; - bcc?: string[]; -} +export const generateEmailHtml = ( + templateName: string, + data: TemplateData +) => { + const renderer = templateRenderers[templateName]; + + if (!renderer) return null; + + return compileTemplate( + renderer.buildData(data), + renderer.fileName + ); +}; export const sendEmail = async (options: EmailOptions): Promise => { try { diff --git a/backend/src/services/permanence.service.ts b/backend/src/services/permanence.service.ts index b47e69a..ae76a0b 100644 --- a/backend/src/services/permanence.service.ts +++ b/backend/src/services/permanence.service.ts @@ -1,30 +1,15 @@ import { and, eq, inArray, sql } from "drizzle-orm"; import fs from "fs"; import Papa from "papaparse"; +import type { PermanenceEmailData } from "../../types/email"; +import type { CsvPermanence, LightUser, Notification, Permanence } from "../../types/permanence"; import { db } from "../database/db"; +import { AlreadyRegisteredError, PermanenceClosedError, PermanenceFullError, PermanenceNotFoundError, RegisterDeadlineError, UnauthorizedError, UnregisterDeadlineError } from "../errors/permanence.error"; import { permanenceSchema } from "../schemas/Basic/permanence.schema"; import { userSchema } from "../schemas/Basic/user.schema"; import { respoPermanenceSchema, userPermanenceSchema } from "../schemas/Relational/userpermanences.schema"; - -type CsvPermanence = { - name: string; - description: string; - location: string; - start_at: string; - end_at: string; - capacity: string; - is_open: string; - difficulty: string; -}; - -// Classes d'erreurs personnalisées -class UnauthorizedError extends Error { } -class AlreadyRegisteredError extends Error { } -class PermanenceNotFoundError extends Error { } -class PermanenceClosedError extends Error { } -class PermanenceFullError extends Error { } -class UnregisterDeadlineError extends Error { } -class RegisterDeadlineError extends Error { } +import { email_from } from "../utils/secret"; +import { generateEmailHtml, sendEmail } from "./email.service"; export const getPermanenceById = async (permId: number) => { const permanence = await db.query.permanenceSchema.findFirst({ @@ -493,3 +478,109 @@ export const claimMember = async (userId: number, permId: number, claimed: boole ) ); }; + +export const getDailyNotifications = async (): Promise => { + const permanences = await db.query.permanenceSchema.findMany({ + where: sql` + ${permanenceSchema.start_at} >= CURRENT_DATE + INTERVAL '1 day' + AND ${permanenceSchema.start_at} < CURRENT_DATE + INTERVAL '2 day' + `, + orderBy: permanenceSchema.start_at, + }); + + return getMembersFromPermanences(permanences); +} + +export const getHourlyNotifications = async (): Promise => { + const permanences = await db.query.permanenceSchema.findMany({ + where: sql` + ${permanenceSchema.start_at} >= date_trunc('hour', now()) + interval '1 hour' + AND ${permanenceSchema.start_at} < date_trunc('hour', now()) + interval '2 hour' + `, + orderBy: permanenceSchema.start_at, + }); + + return getMembersFromPermanences(permanences); +}; + +// Cette fonction est vouée à disparaitre lors du passage à Prisma, avec un simple "with" +export const getMembersFromPermanences = async (permanences: Permanence[]): Promise<{ + permanence: Permanence, + members: LightUser[] +}[]> => { + return await Promise.all( + permanences.map(async (perm) => { + const members = await db + .select({ + id: userSchema.id, + firstName: userSchema.first_name, + lastName: userSchema.last_name, + email: userSchema.email, + }) + .from(userPermanenceSchema) + .innerJoin(userSchema, eq(userSchema.id, userPermanenceSchema.user_id)) + .where(eq(userPermanenceSchema.permanence_id, perm.id)); + + return { + permanence: perm, + members: members + }; + }) + ); +} + +export const sendNotifications = async ( + notifications: Notification[] +) => { + for (const notification of notifications) { + + const permanenceEmailData: PermanenceEmailData = { + permName: notification.permanence.name, + permBeginDate: + new Intl.DateTimeFormat("fr-FR", { + day: "2-digit", + month: "long", + }).format(notification.permanence.start_at), + permBeginHour: + new Intl.DateTimeFormat("fr-FR", { + hour: "2-digit", + minute: "2-digit", + }).format(notification.permanence.start_at), + permEndDate: + new Intl.DateTimeFormat("fr-FR", { + day: "2-digit", + month: "long", + }).format(notification.permanence.end_at), + permEndHour: + new Intl.DateTimeFormat("fr-FR", { + hour: "2-digit", + minute: "2-digit", + }).format(notification.permanence.end_at), + permLocation: notification.permanence.location, + permDescription: notification.permanence.description + } + const subject = `[RAPPEL] Permanence - ${notification.permanence.name}` + + const htmlEmail = generateEmailHtml( + "templateNotifyPermanenceReminder", + permanenceEmailData + ); + + for (const member of notification.members) { + try { + + const emailOptions = { + from: email_from, + to: [member.email], + subject: subject, + text: "", + html: htmlEmail, + }; + + await sendEmail(emailOptions); + } catch (err) { + console.error(err); + } + } + } +}; diff --git a/backend/src/utils/emailtemplates.ts b/backend/src/utils/emailtemplates.ts deleted file mode 100644 index c4538c1..0000000 --- a/backend/src/utils/emailtemplates.ts +++ /dev/null @@ -1,41 +0,0 @@ -import fs from 'fs'; -import Handlebars from 'handlebars'; -import path from 'path'; - -const templateDirectories = [ - path.resolve(__dirname, '../templates/email'), - path.resolve(process.cwd(), 'src/templates/email'), - path.resolve(process.cwd(), 'backend/src/templates/email'), -]; - -const templateCache = new Map(); - -const readTemplate = (templateFileName: string) => { - const cachedTemplate = templateCache.get(templateFileName); - if (cachedTemplate) { - return cachedTemplate; - } - - for (const directory of templateDirectories) { - const templatePath = path.join(directory, templateFileName); - if (fs.existsSync(templatePath)) { - const templateContent = fs.readFileSync(templatePath, 'utf8'); - templateCache.set(templateFileName, templateContent); - return templateContent; - } - } - - throw new Error(`Template HTML introuvable: ${templateFileName}`); -}; - -export const templateResetPassword = 'reset-password.html'; -export const templateNotebook = 'notebook.html'; -export const templateAttributionBus = 'attribution-bus.html'; -export const templateWelcome = 'welcome.html'; -export const templateNotifyNews = 'notify-news.html'; -export const templateNotifyTentConfirmation = 'notify-tent-confirmation.html'; - -export const compileTemplate = (data: any, templateFileName: string) => { - const compiledTemplate = Handlebars.compile(readTemplate(templateFileName)); - return compiledTemplate(data); -}; diff --git a/backend/src/utils/secret.ts b/backend/src/utils/secret.ts index 30acfdc..f7c51fc 100644 --- a/backend/src/utils/secret.ts +++ b/backend/src/utils/secret.ts @@ -30,3 +30,4 @@ export const discord_client_id = process.env.DISCORD_CLIENT_ID || "default"; export const discord_client_secret = process.env.DISCORD_CLIENT_SECRET || "default"; export const discord_redirect_uri = process.env.DISCORD_REDIRECT_URI || "default"; export const shotgun_password = process.env.SHOTGUN_PASSWORD || ""; +export const automation_token = process.env.AUTOMATION_TOKEN || ""; diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 5e26616..8f88859 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -12,6 +12,6 @@ "outDir": "./dist", "resolveJsonModule": true }, - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "types/permanence.d.ts"], "exclude": ["node_modules"] -} \ No newline at end of file +} diff --git a/backend/types/email.d.ts b/backend/types/email.d.ts new file mode 100644 index 0000000..d7b5575 --- /dev/null +++ b/backend/types/email.d.ts @@ -0,0 +1,36 @@ +export interface EmailOptions { + from: string; + to: string[]; + subject: string; + text?: string; + html: string; + cc: string[]; + bcc: string[]; +} + +export type TemplateData = Record; + +export type TemplateRenderer = { + fileName: string; + buildData: (data: TemplateData) => TemplateData; +}; + +export interface EmailOptions { + from: string; + to: string[]; + subject: string; + text?: string; + html?: string; + cc?: string[]; + bcc?: string[]; +} + +export interface PermanenceEmailData extends TemplateData { + permName: string; + permBeginDate: string; + permBeginHour: string; + permEndDate: string; + permEndHour: string; + permLocation: string; + permDescription: string; +} diff --git a/backend/types/permanence.d.ts b/backend/types/permanence.d.ts new file mode 100644 index 0000000..190fbb6 --- /dev/null +++ b/backend/types/permanence.d.ts @@ -0,0 +1,34 @@ +export type CsvPermanence = { + name: string; + description: string; + location: string; + start_at: string; + end_at: string; + capacity: string; + is_open: string; + difficulty: string; +}; + +export type Notification = { + permanence: Permanence; + members: LightUser[] +} + +export type Permanence = { + id: number; + name: string; + description: string; + location: string; + start_at: Date; + end_at: Date; + capacity: number; + is_open: boolean; + difficulty: number; +} + +export type LightUser = { + id: number; + firstName: string; + lastName: string; + email: string; +} From 6a9b39a012b02601a0e05b1a590f8189c62c8ea9 Mon Sep 17 00:00:00 2001 From: Arthur Dodin Date: Tue, 30 Jun 2026 01:06:53 +0200 Subject: [PATCH 16/47] refactor(email): align news email with other emails templates --- backend/src/email/templates/notify-news.html | 81 ++++++++++++++++---- 1 file changed, 68 insertions(+), 13 deletions(-) diff --git a/backend/src/email/templates/notify-news.html b/backend/src/email/templates/notify-news.html index 94fc85f..00b819e 100644 --- a/backend/src/email/templates/notify-news.html +++ b/backend/src/email/templates/notify-news.html @@ -5,18 +5,73 @@ Intégration UTT - -
-
- Logo Intégration UTT -

Intégration UTT

-
-

🗞️ Nouvelle actu !

-

{{title}}

-

👉 Rendez-vous sur le site de l'inté dans l'onglet News pour en savoir plus.

-

- Accéder au site -

-
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ Logo Intégration UTT +
+ INTEGRATION UTT +
+

Nouvelle actu !

+ +

{{title}}

+

👉 Rendez-vous sur le site de l'inté dans l'onglet News pour en savoir plus.

+

+ Accéder au site +

+
+

Retrouve des informations utiles sur notre Instagram !

+
+ + Instagram + +
+

Cordialement,
L'équipe intégration UTT

+

Si vous avez des questions, n'hésitez pas à nous contacter.

+
+

New news !

+ +

{{title}}

+

👉 Visit the integration website in the News tab to find out more.

+

+ Click here +

+
+

Find useful updates on our Instagram!

+
+ + Instagram + +
+

Regards,
The UTT's Integration Team

+

If you have any questions, feel free to contact us.

+
+
From 1f04508b29cbd732380be26f464fe25a3f3c2759 Mon Sep 17 00:00:00 2001 From: Arthur Dodin Date: Tue, 30 Jun 2026 18:13:09 +0200 Subject: [PATCH 17/47] refactor(email): improve front --- backend/.prettierrc | 10 + backend/src/controllers/auth.controller.ts | 99 ++--- backend/src/controllers/bus.controller.ts | 27 +- .../src/controllers/challenge.controller.ts | 59 +-- backend/src/controllers/discord.controller.ts | 16 +- backend/src/controllers/email.controller.ts | 30 +- backend/src/controllers/event.controller.ts | 86 ++-- backend/src/controllers/faction.controller.ts | 16 +- .../src/controllers/im_export.controller.ts | 176 ++++---- backend/src/controllers/news.controller.ts | 76 ++-- .../src/controllers/permanence.controller.ts | 112 +++-- backend/src/controllers/role.controller.ts | 64 +-- backend/src/controllers/team.controller.ts | 80 ++-- backend/src/controllers/tent.controller.ts | 46 +- backend/src/controllers/user.controller.ts | 41 +- backend/src/email/email.preview-data.ts | 10 +- backend/src/email/email.registry.ts | 8 +- backend/src/email/email.renderer.ts | 10 +- .../src/email/templates/attribution-bus.html | 395 ++++++++++++----- backend/src/email/templates/custom.html | 128 ++++-- backend/src/email/templates/notebook.html | 219 ++++++--- backend/src/email/templates/notify-news.html | 223 +++++++--- .../templates/notify-permanence-reminder.html | 216 +++++---- .../templates/notify-tent-confirmation.html | 73 ++- .../src/email/templates/reset-password.html | 234 ++++++---- backend/src/email/templates/welcome.html | 334 +++++++++----- backend/src/errors/permanence.error.ts | 14 +- .../src/middlewares/automation.middleware.ts | 15 +- backend/src/routes/automation.routes.ts | 9 +- backend/src/routes/email.routes.ts | 4 +- backend/src/services/email.service.ts | 41 +- frontend/.prettierrc | 10 + frontend/src/App.tsx | 416 +++++++++++++----- frontend/src/components/Admin/adminEmail.tsx | 180 ++++---- frontend/src/components/Admin/adminEvent.tsx | 81 ++-- .../ui/horizontalMultipleSelect.tsx | 71 +++ .../components/ui/horizontalSingleSelect.tsx | 51 +++ .../src/services/requests/email.service.ts | 8 +- 38 files changed, 2380 insertions(+), 1308 deletions(-) create mode 100644 backend/.prettierrc create mode 100644 frontend/.prettierrc create mode 100644 frontend/src/components/ui/horizontalMultipleSelect.tsx create mode 100644 frontend/src/components/ui/horizontalSingleSelect.tsx diff --git a/backend/.prettierrc b/backend/.prettierrc new file mode 100644 index 0000000..b7db128 --- /dev/null +++ b/backend/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 120, + "tabWidth": 4, + "arrowParens": "always", + "endOfLine": "lf", + "bracketSameLine": true +} diff --git a/backend/src/controllers/auth.controller.ts b/backend/src/controllers/auth.controller.ts index 2719570..6e5cf8a 100644 --- a/backend/src/controllers/auth.controller.ts +++ b/backend/src/controllers/auth.controller.ts @@ -41,7 +41,6 @@ export const register = async (req: Request, res: Response) => { } }; - export const handlecasticket = async (req: Request, res: Response) => { try { const ticket = req.query.ticket as string; @@ -53,14 +52,25 @@ export const handlecasticket = async (req: Request, res: Response) => { // Assurez-vous que user.email est un string let user = await user_service.getUserByEmail(CASuser.email.toLowerCase()); if (!user) { - const password = bigInt.randBetween(bigInt(2).pow(255), bigInt(2).pow(256).minus(1)).toString() - await user_service.createUser(CASuser.givenName, CASuser.sn, CASuser.email, true, "Student", " ", password) - user = await user_service.getUserByEmail(CASuser.email.toLowerCase()) + const password = bigInt.randBetween(bigInt(2).pow(255), bigInt(2).pow(256).minus(1)).toString(); + await user_service.createUser( + CASuser.givenName, + CASuser.sn, + CASuser.email, + true, + 'Student', + ' ', + password, + ); + user = await user_service.getUserByEmail(CASuser.email.toLowerCase()); } - const id = user?.id + const id = user?.id; - if (!id) { Error(res, { msg: "Pas d'id" }); return; } + if (!id) { + Error(res, { msg: "Pas d'id" }); + return; + } await user_service.updateUserStudent(CASuser.givenName, CASuser.sn, CASuser.email); @@ -75,10 +85,7 @@ export const handlecasticket = async (req: Request, res: Response) => { const token = auth_service.generateToken(enrichedUser); - - Ok(res, { data: { token } }) - - + Ok(res, { data: { token } }); } else { Unauthorized(res, { msg: 'Unauthorized: Invalid user email' }); } @@ -88,75 +95,67 @@ export const handlecasticket = async (req: Request, res: Response) => { } catch { Unauthorized(res, { msg: 'Unauthorized: Invalid token' }); } -} - +}; export const isTokenValid = async (req: Request, res: Response) => { try { - const authHeader = req.headers["authorization"]; - if (!authHeader || !authHeader.startsWith("Bearer ")) { + const authHeader = req.headers['authorization']; + if (!authHeader || !authHeader.startsWith('Bearer ')) { Unauthorized(res, { - msg: "Unauthorized: Missing or malformed token", + msg: 'Unauthorized: Missing or malformed token', data: false, }); return; } - const token = authHeader.split(" ")[1]; + const token = authHeader.split(' ')[1]; // Décoder et valider le token const decodedToken = decodeToken(token); if (!decodedToken) { Unauthorized(res, { - msg: "Unauthorized: Token has expired or is invalid", + msg: 'Unauthorized: Token has expired or is invalid', data: false, }); - return + return; } - // Vérifier que l'email est bien présent dans le token if (!decodedToken.userEmail) { Unauthorized(res, { - msg: "Unauthorized: Invalid token content", + msg: 'Unauthorized: Invalid token content', data: false, }); - return + return; } // Répondre une seule fois Ok(res, { data: true }); - return + return; } catch { - Error(res, { msg: "Unauthorized: Token validation failed" }); - return + Error(res, { msg: 'Unauthorized: Token validation failed' }); + return; } }; - export const completeRegistration = async (req: Request, res: Response) => { - const { token, password } = req.body; try { - - await auth_service.completeRegistration(token, password) - Ok(res, { msg: "Inscription complétée avec succès.", data: true }) - + await auth_service.completeRegistration(token, password); + Ok(res, { msg: 'Inscription complétée avec succès.', data: true }); } catch (error) { - Error(res, { msg: error.message || "Une erreur est survenue." }); + Error(res, { msg: error.message || 'Une erreur est survenue.' }); } - -} +}; export const requestPasswordUser = async (req: Request, res: Response) => { - - const { user_email } = req.body + const { user_email } = req.body; const user = await user_service.getUserByEmail(user_email); if (!user) { Error(res, { msg: 'User not found' }); - return + return; } // Générer un token JWT @@ -165,12 +164,11 @@ export const requestPasswordUser = async (req: Request, res: Response) => { // Créer le lien de réinitialisation const resetLink = `${service_url}ResetPassword?token=${token}`; - // Générer le contenu HTML du mail const htmlEmail = compileTemplate({ resetLink: resetLink }, templateResetPassword); if (!htmlEmail) { - Error(res, { msg: "Nom de template invalide" }); + Error(res, { msg: 'Nom de template invalide' }); return; } @@ -187,19 +185,17 @@ export const requestPasswordUser = async (req: Request, res: Response) => { try { // Envoyer l'e-mail await email_service.sendEmail(emailOptions); - Ok(res, { msg: 'Email for password reste sent !' }) - return + Ok(res, { msg: 'Email for password reste sent !' }); + return; } catch { Error(res, { msg: 'Error when reseting password' }); - return + return; } - -} +}; export const resetPasswordUser = async (req: Request, res: Response) => { const { token, password } = req.body; - try { // Vérifiez et décodez le token const decoded: any = verify(token, jwtSecret); @@ -208,7 +204,7 @@ export const resetPasswordUser = async (req: Request, res: Response) => { const user = await user_service.getUserById(decoded.userId); if (!user) { Error(res, { msg: 'Utilisateur non trouvé' }); - return + return; } // Hash du nouveau mot de passe @@ -217,29 +213,30 @@ export const resetPasswordUser = async (req: Request, res: Response) => { // Mettez à jour le mot de passe de l'utilisateur await user_service.updateUserPassword(Number(user.userId), hashedPassword); Ok(res, { msg: 'Mot de passe réinitialisé avec succès' }); - return + return; } catch (error) { console.log(error); Error(res, { msg: 'Token invalid or expire' }); - return + return; } -} +}; export const renewToken = async (req: Request, res: Response) => { const { userId } = req.body; try { - const userToken = await registration_service.getRegistrationByUserId(userId); if (userToken) { await auth_service.deleteUserRegistrationToken(userId); } - const newToken = await auth_service.createRegistrationToken(userId) + const newToken = await auth_service.createRegistrationToken(userId); Ok(res, { - msg: 'Token renouvelé, vous pouvez renvoyer un email de bienvenu avec ce lien : https://integration.utt.fr/Register?token=' + newToken, + msg: + 'Token renouvelé, vous pouvez renvoyer un email de bienvenu avec ce lien : https://integration.utt.fr/Register?token=' + + newToken, }); } catch (err) { Error(res, { msg: err.message }); diff --git a/backend/src/controllers/bus.controller.ts b/backend/src/controllers/bus.controller.ts index 561b50c..89f1406 100644 --- a/backend/src/controllers/bus.controller.ts +++ b/backend/src/controllers/bus.controller.ts @@ -1,8 +1,8 @@ -import { type Request, type Response } from "express"; -import * as bus_service from "../services/bus.service"; -import { generateEmailHtml, sendEmail } from "../services/email.service"; -import { Error, Ok } from "../utils/responses"; -import { email_from } from "../utils/secret"; +import { type Request, type Response } from 'express'; +import * as bus_service from '../services/bus.service'; +import { generateEmailHtml, sendEmail } from '../services/email.service'; +import { Error, Ok } from '../utils/responses'; +import { email_from } from '../utils/secret'; interface MulterRequest extends Request { file?: Express.Multer.File; @@ -13,13 +13,14 @@ export const sendBusAttributionEmails = async (req: Request, res: Response) => { const attributions = await bus_service.getAllBusAttributions(); if (!attributions.length) { - Error(res, { msg: "Aucune attribution de bus trouvée." }); + Error(res, { msg: 'Aucune attribution de bus trouvée.' }); return; } for (const attr of attributions) { - const htmlEmail = generateEmailHtml("templateAttributionBus", { - bus: attr.bus, time: attr.departure_time + const htmlEmail = generateEmailHtml('templateAttributionBus', { + bus: attr.bus, + time: attr.departure_time, }); const emailOptions = { @@ -27,9 +28,9 @@ export const sendBusAttributionEmails = async (req: Request, res: Response) => { to: [attr.email], cc: [], bcc: [], - subject: `Attribution Bus - ${attr.firstName ?? ""} ${attr.lastName ?? ""}`, + subject: `Attribution Bus - ${attr.firstName ?? ''} ${attr.lastName ?? ''}`, text: `Votre bus attribué est le numéro ${attr.bus}`, - html: htmlEmail || "", + html: htmlEmail || '', }; await sendEmail(emailOptions); @@ -46,13 +47,13 @@ export const uploadbusCSV = async (req: MulterRequest, res: Response) => { try { const file = req.file; if (!file) { - Error(res, { msg: "Fichier CSV manquant." }); + Error(res, { msg: 'Fichier CSV manquant.' }); } await bus_service.importBusFromCSV(file.path); - Ok(res, { msg: "Importation réalisée avec succès." }); + Ok(res, { msg: 'Importation réalisée avec succès.' }); } catch (error) { - console.error("Erreur import CSV :", error); + console.error('Erreur import CSV :', error); Error(res, { msg: "Échec de l'importation." }); } }; diff --git a/backend/src/controllers/challenge.controller.ts b/backend/src/controllers/challenge.controller.ts index ee02edf..636b505 100644 --- a/backend/src/controllers/challenge.controller.ts +++ b/backend/src/controllers/challenge.controller.ts @@ -1,6 +1,6 @@ -import { type Request, type Response } from "express"; -import * as challenge_service from "../services/challenge.service"; -import { Created, Error, Ok, Unauthorized } from "../utils/responses"; +import { type Request, type Response } from 'express'; +import * as challenge_service from '../services/challenge.service'; +import { Created, Error, Ok, Unauthorized } from '../utils/responses'; export const createChallenge = async (req: Request, res: Response) => { const { title, description, category, points } = req.body; @@ -8,9 +8,9 @@ export const createChallenge = async (req: Request, res: Response) => { try { const challenge = await challenge_service.createChallenge(title, description, category, points, adminId); - Created(res, { msg: "Challenge créé avec succès", data: challenge }); + Created(res, { msg: 'Challenge créé avec succès', data: challenge }); } catch (err) { - Error(res, { msg: "Erreur lors de la création du challenge : " + err }); + Error(res, { msg: 'Erreur lors de la création du challenge : ' + err }); } }; @@ -19,9 +19,9 @@ export const deleteChallenge = async (req: Request, res: Response) => { try { await challenge_service.deleteChallenge(Number(challengeId)); - Ok(res, { msg: "Challenge supprimée avec succès" }); + Ok(res, { msg: 'Challenge supprimée avec succès' }); } catch (err) { - Error(res, { msg: "Erreur lors de la suppression du challenge : " + err }); + Error(res, { msg: 'Erreur lors de la suppression du challenge : ' + err }); } }; @@ -29,15 +29,20 @@ export const validateChallenge = async (req: Request, res: Response) => { const adminId = req.user.userId; const { challengeId, type, targetId } = req.body; - if (!["user", "team", "faction"].includes(type)) { - return Unauthorized(res, { msg: "Type de validation invalide." }); + if (!['user', 'team', 'faction'].includes(type)) { + return Unauthorized(res, { msg: 'Type de validation invalide.' }); } try { - const validation = await challenge_service.validateChallenge({ challengeId, type, targetId, validatedBy: adminId }); - Ok(res, { msg: "Challenge validé avec succès", data: validation }); + const validation = await challenge_service.validateChallenge({ + challengeId, + type, + targetId, + validatedBy: adminId, + }); + Ok(res, { msg: 'Challenge validé avec succès', data: validation }); } catch (err) { - Error(res, { msg: "Erreur lors de la validation du challenge : " + err }); + Error(res, { msg: 'Erreur lors de la validation du challenge : ' + err }); } }; export const unvalidateChallenge = async (req: Request, res: Response) => { @@ -45,7 +50,7 @@ export const unvalidateChallenge = async (req: Request, res: Response) => { try { const unvalidation = await challenge_service.unvalidateChallenge({ challengeId, factionId, teamId, userId }); - Ok(res, { msg: "Challenge invalidé avec succès", data: unvalidation }); + Ok(res, { msg: 'Challenge invalidé avec succès', data: unvalidation }); } catch (err) { Error(res, { msg: "Erreur lors de l'invalidation du challenge : " + err }); } @@ -57,7 +62,7 @@ export const addPointsToFaction = async (req: Request, res: Response) => { try { const result = await challenge_service.modifyFactionPoints({ title, factionId, points, reason, adminId }); - Ok(res, { msg: "Points ajoutés à la faction", data: result }); + Ok(res, { msg: 'Points ajoutés à la faction', data: result }); } catch (err) { Error(res, { msg: "Erreur lors de l'ajout de points : " + err }); } @@ -68,10 +73,16 @@ export const removePointsFromFaction = async (req: Request, res: Response) => { const { title, factionId, points, reason } = req.body; try { - const result = await challenge_service.modifyFactionPoints({ title, factionId, points: -Math.abs(points), reason, adminId }); - Ok(res, { msg: "Points retirés à la faction", data: result }); + const result = await challenge_service.modifyFactionPoints({ + title, + factionId, + points: -Math.abs(points), + reason, + adminId, + }); + Ok(res, { msg: 'Points retirés à la faction', data: result }); } catch (err) { - Error(res, { msg: "Erreur lors du retrait de points : " + err }); + Error(res, { msg: 'Erreur lors du retrait de points : ' + err }); } }; @@ -85,23 +96,20 @@ export const updateChallenge = async (req: Request, res: Response) => { category, points, }); - Ok(res, { msg: "Challenge mis à jour avec succès", data: updated }); + Ok(res, { msg: 'Challenge mis à jour avec succès', data: updated }); } catch (err) { - Error(res, { msg: "Erreur lors de la mise à jour : " + err }); + Error(res, { msg: 'Erreur lors de la mise à jour : ' + err }); } }; export const getValidatedChallenges = async (req: Request, res: Response) => { - try { const challengesValidated = await challenge_service.getValidatedChallenges(); Ok(res, { data: challengesValidated }); } catch (err) { - Error(res, { msg: "Erreur lors de la récupération des challenges validés " + err }); + Error(res, { msg: 'Erreur lors de la récupération des challenges validés ' + err }); } }; - - // === PUBLIC === export const getAllChallenges = async (req: Request, res: Response) => { @@ -109,18 +117,17 @@ export const getAllChallenges = async (req: Request, res: Response) => { const challenges = await challenge_service.getAllChallenges(); Ok(res, { data: challenges }); } catch (err) { - Error(res, { msg: "Erreur lors de la récupération des challenges : " + err }); + Error(res, { msg: 'Erreur lors de la récupération des challenges : ' + err }); } }; export const getTotalFactionPoints = async (req: Request, res: Response) => { - const { factionId } = req.query; try { const points = await challenge_service.getTotalFactionPoints(Number(factionId)); Ok(res, { data: points }); } catch (err) { - Error(res, { msg: "Erreur lors de la récupération des points : " + err }); + Error(res, { msg: 'Erreur lors de la récupération des points : ' + err }); } }; diff --git a/backend/src/controllers/discord.controller.ts b/backend/src/controllers/discord.controller.ts index 50b9aea..5dcc35e 100644 --- a/backend/src/controllers/discord.controller.ts +++ b/backend/src/controllers/discord.controller.ts @@ -1,21 +1,23 @@ -import { type Request, type Response } from "express"; -import * as discord_service from "../services/discord.service"; -import { Error, Ok } from "../utils/responses"; +import { type Request, type Response } from 'express'; +import * as discord_service from '../services/discord.service'; +import { Error, Ok } from '../utils/responses'; export const createChallenge = async (req: Request, res: Response) => { const { code } = req.body; const userId = req.user?.userId; if (!code) { - Error(res, { msg: "Code manquant dans l'URL" }) + Error(res, { msg: "Code manquant dans l'URL" }); return; } try { const discordUser = await discord_service.syncDiscordUserId(String(code), userId); - Ok(res, { msg: `Ton compte Discord (${discordUser.username}#${discordUser.discriminator}) a bien été lié à ton profil UTT.` }); + Ok(res, { + msg: `Ton compte Discord (${discordUser.username}#${discordUser.discriminator}) a bien été lié à ton profil UTT.`, + }); } catch (err) { - console.error("Erreur dans handleDiscordCallback:", err); - Error(res, { msg: "Erreur pendant la liaison avec Discord" }); + console.error('Erreur dans handleDiscordCallback:', err); + Error(res, { msg: 'Erreur pendant la liaison avec Discord' }); } }; diff --git a/backend/src/controllers/email.controller.ts b/backend/src/controllers/email.controller.ts index 02f8d95..03aef66 100644 --- a/backend/src/controllers/email.controller.ts +++ b/backend/src/controllers/email.controller.ts @@ -1,29 +1,19 @@ import { type Request, type Response } from 'express'; -import type { EmailOptions } from "../../types/email"; +import type { EmailOptions } from '../../types/email'; import { defaultPreviewData } from '../email/email.preview-data'; -import { generateEmailHtml, sendEmail } from '../services/email.service'; +import { generateEmailHtml, getRecipients, sendEmail } from '../services/email.service'; import * as registration_service from '../services/registration.service'; import * as user_service from '../services/user.service'; import { Error, Ok } from '../utils/responses'; import { email_from, service_url } from '../utils/secret'; import { getLatestUploadedDocument } from '../utils/uploadDocuments'; -// Fonction utilitaire pour récupérer les destinataires -const getRecipients = async (permission: string | undefined, sendTo: string[] | undefined) => { - if (permission) { - const users = await user_service.getUsersbyPermission(permission); - return users.map((user) => user.email); - } else { - return sendTo || []; - } -}; - export const handleSendEmail = async (req: Request, res: Response) => { - const { subject, templateName, permission, sendTo, html, title, content } = req.body.payload; + const { subject, templateName, recipientsGroups, sendTo, html, title, content } = req.body.payload; try { // Récupérer les destinataires - const recipients = await getRecipients(permission, sendTo); + const recipients = await getRecipients(recipientsGroups, sendTo); if (!recipients.length) { Error(res, { msg: 'Aucun destinataire trouvé.' }); @@ -49,20 +39,22 @@ export const handleSendEmail = async (req: Request, res: Response) => { continue; } - htmlEmail = generateEmailHtml(templateName, { token: registrationToken }); + htmlEmail = generateEmailHtml(templateName, { + token: registrationToken, + }); } else if (templateName === 'templateNotebook') { const notebook_fr = await getLatestUploadedDocument('notebooks', 'fr'); const notebook_en = await getLatestUploadedDocument('notebooks', 'en'); if (!notebook_fr || !notebook_en) { return Error(res, { - msg: 'Cahier de vacances manquant (fr ou en).' + msg: 'Cahier de vacances manquant (fr ou en).', }); } htmlEmail = generateEmailHtml(templateName, { notebook_fr: `${service_url}api/uploads/notebooks/fr.pdf`, - notebook_en: `${service_url}api/uploads/notebooks/en.pdf` + notebook_en: `${service_url}api/uploads/notebooks/en.pdf`, }); } else { htmlEmail = generateEmailHtml(templateName, defaultPreviewData[templateName] || {}); @@ -90,7 +82,7 @@ export const handleSendEmail = async (req: Request, res: Response) => { return; } catch (err) { console.error(err); - Error(res, { msg: 'Erreur lors de l\'envoi de l\'email.' }); + Error(res, { msg: "Erreur lors de l'envoi de l'email." }); return; } }; @@ -108,7 +100,7 @@ export const handlePreviewEmail = async (req: Request, res: Response) => { }); if (!htmlEmail) { - Error(res, { msg: "Nom de template invalide" }); + Error(res, { msg: 'Nom de template invalide' }); return; } diff --git a/backend/src/controllers/event.controller.ts b/backend/src/controllers/event.controller.ts index f9c5970..90049f6 100644 --- a/backend/src/controllers/event.controller.ts +++ b/backend/src/controllers/event.controller.ts @@ -1,62 +1,64 @@ -import { type Request, type Response } from "express"; -import * as event_service from "../services/event.service"; -import * as team_service from "../services/team.service"; -import { Conflict, Error, Ok, Teapot, Unauthorized } from "../utils/responses"; -import { shotgun_password } from "../utils/secret"; +import { type Request, type Response } from 'express'; +import * as event_service from '../services/event.service'; +import * as team_service from '../services/team.service'; +import { Conflict, Error, Ok, Teapot, Unauthorized } from '../utils/responses'; +import { shotgun_password } from '../utils/secret'; type AuthenticatedRequest = Request & { user?: { userId?: number } }; export const checkShotgunStatus = async (req: Request, res: Response) => { try { const status = await event_service.getEventsStatus(); - Ok(res, ({ data: { status: Boolean(status?.shotgun_open), password: status?.shotgun_open ? shotgun_password : "" } })); + Ok(res, { + data: { status: Boolean(status?.shotgun_open), password: status?.shotgun_open ? shotgun_password : '' }, + }); } catch (error) { - Error(res, { msg: "Error while catching shotgun status :" + error }) + Error(res, { msg: 'Error while catching shotgun status :' + error }); } }; export const checkPreRegisterStatus = async (req: Request, res: Response) => { try { const status = await event_service.getEventsStatus(); - Ok(res, ({ data: status?.pre_registration_open })); + Ok(res, { data: status?.pre_registration_open }); } catch (error) { - Error(res, { msg: "Error while catching pre-registration status :" + error }) + Error(res, { msg: 'Error while catching pre-registration status :' + error }); } }; export const checkSDIStatus = async (req: Request, res: Response) => { try { const status = await event_service.getEventsStatus(); - Ok(res, ({ data: status?.sdi_open })); + Ok(res, { data: status?.sdi_open }); } catch (error) { - Error(res, { msg: "Error while catching SDI status :" + error }) + Error(res, { msg: 'Error while catching SDI status :' + error }); } }; export const checkWEIStatus = async (req: Request, res: Response) => { try { const status = await event_service.getEventsStatus(); - Ok(res, ({ data: status?.wei_open })); + Ok(res, { data: status?.wei_open }); } catch (error) { - Error(res, { msg: "Error while catching WEI status :" + error }) + Error(res, { msg: 'Error while catching WEI status :' + error }); } }; export const checkFoodStatus = async (req: Request, res: Response) => { try { const status = await event_service.getEventsStatus(); - Ok(res, ({ data: status?.food_open })); + Ok(res, { data: status?.food_open }); } catch (error) { - Error(res, { msg: "Error while catching Food status :" + error }) + Error(res, { msg: 'Error while catching Food status :' + error }); } }; export const checkChallStatus = async (req: Request, res: Response) => { try { const status = await event_service.getEventsStatus(); - Ok(res, ({ data: status?.chall_open })); + Ok(res, { data: status?.chall_open }); } catch (error) { - Error(res, { msg: "Error while catching Challenge status :" + error }) + Error(res, { msg: 'Error while catching Challenge status :' + error }); } }; @@ -70,65 +72,63 @@ export const getShotgunAttempts = async (req: Request, res: Response) => { } const teamUsers = await team_service.getTeamUsers(attempt.teamId); - const leaderCount = teamUsers.filter((user) => user.permission !== "Nouveau").length; + const leaderCount = teamUsers.filter((user) => user.permission !== 'Nouveau').length; return { ...attempt, leaderCount }; - }) + }), ); Ok(res, { data: shotgunAttemptsWithLeaders }); } catch (error) { - Error(res, { msg: "Erreur lors de la récupération des tentatives shotgun : " + error }); + Error(res, { msg: 'Erreur lors de la récupération des tentatives shotgun : ' + error }); } }; - export const shotgunAttempt = async (req: Request, res: Response) => { - const { password } = req.body as { password?: string }; const userId = (req as AuthenticatedRequest).user?.userId; if (!userId) { - Unauthorized(res, { msg: "Utilisateur non authentifié." }); + Unauthorized(res, { msg: 'Utilisateur non authentifié.' }); return; } if (!shotgun_password) { - Error(res, { msg: "Mot de passe shotgun non configuré côté serveur." }); + Error(res, { msg: 'Mot de passe shotgun non configuré côté serveur.' }); return; } if (password !== shotgun_password) { - Teapot(res, { msg: "Le mot de passe shotgun est incorrect." }); + Teapot(res, { msg: 'Le mot de passe shotgun est incorrect.' }); return; } const status = await event_service.getEventsStatus(); if (!status?.shotgun_open) { - Unauthorized(res, { msg: "Le shotgun est fermé." }); + Unauthorized(res, { msg: 'Le shotgun est fermé.' }); return; } try { - const userTeam = await team_service.getUserTeam(userId) + const userTeam = await team_service.getUserTeam(userId); if (!userTeam) { Error(res, { msg: "Erreur : Tu n'as pas d'équipe !" }); return; } - const alreadyShotgun = await event_service.alreadyShotgun(userTeam) + const alreadyShotgun = await event_service.alreadyShotgun(userTeam); if (alreadyShotgun) { - Conflict(res, { msg: "Votre équipe est déjà dans le shotgun." }); + Conflict(res, { msg: 'Votre équipe est déjà dans le shotgun.' }); return; } await event_service.validateShotgun(userTeam); - Ok(res, { msg: "Shotgun validé !" }); + Ok(res, { msg: 'Shotgun validé !' }); return; } catch (error) { - Error(res, { msg: "Erreur pendant le shotguns : " + error }); + Error(res, { msg: 'Erreur pendant le shotguns : ' + error }); return; } }; @@ -138,9 +138,9 @@ export const togglePreRegistration = async (req: Request, res: Response) => { try { const result = await event_service.updatepreRegistrationStatus(preRegistrationOpen); - Ok(res, { msg: "Paramètres mis à jour.", data: result }); + Ok(res, { msg: 'Paramètres mis à jour.', data: result }); } catch { - Error(res, { msg: "Erreur lors de la mise à jour." }); + Error(res, { msg: 'Erreur lors de la mise à jour.' }); } }; @@ -149,9 +149,9 @@ export const toggleShotgun = async (req: Request, res: Response) => { try { const result = await event_service.updateShotgunStatus(shotgunOpen); - Ok(res, { msg: "Paramètres mis à jour.", data: result }); + Ok(res, { msg: 'Paramètres mis à jour.', data: result }); } catch { - Error(res, { msg: "Erreur lors de la mise à jour." }); + Error(res, { msg: 'Erreur lors de la mise à jour.' }); } }; @@ -160,9 +160,9 @@ export const toggleSDI = async (req: Request, res: Response) => { try { const result = await event_service.updateSDIStatus(sdiOpen); - Ok(res, { msg: "Paramètres mis à jour.", data: result }); + Ok(res, { msg: 'Paramètres mis à jour.', data: result }); } catch { - Error(res, { msg: "Erreur lors de la mise à jour." }); + Error(res, { msg: 'Erreur lors de la mise à jour.' }); } }; @@ -171,9 +171,9 @@ export const toggleWEI = async (req: Request, res: Response) => { try { const result = await event_service.updateWEIStatus(weiOpen); - Ok(res, { msg: "Paramètres mis à jour.", data: result }); + Ok(res, { msg: 'Paramètres mis à jour.', data: result }); } catch { - Error(res, { msg: "Erreur lors de la mise à jour." }); + Error(res, { msg: 'Erreur lors de la mise à jour.' }); } }; @@ -182,9 +182,9 @@ export const toggleFood = async (req: Request, res: Response) => { try { const result = await event_service.updateFoodStatus(foodOpen); - Ok(res, { msg: "Paramètres mis à jour.", data: result }); + Ok(res, { msg: 'Paramètres mis à jour.', data: result }); } catch { - Error(res, { msg: "Erreur lors de la mise à jour." }); + Error(res, { msg: 'Erreur lors de la mise à jour.' }); } }; @@ -193,8 +193,8 @@ export const toggleChall = async (req: Request, res: Response) => { try { const result = await event_service.updateChallStatus(challOpen); - Ok(res, { msg: "Paramètres mis à jour.", data: result }); + Ok(res, { msg: 'Paramètres mis à jour.', data: result }); } catch { - Error(res, { msg: "Erreur lors de la mise à jour." }); + Error(res, { msg: 'Erreur lors de la mise à jour.' }); } }; diff --git a/backend/src/controllers/faction.controller.ts b/backend/src/controllers/faction.controller.ts index 312c72e..6af0041 100644 --- a/backend/src/controllers/faction.controller.ts +++ b/backend/src/controllers/faction.controller.ts @@ -8,7 +8,7 @@ export const getFactions = async (req: Request, res: Response) => { Ok(res, { data: factions }); return; } catch { - Error(res, { msg: "Erreur lors de la récupération des factions" }); + Error(res, { msg: 'Erreur lors de la récupération des factions' }); } }; @@ -20,30 +20,30 @@ export const getFaction = async (req: Request, res: Response) => { Ok(res, { data: faction }); return; } catch { - Error(res, { msg: "Erreur lors de la récupération des factions" }); + Error(res, { msg: 'Erreur lors de la récupération des factions' }); } }; export const createFaction = async (req: Request, res: Response) => { - const { factionName } = req.body + const { factionName } = req.body; try { await faction_service.createFaction(factionName); - Ok(res, { msg: "Faction crée avec succès !" }); + Ok(res, { msg: 'Faction crée avec succès !' }); return; } catch { - Error(res, { msg: "Erreur lors de la création de la faction" }); + Error(res, { msg: 'Erreur lors de la création de la faction' }); } }; export const deleteFaction = async (req: Request, res: Response) => { - const { factionId } = req.query + const { factionId } = req.query; try { await faction_service.deleteFaction(Number(factionId)); - Ok(res, { msg: "Faction supprimée avec succès !" }); + Ok(res, { msg: 'Faction supprimée avec succès !' }); return; } catch { - Error(res, { msg: "Erreur lors de la suppression de la faction" }); + Error(res, { msg: 'Erreur lors de la suppression de la faction' }); } }; diff --git a/backend/src/controllers/im_export.controller.ts b/backend/src/controllers/im_export.controller.ts index a65a0cf..a8f8480 100644 --- a/backend/src/controllers/im_export.controller.ts +++ b/backend/src/controllers/im_export.controller.ts @@ -1,14 +1,19 @@ -import { type Request, type Response } from "express"; -import fs from "fs"; -import path from "path"; -import * as event_service from "../services/event.service"; -import * as export_service from "../services/im_export.service"; -import * as permanence_service from "../services/permanence.service"; -import * as team_service from "../services/team.service"; +import { type Request, type Response } from 'express'; +import fs from 'fs'; +import path from 'path'; +import * as event_service from '../services/event.service'; +import * as export_service from '../services/im_export.service'; +import * as permanence_service from '../services/permanence.service'; +import * as team_service from '../services/team.service'; import * as user_service from '../services/user.service'; -import { Error, Ok } from "../utils/responses"; -import { spreadsheet_id } from "../utils/secret"; -import { getLatestUploadedDocument, isSafeUploadSegment, removeUploadedDocuments, toUploadedDocumentStatus } from "../utils/uploadDocuments"; +import { Error, Ok } from '../utils/responses'; +import { spreadsheet_id } from '../utils/secret'; +import { + getLatestUploadedDocument, + isSafeUploadSegment, + removeUploadedDocuments, + toUploadedDocumentStatus, +} from '../utils/uploadDocuments'; export const exportAllDataToSheets = async (req: Request, res: Response) => { try { @@ -20,84 +25,104 @@ export const exportAllDataToSheets = async (req: Request, res: Response) => { // 2. Mapping -> format pour Google Sheets (array de array) const usersValues = [ - ["ID", "Prénom", "Nom", "Email", "Branche", "Permission", "Majeur", "Contact", "Discord", "Team", "Faction"], - ...userList.map(u => [ + [ + 'ID', + 'Prénom', + 'Nom', + 'Email', + 'Branche', + 'Permission', + 'Majeur', + 'Contact', + 'Discord', + 'Team', + 'Faction', + ], + ...userList.map((u) => [ u.id ?? 0, - u.first_name ?? "No first name", - u.last_name ?? "No last name", - u.email ?? "No email", - u.branch ?? "No branch", - u.permission ?? "No permissions", - u.majeur ?? "Pas de données", - u.contact ?? "No contact", - u.discord_id ?? "No discord ID", - u.teamName ?? "No Team", - u.factionName ?? "No faction" - ]) + u.first_name ?? 'No first name', + u.last_name ?? 'No last name', + u.email ?? 'No email', + u.branch ?? 'No branch', + u.permission ?? 'No permissions', + u.majeur ?? 'Pas de données', + u.contact ?? 'No contact', + u.discord_id ?? 'No discord ID', + u.teamName ?? 'No Team', + u.factionName ?? 'No faction', + ]), ]; const teamsValues = [ - ["ID", "Nom", "Type", "Faction"], - ...teamList.map(t => [ + ['ID', 'Nom', 'Type', 'Faction'], + ...teamList.map((t) => [ t.id, - t.name ?? "No name", - t.type ?? "No type", - t.teamFaction?.name ?? "No faction" - ]) + t.name ?? 'No name', + t.type ?? 'No type', + t.teamFaction?.name ?? 'No faction', + ]), ]; const permanenceValues = [ [ - "ID", - "Nom", - "Début", - "Fin", - "Lieu", - "Responsables", - "Inscrits (noms)", - "Inscrits (emails)", - "Présents", - "Absents" + 'ID', + 'Nom', + 'Début', + 'Fin', + 'Lieu', + 'Responsables', + 'Inscrits (noms)', + 'Inscrits (emails)', + 'Présents', + 'Absents', ], ...permanenceList.map((p) => { - const respoNames = p.respo ? p.respo.firstName + " " + p.respo.lastName : "Aucun"; - const userNames = p.users?.map((u) => `${u.first_name} ${u.last_name}`)?.join(" ; ") || "Aucun inscrit"; - const userEmails = p.users?.map((u) => u.email)?.join(" ; ") || "Aucun inscrit"; - - const claimedUsers = p.users?.filter((u) => u.claimed)?.map((u) => `${u.first_name} ${u.last_name}`)?.join(" ; ") || "Aucun"; - const unclaimedUsers = p.users?.filter((u) => !u.claimed)?.map((u) => `${u.first_name} ${u.last_name}`)?.join(" ; ") || "Aucun"; + const respoNames = p.respo ? p.respo.firstName + ' ' + p.respo.lastName : 'Aucun'; + const userNames = p.users?.map((u) => `${u.first_name} ${u.last_name}`)?.join(' ; ') || 'Aucun inscrit'; + const userEmails = p.users?.map((u) => u.email)?.join(' ; ') || 'Aucun inscrit'; + + const claimedUsers = + p.users + ?.filter((u) => u.claimed) + ?.map((u) => `${u.first_name} ${u.last_name}`) + ?.join(' ; ') || 'Aucun'; + const unclaimedUsers = + p.users + ?.filter((u) => !u.claimed) + ?.map((u) => `${u.first_name} ${u.last_name}`) + ?.join(' ; ') || 'Aucun'; return [ p.id, - p.name ?? "Sans nom", - p.start_at ? new Date(p.start_at).toLocaleString("fr-FR") : "N/A", - p.end_at ? new Date(p.end_at).toLocaleString("fr-FR") : "N/A", - p.location ?? "Sans lieu", + p.name ?? 'Sans nom', + p.start_at ? new Date(p.start_at).toLocaleString('fr-FR') : 'N/A', + p.end_at ? new Date(p.end_at).toLocaleString('fr-FR') : 'N/A', + p.location ?? 'Sans lieu', respoNames, userNames, userEmails, claimedUsers, - unclaimedUsers + unclaimedUsers, ]; - }) + }), ]; const shotgunValues = [ - ["ID", "Nom de l'équipe", "Type", "Horodatage"], - ...shotgunList.map(s => [ + ['ID', "Nom de l'équipe", 'Type', 'Horodatage'], + ...shotgunList.map((s) => [ s.id, - s.teamName ?? "No name", - s.teamType ?? "No type", - s.timestamp?.toISOString() ?? "No timestamp" - ]) + s.teamName ?? 'No name', + s.teamType ?? 'No type', + s.timestamp?.toISOString() ?? 'No timestamp', + ]), ]; // 3. Envoi vers les feuilles - await export_service.writeToGoogleSheet(spreadsheet_id, "USER!A1", usersValues); - await export_service.writeToGoogleSheet(spreadsheet_id, "TEAM!A1", teamsValues); - await export_service.writeToGoogleSheet(spreadsheet_id, "PERMANENCES!A1", permanenceValues); - await export_service.writeToGoogleSheet(spreadsheet_id, "SHOTGUN!A1", shotgunValues); + await export_service.writeToGoogleSheet(spreadsheet_id, 'USER!A1', usersValues); + await export_service.writeToGoogleSheet(spreadsheet_id, 'TEAM!A1', teamsValues); + await export_service.writeToGoogleSheet(spreadsheet_id, 'PERMANENCES!A1', permanenceValues); + await export_service.writeToGoogleSheet(spreadsheet_id, 'SHOTGUN!A1', shotgunValues); - Ok(res, { msg: "Export réalisé avec succès !" }); + Ok(res, { msg: 'Export réalisé avec succès !' }); } catch (error) { console.error(error); Error(res, { msg: "Erreur lors de l'export vers Google Sheets" }); @@ -105,37 +130,34 @@ export const exportAllDataToSheets = async (req: Request, res: Response) => { }; export const updateFoodMenu = async (req: Request, res: Response) => { - const file = req.file; try { - // Supprimer l'ancien Menu si un nouveau est uploadé if (file) { - const targetDir = path.join(__dirname, "../../foodmenu"); + const targetDir = path.join(__dirname, '../../foodmenu'); if (fs.existsSync(targetDir)) { fs.rmSync(targetDir, { recursive: true, force: true }); fs.mkdirSync(targetDir); } - } - Ok(res, { msg: "Menu mis à jour avec succès" }); + Ok(res, { msg: 'Menu mis à jour avec succès' }); return; } catch (err) { console.error(err); - Error(res, { msg: "Erreur lors de la mise à jour du Menu" }); + Error(res, { msg: 'Erreur lors de la mise à jour du Menu' }); } }; export const updatePlannings = async (req: Request, res: Response) => { try { - Ok(res, { msg: "Planning mis à jour avec succès" }); + Ok(res, { msg: 'Planning mis à jour avec succès' }); return; } catch (err) { console.error(err); - Error(res, { msg: "Erreur lors de la mise à jour du Planning" }); + Error(res, { msg: 'Erreur lors de la mise à jour du Planning' }); } }; @@ -153,7 +175,7 @@ export const getUploadedDocumentStatus = async (req: Request, res: Response) => const { category, item } = req.params; if (!isSafeUploadSegment(category) || !isSafeUploadSegment(item)) { - Error(res, { msg: "Paramètres invalides" }); + Error(res, { msg: 'Paramètres invalides' }); return; } @@ -164,7 +186,7 @@ export const getUploadedDocumentStatus = async (req: Request, res: Response) => data: toUploadedDocumentStatus(category, latestDocument), }); } catch (err: unknown) { - if ((err as NodeJS.ErrnoException)?.code === "ENOENT") { + if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') { Ok(res, { data: toUploadedDocumentStatus(category, null), }); @@ -172,7 +194,7 @@ export const getUploadedDocumentStatus = async (req: Request, res: Response) => } console.error(err); - Error(res, { msg: "Erreur lors de la vérification du document" }); + Error(res, { msg: 'Erreur lors de la vérification du document' }); } }; @@ -180,7 +202,7 @@ export const deleteDocument = async (req: Request, res: Response) => { const { category, item } = req.params; if (!isSafeUploadSegment(category) || !isSafeUploadSegment(item)) { - Error(res, { msg: "Paramètres invalides" }); + Error(res, { msg: 'Paramètres invalides' }); return; } @@ -188,16 +210,16 @@ export const deleteDocument = async (req: Request, res: Response) => { const deletedCount = await removeUploadedDocuments(category, item); if (deletedCount === 0) { - return Ok(res, { msg: "Aucun document à supprimer" }); + return Ok(res, { msg: 'Aucun document à supprimer' }); } Ok(res, {}); } catch (err: unknown) { - if ((err as NodeJS.ErrnoException)?.code === "ENOENT") { + if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') { Ok(res, {}); return; } - Error(res, { msg: "Erreur lors de la vérification du document" }); + Error(res, { msg: 'Erreur lors de la vérification du document' }); } }; diff --git a/backend/src/controllers/news.controller.ts b/backend/src/controllers/news.controller.ts index 3b40a52..8e24cd7 100644 --- a/backend/src/controllers/news.controller.ts +++ b/backend/src/controllers/news.controller.ts @@ -1,12 +1,12 @@ -import { type Request, type Response } from "express"; -import fs from "fs"; -import path from "path"; -import * as email_service from "../services/email.service"; -import { generateEmailHtml } from "../services/email.service"; -import * as news_service from "../services/news.service"; +import { type Request, type Response } from 'express'; +import fs from 'fs'; +import path from 'path'; +import * as email_service from '../services/email.service'; +import { generateEmailHtml } from '../services/email.service'; +import * as news_service from '../services/news.service'; import * as user_service from '../services/user.service'; -import { Error, Ok } from "../utils/responses"; -import { email_from } from "../utils/secret"; +import { Error, Ok } from '../utils/responses'; +import { email_from } from '../utils/secret'; const toStoredUploadPath = (imageUrl: string) => { if (!imageUrl) { @@ -19,7 +19,7 @@ const toStoredUploadPath = (imageUrl: string) => { } // Accept absolute URLs and keep only the pathname part. - if (normalized.startsWith("http://") || normalized.startsWith("https://")) { + if (normalized.startsWith('http://') || normalized.startsWith('https://')) { try { normalized = new URL(normalized).pathname; } catch { @@ -27,11 +27,11 @@ const toStoredUploadPath = (imageUrl: string) => { } } - if (normalized.startsWith("/api/")) { + if (normalized.startsWith('/api/')) { normalized = normalized.slice(4); } - if (!normalized.startsWith("/uploads/")) { + if (!normalized.startsWith('/uploads/')) { return null; } @@ -44,7 +44,7 @@ const resolveStoredImagePath = (imageUrl: string) => { return null; } - return path.resolve(process.cwd(), storedPath.replace(/^\//, "")); + return path.resolve(process.cwd(), storedPath.replace(/^\//, '')); }; const deleteImageIfExists = (imageUrl: string) => { @@ -63,18 +63,17 @@ export const createNews = async (req: Request, res: Response) => { const file = req.file; try { - const resolvedImageUrl = file - ? `/uploads/news/${file.filename}` - : image_url; + const resolvedImageUrl = file ? `/uploads/news/${file.filename}` : image_url; const news = await news_service.createNews( title, description, type, - published === true || published === "true", + published === true || published === 'true', target, - resolvedImageUrl); - Ok(res, { msg: "Actu créée avec succès", data: news }); + resolvedImageUrl, + ); + Ok(res, { msg: 'Actu créée avec succès', data: news }); } catch (err) { console.error(err); Error(res, { msg: "Erreur lors de la création de l'actu" }); @@ -87,7 +86,7 @@ export const listAllNews = async (_req: Request, res: Response) => { Ok(res, { data: news }); } catch (err) { console.error(err); - Error(res, { msg: "Erreur lors de la récupération des actus" }); + Error(res, { msg: 'Erreur lors de la récupération des actus' }); } }; @@ -97,7 +96,7 @@ export const listPublishedNews = async (_req: Request, res: Response) => { Ok(res, { data: news }); } catch (err) { console.error(err); - Error(res, { msg: "Erreur lors de la récupération des actus publiées" }); + Error(res, { msg: 'Erreur lors de la récupération des actus publiées' }); } }; @@ -109,7 +108,7 @@ export const listPublishedNewsByType = async (req: Request, res: Response) => { Ok(res, { data: news }); } catch (err) { console.error(err); - Error(res, { msg: "Erreur lors de la récupération des actus par type" }); + Error(res, { msg: 'Erreur lors de la récupération des actus par type' }); } }; @@ -128,12 +127,13 @@ export const publishNews = async (req: Request, res: Response) => { return; } - const recipients = news.target === "Tous" - ? (await user_service.getUsersAdmin()).map(u => u.email) - : (await user_service.getUsersbyPermission(news.target)).map(u => u.email); + const recipients = + news.target === 'Tous' + ? (await user_service.getUsersAdmin()).map((u) => u.email) + : (await user_service.getUsersbyPermission(news.target)).map((u) => u.email); if (recipients.length === 0) { - Error(res, { msg: "No recipients" }); + Error(res, { msg: 'No recipients' }); } const email = { @@ -148,15 +148,15 @@ export const publishNews = async (req: Request, res: Response) => { await email_service.sendEmail(email); } - Ok(res, { msg: "Actu publiée" }); + Ok(res, { msg: 'Actu publiée' }); } catch (err) { console.error(err); - Error(res, { msg: "Erreur lors de la publication ou de la notification" }); + Error(res, { msg: 'Erreur lors de la publication ou de la notification' }); } }; export const deleteNews = async (req: Request, res: Response) => { - const { newsId } = req.query + const { newsId } = req.query; try { const existing = await news_service.getNewsById(Number(newsId)); @@ -165,8 +165,7 @@ export const deleteNews = async (req: Request, res: Response) => { } await news_service.deleteNews(Number(newsId)); - Ok(res, { msg: "Actus supprimée avec succès !" }); - + Ok(res, { msg: 'Actus supprimée avec succès !' }); } catch (err) { console.error(err); Error(res, { msg: "Erreur lors de la suppression de l'actus" }); @@ -176,25 +175,28 @@ export const deleteNews = async (req: Request, res: Response) => { export const updateNews = async (req: Request, res: Response) => { const { id, title, description, type, target, image_url } = req.body; const file = req.file; - const hasImageUrlField = Object.prototype.hasOwnProperty.call(req.body, "image_url"); + const hasImageUrlField = Object.prototype.hasOwnProperty.call(req.body, 'image_url'); const resolvedImageUrl = file ? `/uploads/news/${file.filename}` : hasImageUrlField - ? (image_url ?? null) - : undefined; + ? (image_url ?? null) + : undefined; try { const existing = await news_service.getNewsById(Number(id)); if (!existing) { - Error(res, { msg: "Actu introuvable" }); + Error(res, { msg: 'Actu introuvable' }); return; } - const shouldReplaceImage = typeof resolvedImageUrl === "string"; + const shouldReplaceImage = typeof resolvedImageUrl === 'string'; const shouldRemoveImage = resolvedImageUrl === null; // Supprimer l'ancienne image si elle est remplacée ou explicitement supprimée. - if (existing.image_url && ((shouldReplaceImage && existing.image_url !== resolvedImageUrl) || shouldRemoveImage)) { + if ( + existing.image_url && + ((shouldReplaceImage && existing.image_url !== resolvedImageUrl) || shouldRemoveImage) + ) { deleteImageIfExists(existing.image_url); } @@ -211,7 +213,7 @@ export const updateNews = async (req: Request, res: Response) => { const updated = await news_service.updateNews(Number(id), updates); - Ok(res, { msg: "Actu mise à jour avec succès", data: updated }); + Ok(res, { msg: 'Actu mise à jour avec succès', data: updated }); } catch (err) { console.error(err); Error(res, { msg: "Erreur lors de la mise à jour de l'actu" }); diff --git a/backend/src/controllers/permanence.controller.ts b/backend/src/controllers/permanence.controller.ts index 956fdae..4e3d770 100644 --- a/backend/src/controllers/permanence.controller.ts +++ b/backend/src/controllers/permanence.controller.ts @@ -1,6 +1,6 @@ -import { type Request, type Response } from "express"; -import * as permanence_service from "../services/permanence.service"; -import { Error, Ok } from "../utils/responses"; +import { type Request, type Response } from 'express'; +import * as permanence_service from '../services/permanence.service'; +import { Error, Ok } from '../utils/responses'; interface MulterRequest extends Request { file?: Express.Multer.File; @@ -12,11 +12,11 @@ const validatePermanenceData = (start_at: string, end_at: string) => { const endDate = new Date(end_at); if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { - return { valid: false, msg: "Les dates de début et de fin doivent être valides" }; + return { valid: false, msg: 'Les dates de début et de fin doivent être valides' }; } if (startDate >= endDate) { - return { valid: false, msg: "La date de début doit être avant la date de fin" }; + return { valid: false, msg: 'La date de début doit être avant la date de fin' }; } return { valid: true }; @@ -27,7 +27,7 @@ export const createPermanence = async (req: Request, res: Response) => { const { name, description, location, start_at, end_at, capacity, difficulty, respoId } = req.body; if (!name || !location || !start_at || !end_at || !capacity || !difficulty) { - Error(res, { msg: "Tous les champs sont requis" }); + Error(res, { msg: 'Tous les champs sont requis' }); return; } @@ -48,15 +48,14 @@ export const createPermanence = async (req: Request, res: Response) => { Number(difficulty), Number(respoId), ); - Ok(res, { msg: "Permanence créée avec succès" }); + Ok(res, { msg: 'Permanence créée avec succès' }); return; } catch (err) { console.error(err); - Error(res, { msg: "Erreur lors de la création de la permanence" }); + Error(res, { msg: 'Erreur lors de la création de la permanence' }); } }; - export const updatePermanence = async (req: Request, res: Response) => { const { permId, name, description, location, start_at, end_at, capacity, difficulty, respoId } = req.body; @@ -78,29 +77,27 @@ export const updatePermanence = async (req: Request, res: Response) => { end_at ? new Date(end_at) : perm.end_at, capacity !== undefined ? Number(capacity) : perm.capacity, difficulty !== undefined ? Number(difficulty) : perm.difficulty, - Number(respoId) + Number(respoId), ); - Ok(res, { msg: "Permanence mise à jour avec succès" }); + Ok(res, { msg: 'Permanence mise à jour avec succès' }); } catch (err) { console.error(err); - Error(res, { msg: "Erreur lors de la mise à jour de la permanence" }); + Error(res, { msg: 'Erreur lors de la mise à jour de la permanence' }); } }; - // ➕ Créer une permanence export const deletePermanence = async (req: Request, res: Response) => { - const { permId } = req.query; try { await permanence_service.deletePermanence(Number(permId)); - Ok(res, { msg: "Permanence supprimée avec succès" }); + Ok(res, { msg: 'Permanence supprimée avec succès' }); return; } catch (err) { console.error(err); - Error(res, { msg: "Erreur lors de la suppression de la permanence" }); + Error(res, { msg: 'Erreur lors de la suppression de la permanence' }); } }; @@ -116,12 +113,12 @@ export const openPermanence = async (req: Request, res: Response) => { try { const permanence = await permanence_service.getPermanenceById(permId); if (permanence.is_open === true) { - Error(res, { msg: "La permanence est déjà ouverte" }); + Error(res, { msg: 'La permanence est déjà ouverte' }); return; } await permanence_service.openPermanence(Number(permId)); - Ok(res, { msg: "Permanence ouverte avec succès" }); + Ok(res, { msg: 'Permanence ouverte avec succès' }); } catch (err) { console.error(err); Error(res, { msg: "Erreur lors de l'ouverture de la permanence" }); @@ -140,16 +137,16 @@ export const closePermanence = async (req: Request, res: Response) => { try { const permanence = await permanence_service.getPermanenceById(permId); if (permanence.is_open === false) { - Error(res, { msg: "La permanence est déjà fermée" }); + Error(res, { msg: 'La permanence est déjà fermée' }); return; } await permanence_service.closePermanence(Number(permId)); - Ok(res, { msg: "Permanence fermée avec succès" }); + Ok(res, { msg: 'Permanence fermée avec succès' }); return; } catch (err) { console.error(err); - Error(res, { msg: "Erreur lors de la fermeture de la permanence" }); + Error(res, { msg: 'Erreur lors de la fermeture de la permanence' }); return; } }; @@ -159,21 +156,20 @@ export const applyToPermanence = async (req: Request, res: Response) => { const { permId } = req.body; const userId = req.user?.userId; - if (!userId || !permId) { - Error(res, { msg: "Requête invalide, permId ou userId manquant" }); + Error(res, { msg: 'Requête invalide, permId ou userId manquant' }); return; } try { const permanence = await permanence_service.getPermanenceById(permId); if (permanence.is_open === false) { - Error(res, { msg: "La permanence est fermée, vous ne pouvez pas vous y inscrire" }); + Error(res, { msg: 'La permanence est fermée, vous ne pouvez pas vous y inscrire' }); return; } await permanence_service.registerUserToPermanence(Number(userId), Number(permId)); - Ok(res, { msg: "Inscription réussie" }); + Ok(res, { msg: 'Inscription réussie' }); return; } catch (err) { console.error(err); @@ -188,17 +184,17 @@ export const leavePermanence = async (req: Request, res: Response) => { const userId = req.user?.userId; if (!userId || !permId) { - Error(res, { msg: "Requête invalide, permId ou userId manquant" }); + Error(res, { msg: 'Requête invalide, permId ou userId manquant' }); return; } try { - await permanence_service.unregisterUserFromPermanence(Number(userId), Number(permId),); - Ok(res, { msg: "Désinscription réussie" }); + await permanence_service.unregisterUserFromPermanence(Number(userId), Number(permId)); + Ok(res, { msg: 'Désinscription réussie' }); return; } catch (err) { console.error(err); - Error(res, { msg: err.message || "Erreur pendant la désinscription" }); + Error(res, { msg: err.message || 'Erreur pendant la désinscription' }); return; } }; @@ -208,7 +204,7 @@ export const getMyPermanences = async (req: Request, res: Response) => { const userId = req.user?.userId; if (!userId) { - Error(res, { msg: "Utilisateur non identifié" }); + Error(res, { msg: 'Utilisateur non identifié' }); return; } @@ -218,7 +214,7 @@ export const getMyPermanences = async (req: Request, res: Response) => { return; } catch (err) { console.error(err); - Error(res, { msg: "Erreur pendant la récupération des permanences" }); + Error(res, { msg: 'Erreur pendant la récupération des permanences' }); return; } }; @@ -231,7 +227,7 @@ export const getAllPermanences = async (req: Request, res: Response) => { return; } catch (err) { console.error(err); - Error(res, { msg: "Erreur lors de la récupération des permanences" }); + Error(res, { msg: 'Erreur lors de la récupération des permanences' }); return; } }; @@ -244,36 +240,35 @@ export const getOpenPermanences = async (req: Request, res: Response) => { return; } catch (err) { console.error(err); - Error(res, { msg: "Erreur lors de la récupération des permanences ouvertes" }); + Error(res, { msg: 'Erreur lors de la récupération des permanences ouvertes' }); return; } }; export const getUsersInPermanence = async (req: Request, res: Response) => { try { - const { permId } = req.query - const users = await permanence_service.getUsersInPermanence(Number(permId)) + const { permId } = req.query; + const users = await permanence_service.getUsersInPermanence(Number(permId)); Ok(res, { data: users }); return; } catch (err) { console.error(err); - Error(res, { msg: "Erreur lors de la récupération des utilisateurs par permanences" }); + Error(res, { msg: 'Erreur lors de la récupération des utilisateurs par permanences' }); return; } }; export const addUserToPermanence = async (req: Request, res: Response) => { - const { permId, userId } = req.body; if (!userId || !permId) { - Error(res, { msg: "Requête invalide, permId ou userId manquant" }); + Error(res, { msg: 'Requête invalide, permId ou userId manquant' }); return; } try { await permanence_service.addUserToPermanence(Number(userId), Number(permId)); - Ok(res, { msg: "Inscription réussite" }); + Ok(res, { msg: 'Inscription réussite' }); return; } catch (err) { console.error(err); @@ -283,21 +278,20 @@ export const addUserToPermanence = async (req: Request, res: Response) => { }; export const removeUserToPermanence = async (req: Request, res: Response) => { - const { permId, userId } = req.body; if (!userId || !permId) { - Error(res, { msg: "Requête invalide, permId ou userId manquant" }); + Error(res, { msg: 'Requête invalide, permId ou userId manquant' }); return; } try { await permanence_service.removeUserToPermanence(Number(userId), Number(permId)); - Ok(res, { msg: "Désinscription réussite" }); + Ok(res, { msg: 'Désinscription réussite' }); return; } catch (err) { console.error(err); - Error(res, { msg: err.message || "Erreur pendant la désinscription" }); + Error(res, { msg: err.message || 'Erreur pendant la désinscription' }); return; } }; @@ -306,13 +300,13 @@ export const uploadPermanencesCSV = async (req: MulterRequest, res: Response) => try { const file = req.file; if (!file) { - Error(res, { msg: "Fichier CSV manquant." }); + Error(res, { msg: 'Fichier CSV manquant.' }); } await permanence_service.importPermanencesFromCSV(file.path); - Ok(res, { msg: "Importation réalisée avec succès." }); + Ok(res, { msg: 'Importation réalisée avec succès.' }); } catch (error) { - console.error("Erreur import CSV :", error); + console.error('Erreur import CSV :', error); Error(res, { msg: "Échec de l'importation." }); } }; @@ -321,18 +315,16 @@ export const isUserRespo = async (req: Request, res: Response) => { const { userId } = req.query; if (!userId) { - Error(res, { msg: "userId est requis" }); + Error(res, { msg: 'userId est requis' }); return; } try { - const isRespo = await permanence_service.isUserRespoOfPermanence( - Number(userId) - ); + const isRespo = await permanence_service.isUserRespoOfPermanence(Number(userId)); Ok(res, { data: isRespo }); } catch (err) { console.error(err); - Error(res, { msg: "Erreur lors de la vérification du responsable" }); + Error(res, { msg: 'Erreur lors de la vérification du responsable' }); } }; @@ -340,7 +332,7 @@ export const getRespoPermanencesWithMembers = async (req: Request, res: Response const respoId = req.user?.userId; if (!respoId) { - Error(res, { msg: "respoId est requis" }); + Error(res, { msg: 'respoId est requis' }); return; } @@ -349,7 +341,7 @@ export const getRespoPermanencesWithMembers = async (req: Request, res: Response Ok(res, { data }); } catch (err) { console.error(err); - Error(res, { msg: "Erreur lors de la récupération des permanences du responsable" }); + Error(res, { msg: 'Erreur lors de la récupération des permanences du responsable' }); } }; @@ -357,7 +349,7 @@ export const claimMember = async (req: Request, res: Response) => { const { userId, permId, claimed } = req.body; if (userId === undefined || permId === undefined || claimed === undefined) { - Error(res, { msg: "userId, permId et claimed sont requis" }); + Error(res, { msg: 'userId, permId et claimed sont requis' }); return; } @@ -368,34 +360,32 @@ export const claimMember = async (req: Request, res: Response) => { }); } catch (err) { console.error(err); - Error(res, { msg: "Erreur lors de la mise à jour du statut du membre" }); + Error(res, { msg: 'Erreur lors de la mise à jour du statut du membre' }); } }; export const sendHourlyNotificationToUsers = async (req: Request, res: Response) => { - const notifications = await permanence_service.getHourlyNotifications(); if (notifications.length === 0) { - Ok(res, { msg: "Aucune notification horaire à envoyer." }); + Ok(res, { msg: 'Aucune notification horaire à envoyer.' }); return; } permanence_service.sendNotifications(notifications); - Ok(res, { msg: "Notifications horaires envoyées avec succès" }); + Ok(res, { msg: 'Notifications horaires envoyées avec succès' }); }; export const sendDailyNotificationToUsers = async (req: Request, res: Response) => { - const notifications = await permanence_service.getDailyNotifications(); if (notifications.length === 0) { - Ok(res, { msg: "Aucune notification quotidienne à envoyer." }); + Ok(res, { msg: 'Aucune notification quotidienne à envoyer.' }); return; } permanence_service.sendNotifications(notifications); - Ok(res, { msg: "Notifications quotidiennes envoyées avec succès" }); + Ok(res, { msg: 'Notifications quotidiennes envoyées avec succès' }); }; diff --git a/backend/src/controllers/role.controller.ts b/backend/src/controllers/role.controller.ts index 2855e95..dfa4d4f 100644 --- a/backend/src/controllers/role.controller.ts +++ b/backend/src/controllers/role.controller.ts @@ -1,6 +1,6 @@ -import { type Request, type Response } from "express"; -import * as role_service from "../services/role.service"; -import { Error, Ok } from "../utils/responses"; +import { type Request, type Response } from 'express'; +import * as role_service from '../services/role.service'; +import { Error, Ok } from '../utils/responses'; // 🎯 Préférences utilisateur export const updateUserPreferences = async (req: Request, res: Response) => { @@ -9,14 +9,14 @@ export const updateUserPreferences = async (req: Request, res: Response) => { const { roleIds } = req.body; if (!userId || !Array.isArray(roleIds)) { - Error(res, { msg: "Données invalides" }); + Error(res, { msg: 'Données invalides' }); } await role_service.updateUserPreferences(userId, roleIds); - Ok(res, { msg: "Préférences mises à jour avec succès" }); + Ok(res, { msg: 'Préférences mises à jour avec succès' }); } catch (error) { console.error(error); - Error(res, { msg: "Erreur interne serveur" }); + Error(res, { msg: 'Erreur interne serveur' }); } }; @@ -24,14 +24,14 @@ export const getUserPreferences = async (req: Request, res: Response) => { try { const userId = req.user?.userId; - if (!userId) Error(res, { msg: "Utilisateur non authentifié" }); + if (!userId) Error(res, { msg: 'Utilisateur non authentifié' }); const preferences = await role_service.getUserPreferences(userId); const roleIds = preferences.map((pref) => pref.roleId); - Ok(res, { msg: "Préférences récupérées", data: roleIds }); + Ok(res, { msg: 'Préférences récupérées', data: roleIds }); } catch (error) { console.error(error); - Error(res, { msg: "Erreur interne serveur" }); + Error(res, { msg: 'Erreur interne serveur' }); } }; @@ -39,13 +39,13 @@ export const getUserPreferences = async (req: Request, res: Response) => { export const getUsersByRoleHandler = async (req: Request, res: Response) => { try { const { roleName } = req.params; - if (!roleName) Error(res, { msg: "Nom du rôle requis" }); + if (!roleName) Error(res, { msg: 'Nom du rôle requis' }); const users = await role_service.getUsersByRoleName(roleName); - Ok(res, { msg: "Utilisateurs récupérés", data: users }); + Ok(res, { msg: 'Utilisateurs récupérés', data: users }); } catch (error) { console.error(error); - Error(res, { msg: "Erreur interne serveur" }); + Error(res, { msg: 'Erreur interne serveur' }); } }; @@ -55,7 +55,7 @@ export const addRoleToUser = async (req: Request, res: Response) => { const { userId, roleIds } = req.body; if (!userId || !Array.isArray(roleIds)) { - Error(res, { msg: "userId et roleIds requis" }); + Error(res, { msg: 'userId et roleIds requis' }); } for (const roleId of roleIds) { @@ -65,7 +65,7 @@ export const addRoleToUser = async (req: Request, res: Response) => { } } - Ok(res, { msg: "Rôles ajoutés avec succès" }); + Ok(res, { msg: 'Rôles ajoutés avec succès' }); } catch (error) { console.error(error); Error(res, { msg: "Erreur lors de l'ajout des rôles" }); @@ -78,14 +78,14 @@ export const deleteRoleToUser = async (req: Request, res: Response) => { const { userId, roleId } = req.body; if (!userId || !roleId) { - Error(res, { msg: "userId et roleId requis" }); + Error(res, { msg: 'userId et roleId requis' }); } await role_service.removeRoleFromUser(userId, roleId); - Ok(res, { msg: "Rôle supprimé avec succès" }); + Ok(res, { msg: 'Rôle supprimé avec succès' }); } catch (error) { console.error(error); - Error(res, { msg: "Erreur lors de la suppression du rôle" }); + Error(res, { msg: 'Erreur lors de la suppression du rôle' }); } }; @@ -96,7 +96,7 @@ export const getUsersWithRoles = async (req: Request, res: Response) => { Ok(res, { data: users }); } catch (error) { console.error(error); - Error(res, { msg: "Erreur lors de la récupération des utilisateurs" }); + Error(res, { msg: 'Erreur lors de la récupération des utilisateurs' }); } }; @@ -107,7 +107,7 @@ export const getRoles = async (req: Request, res: Response) => { Ok(res, { data: roles }); } catch (error) { console.error(error); - Error(res, { msg: "Erreur lors de la récupération des rôles" }); + Error(res, { msg: 'Erreur lors de la récupération des rôles' }); } }; @@ -115,13 +115,13 @@ export const getRoles = async (req: Request, res: Response) => { export const getUserRoles = async (req: Request, res: Response) => { try { const { userId } = req.query; - if (!userId) Error(res, { msg: "userId requis" }); + if (!userId) Error(res, { msg: 'userId requis' }); const roles = await role_service.getUserRoles(Number(userId)); Ok(res, { data: roles }); } catch (error) { console.error(error); - Error(res, { msg: "Erreur lors de la récupération des rôles utilisateur" }); + Error(res, { msg: 'Erreur lors de la récupération des rôles utilisateur' }); } }; @@ -134,12 +134,12 @@ export const addPointsToRole = async (req: Request, res: Response) => { try { const { roleId, points } = req.body; - if (!roleId || typeof points !== "number") { - Error(res, { msg: "roleId et points requis" }); + if (!roleId || typeof points !== 'number') { + Error(res, { msg: 'roleId et points requis' }); } await role_service.addPointsToRole(roleId, points); - Ok(res, { msg: "Points ajoutés avec succès" }); + Ok(res, { msg: 'Points ajoutés avec succès' }); } catch (error) { console.error(error); Error(res, { msg: "Erreur lors de l'ajout des points" }); @@ -151,15 +151,15 @@ export const removePointsFromRole = async (req: Request, res: Response) => { try { const { roleId, points } = req.body; - if (!roleId || typeof points !== "number") { - Error(res, { msg: "roleId et points requis" }); + if (!roleId || typeof points !== 'number') { + Error(res, { msg: 'roleId et points requis' }); } await role_service.removePointsFromRole(roleId, points); - Ok(res, { msg: "Points retirés avec succès" }); + Ok(res, { msg: 'Points retirés avec succès' }); } catch (error) { console.error(error); - Error(res, { msg: "Erreur lors du retrait des points" }); + Error(res, { msg: 'Erreur lors du retrait des points' }); } }; @@ -170,7 +170,7 @@ export const getAllRolePoints = async (_req: Request, res: Response) => { Ok(res, { data: roles }); } catch (error) { console.error(error); - Error(res, { msg: "Erreur lors de la récupération des points" }); + Error(res, { msg: 'Erreur lors de la récupération des points' }); } }; @@ -178,14 +178,14 @@ export const getAllRolePoints = async (_req: Request, res: Response) => { export const getRolePoints = async (req: Request, res: Response) => { try { const { roleId } = req.params; - if (!roleId) Error(res, { msg: "roleId requis" }); + if (!roleId) Error(res, { msg: 'roleId requis' }); const role = await role_service.getRolePoints(Number(roleId)); - if (!role) Error(res, { msg: "Rôle introuvable" }); + if (!role) Error(res, { msg: 'Rôle introuvable' }); Ok(res, { data: role }); } catch (error) { console.error(error); - Error(res, { msg: "Erreur lors de la récupération des points du rôle" }); + Error(res, { msg: 'Erreur lors de la récupération des points du rôle' }); } }; diff --git a/backend/src/controllers/team.controller.ts b/backend/src/controllers/team.controller.ts index b679821..b33de1a 100644 --- a/backend/src/controllers/team.controller.ts +++ b/backend/src/controllers/team.controller.ts @@ -1,10 +1,10 @@ -import { type Request, type Response } from "express"; -import { type Event } from "../schemas/Basic/event.schema"; +import { type Request, type Response } from 'express'; +import { type Event } from '../schemas/Basic/event.schema'; import * as event_service from '../services/event.service'; -import * as faction_service from "../services/faction.service"; -import * as team_service from "../services/team.service"; -import * as user_service from "../services/user.service"; -import { Error, Ok } from "../utils/responses"; +import * as faction_service from '../services/faction.service'; +import * as team_service from '../services/team.service'; +import * as user_service from '../services/user.service'; +import { Error, Ok } from '../utils/responses'; export const createNewTeam = async (req: Request, res: Response) => { const { teamName, members } = req.body; @@ -39,7 +39,7 @@ export const createNewTeam = async (req: Request, res: Response) => { // Create the new team if no one is already in a team const newTeam = await team_service.createTeam(teamName, members); - Ok(res, { msg: "Équipe créée avec succès !", data: newTeam }); + Ok(res, { msg: 'Équipe créée avec succès !', data: newTeam }); return; } catch { Error(res, { msg: "Erreur lors de la création de l'équipe." }); @@ -51,7 +51,7 @@ export const createNewTeamLight = async (req: Request, res: Response) => { try { await team_service.createTeamLight(teamName, factionId); - Ok(res, { msg: "Equipe créée !" }); + Ok(res, { msg: 'Equipe créée !' }); } catch { Error(res, { msg: "Erreur lors de la création de l'équipe." }); } @@ -63,7 +63,7 @@ export const getTeams = async (req: Request, res: Response) => { Ok(res, { data: teams }); return; } catch { - Error(res, { msg: "Erreur lors de la récupération des équipes." }); + Error(res, { msg: 'Erreur lors de la récupération des équipes.' }); } }; @@ -73,7 +73,7 @@ export const getTeamsWithfactions = async (req: Request, res: Response) => { Ok(res, { data: teams }); return; } catch { - Error(res, { msg: "Erreur lors de la récupération des équipes et de leur faction." }); + Error(res, { msg: 'Erreur lors de la récupération des équipes et de leur faction.' }); } }; @@ -82,7 +82,7 @@ export const modifyTeam = async (req: Request, res: Response) => { const { teamID, teamName, teamMembers, factionID, type } = req.body; if (!teamID) { - Error(res, { msg: "teamID est requis pour la mise à jour." }); + Error(res, { msg: 'teamID est requis pour la mise à jour.' }); } const updatedTeam = await team_service.modifyTeam(teamID, teamMembers, factionID, teamName, type); @@ -102,10 +102,10 @@ export const getTeamUsers = async (req: Request, res: Response) => { return; } catch (error) { console.error(error); - Error(res, { msg: "Erreur interne lors de la récupération des utilisateurs avec leurs rôles." }); + Error(res, { msg: 'Erreur interne lors de la récupération des utilisateurs avec leurs rôles.' }); return; } -} +}; export const getAllTeamsWithUsers = async (req: Request, res: Response) => { try { @@ -114,10 +114,10 @@ export const getAllTeamsWithUsers = async (req: Request, res: Response) => { return; } catch (error) { console.error(error); - Error(res, { msg: "Erreur interne lors de la récupération des utilisateurs avec leurs rôles." }); + Error(res, { msg: 'Erreur interne lors de la récupération des utilisateurs avec leurs rôles.' }); return; } -} +}; export const getTeamFaction = async (req: Request, res: Response) => { const { teamId } = req.query; @@ -129,21 +129,21 @@ export const getTeamFaction = async (req: Request, res: Response) => { return; } catch (error) { console.error(error); - Error(res, { msg: "Erreur interne lors de la récupération des utilisateurs avec leurs rôles." }); + Error(res, { msg: 'Erreur interne lors de la récupération des utilisateurs avec leurs rôles.' }); return; } -} +}; export const deleteTeam = async (req: Request, res: Response) => { try { const { teamID } = req.query; // Assumes the teamID is passed as a parameter if (!teamID) { - Error(res, { msg: "teamID est requis." }); + Error(res, { msg: 'teamID est requis.' }); } const deletedTeam = await team_service.deleteTeam(Number(teamID)); - Ok(res, { msg: "Équipe supprimée avec succès.", data: deletedTeam }); + Ok(res, { msg: 'Équipe supprimée avec succès.', data: deletedTeam }); } catch (error) { console.error(error); Error(res, { msg: "Erreur lors de la suppression de l'équipe." }); @@ -152,7 +152,7 @@ export const deleteTeam = async (req: Request, res: Response) => { export const teamDistribution = async (req: Request, res: Response) => { try { - const newStudents = await user_service.getUsersbyPermission("Nouveau"); + const newStudents = await user_service.getUsersbyPermission('Nouveau'); const userswithteams = (await team_service.getUsersWithTeam()).map((entry: any) => entry.userId); const teams = await team_service.getTeams(); @@ -163,46 +163,48 @@ export const teamDistribution = async (req: Request, res: Response) => { // Filtrer les utilisateurs en fonction de la spécialité const tcStudents = filteredStudents - .filter((student: any) => student.branch === "TC") + .filter((student: any) => student.branch === 'TC') .map((student: any) => ({ id: student.userId, email: student.email, - branch: student.branch + branch: student.branch, })); const otherStudents = filteredStudents // .filter((student: any) => student.branch !== "TC" && student.branch !== "RI" && student.branch !== "MM") A decommenter pour ignorer les RI dans la répartition automatique - .filter((student: any) => student.branch !== "TC" && student.branch !== "MM") + .filter((student: any) => student.branch !== 'TC' && student.branch !== 'MM') .map((student: any) => ({ id: student.userId, email: student.email, - branch: student.branch + branch: student.branch, })); const PMOMStudents = filteredStudents - .filter((student: any) => student.branch == "MM") + .filter((student: any) => student.branch == 'MM') .map((student: any) => ({ id: student.userId, email: student.email, - branch: student.branch + branch: student.branch, })); // Filtrer les équipes en fonction de leur type - const tcTeams = teams.filter(team => team.type === "TC"); - const PMOMTeams = teams.filter(team => team.type === "MM"); + const tcTeams = teams.filter((team) => team.type === 'TC'); + const PMOMTeams = teams.filter((team) => team.type === 'MM'); // const otherTeams = teams.filter(team => team.type !== "TC" && team.type !== "RI" && team.type !== "MM"); A decommenter pour ignorer les RI dans la répartition automatique - const otherTeams = teams.filter(team => team.type !== "TC" && team.type !== "MM"); + const otherTeams = teams.filter((team) => team.type !== 'TC' && team.type !== 'MM'); // Fonction pour assigner les utilisateurs à des équipes équilibrées async function assignUsersToTeams(users: any, teams: any) { // Calculer la taille actuelle des équipes - const teamSizes = await Promise.all(teams.map(async (team: any) => { - const members = await team_service.getTeamUsers(team.teamId); - return { - teamId: team.teamId, - size: members.length - }; - })); + const teamSizes = await Promise.all( + teams.map(async (team: any) => { + const members = await team_service.getTeamUsers(team.teamId); + return { + teamId: team.teamId, + size: members.length, + }; + }), + ); // Trier les équipes par taille (ascendant) teamSizes.sort((a: any, b: any) => a.size - b.size); @@ -235,9 +237,9 @@ export const teamDistribution = async (req: Request, res: Response) => { await assignUsersToTeams(PMOMStudents, PMOMTeams); } - Ok(res, { msg: "NewStudents distributed!" }); + Ok(res, { msg: 'NewStudents distributed!' }); } catch (error) { Error(res, { error }); - return + return; } -} +}; diff --git a/backend/src/controllers/tent.controller.ts b/backend/src/controllers/tent.controller.ts index ed62e44..51c2bbe 100644 --- a/backend/src/controllers/tent.controller.ts +++ b/backend/src/controllers/tent.controller.ts @@ -1,23 +1,23 @@ -import { type Request, type Response } from "express"; -import { generateEmailHtml, sendEmail } from "../services/email.service"; -import * as tent_service from "../services/tent.service"; -import { getUserById } from "../services/user.service"; -import { Error, Ok } from "../utils/responses"; -import { email_from } from "../utils/secret"; +import { type Request, type Response } from 'express'; +import { generateEmailHtml, sendEmail } from '../services/email.service'; +import * as tent_service from '../services/tent.service'; +import { getUserById } from '../services/user.service'; +import { Error, Ok } from '../utils/responses'; +import { email_from } from '../utils/secret'; export const createTent = async (req: Request, res: Response) => { const { userId2 } = req.body; const userId1 = req.user?.userId; // Créateur = utilisateur connecté if (!userId1 || !userId2) { - Error(res, { msg: "Identifiants utilisateurs manquants." }); + Error(res, { msg: 'Identifiants utilisateurs manquants.' }); } try { await tent_service.createTent(userId1, userId2); - Ok(res, { msg: "Tente réservée avec succès." }); + Ok(res, { msg: 'Tente réservée avec succès.' }); } catch (err: any) { - Error(res, { msg: err.message || "Erreur lors de la création de la tente." }); + Error(res, { msg: err.message || 'Erreur lors de la création de la tente.' }); } }; @@ -25,12 +25,12 @@ export const cancelTent = async (req: Request, res: Response) => { const userId1 = req.user?.userId; if (!userId1) { - Error(res, { msg: "Identifiants utilisateurs manquants." }); + Error(res, { msg: 'Identifiants utilisateurs manquants.' }); } try { await tent_service.cancelTent(userId1); - Ok(res, { msg: "Tente annulée." }); + Ok(res, { msg: 'Tente annulée.' }); } catch { Error(res, { msg: "Erreur lors de l'annulation." }); } @@ -39,13 +39,13 @@ export const cancelTent = async (req: Request, res: Response) => { export const getUserTent = async (req: Request, res: Response) => { const userId = req.user?.userId; - if (!userId) Error(res, { msg: "Utilisateur non authentifié." }); + if (!userId) Error(res, { msg: 'Utilisateur non authentifié.' }); try { const tent = await tent_service.getTentByUser(userId); Ok(res, { data: tent }); } catch { - Error(res, { msg: "Erreur lors de la récupération." }); + Error(res, { msg: 'Erreur lors de la récupération.' }); } }; @@ -54,15 +54,15 @@ export const getAllTentPairs = async (req: Request, res: Response) => { const tents = await tent_service.getAllTents(); Ok(res, { data: tents }); } catch { - Error(res, { msg: "Erreur lors de la récupération des binômes." }); + Error(res, { msg: 'Erreur lors de la récupération des binômes.' }); } }; export const toggleTentConfirmation = async (req: Request, res: Response) => { const { userId1, userId2, confirmed } = req.body; - if (!userId1 || !userId2 || typeof confirmed !== "boolean") { - Error(res, { msg: "Paramètres manquants ou invalides." }); + if (!userId1 || !userId2 || typeof confirmed !== 'boolean') { + Error(res, { msg: 'Paramètres manquants ou invalides.' }); } try { @@ -74,11 +74,11 @@ export const toggleTentConfirmation = async (req: Request, res: Response) => { const user2 = await getUserById(userId2); if (!user1 || !user2) { - Error(res, { msg: "Impossible de récupérer les utilisateurs." }); + Error(res, { msg: 'Impossible de récupérer les utilisateurs.' }); } // Génération du contenu HTML - const htmlEmail = generateEmailHtml("templateNotifyTentConfirmation", { + const htmlEmail = generateEmailHtml('templateNotifyTentConfirmation', { user1: `${user1.firstName} ${user1.lastName}`, user2: `${user2.firstName} ${user2.lastName}`, confirmed, @@ -88,10 +88,8 @@ export const toggleTentConfirmation = async (req: Request, res: Response) => { const emailOptions = { from: email_from, to: [user1.email, user2.email], - subject: confirmed - ? "🎉 Votre tente a été validée !" - : "⛺ Votre tente a été dévalidée", - text: "", // optionnel + subject: confirmed ? '🎉 Votre tente a été validée !' : '⛺ Votre tente a été dévalidée', + text: '', // optionnel html: htmlEmail, }; @@ -99,9 +97,7 @@ export const toggleTentConfirmation = async (req: Request, res: Response) => { await sendEmail(emailOptions); Ok(res, { - msg: confirmed - ? "Tente validée et email envoyé." - : "Tente dévalidée et email envoyé.", + msg: confirmed ? 'Tente validée et email envoyé.' : 'Tente dévalidée et email envoyé.', }); } catch (err: any) { console.error(err); diff --git a/backend/src/controllers/user.controller.ts b/backend/src/controllers/user.controller.ts index 0e0bb3c..d7008dd 100644 --- a/backend/src/controllers/user.controller.ts +++ b/backend/src/controllers/user.controller.ts @@ -1,7 +1,7 @@ import bcrypt from 'bcryptjs'; -import { type Request, type Response } from "express"; +import { type Request, type Response } from 'express'; import * as randomstring from 'randomstring'; -import * as auth_service from "../services/auth.service"; +import * as auth_service from '../services/auth.service'; import * as user_service from '../services/user.service'; import { noSyncEmails } from '../utils/no_sync_list'; import { Error, Ok } from '../utils/responses'; @@ -14,7 +14,7 @@ export const getUsersAdmin = async (req: Request, res: Response) => { return; } catch (error) { console.error(error); - Error(res, { msg: "Erreur interne lors de la récupération des utilisateurs avec leurs rôles." }); + Error(res, { msg: 'Erreur interne lors de la récupération des utilisateurs avec leurs rôles.' }); return; } }; @@ -26,13 +26,13 @@ export const getUsers = async (req: Request, res: Response) => { return; } catch (error) { console.error(error); - Error(res, { msg: "Erreur interne lors de la récupération des utilisateurs avec leurs rôles." }); + Error(res, { msg: 'Erreur interne lors de la récupération des utilisateurs avec leurs rôles.' }); return; } }; export const getUsersByPermission = async (req: Request, res: Response) => { - const { permission } = req.params + const { permission } = req.params; try { const users = await user_service.getUsersbyPermission(permission); @@ -40,19 +40,18 @@ export const getUsersByPermission = async (req: Request, res: Response) => { return; } catch (error) { console.error(error); - Error(res, { msg: "Erreur interne lors de la récupération des utilisateurs avec leurs rôles." }); + Error(res, { msg: 'Erreur interne lors de la récupération des utilisateurs avec leurs rôles.' }); return; } }; - export const syncNewstudent = async (req: Request, res: Response) => { const { date } = req.body; try { const token = await SIEP_Utils.getTokenUTTAPI(); const newStudents = await SIEP_Utils.getNewStudentsFromUTTAPI_NOPAGE(token, date); - const newStudentfiltered = newStudents.filter((student: any) => !noSyncEmails.includes(student.email));//Nouveau à ne pas sync (Démissionnaires, etc) + const newStudentfiltered = newStudents.filter((student: any) => !noSyncEmails.includes(student.email)); //Nouveau à ne pas sync (Démissionnaires, etc) for (const element of newStudentfiltered) { const userInDb = await user_service.getUserByEmail(element.email.toLowerCase()); @@ -63,18 +62,19 @@ export const syncNewstudent = async (req: Request, res: Response) => { element.nom, element.email.toLowerCase(), element.Majeur, - "Nouveau", - element.diplome === "MA" ? "Master" : element.specialite, - tmpPassword); + 'Nouveau', + element.diplome === 'MA' ? 'Master' : element.specialite, + tmpPassword, + ); - await auth_service.createRegistrationToken(newUser.id) + await auth_service.createRegistrationToken(newUser.id); } } - Ok(res, { msg: "All NewStudent created and synced" }) + Ok(res, { msg: 'All NewStudent created and synced' }); } catch (error) { - Error(res, { error }) + Error(res, { error }); } -} +}; export const getCurrentUser = async (req: Request, res: Response) => { const userId = req.user?.userId; @@ -83,7 +83,7 @@ export const getCurrentUser = async (req: Request, res: Response) => { const user = await user_service.getUserById(userId); Ok(res, { data: user }); } catch { - Error(res, { msg: "Erreur lors de la mise à jour du profil." }); + Error(res, { msg: 'Erreur lors de la mise à jour du profil.' }); } }; @@ -93,20 +93,19 @@ export const updateProfile = async (req: Request, res: Response) => { try { const result = await user_service.updateUserInfoByUserId(userId, branch, contact); - Ok(res, { msg: "Profil mis à jour", data: result }); + Ok(res, { msg: 'Profil mis à jour', data: result }); } catch { - Error(res, { msg: "Erreur lors de la mise à jour du profil." }); + Error(res, { msg: 'Erreur lors de la mise à jour du profil.' }); } }; - export const adminUpdateUser = async (req: Request, res: Response) => { const { userId } = req.params; const updates = req.body; try { const result = await user_service.updateUserByAdmin(parseInt(userId), updates); - Ok(res, { msg: "Utilisateur mis à jour", data: result }); + Ok(res, { msg: 'Utilisateur mis à jour', data: result }); } catch { Error(res, { msg: "Erreur lors de la mise à jour de l'utilisateur." }); } @@ -117,7 +116,7 @@ export const adminDeleteUser = async (req: Request, res: Response) => { try { const result = await user_service.deleteUserById(parseInt(userId)); - Ok(res, { msg: "Utilisateur supprimé", data: result }); + Ok(res, { msg: 'Utilisateur supprimé', data: result }); } catch { Error(res, { msg: "Erreur lors de la suppression de l'utilisateur." }); } diff --git a/backend/src/email/email.preview-data.ts b/backend/src/email/email.preview-data.ts index f2742b1..713bb8a 100644 --- a/backend/src/email/email.preview-data.ts +++ b/backend/src/email/email.preview-data.ts @@ -1,4 +1,4 @@ -import type { TemplateData } from "../../types/email"; +import type { TemplateData } from '../../types/email'; import { service_url } from '../utils/secret'; export const defaultPreviewData: Record = { @@ -8,7 +8,7 @@ export const defaultPreviewData: Record = { }, templateNotebook: { notebook_fr: `${service_url}api/uploads/notebooks/fr.pdf`, - notebook_en: `${service_url}api/uploads/notebooks/en.pdf` + notebook_en: `${service_url}api/uploads/notebooks/en.pdf`, }, templateAttributionBus: { bus: 'bus', @@ -29,8 +29,6 @@ export const defaultPreviewData: Record = { resetLink: `${service_url}resetpassword?token=preview-token`, }, templateNotifyPermanenceReminder: { - permanence: { - - } - } + permanence: {}, + }, }; diff --git a/backend/src/email/email.registry.ts b/backend/src/email/email.registry.ts index a1f4972..6a4c461 100644 --- a/backend/src/email/email.registry.ts +++ b/backend/src/email/email.registry.ts @@ -1,4 +1,4 @@ -import type { PermanenceEmailData, TemplateRenderer } from "../../types/email"; +import type { PermanenceEmailData, TemplateRenderer } from '../../types/email'; export const templateResetPassword = 'reset-password.html'; const templateNotebook = 'notebook.html'; @@ -26,9 +26,9 @@ export const templateRenderers: Record = { notebook_fr?: string; notebook_en?: string; }; - return { + return { notebook_fr: typedData.notebook_fr, - notebook_en: typedData.notebook_en + notebook_en: typedData.notebook_en, }; }, }, @@ -77,5 +77,5 @@ export const templateRenderers: Record = { const typedData = data as PermanenceEmailData; return typedData; }, - } + }, }; diff --git a/backend/src/email/email.renderer.ts b/backend/src/email/email.renderer.ts index ec3fef5..be1eadf 100644 --- a/backend/src/email/email.renderer.ts +++ b/backend/src/email/email.renderer.ts @@ -1,6 +1,6 @@ -import fs from "fs"; -import Handlebars from "handlebars"; -import path from "path"; +import fs from 'fs'; +import Handlebars from 'handlebars'; +import path from 'path'; const templateCache = new Map(); @@ -8,10 +8,10 @@ const readTemplate = (templateFileName: string) => { const cached = templateCache.get(templateFileName); if (cached) return cached; - const templatePath = path.join(path.resolve(__dirname, "./templates"), templateFileName); + const templatePath = path.join(path.resolve(__dirname, './templates'), templateFileName); if (fs.existsSync(templatePath)) { - const content = fs.readFileSync(templatePath, "utf8"); + const content = fs.readFileSync(templatePath, 'utf8'); templateCache.set(templateFileName, content); return content; } diff --git a/backend/src/email/templates/attribution-bus.html b/backend/src/email/templates/attribution-bus.html index ffee593..ba13336 100644 --- a/backend/src/email/templates/attribution-bus.html +++ b/backend/src/email/templates/attribution-bus.html @@ -1,120 +1,279 @@ - + - - - - Intégration UTT - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - -
- Logo Intégration UTT -
- INTEGRATION UTT -
-

Le Week-End d'Intégration approhe à grands pas !

-

Si tu reçois ce message c'est que tu pars au WEI (youhouu !), tu trouveras dans celui-ci le bus avec lequel tu vas te rendre sur le lieu pour ce week-end.

-

Fais bien attention à ne pas être en retard sous peine de rater ton bus, ça serait embêtant à la fois pour toi et pour nous.

-

Autre point très important : les essentiels pour le WEI. Tu trouveras ci-dessous un rappel des objets obligatoires à ramener pour passer un bon week-end. Il risque de pleuvoir alors prévoyez bien en conséquence !

-
    -
  • Duvet et matelas gonflable/tapis de sol 🛏️ (si vous n'avez pas de duvet, vous ne partirez pas)
  • -
  • Gourde, Tupperware, couverts, gobby (=écocup) 🍴
  • -
  • Vêtements : changes pour 2 jours, pull, maillot de bain 👙
  • -
  • Manteau imperméable 🧥
  • -
  • Affaires salissables : change complet & chaussures (à mettre dès le départ en bus) 🚌
  • -
  • Produits d'hygiène : brosse à dent, serviette, nécessaire de toilette 🪥
  • -
  • Tongues/crocs pour les douches 🩴
  • -
  • Papiers importants : Carte d'identité, CB & liquide, autorisation parentale (pour les mineurs) 💳
  • -
  • Ta place au WEI 📩
  • -
  • Crème solaire & anti-moustique ☀️
  • -
  • De quoi grignoter (prenez un pique-nique à manger avant de prendre le bus, pas dans le bus) 😋
  • -
-

🚫 Affaires interdites :

-
    -
  • Boissons autres que de l'eau
  • -
  • Substances illicites
  • -
  • Armes blanches
  • -
  • Déodorant en spray
  • -
-

Pour rappel, voici la vidéo des indispensables du WEI ici

-

Concernant ton bus, tu as été attribué au bus {{bus}}

-

Maintenant il faut que tu sois présent en amphi de verdure à l'UTT à {{time}}

-

Voilà, toute l'équipe de l'intégration te souhaite un excellent WEI ;)

-
-

Retrouve des informations utiles sur notre Instagram !

-
- - Instagram - -
-

Cordialement,
L'équipe intégration UTT

-

Si vous avez des questions, n'hésitez pas à nous contacter.

-
-

The Integration Weekend is just around the corner!

-

If you are receiving this message, it means you're coming to the WEI (woohoo!). In this email, you'll find the bus you have been assigned to travel to the venue for the weekend.

-

Please make sure not to be late, otherwise you may miss your bus, which would be inconvenient for both you and us.

-

Another very important point: the WEI essentials. Below, you'll find a reminder of the mandatory items you need to bring to have a great weekend. Rain is possible, so make sure to pack accordingly!

-
    -
  • Sleeping bag and inflatable mattress/sleeping mat 🛏️ (if you do not have a sleeping bag, you will not be allowed to leave)
  • -
  • Water bottle, Tupperware container, cutlery, gobby (= reusable cup) 🍴
  • -
  • Clothing: changes of clothes for 2 days, sweater, swimsuit 👙
  • -
  • Waterproof jacket 🧥
  • -
  • Clothes you don't mind getting dirty: a full change of clothes & shoes (to be worn from the moment you board the bus) 🚌
  • -
  • Toiletries: toothbrush, towel, personal hygiene essentials 🪥
  • -
  • Flip-flops/Crocs for the showers 🩴
  • -
  • Important documents: ID card, bank card & cash, parental authorization form (for minors) 💳
  • -
  • Your WEI ticket 📩
  • -
  • Sunscreen & mosquito repellent ☀️
  • -
  • Something to snack on (bring a picnic to eat before boarding the bus, not on the bus) 😋
  • -
-

🚫 Prohibited items:

-
    -
  • Any drinks other than water
  • -
  • Illegal substances
  • -
  • Bladed weapons
  • -
  • Spray deodorant
  • -
-

As a reminder, you can find the WEI essentials video here

-

Regarding your bus assignment, you have been assigned to {{bus}}

-

You must now be present at the UTT Green Amphitheater at {{time}}

-

That's it! The entire Integration Team wishes you an amazing WEI ;)

- -
-

Find useful updates on our Instagram!

-
- - Instagram - -
-

Regards,
The UTT's Integration Team

-

If you have any questions, feel free to contact us.

-
-
- + + + + Intégration UTT + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ Logo Intégration UTT +
+ INTEGRATION UTT +
+

+ Le Week-End d'Intégration approhe à grands pas ! +

+

+ Si tu reçois ce message c'est que tu pars au WEI (youhouu !), tu trouveras dans + celui-ci le bus avec lequel tu vas te rendre sur le lieu pour ce week-end. +

+

+ Fais bien attention à ne pas être en retard sous peine de rater ton + bus, ça serait embêtant à la fois pour toi et pour nous. +

+

+ Autre point très important : les essentiels pour le WEI. Tu trouveras ci-dessous un + rappel des objets obligatoires à ramener pour passer un bon week-end. Il risque de + pleuvoir alors prévoyez bien en conséquence ! +

+
    +
  • + Duvet et matelas gonflable/tapis de sol 🛏️ (si vous n'avez pas de duvet, vous ne + partirez pas) +
  • +
  • Gourde, Tupperware, couverts, gobby (=écocup) 🍴
  • +
  • + Vêtements : changes pour 2 jours, pull, maillot de bain 👙 +
  • +
  • Manteau imperméable 🧥
  • +
  • + Affaires salissables : change complet & chaussures (à mettre dès le départ + en bus) 🚌 +
  • +
  • + Produits d'hygiène : brosse à dent, serviette, nécessaire de toilette 🪥 +
  • +
  • Tongues/crocs pour les douches 🩴
  • +
  • + Papiers importants : Carte d'identité, CB & liquide, autorisation parentale + (pour les mineurs) 💳 +
  • +
  • Ta place au WEI 📩
  • +
  • Crème solaire & anti-moustique ☀️
  • +
  • + De quoi grignoter (prenez un pique-nique à manger avant de prendre le bus, pas + dans le bus) 😋 +
  • +
+

+ 🚫 Affaires interdites : +

+
    +
  • Boissons autres que de l'eau
  • +
  • Substances illicites
  • +
  • Armes blanches
  • +
  • Déodorant en spray
  • +
+

+ Pour rappel, voici la vidéo des indispensables du WEI + ici +

+

Concernant ton bus, tu as été attribué au bus {{bus}}

+

+ Maintenant il faut que tu sois présent en amphi de verdure à l'UTT à + {{time}} +

+

Voilà, toute l'équipe de l'intégration te souhaite un excellent WEI ;)

+
+

+ Retrouve des informations utiles sur notre Instagram ! +

+
+ + Instagram + +
+

Cordialement,
L'équipe intégration UTT

+

+ Si vous avez des questions, n'hésitez pas à + nous contacter. +

+
+

+ The Integration Weekend is just around the corner! +

+

+ If you are receiving this message, it means you're coming to the WEI (woohoo!). In + this email, you'll find the bus you have been assigned to travel to the venue for + the weekend. +

+

+ Please make sure not to be late, otherwise you may miss your bus, + which would be inconvenient for both you and us. +

+

+ Another very important point: the WEI essentials. Below, you'll find a reminder of + the mandatory items you need to bring to have a great weekend. Rain is possible, so + make sure to pack accordingly! +

+
    +
  • + Sleeping bag and inflatable mattress/sleeping mat 🛏️ (if you do not have a + sleeping bag, you will not be allowed to leave) +
  • +
  • + Water bottle, Tupperware container, cutlery, gobby (= reusable cup) 🍴 +
  • +
  • + Clothing: changes of clothes for 2 days, sweater, swimsuit 👙 +
  • +
  • Waterproof jacket 🧥
  • +
  • + Clothes you don't mind getting dirty: a full change of clothes & shoes (to + be worn from the moment you board the bus) 🚌 +
  • +
  • + Toiletries: toothbrush, towel, personal hygiene essentials 🪥 +
  • +
  • Flip-flops/Crocs for the showers 🩴
  • +
  • + Important documents: ID card, bank card & cash, parental authorization form + (for minors) 💳 +
  • +
  • Your WEI ticket 📩
  • +
  • Sunscreen & mosquito repellent ☀️
  • +
  • + Something to snack on (bring a picnic to eat before boarding the bus, not on the + bus) 😋 +
  • +
+

🚫 Prohibited items:

+
    +
  • Any drinks other than water
  • +
  • Illegal substances
  • +
  • Bladed weapons
  • +
  • Spray deodorant
  • +
+

+ As a reminder, you can find the WEI essentials video + here +

+

Regarding your bus assignment, you have been assigned to {{bus}}

+

+ You must now be present at the UTT Green Amphitheater at {{time}} +

+

That's it! The entire Integration Team wishes you an amazing WEI ;)

+
+

Find useful updates on our Instagram!

+
+ + Instagram + +
+

Regards,
The UTT's Integration Team

+

+ If you have any questions, feel free to + contact us. +

+
+
+ diff --git a/backend/src/email/templates/custom.html b/backend/src/email/templates/custom.html index 3a16289..3262d94 100644 --- a/backend/src/email/templates/custom.html +++ b/backend/src/email/templates/custom.html @@ -1,46 +1,86 @@ - + - - - - Réinitialisation de mot de passe - - - - - - -
- - - - - - - - - - - - - -
- Logo Intégration UTT -
- INTEGRATION UTT -
-

{{title}}

-

{{content}}

-
-

Retrouve des informations utiles sur notre Instagram !

-
- - Instagram - -
-

Cordialement,
L'équipe intégration UTT

-

Si vous avez des questions, n'hésitez pas à nous contacter.

-
-
- + + + + Intégration UTT + + + + + + +
+ + + + + + + + + + + + + +
+ Logo Intégration UTT +
+ INTEGRATION UTT +
+

{{title}}

+

{{content}}

+
+

+ Retrouve des informations utiles sur notre Instagram ! +

+
+ + Instagram + +
+

Cordialement,
L'équipe intégration UTT

+

+ Si vous avez des questions, n'hésitez pas à + nous contacter. +

+
+
+ diff --git a/backend/src/email/templates/notebook.html b/backend/src/email/templates/notebook.html index 5c3993b..a87f9c0 100644 --- a/backend/src/email/templates/notebook.html +++ b/backend/src/email/templates/notebook.html @@ -1,61 +1,162 @@ - + - - - - Intégration UTT - - - - - - -
- - - - - - - - - - - - - -
- Logo Intégration UTT -
- INTEGRATION UTT -
-

Un peu de travail...

-

Si tu reçois ce mail, c'est que tu es sur le point de rejoindre l'UTT et de vivre tes premières années en école supérieure.

-

Mais après toutes ces vacances, il est important de ne pas s'endormir et de vite se remettre au travail !

-

C'est pourquoi l'intégration te propose un cahier de vacances qui te permettra de te remettre à niveau.

-

Toutes les bases y sont revues, de la terminale… jusqu'au CP. À toi de nous prouver que tu en es capable ! Méthodologie et rigueur seront nécessaires pour en venir à bout (et pas mal d'humour également).

-

Ce cahier sera examiné par un jury extrêmement talentueux : des ingénieurs hors pair, ayant déjà prouvé leur valeur lors d'un concours de Ricard sur la plage de Banyuls-sur-Mer.

-

À toi de leur montrer que tu peux égaler leurs compétences ! Ce jury n'hésitera pas à te récompenser pour tes efforts si tu nous renvoies tes réponses à cette adresse mail.

-

Alors si tu veux y participer, tu peux le télécharger juste ici et le renvoyer à clement.duranson@utt.fr avant le dimanche 30 août.

-

- Cahier de vacances ! -

-

Nous serons présents sur les réseaux tout au long de l'été pour te tenir informé(e), te partager des astuces, et plein d'autres trucs trop cools ! Rejoins le site de l'intégration pour bien être informé des actus ! Tu as reçu dans le premier mail de notre part, un lien pour réinitialiser ton mot de passe et te connecter.

-

- Accéder au site -

-

Alors, bon courage à toi, nous sommes impatients de lire tes meilleures réponses.

-

À très vite !

-
-

Retrouve des informations utiles sur notre Instagram !

-
- - Instagram - -
-

Cordialement,
L'équipe intégration UTT

-

Si vous avez des questions, n'hésitez pas à nous contacter.

-
-
- + + + + Intégration UTT + + + + + + +
+ + + + + + + + + + + + + +
+ Logo Intégration UTT +
+ INTEGRATION UTT +
+

+ Un peu de travail... +

+

+ Si tu reçois ce mail, c'est que tu es sur le point de rejoindre l'UTT et de vivre + tes premières années en école supérieure. +

+

+ Mais après toutes ces vacances, il est important de ne pas s'endormir et de vite se + remettre au travail ! +

+

+ C'est pourquoi l'intégration te propose un cahier de vacances qui te permettra de te + remettre à niveau. +

+

+ Toutes les bases y sont revues, de la terminale… jusqu'au CP. À toi de nous prouver + que tu en es capable ! Méthodologie et rigueur seront nécessaires pour en venir à + bout (et pas mal d'humour également). +

+

+ Ce cahier sera examiné par un jury extrêmement talentueux : des ingénieurs hors + pair, ayant déjà prouvé leur valeur lors d'un concours de Ricard sur la plage de + Banyuls-sur-Mer. +

+

+ À toi de leur montrer que tu peux égaler leurs compétences ! Ce jury n'hésitera pas + à te récompenser pour tes efforts si tu nous renvoies tes réponses à cette adresse + mail. +

+

+ Alors si tu veux y participer, tu peux le télécharger juste ici et le renvoyer à + clement.duranson@utt.fr + avant le dimanche 30 août. +

+

+ Cahier de vacances ! +

+

+ Nous serons présents sur les réseaux tout au long de l'été pour te tenir informé(e), + te partager des astuces, et plein d'autres trucs trop cools ! + Rejoins le site de l'intégration pour bien être informé des actus ! + Tu as reçu dans le premier mail de notre part, un lien pour réinitialiser ton mot de + passe et te connecter. +

+

+ Accéder au site +

+

Alors, bon courage à toi, nous sommes impatients de lire tes meilleures réponses.

+

À très vite !

+
+

+ Retrouve des informations utiles sur notre Instagram ! +

+
+ + Instagram + +
+

Cordialement,
L'équipe intégration UTT

+

+ Si vous avez des questions, n'hésitez pas à + nous contacter. +

+
+
+ diff --git a/backend/src/email/templates/notify-news.html b/backend/src/email/templates/notify-news.html index 00b819e..a633f3b 100644 --- a/backend/src/email/templates/notify-news.html +++ b/backend/src/email/templates/notify-news.html @@ -1,77 +1,154 @@ - + - - - - Intégration UTT - - - - - + +
- - - - - - - - - + + + + +
- Logo Intégration UTT -
- INTEGRATION UTT -
-

Nouvelle actu !

+ + + + Intégration UTT + + + + + - -
+ + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - -
+ Logo Intégration UTT +
+ INTEGRATION UTT +
+

Nouvelle actu !

-

{{title}}

-

👉 Rendez-vous sur le site de l'inté dans l'onglet News pour en savoir plus.

-

- Accéder au site -

-
-

Retrouve des informations utiles sur notre Instagram !

-
- - Instagram - -
-

Cordialement,
L'équipe intégration UTT

-

Si vous avez des questions, n'hésitez pas à nous contacter.

-
-

New news !

+

{{title}}

+

+ 👉 Rendez-vous sur le site de l'inté dans l'onglet News pour en + savoir plus. +

+

+ Accéder au site +

+
+

+ Retrouve des informations utiles sur notre Instagram ! +

+
+ + Instagram + +
+

Cordialement,
L'équipe intégration UTT

+

+ Si vous avez des questions, n'hésitez pas à + nous contacter. +

+
+

New news !

-

{{title}}

-

👉 Visit the integration website in the News tab to find out more.

-

- Click here -

-
-

Find useful updates on our Instagram!

-
- - Instagram - -
-

Regards,
The UTT's Integration Team

-

If you have any questions, feel free to contact us.

-
-
- +

{{title}}

+

+ 👉 Visit the integration website in the News tab to find out more. +

+

+ Click here +

+
+

Find useful updates on our Instagram!

+
+ + Instagram + +
+

Regards,
The UTT's Integration Team

+

+ If you have any questions, feel free to + contact us. +

+
+
+ diff --git a/backend/src/email/templates/notify-permanence-reminder.html b/backend/src/email/templates/notify-permanence-reminder.html index 3a3b6b9..31ca79a 100644 --- a/backend/src/email/templates/notify-permanence-reminder.html +++ b/backend/src/email/templates/notify-permanence-reminder.html @@ -1,78 +1,146 @@ - + - - - - Intégration UTT - - - - - + +
- - - - - - - - - + + + + +
- Logo Intégration UTT -
- INTEGRATION UTT -
-

Rappel de permanence

+ + + + Intégration UTT + + + + + - -
+ + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - -
+ Logo Intégration UTT +
+ INTEGRATION UTT +
+

+ Rappel de permanence +

-

{{permName}}

-

Entre le {{permBeginDate}} à {{permBeginHour}} et le {{permEndDate}} à {{permEndHour}}

-

Point de rendez-vous: {{permLocation}}

-

Description de la permanence: {{permDescription}}

-

Pour le bon déroulement des permanences, nous vous demandons de vous présenter 15 minutes à l'avance sur les lieux.

-

Le ou la reponsable de permanence effectuera l'appel. Seules les permanences honorées seront comptabilisés pour le total de permanences des chefs et cheffes d'équipe.

-
-

Retrouve des informations utiles sur notre Instagram !

-
- - Instagram - -
-

Cordialement,
L'équipe intégration UTT

-

Si vous avez des questions, n'hésitez pas à nous contacter.

-
-

Permanence reminder

+

{{permName}}

+

+ Entre le {{permBeginDate}} à {{permBeginHour}} et le {{permEndDate}} à + {{permEndHour}} +

+

Point de rendez-vous: {{permLocation}}

+

Description de la permanence: {{permDescription}}

+

+ Pour le bon déroulement des permanences, nous vous demandons de vous présenter + 15 minutes à l'avance sur les lieux. +

+

+ Le ou la reponsable de permanence effectuera l'appel. Seules les permanences + honorées seront comptabilisés pour le total de permanences des chefs et cheffes + d'équipe. +

+
+

+ Retrouve des informations utiles sur notre Instagram ! +

+
+ + Instagram + +
+

Cordialement,
L'équipe intégration UTT

+

+ Si vous avez des questions, n'hésitez pas à + nous contacter. +

+
+

+ Permanence reminder +

-

{{permName}}

-

From the {{permBeginDate}} at {{permBeginHour}} to the {{permEndDate}} at {{permEndHour}}

-

Meeting point: {{permLocation}}

-

Description: {{permDescription}}

-

To ensure the smooth running of the permanence hours, we ask that you arrive 15 minutes early on the premises.

-
-

Find useful updates on our Instagram!

-
- - Instagram - -
-

Regards,
The UTT's Integration Team

-

If you have any questions, feel free to contact us.

-
-
- +

{{permName}}

+

+ From the {{permBeginDate}} at {{permBeginHour}} to the {{permEndDate}} at + {{permEndHour}} +

+

Meeting point: {{permLocation}}

+

Description: {{permDescription}}

+

+ To ensure the smooth running of the permanence hours, we ask that you arrive + 15 minutes early on the premises. +

+
+

Find useful updates on our Instagram!

+
+ + Instagram + +
+

Regards,
The UTT's Integration Team

+

+ If you have any questions, feel free to + contact us. +

+
+
+ diff --git a/backend/src/email/templates/notify-tent-confirmation.html b/backend/src/email/templates/notify-tent-confirmation.html index a77e7fe..ce5e619 100644 --- a/backend/src/email/templates/notify-tent-confirmation.html +++ b/backend/src/email/templates/notify-tent-confirmation.html @@ -1,27 +1,52 @@ - + - - - - Intégration UTT - - -
-
- Logo Intégration UTT -

Intégration UTT

+ + + + Intégration UTT + + +
+
+ Logo Intégration UTT +

Intégration UTT

+
+

⛺ Mise à jour de ta tente

+

+ La tente entre {{user1}} et {{user2}} a été + + {{#if confirmed}}validée{{else}}invalidée{{/if}} . +

+

+ 👉 Tu peux consulter l'état de ta tente sur le site de l'inté dans l'onglet Tentes. +

+

+ Accéder au site +

-

⛺ Mise à jour de ta tente

-

- La tente entre {{user1}} et {{user2}} a été - - {{#if confirmed}}validée{{else}}invalidée{{/if}} - . -

-

👉 Tu peux consulter l'état de ta tente sur le site de l'inté dans l'onglet Tentes.

-

- Accéder au site -

-
- + diff --git a/backend/src/email/templates/reset-password.html b/backend/src/email/templates/reset-password.html index 0be8879..b36793a 100644 --- a/backend/src/email/templates/reset-password.html +++ b/backend/src/email/templates/reset-password.html @@ -1,79 +1,159 @@ - + - - - - Réinitialisation de mot de passe - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - -
- Logo Intégration UTT -
- INTEGRATION UTT -
-

Réinitialisation de mot de passe

-

Bonjour,

-

Vous avez demandé à réinitialiser votre mot de passe. Cliquez sur le bouton ci-dessous pour choisir un nouveau mot de passe :

-

- Réinitialiser mon mot de passe -

-

Attention, le lien n'est valide que pendant 1h.

-

Si vous n'avez pas demandé cette réinitialisation, veuillez ignorer cet e-mail.

-
-

Retrouve des informations utiles sur notre Instagram !

-
- - Instagram - -
-

Cordialement,
L'équipe intégration UTT

-

Si vous avez des questions, n'hésitez pas à nous contacter.

-
-

Password Reset

-

Hello,

-

You requested to reset your password. Click the button below to choose a new password:

-

- Reset My Password -

-

Please note that this link is only valid for 1 hour.

-

If you did not request this password reset, please ignore this email.

-
-

Find useful updates on our Instagram!

-
- - Instagram - -
-

Regards,
The UTT's Integration Team

-

If you have any questions, feel free to contact us.

-
-
- + + + + Réinitialisation de mot de passe + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ Logo Intégration UTT +
+ INTEGRATION UTT +
+

+ Réinitialisation de mot de passe +

+

Bonjour,

+

+ Vous avez demandé à réinitialiser votre mot de passe. Cliquez sur le bouton + ci-dessous pour choisir un nouveau mot de passe : +

+

+ Réinitialiser mon mot de passe +

+

Attention, le lien n'est valide que pendant 1h.

+

Si vous n'avez pas demandé cette réinitialisation, veuillez ignorer cet e-mail.

+
+

+ Retrouve des informations utiles sur notre Instagram ! +

+
+ + Instagram + +
+

Cordialement,
L'équipe intégration UTT

+

+ Si vous avez des questions, n'hésitez pas à + nous contacter. +

+
+

Password Reset

+

Hello,

+

+ You requested to reset your password. Click the button below to choose a new + password: +

+

+ Reset My Password +

+

Please note that this link is only valid for 1 hour.

+

If you did not request this password reset, please ignore this email.

+
+

Find useful updates on our Instagram!

+
+ + Instagram + +
+

Regards,
The UTT's Integration Team

+

+ If you have any questions, feel free to + contact us. +

+
+
+ diff --git a/backend/src/email/templates/welcome.html b/backend/src/email/templates/welcome.html index 030870b..11b6528 100644 --- a/backend/src/email/templates/welcome.html +++ b/backend/src/email/templates/welcome.html @@ -1,107 +1,239 @@ - + - - - - Intégration UTT - - - - - + +
- - - - - - - - - + + + + +
- Logo Intégration UTT -
- INTEGRATION UTT -
-

Salut à toi jeune nouveau !

-

Bravo pour ton admission à l'UTT ! Nous sommes l'équipe d'intégration, des étudiants bénévoles qui préparent minutieusement ton arrivée pour que celle-ci reste inoubliable.

-

Un tas d'événements incroyables, dont la participation est basée sur le volontariat, t'attendent dès le Lundi 31 Août que tu arrives en 1ère année, en 3ème année, en master ou en Bachelor !

-

Tout est fait pour que tu t'éclates et que tu rencontres les personnes qui feront de ton passage à l'UTT un moment inoubliable. Mais avant toute chose, il faut te préparer.

-

Assure-toi de réaliser les tâches suivantes avant ton arrivée :

+ + + + Intégration UTT + + + + + - -
+ + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - -
+ Logo Intégration UTT +
+ INTEGRATION UTT +
+

+ Salut à toi jeune nouveau ! +

+

+ Bravo pour ton admission à l'UTT ! Nous sommes l'équipe d'intégration, des étudiants + bénévoles qui préparent minutieusement ton arrivée pour que celle-ci reste + inoubliable. +

+

+ Un tas d'événements incroyables, dont la participation est basée sur le volontariat, + t'attendent dès le Lundi 31 Août que tu arrives en 1ère + année, en 3ème année, en master ou en Bachelor ! +

+

+ Tout est fait pour que tu t'éclates et que tu rencontres les personnes qui feront de + ton passage à l'UTT un moment inoubliable. Mais avant toute chose, il faut te + préparer. +

+

Assure-toi de réaliser les tâches suivantes avant ton arrivée :

-

1. Se connecter sur le site de l'intégration

-

Pour pouvoir te connecter au site de l'intégration il te suffit de changer ton mot de passe en cliquant sur ce lien suivant :

-

- Changer ton mot de passe -

-

Attention, ce lien est valable uniquement une fois !

-

Une fois cela fait, tu pourras te connecter à ton compte et y retrouver toutes les informations relatives aux événements de la semaine via le lien suivant :

-

- https://integration.utt.fr -

+

1. Se connecter sur le site de l'intégration

+

+ Pour pouvoir te connecter au site de l'intégration il te suffit de changer ton mot + de passe en cliquant sur ce lien suivant : +

+

+ Changer ton mot de passe +

+

+ Attention, ce lien est valable uniquement une fois ! +

+

+ Une fois cela fait, tu pourras te connecter à ton compte et y retrouver toutes les + informations relatives aux événements de la semaine via le lien suivant : +

+

+ https://integration.utt.fr +

-

2. Parrainage

-

Lorsque tu arrives à l'UTT, un.e étudiant.e plus ancien.ne devient ton parrain ou ta marraine. Il ou elle sera ton contact privilégié pour découvrir l'école mais aussi la vie étudiante troyenne et répondre à toutes tes questions que ce soit sur l'UTT, les logements, les cours, la vie à Troyes,...

-

Pour t'attribuer quelqu'un qui te correspond au mieux on t'invite à remplir - ce questionnaire -

-
-

Retrouve des informations utiles sur notre Instagram !

-
- - Instagram - -
-

Cordialement,
L'équipe intégration UTT

-

Si vous avez des questions, n'hésitez pas à nous contacter.

-
-

Hello there, new arrival!

-

Congratulations on being admitted to UTT! We are the integration team, a group of volunteer students who carefully prepare your arrival so that it becomes truly unforgettable.

-

A lot of incredible events, all based on voluntary participation, are waiting for you starting on Monday, August 31st, whether you are entering your first year, third year, a master's program, or a bachelor's program!

-

Everything is set up so you can have fun and meet the people who will make your time at UTT unforgettable. But first, you need to get ready.

-

Make sure you complete the following tasks before you arrive:

+

2. Parrainage

+

+ Lorsque tu arrives à l'UTT, un.e étudiant.e plus ancien.ne devient ton parrain ou ta + marraine. Il ou elle sera ton contact privilégié pour découvrir l'école mais aussi + la vie étudiante troyenne et répondre à toutes tes questions que ce soit sur l'UTT, + les logements, les cours, la vie à Troyes,... +

+

+ Pour t'attribuer quelqu'un qui te correspond au mieux on t'invite à remplir + ce questionnaire +

+
+

+ Retrouve des informations utiles sur notre Instagram ! +

+
+ + Instagram + +
+

Cordialement,
L'équipe intégration UTT

+

+ Si vous avez des questions, n'hésitez pas à + nous contacter. +

+
+

+ Hello there, new arrival! +

+

+ Congratulations on being admitted to UTT! We are the integration team, a group of + volunteer students who carefully prepare your arrival so that it becomes truly + unforgettable. +

+

+ A lot of incredible events, all based on voluntary participation, are waiting for + you starting on Monday, August 31st, whether you are entering your first year, third year, a master's program, or a + bachelor's program! +

+

+ Everything is set up so you can have fun and meet the people who will make your time + at UTT unforgettable. But first, you need to get ready. +

+

Make sure you complete the following tasks before you arrive:

-

1. Log in to the integration website

-

To log in to the integration website, you simply need to change your password by clicking the following link:

-

- Change your password -

-

Warning: this link is valid only once!

-

Once that is done, you will be able to log into your account and find all the information about the week's events through the following link:

-

- https://integration.utt.fr -

+

1. Log in to the integration website

+

+ To log in to the integration website, you simply need to change your password by + clicking the following link: +

+

+ Change your password +

+

Warning: this link is valid only once!

+

+ Once that is done, you will be able to log into your account and find all the + information about the week's events through the following link: +

+

+ https://integration.utt.fr +

-

2. Mentorship

-

When you arrive at UTT, an older student will become your sponsor or mentor. They will be your main contact to help you discover the school as well as student life in Troyes, and to answer all your questions about UTT, housing, classes, and life in Troyes.

-

To be matched with someone who suits you best, we invite you to fill out - this questionnaire -

-
-

Find useful updates on our Instagram!

-
- - Instagram - -
-

Regards,
The UTT's Integration Team

-

If you have any questions, feel free to contact us.

-
-
- +

2. Mentorship

+

+ When you arrive at UTT, an older student will become your sponsor or mentor. They + will be your main contact to help you discover the school as well as student life in + Troyes, and to answer all your questions about UTT, housing, classes, and life in + Troyes. +

+

+ To be matched with someone who suits you best, we invite you to fill out + this questionnaire +

+
+

Find useful updates on our Instagram!

+
+ + Instagram + +
+

Regards,
The UTT's Integration Team

+

+ If you have any questions, feel free to + contact us. +

+
+
+ diff --git a/backend/src/errors/permanence.error.ts b/backend/src/errors/permanence.error.ts index 1092c0f..61c1398 100644 --- a/backend/src/errors/permanence.error.ts +++ b/backend/src/errors/permanence.error.ts @@ -1,7 +1,7 @@ -export class UnauthorizedError extends Error { } -export class AlreadyRegisteredError extends Error { } -export class PermanenceNotFoundError extends Error { } -export class PermanenceClosedError extends Error { } -export class PermanenceFullError extends Error { } -export class UnregisterDeadlineError extends Error { } -export class RegisterDeadlineError extends Error { } +export class UnauthorizedError extends Error {} +export class AlreadyRegisteredError extends Error {} +export class PermanenceNotFoundError extends Error {} +export class PermanenceClosedError extends Error {} +export class PermanenceFullError extends Error {} +export class UnregisterDeadlineError extends Error {} +export class RegisterDeadlineError extends Error {} diff --git a/backend/src/middlewares/automation.middleware.ts b/backend/src/middlewares/automation.middleware.ts index d3a41bf..606e75d 100644 --- a/backend/src/middlewares/automation.middleware.ts +++ b/backend/src/middlewares/automation.middleware.ts @@ -1,22 +1,21 @@ -import { type NextFunction, type Request, type Response } from "express"; -import { Unauthorized } from "../utils/responses"; // Assurez-vous que cette fonction est bien définie -import { automation_token } from "../utils/secret"; +import { type NextFunction, type Request, type Response } from 'express'; +import { Unauthorized } from '../utils/responses'; // Assurez-vous que cette fonction est bien définie +import { automation_token } from '../utils/secret'; export const authenticateAutomation = (req: Request, res: Response, next: NextFunction) => { try { - const { token } = req.body; - + if (!token) { - return Unauthorized(res, { msg: "Unauthorized: Missing or malformed token" }); + return Unauthorized(res, { msg: 'Unauthorized: Missing or malformed token' }); } if (token !== automation_token) { - return Unauthorized(res, { msg: "Unauthorized: Invalid token" }) + return Unauthorized(res, { msg: 'Unauthorized: Invalid token' }); } next(); } catch { - return Unauthorized(res, { msg: "Unauthorized: Invalid token" }); + return Unauthorized(res, { msg: 'Unauthorized: Invalid token' }); } }; diff --git a/backend/src/routes/automation.routes.ts b/backend/src/routes/automation.routes.ts index 6fdec9a..214f4a4 100644 --- a/backend/src/routes/automation.routes.ts +++ b/backend/src/routes/automation.routes.ts @@ -1,11 +1,10 @@ -import express from "express"; -import * as permanenceController from "../controllers/permanence.controller"; - +import express from 'express'; +import * as permanenceController from '../controllers/permanence.controller'; const automationRoutes = express.Router(); // Permanences routes -automationRoutes.post("/permanence/notification/hourly", permanenceController.sendHourlyNotificationToUsers); -automationRoutes.post("/permanence/notification/daily", permanenceController.sendDailyNotificationToUsers); +automationRoutes.post('/permanence/notification/hourly', permanenceController.sendHourlyNotificationToUsers); +automationRoutes.post('/permanence/notification/daily', permanenceController.sendDailyNotificationToUsers); export default automationRoutes; diff --git a/backend/src/routes/email.routes.ts b/backend/src/routes/email.routes.ts index ea994be..cb45b5e 100644 --- a/backend/src/routes/email.routes.ts +++ b/backend/src/routes/email.routes.ts @@ -4,7 +4,7 @@ import { checkRole } from '../middlewares/user.middleware'; const emailRouter = express.Router(); -emailRouter.post('/admin/sendemail', checkRole("Admin", []), emailController.handleSendEmail); -emailRouter.post('/admin/previewemail', checkRole("Admin", []), emailController.handlePreviewEmail); +emailRouter.post('/admin/sendemail', checkRole('Admin', []), emailController.handleSendEmail); +emailRouter.post('/admin/previewemail', checkRole('Admin', []), emailController.handlePreviewEmail); export default emailRouter; diff --git a/backend/src/services/email.service.ts b/backend/src/services/email.service.ts index 53f9abc..a345b5b 100644 --- a/backend/src/services/email.service.ts +++ b/backend/src/services/email.service.ts @@ -1,21 +1,36 @@ import nodemailer from 'nodemailer'; -import type { EmailOptions, TemplateData } from "../../types/email"; -import { templateRenderers } from "../email/email.registry"; -import { compileTemplate } from "../email/email.renderer"; +import type { EmailOptions, TemplateData } from '../../types/email'; +import { templateRenderers } from '../email/email.registry'; +import { compileTemplate } from '../email/email.renderer'; +import * as user_service from '../services/user.service'; import { email_from, email_host, email_password, email_user } from '../utils/secret'; -export const generateEmailHtml = ( - templateName: string, - data: TemplateData -) => { +export const getRecipients = async ( + recipientsGroups: string[] | undefined, + sendTo: string[] | undefined, +): Promise => { + if (recipientsGroups?.length) { + const groupsEmails = await Promise.all( + recipientsGroups.map(async (recipientGroup) => { + const users = await user_service.getUsersbyPermission(recipientGroup); + return users.map((user) => user.email); + }), + ); + + const flatEmails = groupsEmails.flat(); + + return [...new Set(flatEmails)]; + } + + return [...new Set(sendTo || [])]; +}; + +export const generateEmailHtml = (templateName: string, data: TemplateData) => { const renderer = templateRenderers[templateName]; if (!renderer) return null; - return compileTemplate( - renderer.buildData(data), - renderer.fileName - ); + return compileTemplate(renderer.buildData(data), renderer.fileName); }; export const sendEmail = async (options: EmailOptions): Promise => { @@ -45,7 +60,7 @@ export const sendEmail = async (options: EmailOptions): Promise => { await transporter.sendMail(mailOptions); } catch (error) { - console.log(error) - throw new Error('Erreur lors de l\'envoi de l\'email:'); + console.log(error); + throw new Error("Erreur lors de l'envoi de l'email:"); } }; diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..b7db128 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 120, + "tabWidth": 4, + "arrowParens": "always", + "endOfLine": "lf", + "bracketSameLine": true +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b167825..64e433a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,118 +1,328 @@ -import React from 'react'; -import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; +import React from "react"; +import { Route, BrowserRouter as Router, Routes } from "react-router-dom"; -import AdminRoute from './components/utils/adminroute'; -import PrivateRoute from './components/utils/privateroute'; -import ProtectedRoute from './components/utils/protectedroute'; +import AdminRoute from "./components/utils/adminroute"; +import PrivateRoute from "./components/utils/privateroute"; +import ProtectedRoute from "./components/utils/protectedroute"; import { - AdminPageBus, - AdminPageChall, - AdminPageEmail, - AdminPageEvents, - AdminPageExport, - AdminPageFaction, - AdminPageGames, - AdminPageNews, - AdminPagePerm, - AdminPageRole, - AdminPageShotgun, - AdminPageTeam, - AdminPageTent, - AdminPageUser -} from './pages/admin'; -import LoginPage from './pages/auth'; -import ChallPage from './pages/challenge'; -import DiscordPage from './pages/discord'; -import FoodPage from './pages/food'; -import GamesPage from './pages/games'; -import HomePage from './pages/home'; -import LegalsPage from './pages/legals'; -import NewsPage from './pages/news'; -import NotFoundPage from './pages/notFound'; -import ParrainagePage from './pages/parrainage'; -import { AvailablePermanencesPage, MyPermanencesPage, RespoCallPage } from './pages/perm'; -import PlanningsPage from './pages/plannings'; -import PrivacyPage from './pages/privacy'; -import ProfilPage from './pages/profil'; -import RegisterPage from './pages/register'; -import ResetPasswordPage from './pages/resetPassword'; -import Roadbook from './pages/roadbook'; -import SdiPage from './pages/sdi'; -import ShotgunPage from './pages/shotgun'; -import WeiPage from './pages/wei'; + AdminPageBus, + AdminPageChall, + AdminPageEmail, + AdminPageEvents, + AdminPageExport, + AdminPageFaction, + AdminPageGames, + AdminPageNews, + AdminPagePerm, + AdminPageRole, + AdminPageShotgun, + AdminPageTeam, + AdminPageTent, + AdminPageUser, +} from "./pages/admin"; +import LoginPage from "./pages/auth"; +import ChallPage from "./pages/challenge"; +import DiscordPage from "./pages/discord"; +import FoodPage from "./pages/food"; +import GamesPage from "./pages/games"; +import HomePage from "./pages/home"; +import LegalsPage from "./pages/legals"; +import NewsPage from "./pages/news"; +import NotFoundPage from "./pages/notFound"; +import ParrainagePage from "./pages/parrainage"; +import { + AvailablePermanencesPage, + MyPermanencesPage, + RespoCallPage, +} from "./pages/perm"; +import PlanningsPage from "./pages/plannings"; +import PrivacyPage from "./pages/privacy"; +import ProfilPage from "./pages/profil"; +import RegisterPage from "./pages/register"; +import ResetPasswordPage from "./pages/resetPassword"; +import Roadbook from "./pages/roadbook"; +import SdiPage from "./pages/sdi"; +import ShotgunPage from "./pages/shotgun"; +import WeiPage from "./pages/wei"; const App: React.FC = () => { - const VITE_ANALYTICS_WEBSITE_ID = import.meta.env.VITE_ANALYTICS_WEBSITE_ID; + const VITE_ANALYTICS_WEBSITE_ID = import.meta.env.VITE_ANALYTICS_WEBSITE_ID; - React.useEffect(() => { - const script = document.createElement('script'); - script.src = "https://analytics.uttnetgroup.fr/script.js"; - script.defer = true; - if (VITE_ANALYTICS_WEBSITE_ID) { - script.setAttribute('data-website-id', VITE_ANALYTICS_WEBSITE_ID); - } - document.body.appendChild(script); - return () => { - document.body.removeChild(script); - }; - }, [VITE_ANALYTICS_WEBSITE_ID]); + React.useEffect(() => { + const script = document.createElement("script"); + script.src = "https://analytics.uttnetgroup.fr/script.js"; + script.defer = true; + if (VITE_ANALYTICS_WEBSITE_ID) { + script.setAttribute("data-website-id", VITE_ANALYTICS_WEBSITE_ID); + } + document.body.appendChild(script); + return () => { + document.body.removeChild(script); + }; + }, [VITE_ANALYTICS_WEBSITE_ID]); - return ( - - - {/* Public */} - } /> - } /> - } /> - } /> - } /> - } /> + return ( + + + {/* Public */} + } /> + } /> + } /> + } /> + } /> + } /> - {/* Utilisateurs connectés */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + {/* Utilisateurs connectés */} + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> - {/* Étudiant et Admin */} - } /> - } /> - } /> - } /> - } /> + {/* Étudiant et Admin */} + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> - {/* ResposCE et Admin */} - } /> - } /> - } /> - } /> + {/* ResposCE et Admin */} + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> - {/* ResposCE et Admin */} - } /> + {/* ResposCE et Admin */} + + + + } + /> - {/* Arbitre et Admin*/} - } /> - {/* Admin uniquement */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + {/* Arbitre et Admin*/} + + + + } + /> + {/* Admin uniquement */} + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> - {/* Fallback */} - } /> - - - ); + {/* Fallback */} + } /> + + + ); }; export default App; diff --git a/frontend/src/components/Admin/adminEmail.tsx b/frontend/src/components/Admin/adminEmail.tsx index 8737caa..f719afb 100644 --- a/frontend/src/components/Admin/adminEmail.tsx +++ b/frontend/src/components/Admin/adminEmail.tsx @@ -6,7 +6,9 @@ import { type User } from '../../interfaces/user.interface'; import { emailPreview, sendEmail } from '../../services/requests/email.service'; import { getUsers } from '../../services/requests/user.service'; import { Button } from '../ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"; +import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; +import { HorizontalMultipleSelect } from '../ui/horizontalMultipleSelect'; +import { HorizontalSingleSelect } from '../ui/horizontalSingleSelect'; import { Input } from '../ui/input'; type SelectOption = { @@ -16,44 +18,50 @@ type SelectOption = { export const AdminEmail = () => { const [subject, setSubject] = useState(''); - const [templateName, setTemplateName] = useState(''); + const [templateName, setTemplateName] = useState('custom'); const [format] = useState<'html' | 'txt'>('html'); - const [isCustom, setIsCustom] = useState(false); const [customTitle, setCustomTitle] = useState(''); const [customContent, setCustomContent] = useState(''); - const [permission, setPermission] = useState(null); + const [recipientsGroups, setRecipientsGroups] = useState([]); const [sendTo, setSendTo] = useState([]); const [preview, setPreview] = useState(''); const [users, setUsers] = useState([]); - const permissionOptions = [ - { value: 'Nouveau', label: 'Nouveau' }, - { value: 'RespoCE', label: 'RespoCE' }, - { value: 'Admin', label: 'Admin' }, - { value: 'Student', label: 'Student' }, + const recipientsOptions = [ + { name: 'Nouveau', value: 'Nouveau' }, + { name: 'CE', value: 'Student' }, + { name: 'RespoCE', value: 'RespoCE' }, + { name: 'Admin', value: 'Admin' }, ]; const templateOptions = [ - { value: 'templateWelcome', label: 'Template Welcome' }, - { value: 'templateNotebook', label: 'Template Cahier de Vacances' }, + { name: 'Personnalisé', value: 'custom' }, + { name: 'Welcome', value: 'templateWelcome' }, + { name: 'Cahier de Vacances', value: 'templateNotebook' }, ]; useEffect(() => { fetchData(); }, []); + useEffect(() => { + setPreview(''); + }, [templateName, customTitle, customContent]); + + const isCustom = () => templateName === 'custom'; + const fetchData = async () => { try { const usersRes = await getUsers(); setUsers(usersRes); } catch (err) { - console.error("Erreur lors du chargement des données", err); + console.error('Erreur lors du chargement des données', err); } }; const handlePreview = async () => { try { - if (isCustom) { + if (isCustom()) { const html = await emailPreview({ templateName: 'custom', title: customTitle || subject, @@ -70,19 +78,17 @@ export const AdminEmail = () => { }; const handleSend = async () => { - // On mappe toujours pour avoir un tableau de string - const emails = sendTo.map((u) => u.value); const payload = { subject, - templateName: isCustom ? 'custom' : templateName, + templateName: isCustom() ? 'custom' : templateName, format, - permission, - sendTo: permission ? null : emails, - title: isCustom ? customTitle || subject : undefined, - content: isCustom ? customContent : undefined, - html: isCustom ? customContent : undefined, + recipientsGroups, + sendTo: recipientsGroups.length ? null : emails, + title: isCustom() ? customTitle || subject : undefined, + content: isCustom() ? customContent : undefined, + html: isCustom() ? customContent : undefined, }; try { @@ -92,18 +98,27 @@ export const AdminEmail = () => { title: 'Email envoyé', text: res.message, }); - } catch(error) { + } catch (error) { Swal.fire({ - title: "Erreur ❌", - text: error?.response?.data?.message || "Une erreur est survenue.", - icon: "error", + title: 'Erreur ❌', + text: error?.response?.data?.message || 'Une erreur est survenue.', + icon: 'error', }); } }; const confirmSend = async () => { + if (!subject) { + Swal.fire({ + title: 'Objet vide', + text: "Impossible d'envoyer un email sans objet.", + icon: 'error', + }); + return; + } + const result = await Swal.fire({ - title: 'Confirmer l\'envoi', + title: "Confirmer l'envoi", text: 'Êtes-vous sûr de vouloir envoyer cet email ?', icon: 'warning', showCancelButton: true, @@ -118,69 +133,76 @@ export const AdminEmail = () => { } }; - - return ( - - 📬 Envoi d'e-mail - + 📬 Envoi d'e-mail - setSubject(e.target.value)} /> -
- { - setIsCustom(e.target.checked); - if (e.target.checked) setTemplateName(''); - }} - /> - -
- {isCustom && ( - setCustomTitle(e.target.value)} - /> - )} - {!isCustom ? ( -