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/.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/server.ts b/backend/server.ts index d691fef..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); @@ -63,6 +66,7 @@ async function startServer() { app.use('/api/tent', authenticateUser, tentRoutes); app.use('/api/bus', authenticateUser, busRoutes); app.use("/api/uploads/news", express.static(path.join(__dirname, "/uploads/news"))); + app.use("/api/uploads/notebooks", express.static(path.join(__dirname, "/uploads/notebooks"))); 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/auth.controller.ts b/backend/src/controllers/auth.controller.ts index c71871c..6e5cf8a 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) => { @@ -40,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; @@ -52,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); @@ -74,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' }); } @@ -87,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 @@ -164,18 +164,17 @@ 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 = template.compileTemplate({ resetLink: resetLink }, template.templateResetPassword); + const htmlEmail = compileTemplate({ resetLink: resetLink }, templateResetPassword); if (!htmlEmail) { - Error(res, { msg: "Nom de template invalide" }); + Error(res, { msg: 'Nom de template invalide' }); return; } // Préparer les options d'email const emailOptions: EmailOptions = { - from: "integration@utt.fr", + from: email_from, to: [user_email], cc: [], bcc: [], @@ -186,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); @@ -207,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 @@ -216,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 5b3a142..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 { sendEmail } from "../services/email.service"; -import { Error, Ok } from "../utils/responses"; -import { generateEmailHtml } from "./email.controller"; +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,23 +13,24 @@ 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 = { - from: "integration@utt.fr", + from: email_from, 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 0a90833..03aef66 100644 --- a/backend/src/controllers/email.controller.ts +++ b/backend/src/controllers/email.controller.ts @@ -1,68 +1,19 @@ import { type Request, type Response } from 'express'; -import sanitizeHtml from 'sanitize-html'; -import { sendEmail } from '../services/email.service'; +import type { EmailOptions } from '../../types/email'; +import { defaultPreviewData } from '../email/email.preview-data'; +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 * as template from '../utils/emailtemplates'; import { Error, Ok } from '../utils/responses'; - -export interface EmailOptions { - from: string; - to: string[]; - subject: string; - text?: string; - html: string; - cc: string[]; - bcc: string[]; -} - -// Fonction pour générer l'HTML à partir du template -export const generateEmailHtml = (templateName: string, data: any) => { - switch (templateName) { - case 'templateNotebook': - return template.compileTemplate({ notebook: data.notebook }, template.templateNotebook); - - case 'templateAttributionBus': - return template.compileTemplate({ bus: data.bus, time: data.time }, template.templateAttributionBus); - - case 'templateWelcome': - return template.compileTemplate({ token: data.token }, template.templateWelcome); - - case 'templateNotifyNews': - return template.compileTemplate( - { title: data.title, description: data.description }, - template.templateNotifyNews - ); - - case 'templateNotifyTentConfirmation': - return template.compileTemplate( - { user1: data.user1, user2: data.user2, confirmed: data.confirmed }, - template.templateNotifyTentConfirmation - ); - - default: - return null; - } -}; - -// 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 || []; - } -}; +import { email_from, service_url } from '../utils/secret'; +import { getLatestUploadedDocument } from '../utils/uploadDocuments'; export const handleSendEmail = async (req: Request, res: Response) => { - const { subject, templateName, permission, sendTo, html } = 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é.' }); @@ -72,30 +23,50 @@ export const handleSendEmail = async (req: Request, res: Response) => { for (const recp of recipients) { let htmlEmail = ''; - if (templateName !== 'custom') { - - if (templateName === "templateWelcome") { - const user = await user_service.getUserByEmail(recp); - const token = await registration_service.getRegistrationByUserId(user.id); - if (!token) continue; - // Générer le contenu HTML du mail - htmlEmail = generateEmailHtml(templateName, { token: token }); - + if (templateName === 'custom') { + htmlEmail = generateEmailHtml('custom', { + title: title || subject, + content: content || html || '', + }); + } else if (templateName === 'templateWelcome') { + const user = await user_service.getUserByEmail(recp); + if (!user) { + continue; } - if (templateName === "templateNotebook") { - htmlEmail = generateEmailHtml(templateName, { notebook: 'https://drive.google.com/file/d/1Tl8UeILFlAdj9IC2vy3gYXdXCOzD4ugX/view?usp=sharing' }); + + const registrationToken = await registration_service.getRegistrationByUserId(user.id); + if (!registrationToken) { + continue; } - if (templateName === "templateAttributionBus") { - htmlEmail = generateEmailHtml(templateName, { bus: 'bus', time: '09h00' }); + + 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).', + }); } + + htmlEmail = generateEmailHtml(templateName, { + notebook_fr: `${service_url}api/uploads/notebooks/fr.pdf`, + notebook_en: `${service_url}api/uploads/notebooks/en.pdf`, + }); + } else { + htmlEmail = generateEmailHtml(templateName, defaultPreviewData[templateName] || {}); } - else { - htmlEmail = sanitizeHtml(html || ''); + if (!htmlEmail) { + Error(res, { msg: 'Template HTML introuvable ou invalide.' }); + return; } const emailOptions: EmailOptions = { - from: "integration@utt.fr", + from: email_from, to: [recp], cc: [], bcc: [], @@ -107,25 +78,29 @@ export const handleSendEmail = async (req: Request, res: Response) => { await sendEmail(emailOptions); } - Ok(res, { msg: 'Email envoyé avec succès !' }); 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; } }; export const handlePreviewEmail = async (req: Request, res: Response) => { - const { templateName } = req.body; + const { templateName, title, content, ...rest } = req.body.payload ?? req.body; try { // Générer le contenu HTML pour l'aperçu - const htmlEmail = generateEmailHtml(templateName, {}); + const htmlEmail = generateEmailHtml(templateName, { + ...(defaultPreviewData[templateName] || {}), + ...rest, + title, + content, + }); 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 0b97ac2..8e24cd7 100644 --- a/backend/src/controllers/news.controller.ts +++ b/backend/src/controllers/news.controller.ts @@ -1,11 +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 * 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 * as template from "../utils/emailtemplates"; -import { Error, Ok } from "../utils/responses"; +import { Error, Ok } from '../utils/responses'; +import { email_from } from '../utils/secret'; const toStoredUploadPath = (imageUrl: string) => { if (!imageUrl) { @@ -18,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 { @@ -26,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; } @@ -43,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) => { @@ -62,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" }); @@ -86,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' }); } }; @@ -96,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' }); } }; @@ -108,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' }); } }; @@ -120,19 +120,24 @@ export const publishNews = async (req: Request, res: Response) => { const news = await news_service.getNewsById(Number(id)); if (sendEmail) { - // Génération du mail HTML - const html = template.compileTemplate({ title: news.title }, template.templateNotifyNews); + const html = generateEmailHtml('templateNotifyNews', { title: news.title }); - const recipients = news.target === "Tous" - ? (await user_service.getUsersAdmin()).map(u => u.email) - : (await user_service.getUsersbyPermission(news.target)).map(u => u.email); + if (!html) { + Error(res, { msg: 'Template de notification introuvable.' }); + return; + } + + 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 = { - from: "integration@utt.fr", + from: email_from, to: [], subject: `[INTEGRATION UTT] Nouvelle actu : ${news.title}`, html: html, @@ -143,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)); @@ -160,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" }); @@ -171,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); } @@ -206,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 6f89390..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,6 +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.' }); + 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/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 5dd067b..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 { 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 { 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, @@ -86,12 +86,10 @@ 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 !" - : "⛺ 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/database/initdb/initUser.ts b/backend/src/database/initdb/initUser.ts index 1ea653c..48f4fb8 100644 --- a/backend/src/database/initdb/initUser.ts +++ b/backend/src/database/initdb/initUser.ts @@ -1,12 +1,12 @@ import { userSchema } from "../../schemas/Basic/user.schema"; import { hashPassword } from "../../services/auth.service"; -import { zimbra_password } from "../../utils/secret"; +import { email_password } from "../../utils/secret"; import { db } from "../db"; // Assurez-vous que votre instance db est correcte export const initUser = async () => { const existingUser = await db.select().from(userSchema).limit(1); - const hashedPassword = await hashPassword(zimbra_password); + const hashedPassword = await hashPassword(email_password); // Si il n'y a pas de ligne existante, insérer une nouvelle ligne if (existingUser.length === 0) { diff --git a/backend/src/email/email.preview-data.ts b/backend/src/email/email.preview-data.ts new file mode 100644 index 0000000..f319dd3 --- /dev/null +++ b/backend/src/email/email.preview-data.ts @@ -0,0 +1,35 @@ +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: {}, + }, + templateMentorReminder: {}, +}; diff --git a/backend/src/email/email.registry.ts b/backend/src/email/email.registry.ts new file mode 100644 index 0000000..60247f2 --- /dev/null +++ b/backend/src/email/email.registry.ts @@ -0,0 +1,88 @@ +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'; +const templateMentorReminder = 'mentor-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; + }, + }, + templateMentorReminder: { + fileName: templateMentorReminder, + buildData: () => { + return {}; + }, + }, +}; diff --git a/backend/src/email/email.renderer.ts b/backend/src/email/email.renderer.ts new file mode 100644 index 0000000..be1eadf --- /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/email/templates/attribution-bus.html b/backend/src/email/templates/attribution-bus.html new file mode 100644 index 0000000..7d2702b --- /dev/null +++ b/backend/src/email/templates/attribution-bus.html @@ -0,0 +1,283 @@ + + + + + + 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 new file mode 100644 index 0000000..f5ef71d --- /dev/null +++ b/backend/src/email/templates/custom.html @@ -0,0 +1,88 @@ + + + + + + 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/mentor-reminder.html b/backend/src/email/templates/mentor-reminder.html new file mode 100644 index 0000000..fc9f691 --- /dev/null +++ b/backend/src/email/templates/mentor-reminder.html @@ -0,0 +1,193 @@ + + + + + + Intégration UTT + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ Logo Intégration UTT +
+ INTEGRATION UTT +
+

+ Tu veux un parrain ? +

+ +

+ Ce mail te concerne seulement si tu n'as pas encore reçu de parain attribué ! +

+ +

+ 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 le + formulaire sur la page Parrainage du + site de l'Intégration ! +

+ +

+ https://integration.utt.fr +

+ +

+ Tu as reçu un mail il y a quelques jours permettant de te connecter au site, si tu + ne le retrouves pas, contacte nous: + integration+site-support@utt.fr +

+
+

+ 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. +

+
+

+ Looking for a mentor? +

+ +

+ This email only concerns you if you have not yet been assigned a mentor! +

+ +

+ When you arrive at UTT, an older student becomes your mentor. They will be your main + point of contact to help you discover the school, student life in Troyes, and answer + any questions you may have about UTT, accommodation, courses, life in Troyes, and + much more. +

+ +

+ To help us match you with the mentor who suits you best, we invite you to fill out + the form on the Mentorship page of the + Integration website! +

+ +

+ https://integration.utt.fr +

+ +

+ You received an email a few days ago containing instructions to log in to the + website. If you cannot find it, please contact us: + integration+site-support@utt.fr +

+
+

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/notebook.html b/backend/src/email/templates/notebook.html new file mode 100644 index 0000000..8130e66 --- /dev/null +++ b/backend/src/email/templates/notebook.html @@ -0,0 +1,164 @@ + + + + + + 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 new file mode 100644 index 0000000..8c50ea9 --- /dev/null +++ b/backend/src/email/templates/notify-news.html @@ -0,0 +1,158 @@ + + + + + + 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}}

+

+ 👉 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 new file mode 100644 index 0000000..4fa9491 --- /dev/null +++ b/backend/src/email/templates/notify-permanence-reminder.html @@ -0,0 +1,150 @@ + + + + + + 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/email/templates/notify-tent-confirmation.html b/backend/src/email/templates/notify-tent-confirmation.html new file mode 100644 index 0000000..d9c05ef --- /dev/null +++ b/backend/src/email/templates/notify-tent-confirmation.html @@ -0,0 +1,174 @@ + + + + + + Intégration UTT + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ Logo Intégration UTT +
+ INTEGRATION 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égration dans l'onglet + WEI. +

+

+ 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. +

+
+

⛺ Tent Update

+ +

+ The tent shared by {{user1}} and {{user2}} has + been + + {{#if confirmed}}approved{{else}}rejected{{/if}} . +

+ +

+ 👉 You can check the status of your tent on the Integration website under the + WEI tab. +

+ +

+ Access the website +

+
+

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/reset-password.html b/backend/src/email/templates/reset-password.html new file mode 100644 index 0000000..35b6437 --- /dev/null +++ b/backend/src/email/templates/reset-password.html @@ -0,0 +1,163 @@ + + + + + + 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 new file mode 100644 index 0000000..5de2b50 --- /dev/null +++ b/backend/src/email/templates/welcome.html @@ -0,0 +1,235 @@ + + + + + + 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 +

+ +

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 le + formulaire sur la page Parrainage du + site de l'Intégration ! +

+
+

+ 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 +

+ +

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 match you with someone who is the best fit, we invite you to fill out the form on + the Parrainage page of the + Integration website ! +

+
+

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 new file mode 100644 index 0000000..61c1398 --- /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..606e75d --- /dev/null +++ b/backend/src/middlewares/automation.middleware.ts @@ -0,0 +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'; + +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/middlewares/multer.middleware.ts b/backend/src/middlewares/multer.middleware.ts index 648a871..6b787df 100644 --- a/backend/src/middlewares/multer.middleware.ts +++ b/backend/src/middlewares/multer.middleware.ts @@ -16,6 +16,10 @@ const acceptedMIMETypesByItem: Record> = { menu: [MIMEType.PDF], }, news: {}, + notebooks: { + fr: [MIMEType.PDF], + en: [MIMEType.PDF], + }, plannings: { tc: [MIMEType.PDF], bachelor_ia: [MIMEType.PDF], diff --git a/backend/src/routes/automation.routes.ts b/backend/src/routes/automation.routes.ts new file mode 100644 index 0000000..214f4a4 --- /dev/null +++ b/backend/src/routes/automation.routes.ts @@ -0,0 +1,10 @@ +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/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 fbe972f..a345b5b 100644 --- a/backend/src/services/email.service.ts +++ b/backend/src/services/email.service.ts @@ -1,25 +1,47 @@ import nodemailer from 'nodemailer'; -import { zimbra_host, zimbra_password, zimbra_user } from '../utils/secret'; - -interface EmailOptions { - from: string; - to: string[]; - subject: string; - text?: string; - html?: string; - cc?: string[]; - bcc?: string[]; -} +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 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); +}; export const sendEmail = async (options: EmailOptions): Promise => { try { const transporter = nodemailer.createTransport({ - host: zimbra_host, + host: email_host, port: 587, secure: false, auth: { - user: zimbra_user, - pass: zimbra_password, + user: email_user, + pass: email_password, }, tls: { rejectUnauthorized: false, @@ -27,7 +49,7 @@ export const sendEmail = async (options: EmailOptions): Promise => { }); const mailOptions = { - from: options.from, + from: options.from || email_from, to: options.to ? options.to.join(', ') : '', subject: options.subject, text: options.text, @@ -38,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/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 309068e..0000000 --- a/backend/src/utils/emailtemplates.ts +++ /dev/null @@ -1,364 +0,0 @@ -import Handlebars from 'handlebars'; - -// Template pour l'e-mail de réinitialisation de mot de passe -export const templateResetPassword = ` - - - - - - Réinitialisation de mot de passe - - -
- Integration UTT Logo -

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.

-

Merci,

-

L'équipe intégration UTT

-
-

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

-
-
- - -`; - -export const templateNotebook = ` - - - - Integration UTT - - -
- - -
- Integration UTT Logo -

INTEGRATION UTT

-
- - -
-

Salut à toi !!!!

- -

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 31 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.

- - Inscris toi ! - -

Pense aussi à rejoindre notre Discord, c'est uniquement par ce biais que tu pourras contacter tes chefs d'équipe et en savoir plus sur l'intégration !

- - Rejoindre Discord - -

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

- -

À très vite !

- -

Toute l'équipe de l'intégration

-
- - -
-

Rejoins nous sur les réseaux !

- -
-
- - -`; - -export const templateAttributionBus = ` - - - - - - Intégration UTT - - - - - - -
- - - - - - - - - - -
- Logo Comic -
- INTEGRATION UTT -
-

Salut !

-

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 ;)

-
-
- - -`; - -export const templateWelcome = ` - - - - - - Intégration UTT - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Logo Comic -
- 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 1er Septembre 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 :

-

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

Pense aussi à lier ton compte discord via la rubrique "Mon Compte" pour échanger avec les membres de ton équipe et avec les autres arrivants. -

-

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

-

Pense à nous rejoindre sur les réseaux sociaux !

-

- - Facebook - - - Instagram - - - Discord - -

-
- Logo Comic -
- INTEGRATION UTT -
-

Hello there, newcomer!

-

Congratulations on your admission to UTT! We are the integration team - volunteer students who are carefully preparing your arrival to make it truly unforgettable.

-

A bunch of amazing events, all based on voluntary participation, await you starting on Monday, September 1st, whether you're arriving in your 1st year, 3rd year, Master's or Bachelor's program.

-

Everything is set up for you to have fun and meet the people who will make your time at UTT unforgettable. But first things first - it's time to get ready.

-

Please make sure to complete the following tasks before you arrive:

-

To access the integration website, you just need to change your password by clicking the following link:

- -

Change your password

-

Warning: this link is valid only once!

-

Once that's done, you'll be able to log into your account and find all the information about the integration week events here:

https://integration.utt.fr

Also, don't forget to link your Discord account via the "My Account" section so you can connect with your team and the other newcomers. -

-

When you arrive at UTT, an older student will become your mentor ("parrain" or "marraine"). They will be your main contact to help you discover the school and student life in Troyes, and to answer any questions you may have about UTT, housing, classes, life in Troyes, etc.

-

To match you with someone who fits you best, we invite you to fill out this questionnaire

-

Don't forget to follow us on social media!

-

- - Facebook - - - Instagram - - - Discord - -

-
-
- - -`; - -export const templateNotifyNews = ` -
-
- - -
- 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 -

-
-
-`; - -export const templateNotifyTentConfirmation = ` -
-
- - -
- 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}}dévalidée{{/if}} - . -

- -

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

- - -

- Accéder au site -

-
-
-`; - -// Fonction pour compiler le template -export const compileTemplate = (data: any, templateName: string) => { - const compiledTemplate = Handlebars.compile(templateName); - return compiledTemplate(data); -}; diff --git a/backend/src/utils/secret.ts b/backend/src/utils/secret.ts index 924cabd..f7c51fc 100644 --- a/backend/src/utils/secret.ts +++ b/backend/src/utils/secret.ts @@ -22,10 +22,12 @@ export const api_utt_password = process.env.API_UTT_PASSWORD || "default"; export const api_utt_auth_url = process.env.API_UTT_AUTH_URL || "default"; export const api_utt_admis_url = process.env.API_UTT_ADMIS_URL || "default"; export const api_utt_admis_url_ismajor = process.env.API_UTT_ADMIS_URL_ISMAJOR || "default"; -export const zimbra_host = process.env.ZIMBRA_HOST || "default"; -export const zimbra_user = process.env.ZIMBRA_USER || "default"; -export const zimbra_password = process.env.ZIMBRA_PASSWORD || "default"; +export const email_host = process.env.EMAIL_HOST || "default"; +export const email_user = process.env.EMAIL_USER || "default"; +export const email_password = process.env.EMAIL_PASSWORD || "default"; +export const email_from = process.env.EMAIL_FROM || "default"; 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; +} 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..bf8f1ec 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -18,7 +18,7 @@ import { AdminPageShotgun, AdminPageTeam, AdminPageTent, - AdminPageUser + AdminPageUser, } from './pages/admin'; import LoginPage from './pages/auth'; import ChallPage from './pages/challenge'; @@ -46,7 +46,7 @@ const App: React.FC = () => { React.useEffect(() => { const script = document.createElement('script'); - script.src = "https://analytics.uttnetgroup.fr/script.js"; + 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); @@ -69,44 +69,247 @@ const App: React.FC = () => { } /> {/* Utilisateurs connectés */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> {/* Étudiant et Admin */} - } /> - } /> - } /> - } /> - } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> {/* ResposCE et Admin */} - } /> - } /> - } /> - } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> {/* ResposCE et Admin */} - } /> + + + + } + /> {/* Arbitre et Admin*/} - } /> + + + + } + /> {/* Admin uniquement */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> {/* Fallback */} } /> diff --git a/frontend/src/components/Admin/adminEmail.tsx b/frontend/src/components/Admin/adminEmail.tsx index 3cbd539..ef3682b 100644 --- a/frontend/src/components/Admin/adminEmail.tsx +++ b/frontend/src/components/Admin/adminEmail.tsx @@ -6,51 +6,71 @@ 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 = { + value: string; + label: string; +}; + 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 [sendTo, setSendTo] = useState([]); + 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' }, + { name: 'Rappel Parrainage', value: 'templateMentorReminder' }, ]; 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) { - setPreview(customContent); + if (isCustom()) { + const html = await emailPreview({ + templateName: 'custom', + title: customTitle || subject, + content: customContent, + }); + setPreview(html); } else { - const html = await emailPreview(templateName); + const html = await emailPreview({ templateName }); setPreview(html); } } catch { @@ -59,31 +79,47 @@ 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, - html: isCustom ? customContent : undefined, + recipientsGroups, + sendTo: recipientsGroups.length ? null : emails, + title: isCustom() ? customTitle || subject : undefined, + content: isCustom() ? customContent : undefined, + html: isCustom() ? customContent : undefined, }; - const res = await sendEmail(payload); - Swal.fire({ - icon: 'success', - title: 'Email envoyé', - text: res.message, - }); + try { + const res = await sendEmail(payload); + Swal.fire({ + icon: 'success', + title: 'Email envoyé', + text: res.message, + }); + } catch (error) { + Swal.fire({ + 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, @@ -98,62 +134,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 ? ( -