From 5ef271147916c297c455ff5c067d11e104a8a54d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9=20Landr=C3=A9?= Date: Wed, 6 Nov 2024 20:05:48 +0100 Subject: [PATCH 01/16] feat: new email system --- src/controllers/admin/emails/send.ts | 3 +- src/controllers/auth/askResetPassword.ts | 4 +- src/controllers/auth/register.ts | 4 +- src/controllers/auth/resendEmail.ts | 4 +- .../stripe/paymentCanceledWebhook.ts | 4 +- .../stripe/paymentSucceededWebhook.ts | 4 +- src/operations/user.ts | 2 +- src/scripts/mails/discord.ts | 64 ------ src/scripts/mails/index.ts | 115 ---------- src/scripts/mails/minor.ts | 46 ---- src/scripts/mails/notpaid.ts | 64 ------ src/scripts/mails/notpaidssbu.ts | 72 ------ src/scripts/mails/tickets.ts | 61 ------ src/scripts/mails/unlocked.ts | 46 ---- src/services/email/components.ts | 13 +- src/services/email/generalMails.ts | 28 +++ src/services/email/index.ts | 131 ++++++++--- src/services/email/serializer.ts | 207 ------------------ src/services/email/targets.ts | 166 ++++++++++++++ .../email/templates/accountValidation.ts | 58 +++++ src/services/email/templates/index.ts | 24 ++ src/services/email/templates/joinDiscord.ts | 28 +++ .../email/templates/lastYearAnnounce.ts | 56 +++++ src/services/email/templates/minor.ts | 28 +++ src/services/email/templates/notPaid.ts | 28 +++ src/services/email/templates/notPaidSsbu.ts | 29 +++ .../email/templates/orderConfirmation.ts | 95 ++++++++ src/services/email/templates/passwordReset.ts | 30 +++ src/services/email/templates/tickets.ts | 67 ++++++ src/services/email/{types.d.ts => types.ts} | 53 +++-- tests/auth/resendEmail.test.ts | 2 +- tests/services/email.test.ts | 12 +- 32 files changed, 785 insertions(+), 763 deletions(-) delete mode 100644 src/scripts/mails/discord.ts delete mode 100644 src/scripts/mails/index.ts delete mode 100644 src/scripts/mails/minor.ts delete mode 100644 src/scripts/mails/notpaid.ts delete mode 100644 src/scripts/mails/notpaidssbu.ts delete mode 100644 src/scripts/mails/tickets.ts delete mode 100644 src/scripts/mails/unlocked.ts create mode 100644 src/services/email/generalMails.ts delete mode 100644 src/services/email/serializer.ts create mode 100644 src/services/email/targets.ts create mode 100644 src/services/email/templates/accountValidation.ts create mode 100644 src/services/email/templates/index.ts create mode 100644 src/services/email/templates/joinDiscord.ts create mode 100644 src/services/email/templates/lastYearAnnounce.ts create mode 100644 src/services/email/templates/minor.ts create mode 100644 src/services/email/templates/notPaid.ts create mode 100644 src/services/email/templates/notPaidSsbu.ts create mode 100644 src/services/email/templates/orderConfirmation.ts create mode 100644 src/services/email/templates/passwordReset.ts create mode 100644 src/services/email/templates/tickets.ts rename src/services/email/{types.d.ts => types.ts} (52%) diff --git a/src/controllers/admin/emails/send.ts b/src/controllers/admin/emails/send.ts index efce7a45..5ac48143 100644 --- a/src/controllers/admin/emails/send.ts +++ b/src/controllers/admin/emails/send.ts @@ -5,8 +5,7 @@ import { badRequest, created } from '../../../utils/responses'; import { hasPermission } from '../../../middlewares/authentication'; import { Error as ApiError, MailQuery } from '../../../types'; import { validateBody } from '../../../middlewares/validation'; -import { sendEmail, SerializedMail } from '../../../services/email'; -import { serialize } from '../../../services/email/serializer'; +import { sendEmail, SerializedMail, serialize } from '../../../services/email'; import database from '../../../services/database'; import { getRequestInfo } from '../../../utils/users'; diff --git a/src/controllers/auth/askResetPassword.ts b/src/controllers/auth/askResetPassword.ts index db22fe9f..d1d897ca 100644 --- a/src/controllers/auth/askResetPassword.ts +++ b/src/controllers/auth/askResetPassword.ts @@ -4,7 +4,7 @@ import * as Sentry from '@sentry/node'; import { isNotAuthenticated } from '../../middlewares/authentication'; import { validateBody } from '../../middlewares/validation'; import { fetchUser, generateResetToken } from '../../operations/user'; -import { sendPasswordReset } from '../../services/email'; +import { sendMailsFromTemplate } from '../../services/email'; import { noContent } from '../../utils/responses'; import * as validators from '../../utils/validators'; import logger from '../../utils/logger'; @@ -37,7 +37,7 @@ export default [ // Don't wait for mail to be sent as it could take time // We suppose here that is will pass. If it is not the case, error is // reported through Sentry and staff may resend the email manually - sendPasswordReset(userWithToken).catch((error) => { + sendMailsFromTemplate('passwordreset', [userWithToken]).catch((error) => { logger.error(error); Sentry.captureException(error, { user: { diff --git a/src/controllers/auth/register.ts b/src/controllers/auth/register.ts index ff5973d2..0c9a9e8b 100644 --- a/src/controllers/auth/register.ts +++ b/src/controllers/auth/register.ts @@ -4,7 +4,7 @@ import * as Sentry from '@sentry/node'; import { isNotAuthenticated } from '../../middlewares/authentication'; import { validateBody } from '../../middlewares/validation'; import { createUser } from '../../operations/user'; -import { sendValidationCode } from '../../services/email'; +import { sendMailsFromTemplate } from '../../services/email'; import { Error } from '../../types'; import { conflict, created } from '../../utils/responses'; import * as validators from '../../utils/validators'; @@ -42,7 +42,7 @@ export default [ // Don't send sync when it is not needed // If the mail is not sent, the error will be reported through Sentry // and staff may resend it manually - sendValidationCode(registeredUser).catch((error) => { + sendMailsFromTemplate('accountvalidation', [registeredUser]).catch((error) => { Sentry.captureException(error, { user: { id: registeredUser.id, diff --git a/src/controllers/auth/resendEmail.ts b/src/controllers/auth/resendEmail.ts index a43bcd87..81345463 100644 --- a/src/controllers/auth/resendEmail.ts +++ b/src/controllers/auth/resendEmail.ts @@ -8,7 +8,7 @@ import * as validators from '../../utils/validators'; import { forbidden, success, unauthenticated } from '../../utils/responses'; import { Error as ResponseError } from '../../types'; import { fetchUser } from '../../operations/user'; -import { sendValidationCode } from '../../services/email'; +import { sendMailsFromTemplate } from '../../services/email'; import logger from '../../utils/logger'; export default [ @@ -53,7 +53,7 @@ export default [ // Don't send sync when it is not needed // If the mail is not sent, the error will be reported through Sentry // and staff may resend it manually - sendValidationCode(user).catch((error) => { + sendMailsFromTemplate('accountvalidation', [user]).catch((error) => { Sentry.captureException(error, { user: { id: user.id, diff --git a/src/controllers/stripe/paymentCanceledWebhook.ts b/src/controllers/stripe/paymentCanceledWebhook.ts index 51ed749f..706fa8ca 100644 --- a/src/controllers/stripe/paymentCanceledWebhook.ts +++ b/src/controllers/stripe/paymentCanceledWebhook.ts @@ -1,6 +1,6 @@ import { NextFunction, Request, Response } from 'express'; import { updateCart } from '../../operations/carts'; -import { sendPaymentConfirmation } from '../../services/email'; +import { sendMailsFromTemplate } from '../../services/email'; import { TransactionState } from '../../types'; import { success } from '../../utils/responses'; import { paymentIntentWebhookMiddleware } from '../../utils/stripe'; @@ -24,7 +24,7 @@ export default [ // Update the cart with the callback data const updatedCart = await updateCart(cart.id, { transactionState: TransactionState.canceled }); - await sendPaymentConfirmation(updatedCart); + await sendMailsFromTemplate('orderconfirmation', [updatedCart]); return success(response, { api: 'ok' }); } catch (error) { diff --git a/src/controllers/stripe/paymentSucceededWebhook.ts b/src/controllers/stripe/paymentSucceededWebhook.ts index 0547ac00..a2953b37 100644 --- a/src/controllers/stripe/paymentSucceededWebhook.ts +++ b/src/controllers/stripe/paymentSucceededWebhook.ts @@ -1,6 +1,6 @@ import { NextFunction, Request, Response } from 'express'; import { updateCart } from '../../operations/carts'; -import { sendPaymentConfirmation } from '../../services/email'; +import { sendMailsFromTemplate } from '../../services/email'; import { TransactionState } from '../../types'; import { success } from '../../utils/responses'; import { paymentIntentWebhookMiddleware } from '../../utils/stripe'; @@ -26,7 +26,7 @@ export default [ transactionState: TransactionState.paid, succeededAt: new Date(Date.now()), }); - await sendPaymentConfirmation(updatedCart); + await sendMailsFromTemplate('orderconfirmation', [updatedCart]); return success(response, { api: 'ok' }); } catch (error) { diff --git a/src/operations/user.ts b/src/operations/user.ts index 6b1ca6bf..677dac84 100644 --- a/src/operations/user.ts +++ b/src/operations/user.ts @@ -541,7 +541,7 @@ export const getPaidAndValidatedUsers = () => }, }); -export const getNextPaidAndValidatedUserBatch = async (batchMaxSize: number) => { +export const getNextPaidAndValidatedUserBatch = async (batchMaxSize: number = env.email.maxMailsPerBatch) => { const users = await database.user.findMany({ where: { discordId: { diff --git a/src/scripts/mails/discord.ts b/src/scripts/mails/discord.ts deleted file mode 100644 index 6facb76f..00000000 --- a/src/scripts/mails/discord.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { UserType, TransactionState } from '@prisma/client'; -import { MailGoal } from '.'; -import database from '../../services/database'; -import { fetchGuildMembers } from '../../services/discord'; -import { EmailAttachement } from '../../types'; - -export const discordGoal: MailGoal = { - collector: async () => { - const [members, users] = await Promise.all([ - fetchGuildMembers().then((list) => list.map((member) => member.user.id)), - database.user.findMany({ - where: { - discordId: { - not: null, - }, - email: { - not: null, - }, - OR: [ - { - team: { - lockedAt: { - not: null, - }, - }, - }, - { - type: UserType.spectator, - }, - ], - cartItems: { - some: { - itemId: { - startsWith: 'ticket-', - }, - cart: { - paidAt: { - not: null, - }, - transactionState: TransactionState.paid, - }, - }, - }, - }, - }), - ]); - return users.filter((user) => !members.includes(user.discordId)); - }, - sections: [ - { - title: "Rejoins le serveur discord de l'UTT Arena !", - components: [ - "Tu n'es pas encore sur le serveur discord Arena, nous te conseillons fortement de le rejoindre car il s'agit de notre principal outil de communication avec toi et les autres joueurs.", - 'Sur ce serveur, tu pourras également y discuter avec les autres joueurs, ou poser des questions aux organisateurs de ton tournoi.', - { - name: 'Rejoindre le serveur Discord', - location: 'https://discord.gg/WhxZwKU', - }, - ], - }, - ], - // eslint-disable-next-line require-await - attachments: async () => [] as EmailAttachement[], -}; diff --git a/src/scripts/mails/index.ts b/src/scripts/mails/index.ts deleted file mode 100644 index 0d9205b3..00000000 --- a/src/scripts/mails/index.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { User } from '@prisma/client'; -import { sendEmail } from '../../services/email'; -import { serialize } from '../../services/email/serializer'; -// eslint-disable-next-line import/no-unresolved -import { Mail } from '../../services/email/types'; -import { EmailAttachement } from '../../types'; -import logger from '../../utils/logger'; -import { ticketsGoal } from './tickets'; -import { notPaidGoal } from './notpaid'; -import { notPaidSSBUGoal } from './notpaidssbu'; -import { discordGoal } from './discord'; -import { minorGoal } from './minor'; -import { unlockedPlayersGoal } from './unlocked'; - -export type RecipientCollector = () => Promise; -export type MailGoal = { - collector: RecipientCollector; - sections: Mail['sections']; - attachments: (user: User) => Promise; -}; - -const availableGoals: { - [key: string]: MailGoal; -} = { - discord: discordGoal, - mineurs: minorGoal, - tickets: ticketsGoal, - paslock: unlockedPlayersGoal, - paspayé: notPaidGoal, - paspayéssbu: notPaidSSBUGoal, -}; - -(async () => { - const records: { [key: string]: { sections: Mail['sections']; user: User; attachments: EmailAttachement[] } } = {}; - - if (process.argv.length <= 2) { - throw new Error( - `ERREUR : Tu dois donner au moins un type de mails à envoyer parmi les suivants : ${Object.keys( - availableGoals, - ).join(' ')}`, - ); - } - // Convert goal names to - const goals = process.argv - .splice(2) - .map((name: string) => { - if (name in availableGoals) { - logger.info(`[Scheduled] ${name}`); - return availableGoals[name]; - } - logger.error(`[Skipping] ${name}: Not found`); - return null; - }) - .filter((goal) => !!goal); - - for (const { collector, sections, attachments } of goals) { - const targets = await collector(); - for (const user of targets) { - if (user.email in records) { - records[user.email].sections.push(...sections); - records[user.email].attachments.push(...(await attachments(user))); - } else { - records[user.email] = { - sections: [...sections], - user, - attachments: await attachments(user), - }; - } - } - } - - const outgoingMails = await Promise.allSettled( - Object.keys(records).map(async (recipientEmail) => { - try { - const mail = records[recipientEmail]; - const mailContent = await serialize({ - sections: mail.sections, - reason: 'Tu as reçu ce mail car tu as créé un compte sur arena.utt.fr', - title: { - banner: 'On se retrouve ce weekend !', - highlight: `Cher ${mail.user.firstname}`, - short: "L'UTT Arena arrive à grands pas 🔥", - topic: "Ton ticket pour l'UTT Arena", - }, - receiver: mail.user.email, - }); - return sendEmail(mailContent, mail.attachments); - } catch { - throw recipientEmail; - } - }), - ); - - // Counts mail statuses - const results = outgoingMails.reduce( - (result, state) => { - if (state.status === 'fulfilled') - return { - ...result, - delivered: result.delivered + 1, - }; - logger.error(`Impossible d'envoyer de mail à ${state.reason}`); - return { - ...result, - undelivered: result.undelivered + 1, - }; - }, - { delivered: 0, undelivered: 0 }, - ); - - // eslint-disable-next-line no-console - console.info(`\tMails envoyés: ${results.delivered}\n\tMails non envoyés: ${results.undelivered}`); -})().catch((error) => { - logger.error(error); -}); diff --git a/src/scripts/mails/minor.ts b/src/scripts/mails/minor.ts deleted file mode 100644 index 0275d877..00000000 --- a/src/scripts/mails/minor.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { MailGoal } from '.'; -import database from '../../services/database'; -import { EmailAttachement, TransactionState, UserAge } from '../../types'; - -export const minorGoal: MailGoal = { - collector: () => - database.user.findMany({ - where: { - discordId: { - not: null, - }, - email: { - not: null, - }, - age: UserAge.child, - cartItems: { - some: { - itemId: { - startsWith: 'ticket-', - }, - cart: { - paidAt: { - not: null, - }, - transactionState: TransactionState.paid, - }, - }, - }, - }, - }), - sections: [ - { - title: 'Autorisation parentale', - components: [ - "Tu nous as indiqué que tu seras mineur à la date de l'UTT Arena. N'oublie pas de préparer *ton autorisation parentale, et une photocopie de ta pièce d'identité, et de celle de ton responsable légal* !", - "La vérification se fera à l'entrée de l'UTT Arena, n'hésite pas à envoyer à l'avance ces documents par mail à arena@utt.fr pour simplifier la procédure à l'entrée.", - { - location: 'https://arena.utt.fr/uploads/files/Autorisation_parentale_-_UTT_Arena_2024.pdf', - name: "Télécharger l'autorisation parentale", - }, - ], - }, - ], - // eslint-disable-next-line require-await - attachments: async () => [] as EmailAttachement[], -}; diff --git a/src/scripts/mails/notpaid.ts b/src/scripts/mails/notpaid.ts deleted file mode 100644 index 43c1533d..00000000 --- a/src/scripts/mails/notpaid.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { MailGoal } from '.'; -import database from '../../services/database'; -import { EmailAttachement } from '../../types'; - -export const notPaidGoal: MailGoal = { - collector: () => - database.user.findMany({ - distinct: ['id'], - where: { - AND: [ - { - OR: [ - { - cartItems: { - some: { - AND: [ - { - itemId: { - startsWith: 'ticket-', - }, - forcePaid: false, - }, - { - cart: { - transactionState: { - not: 'paid', - }, - }, - }, - ], - }, - }, - }, - { - cartItems: { - none: {}, - }, - }, - ], - }, - { - team: { - lockedAt: null, - }, - }, - ], - }, - }), - sections: [ - { - title: "Ton inscription n'a pas été confirmée", - components: [ - "L'UTT Arena approche à grand pas, et ton inscription n'est pas encore confirmée. Pour verrouiller ta place, il ne te reste plus qu'à la payer en accédant à la boutique sur le site. \nSi le tournoi auquel tu souhaites participer est d'ores-et-déjà rempli, tu sera placé en file d'attente.", - "\n_Si le taux de remplissage d'un tournoi est trop faible d'ici à deux semaines de l'évènement, l'équipe organisatrice se réserve le droit de l'annuler._", - { - location: 'https://arena.utt.fr/dashboard/team', - name: 'Accéder à arena.utt.fr', - }, - ], - }, - ], - // eslint-disable-next-line require-await - attachments: async () => [] as EmailAttachement[], -}; diff --git a/src/scripts/mails/notpaidssbu.ts b/src/scripts/mails/notpaidssbu.ts deleted file mode 100644 index 60a569c2..00000000 --- a/src/scripts/mails/notpaidssbu.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { MailGoal } from '.'; -import database from '../../services/database'; -import { EmailAttachement } from '../../types'; - -export const notPaidSSBUGoal: MailGoal = { - collector: () => - database.user.findMany({ - distinct: ['id'], - where: { - AND: [ - { - OR: [ - { - cartItems: { - some: { - AND: [ - { - itemId: { - startsWith: 'ticket-', - }, - forcePaid: false, - }, - { - cart: { - transactionState: { - not: 'paid', - }, - }, - }, - ], - }, - }, - }, - { - cartItems: { - none: {}, - }, - }, - ], - }, - { - team: { - tournament: { - id: 'ssbu', - }, - }, - }, - { - team: { - lockedAt: null, - }, - }, - ], - }, - }), - sections: [ - { - title: "Ton inscription n'a pas été confirmée", - components: [ - "L'UTT Arena approche à grand pas, et ton inscription pour le tournoi SSBU n'est pas encore confirmée. Pour verrouiller ta place, il ne te reste plus qu'à la payer en accédant à la boutique sur le site.", - "\nN'oublie pas que tu peux décider de ramener ta propre Nintendo Switch avec SSBU (all DLCs) pour bénéficier d'une *réduction de 3€* sur ta place ! Cela permet également au tournoi de s'enchaîner de façon plus fluide.", - "\nOn se retrouve le 1, 2, 3 décembre dans l'Arène !", - { - location: 'https://arena.utt.fr/dashboard/team', - name: 'Accéder à arena.utt.fr', - }, - ], - }, - ], - // eslint-disable-next-line require-await - attachments: async () => [] as EmailAttachement[], -}; diff --git a/src/scripts/mails/tickets.ts b/src/scripts/mails/tickets.ts deleted file mode 100644 index ac6e048a..00000000 --- a/src/scripts/mails/tickets.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { TransactionState } from '@prisma/client'; -import { MailGoal } from '.'; -import database from '../../services/database'; -import { generateTicket } from '../../utils/ticket'; -import { getNextPaidAndValidatedUserBatch } from '../../operations/user'; -import env from '../../utils/env'; - -export const ticketsGoal: MailGoal = { - collector: () => getNextPaidAndValidatedUserBatch(env.email.maxMailsPerBatch), - sections: [ - { - title: "Ton ticket pour l'UTT Arena", - components: [ - "Tu es bien inscrit à l'UTT Arena ! Tu trouveras ci-joint ton billet, que tu devras présenter à l'entrée de l'UTT Arena. Tu peux aussi le retrouver sur la billetterie, dans l'onglet \"Mon compte\" de ton Dashboard.", - 'Attention, tous les tournois débutent à 10h, *il faudra donc être présent dès 9h00 pour un check-in de toutes les équipes et joueurs.*', - { - location: 'https://arena.utt.fr/dashboard/account', - name: 'Accéder à arena.utt.fr', - }, - ], - }, - { - title: 'Ce que tu dois emporter', - components: [ - "Pour rentrer à l'UTT Arena, tu auras besoin de", - [ - 'ton *billet* (que tu trouveras en pièce jointe, ou sur le site)', - "une *pièce d'identité* (type carte d'identité, titre de séjour ou permis de conduire)", - ], - "Nous te conseillons d'emporter également", - [ - 'Une gourde *vide*', - "une multiprise puisque tu n'auras *qu'une seule prise mise à ta disposition pour brancher tout ton setup*", - "un câble ethernet (d'environ 7m)", - 'ton setup', - ], - "Si tu as encore des questions, n'hésite pas à regarder notre FAQ ou à poser la question sur le serveur discord !", - { - location: 'https://arena.utt.fr/help', - name: 'Ouvrir la FAQ', - }, - ], - }, - ], - attachments: async (user) => { - const cartItem = await database.cartItem.findFirst({ - where: { - cart: { - paidAt: { - not: null, - }, - transactionState: TransactionState.paid, - }, - itemId: `ticket-${user.type}`, - forUserId: user.id, - }, - include: { item: true, forUser: true }, - }); - return [await generateTicket(cartItem)]; - }, -}; diff --git a/src/scripts/mails/unlocked.ts b/src/scripts/mails/unlocked.ts deleted file mode 100644 index d14a2520..00000000 --- a/src/scripts/mails/unlocked.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { TransactionState } from '@prisma/client'; -import { MailGoal } from '.'; -import database from '../../services/database'; -import { EmailAttachement } from '../../types'; - -export const unlockedPlayersGoal: MailGoal = { - collector: () => - database.user.findMany({ - where: { - discordId: { - not: null, - }, - email: { - not: null, - }, - team: { - lockedAt: null, - }, - cartItems: { - some: { - itemId: { - startsWith: 'ticket-', - }, - cart: { - transactionState: TransactionState.paid, - }, - }, - }, - }, - }), - sections: [ - { - title: "Ton inscription n'a pas été confirmée", - components: [ - 'L\'UTT Arena arrive, et tu n\'as plus que *jusqu\'à vendredi 16h pour confirmer ta participation*. Il ne te reste plus qu\'à cliquer sur "*Verrouiller mon équipe*" puis à te rendre sur la page "Mon compte" pour avoir accès à ton billet !', - 'Mais attention, une fois que tu seras verouillé, il sera impossible de changer de tournoi.', - { - location: 'https://arena.utt.fr/dashboard/team', - name: 'Accéder à arena.utt.fr', - }, - ], - }, - ], - // eslint-disable-next-line require-await - attachments: async () => [] as EmailAttachement[], -}; diff --git a/src/services/email/components.ts b/src/services/email/components.ts index a50d3510..5a6d8427 100644 --- a/src/services/email/components.ts +++ b/src/services/email/components.ts @@ -1,5 +1,5 @@ import { escape } from 'mustache'; -import type { Component } from './types'; +import type { Component, MailButton, MailTable } from './types'; export const style = { text: { @@ -35,7 +35,7 @@ export const escapeText = (text: string) => .replaceAll(/_([^<>_]+)_/gi, '$1') .replaceAll(/\*([^*<>]+)\*/gi, '$1'); -const inflateButton = (item: Component.Button) => +const inflateButton = (item: MailButton) => ``; -const inflateButtonWrapper = (item: Component.Button | Component.Button[]) => +const inflateButtonWrapper = (item: MailButton | MailButton[]) => `${ Array.isArray(item) ? item.map(inflateButton).join('') : inflateButton(item) }
`; -const inflateTable = (item: Component.Table) => { +const inflateTable = (item: MailTable) => { const properties = Object.keys(item.items[0] ?? {}); if (properties.length === 0 || item.items.length < 2) return ''; return `${ @@ -99,9 +99,8 @@ const inflateText = (item: string) => export const inflate = (content: Component): string => { if (typeof content === 'string') return inflateText(content); if (Array.isArray(content)) { - if (content.some((item: string | Component.Button) => typeof item !== 'object')) - return inflateList(content); - return inflateButtonWrapper(content); + if (content.some((item: string | MailButton) => typeof item !== 'object')) return inflateList(content); + return inflateButtonWrapper(content); } if ('location' in content) return inflateButtonWrapper(content); if ('items' in content) return inflateTable(content); diff --git a/src/services/email/generalMails.ts b/src/services/email/generalMails.ts new file mode 100644 index 00000000..8a4377e0 --- /dev/null +++ b/src/services/email/generalMails.ts @@ -0,0 +1,28 @@ +import { MailGeneral } from '.'; +import { getNextPaidAndValidatedUserBatch } from '../../operations/user'; +import { getNotOnDiscordServerUsers, getNotPaidSsbuUsers, getNotPaidUsers } from './targets'; + +export const availableGeneralMails: { + [key: string]: MailGeneral; +} = { + joindiscord: { + targets: getNotOnDiscordServerUsers, + template: 'joindiscord', + }, + minor: { + targets: getNotPaidUsers, + template: 'minor', + }, + notpaid: { + targets: getNotPaidUsers, + template: 'notpaid', + }, + notpaidssbu: { + targets: getNotPaidSsbuUsers, + template: 'notpaidssbu', + }, + tickets: { + targets: getNextPaidAndValidatedUserBatch, + template: 'tickets', + }, +}; diff --git a/src/services/email/index.ts b/src/services/email/index.ts index 5c814b2c..5a7af96b 100644 --- a/src/services/email/index.ts +++ b/src/services/email/index.ts @@ -1,14 +1,49 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { readFile } from 'fs/promises'; +import { render } from 'mustache'; import nodemailer from 'nodemailer'; import { Log } from '@prisma/client'; -import { DetailedCart, EmailAttachement, RawUser, User, MailQuery } from '../../types'; +import { EmailAttachement, RawUser, MailQuery } from '../../types'; import env from '../../utils/env'; import logger from '../../utils/logger'; -import { generateTicketsEmail, generateValidationEmail, generatePasswordResetEmail } from './serializer'; -import type { SerializedMail } from './types'; +import type { Component, Mail, SerializedMail } from './types'; import database from '../database'; +import { escapeText, inflate, style } from './components'; +import { availableTemplates } from './templates'; +import { availableGeneralMails } from './generalMails'; export type { Component, Mail, SerializedMail } from './types'; +/** + * Applied {@link Mail} content in the template + * @throws an error when an unknwon component is in the {@link Mail#sections#components} + */ +export const serialize = async (content: Mail) => { + const template = await readFile('assets/email/template.html', 'utf8'); + const year = new Date().getFullYear(); + return { + to: content.receiver, + subject: `${content.title.topic} - UTT Arena ${year}`, + html: render( + template, + { + ...content, + year, + style, + }, + undefined, + { + escape: (text: Component[] | string): string => + // These are the elements mustache tries to write in the mail. They can be + // full text or the list of components (cf. assets/email/template.html) + // We escape text and handle components + typeof text === 'string' ? escapeText(text) : text.map(inflate).join(''), + }, + ), + }; +}; + export const getEmailsLogs = async () => (await database.log.findMany({ where: { @@ -84,38 +119,64 @@ export const sendEmail = async (mail: SerializedMail, attachments?: EmailAttache } }; -/** - * Sends an email to the user, containing information about the event, - * a list of all items bought on the store and his tickets. - * @param cart the cart of the user - * @returns a promise that resolves when the mail has been sent - * @throws an error if the mail declared above (corresponding to this - * request) is invalid ie. contains an object which is not a {@link Component} - */ -export const sendPaymentConfirmation = async (cart: DetailedCart) => { - const content = await generateTicketsEmail(cart); - return sendEmail(content); +export type MailGeneral = { + // TODO: Fix this type + targets: () => any; + template: string; }; +// TODO: Fix this type +export type MailTemplate = (target: any) => Promise; +// TODO: Fix this type +export const sendMailsFromTemplate = async (template: string, targets: any[]) => { + try { + const mailTemplate = availableTemplates[template]; -/** - * Sends an email to the user with his account validation code. - * This code (given to the user as a link) is required before logging in - * @param user the user to send the mail to - * @returns a promise that resolves when the mail was GENERATED. We don't wait - * for the mail to be sent as it may take time (for several reasons, including mail - * processing and network delays) and we don't want the current request to timeout - * @throws an error if the mail declared above (corresponding to this - * request) is invalid ie. contains an object which is not a {@link Component} - */ -export const sendValidationCode = async (user: RawUser | User) => sendEmail(await generateValidationEmail(user)); + if (targets.length === 0 && !mailTemplate) { + return false; + } -/** - * Sends an email to the user with a password reset link. - * @param user the user to send the mail to - * @returns a promise that resolves when the mail was GENERATED. We don't wait - * for the mail to be sent as it may take time (for several reasons, including mail - * processing and network delays) and we don't want the current request to timeout - * @throws an error if the mail declared above (corresponding to this - * request) is invalid ie. contains an object which is not a {@link Component} - */ -export const sendPasswordReset = async (user: RawUser) => sendEmail(await generatePasswordResetEmail(user)); + if (targets.length > 1) { + const outgoingMails = await Promise.allSettled( + targets.map(async (target) => { + await sendEmail(await mailTemplate(target)); + }), + ); + + const results = outgoingMails.reduce( + (result, state) => { + if (state.status === 'fulfilled') + return { + ...result, + delivered: result.delivered + 1, + }; + logger.error(`Impossible d'envoyer de mail à ${state.reason}`); + return { + ...result, + undelivered: result.undelivered + 1, + }; + }, + { delivered: 0, undelivered: 0 }, + ); + + // eslint-disable-next-line no-console + console.info(`\tMails envoyés: ${results.delivered}\n\tMails non envoyés: ${results.undelivered}`); + return results; + } + + return sendEmail(await mailTemplate(targets[0])); + } catch (error) { + logger.error('Error while sending emails', error); + return false; + } +}; + +export const sendGeneralMail = async (generalMail: string) => { + const mail = availableGeneralMails[generalMail]; + + if (!mail) { + return false; + } + + const targets = await mail.targets(); + return sendMailsFromTemplate(generalMail, targets); +}; diff --git a/src/services/email/serializer.ts b/src/services/email/serializer.ts deleted file mode 100644 index 307ae975..00000000 --- a/src/services/email/serializer.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { readFile } from 'fs/promises'; -import { render } from 'mustache'; -import { ActionFeedback, DetailedCart, RawUser, ItemCategory } from '../../types'; -import { escapeText, inflate, style } from './components'; -import env from '../../utils/env'; -import { formatPrice } from '../../utils/helpers'; -import type { Mail, SerializedMail, Component } from '.'; - -/** - * Applied {@link Mail} content in the template - * @throws an error when an unknwon component is in the {@link Mail#sections#components} - */ -export const serialize = async (content: Mail) => { - const template = await readFile('assets/email/template.html', 'utf8'); - const year = new Date().getFullYear(); - return { - to: content.receiver, - subject: `${content.title.topic} - UTT Arena ${year}`, - html: render( - template, - { - ...content, - year, - style, - }, - undefined, - { - escape: (text: Component[] | string): string => - // These are the elements mustache tries to write in the mail. They can be - // full text or the list of components (cf. assets/email/template.html) - // We escape text and handle components - typeof text === 'string' ? escapeText(text) : text.map(inflate).join(''), - }, - ), - }; -}; - -export const generateTicketsEmail = (cart: DetailedCart) => - serialize({ - title: { - topic: 'Confirmation de commande', - banner: 'Informations importantes', - short: `Salut ${cart.user.firstname},`, - highlight: "Bienvenue à l'UTT Arena 🔥 !", - }, - reason: - "Tu as reçu cet email car tu es inscrit à l'UTT Arena 2024. Si ce n'est pas le cas, contacte-nous et change le mot de passe de ta boîte mail.", - receiver: cart.user.email, - sections: [ - { - title: 'Confirmation de commande', - components: [ - 'On te confirme aussi ta commande', - { - name: 'Tickets', - items: [ - { - name: '*Nom*', - type: '*Type*', - price: '*Prix*', - }, - ...cart.cartItems - .filter((cartItem) => cartItem.item.category === ItemCategory.ticket) - .map((ticket) => ({ - name: `${ticket.forUser.firstname} ${ticket.forUser.lastname}`, - type: ticket.item.name, - price: formatPrice(ticket.reducedPrice ?? ticket.price), - })), - ], - }, - { - name: 'Suppléments', - items: [ - { - name: '*Nom*', - amount: '*Quantité*', - price: '*Prix*', - }, - ...cart.cartItems - .filter((cartItem) => cartItem.item.category === ItemCategory.supplement) - .map((item) => ({ - name: item.item.name, - amount: `${item.quantity}`, - price: formatPrice(item.reducedPrice ?? item.price), - })), - ], - }, - { - name: 'Location de matériel', - items: [ - { - name: '*Nom*', - amount: '*Quantité*', - price: '*Prix*', - }, - ...cart.cartItems - .filter((cartItem) => cartItem.item.category === ItemCategory.rent) - .map((item) => ({ - name: item.item.name, - amount: `${item.quantity}`, - price: formatPrice(item.reducedPrice ?? item.price), - })), - ], - }, - ], - }, - { - title: 'Tournoi', - components: [ - 'Voilà les dernières informations importantes nécessaires au bon déroulement de la compétition :', - [ - 'Il est nécessaire que *tous les joueurs* de *toutes les équipes* soient présents sur notre Discord', - 'Tous les tournois débutent samedi à 10h, il faudra donc être présent *à partir de 9h00* pour un check-in de toutes les équipes et joueurs', - "N'hésite pas à contacter un membre du staff sur Discord si tu as une question ou que tu rencontres un quelconque problème 😉", - ], - { - name: 'Rejoindre le serveur Discord', - location: 'https://discord.gg/WhxZwKU', - }, - ], - }, - { - title: 'Billet', - components: ["Tu recevras ton *billet personnalisé* par mail quelques jours avant l'UTT Arena !"], - }, - ], - }); - -export const generateValidationEmail = (user: Omit) => - serialize({ - receiver: user.email, - reason: - "Tu as reçu ce mail car tu as envoyé une demande de création de compte à l'UTT Arena. Si ce n'est pas toi, ignore ce message ou contacte-nous.", - title: { - topic: 'Code de validation', - banner: 'Création du compte', - short: `Salut ${user.firstname},`, - highlight: "Bienvenue à l'UTT Arena !", - }, - sections: [ - { - title: 'Avant de commencer...', - components: [ - "On sait bien que c'est pénible mais on doit vérifier que ton adresse email fonctionne bien (sinon tu ne pourras pas recevoir tes billets !).", - { - name: 'Confirme ton adresse email', - location: `${env.front.website}/${ActionFeedback.VALIDATE}/${user.registerToken}`, - }, - `_Si le bouton ne marche pas, tu peux utiliser ce lien:_\n_${env.front.website}/${ActionFeedback.VALIDATE}/${user.registerToken}_`, - ], - }, - { - title: 'Discord', - components: [ - "On utilise Discord pendant l'évènement, et tu auras besoin de lier ton compte discord avec ton compte UTT Arena pour pouvoir créer ou rejoindre une équipe. On te donnera plus de détails là-dessus à ce moment-là 😉", - ], - }, - { - title: 'Tournoi Super Smash Bros Ultimate', - components: [ - "Si tu as choisi de t'inscrire à ce tournoi et que tu choisis de venir avec ta propre console, tu peux bénéficier d'une réduction sur ton billet 😉 _(offre limitée à un certain nombre de places)_", - ], - }, - { - title: 'Des questions ?', - components: [ - "On t'invite à lire la FAQ ou à poser tes questions directement sur Discord.", - [ - { - name: 'FAQ', - location: `${env.front.website}/help`, - }, - { - name: 'Rejoindre le serveur Discord', - location: 'https://discord.gg/WhxZwKU', - }, - ], - ], - }, - ], - }); - -export const generatePasswordResetEmail = (user: Omit) => - serialize({ - receiver: user.email, - reason: - "Tu as reçu ce mail car tu as demandé à réinitialiser ton mot de passe. Si ce n'est pas le cas, ignore ce message.", - title: { - topic: 'Réinitialisation de ton mot de passe', - banner: 'Réinitialisation du mot de passe', - short: `Salut ${user.firstname},`, - highlight: 'Tu es sur le point de réinitialiser ton mot de passe', - }, - sections: [ - { - title: 'Code de vérification', - components: [ - "On doit s'assurer que tu es bien à l'origine de cette demande. Tu peux finaliser la procédure en cliquant sur le bouton ci-dessous.", - { - name: 'Réinitialise ton mot de passe', - location: `${env.front.website}/${ActionFeedback.PASSWORD_RESET}/${user.resetToken}`, - }, - `_Si le bouton ne marche pas, tu peux utiliser ce lien:_\n_${env.front.website}/${ActionFeedback.PASSWORD_RESET}/${user.resetToken}_`, - ], - }, - ], - }); diff --git a/src/services/email/targets.ts b/src/services/email/targets.ts new file mode 100644 index 00000000..ed2bd30b --- /dev/null +++ b/src/services/email/targets.ts @@ -0,0 +1,166 @@ +import { UserType, TransactionState, UserAge } from '@prisma/client'; +import database from '../database'; +import { fetchGuildMembers } from '../discord'; + +export const getNotOnDiscordServerUsers = async () => { + const [members, users] = await Promise.all([ + fetchGuildMembers().then((list) => list.map((member) => member.user.id)), + database.user.findMany({ + where: { + discordId: { + not: null, + }, + email: { + not: null, + }, + OR: [ + { + team: { + lockedAt: { + not: null, + }, + }, + }, + { + type: UserType.spectator, + }, + ], + cartItems: { + some: { + itemId: { + startsWith: 'ticket-', + }, + cart: { + paidAt: { + not: null, + }, + transactionState: TransactionState.paid, + }, + }, + }, + }, + }), + ]); + return users.filter((user) => !members.includes(user.discordId)); +}; + +export const getNotPaidUsers = () => + database.user.findMany({ + distinct: ['id'], + where: { + AND: [ + { + OR: [ + { + cartItems: { + some: { + AND: [ + { + itemId: { + startsWith: 'ticket-', + }, + forcePaid: false, + }, + { + cart: { + transactionState: { + not: 'paid', + }, + }, + }, + ], + }, + }, + }, + { + cartItems: { + none: {}, + }, + }, + ], + }, + { + team: { + lockedAt: null, + }, + }, + ], + }, + }); + +export const getNotPaidSsbuUsers = () => + database.user.findMany({ + distinct: ['id'], + where: { + AND: [ + { + OR: [ + { + cartItems: { + some: { + AND: [ + { + itemId: { + startsWith: 'ticket-', + }, + forcePaid: false, + }, + { + cart: { + transactionState: { + not: 'paid', + }, + }, + }, + ], + }, + }, + }, + { + cartItems: { + none: {}, + }, + }, + ], + }, + { + team: { + tournament: { + id: 'ssbu', + }, + }, + }, + { + team: { + lockedAt: null, + }, + }, + ], + }, + }); + +export const getMinorUsers = () => + database.user.findMany({ + where: { + discordId: { + not: null, + }, + email: { + not: null, + }, + age: UserAge.child, + cartItems: { + some: { + itemId: { + startsWith: 'ticket-', + }, + cart: { + paidAt: { + not: null, + }, + transactionState: TransactionState.paid, + }, + }, + }, + }, + }); diff --git a/src/services/email/templates/accountValidation.ts b/src/services/email/templates/accountValidation.ts new file mode 100644 index 00000000..78f96bd8 --- /dev/null +++ b/src/services/email/templates/accountValidation.ts @@ -0,0 +1,58 @@ +import { RawUser, ActionFeedback } from '../../../types'; +import { serialize } from '..'; +import env from '../../../utils/env'; + +export const generateAccountValidationEmail = (user: Omit) => + serialize({ + receiver: user.email, + reason: + "Tu as reçu ce mail car tu as envoyé une demande de création de compte à l'UTT Arena. Si ce n'est pas toi, ignore ce message ou contacte-nous.", + title: { + topic: 'Code de validation', + banner: 'Création du compte', + short: `Salut ${user.firstname},`, + highlight: "Bienvenue à l'UTT Arena !", + }, + sections: [ + { + title: 'Avant de commencer...', + components: [ + "On sait bien que c'est pénible mais on doit vérifier que ton adresse email fonctionne bien (sinon tu ne pourras pas recevoir tes billets !).", + { + name: 'Confirme ton adresse email', + location: `${env.front.website}/${ActionFeedback.VALIDATE}/${user.registerToken}`, + }, + `_Si le bouton ne marche pas, tu peux utiliser ce lien:_\n_${env.front.website}/${ActionFeedback.VALIDATE}/${user.registerToken}_`, + ], + }, + { + title: 'Discord', + components: [ + "On utilise Discord pendant l'évènement, et tu auras besoin de lier ton compte discord avec ton compte UTT Arena pour pouvoir créer ou rejoindre une équipe. On te donnera plus de détails là-dessus à ce moment-là 😉", + ], + }, + { + title: 'Tournoi Super Smash Bros Ultimate', + components: [ + "Si tu as choisi de t'inscrire à ce tournoi et que tu choisis de venir avec ta propre console, tu peux bénéficier d'une réduction sur ton billet 😉 _(offre limitée à un certain nombre de places)_", + ], + }, + { + title: 'Des questions ?', + components: [ + "On t'invite à lire la FAQ ou à poser tes questions directement sur Discord.", + [ + { + name: 'FAQ', + location: `${env.front.website}/help`, + }, + { + name: 'Rejoindre le serveur Discord', + location: 'https://discord.gg/WhxZwKU', + }, + ], + ], + }, + ], + attachments: [], + }); diff --git a/src/services/email/templates/index.ts b/src/services/email/templates/index.ts new file mode 100644 index 00000000..0c98f8fe --- /dev/null +++ b/src/services/email/templates/index.ts @@ -0,0 +1,24 @@ +import { MailTemplate } from '..'; +import { generateAccountValidationEmail } from './accountValidation'; +import { generateJoinDiscordEmail } from './joinDiscord'; +import { generateLastYearPublicAnnounce } from './lastYearAnnounce'; +import { generateMinorEmail } from './minor'; +import { generateNotPaidEmail } from './notPaid'; +import { generateNotPaidSSBUEmail } from './notPaidSsbu'; +import { generateOrderConfirmationEmail } from './orderConfirmation'; +import { generatePasswordResetEmail } from './passwordReset'; +import { generateTicketsEmail } from './tickets'; + +export const availableTemplates: { + [key: string]: MailTemplate; +} = { + accountvalidation: generateAccountValidationEmail, + joindiscord: generateJoinDiscordEmail, + lastyearannounce: generateLastYearPublicAnnounce, + minor: generateMinorEmail, + notpaid: generateNotPaidEmail, + notpaidssbu: generateNotPaidSSBUEmail, + orderconfirmation: generateOrderConfirmationEmail, + passwordreset: generatePasswordResetEmail, + tickets: generateTicketsEmail, +}; diff --git a/src/services/email/templates/joinDiscord.ts b/src/services/email/templates/joinDiscord.ts new file mode 100644 index 00000000..fcab79d2 --- /dev/null +++ b/src/services/email/templates/joinDiscord.ts @@ -0,0 +1,28 @@ +import { RawUser } from '../../../types'; +import { serialize } from '..'; + +export const generateJoinDiscordEmail = (user: Omit) => + serialize({ + reason: 'Tu as reçu ce mail car tu as créé un compte sur arena.utt.fr', + title: { + banner: 'On se retrouve ce weekend !', + highlight: `Salut ${user.firstname}`, + short: "L'UTT Arena arrive à grands pas 🔥", + topic: "Ton ticket pour l'UTT Arena", + }, + receiver: user.email, + sections: [ + { + title: "Rejoins le serveur discord de l'UTT Arena !", + components: [ + "Tu n'es pas encore sur le serveur discord Arena, nous te conseillons fortement de le rejoindre car il s'agit de notre principal outil de communication avec toi et les autres joueurs.", + 'Sur ce serveur, tu pourras également y discuter avec les autres joueurs, ou poser des questions aux organisateurs de ton tournoi.', + { + name: 'Rejoindre le serveur Discord', + location: 'https://discord.gg/WhxZwKU', + }, + ], + }, + ], + attachments: [], + }); diff --git a/src/services/email/templates/lastYearAnnounce.ts b/src/services/email/templates/lastYearAnnounce.ts new file mode 100644 index 00000000..c1ad2fb6 --- /dev/null +++ b/src/services/email/templates/lastYearAnnounce.ts @@ -0,0 +1,56 @@ +import { serialize } from '..'; +import { RawUser } from '../../../types'; +import env from '../../../utils/env'; + +export const generateLastYearPublicAnnounce = (user: Omit) => + serialize({ + receiver: user.email, + reason: + "Tu as reçu ce mail car tu as participé à l'UTT Arena en décembre 2023. Si ce n'est pas le cas, ignore ce message.", + title: { + topic: "L'UTT ARENA est de retour !", + banner: '', + short: `Salut,`, + highlight: "L'UTT Arena est de retour !", + }, + sections: [ + { + title: "L'UTT Arena revient du 6 au 8 décembre à Troyes pour un nouveau tournoi League of Legends", + components: [ + '🖥️ **160 places** joueurs à 28€', + '🎙️ Casté par **Drako** et **Headen**', + '💰 **2000€** de cashprize', + '🚅 **Troyes** à 1h30 de Paris', + '🎊 Buvette et animations tout le weekend', + ], + }, + { + title: 'Inscriptions', + components: [ + "Pour s'inscrire, ça se passe sur le site !", + { + name: "Inscris toi à l'UTT Arena 2024 !", + location: `https://arena.utt.fr/`, + }, + `_Si le bouton ne marche pas, tu peux utiliser ce lien:_\n_https://arena.utt.fr/_`, + ], + }, + { + title: 'Des questions ?', + components: [ + "On t'invite à lire la FAQ ou à poser tes questions directement sur Discord.", + [ + { + name: 'FAQ', + location: `${env.front.website}/help`, + }, + { + name: 'Rejoindre le serveur Discord', + location: 'https://discord.gg/WhxZwKU', + }, + ], + ], + }, + ], + attachments: [], + }); diff --git a/src/services/email/templates/minor.ts b/src/services/email/templates/minor.ts new file mode 100644 index 00000000..36349e8a --- /dev/null +++ b/src/services/email/templates/minor.ts @@ -0,0 +1,28 @@ +import { RawUser } from '../../../types'; +import { serialize } from '..'; + +export const generateMinorEmail = (user: Omit) => + serialize({ + reason: 'Tu as reçu ce mail car tu as créé un compte sur arena.utt.fr', + title: { + banner: 'On se retrouve ce weekend !', + highlight: `Salut ${user.firstname}`, + short: "L'UTT Arena arrive à grands pas 🔥", + topic: "Ton ticket pour l'UTT Arena", + }, + receiver: user.email, + sections: [ + { + title: 'Autorisation parentale', + components: [ + "Tu nous as indiqué que tu seras mineur à la date de l'UTT Arena. N'oublie pas de préparer *ton autorisation parentale, et une photocopie de ta pièce d'identité, et de celle de ton responsable légal* !", + "La vérification se fera à l'entrée de l'UTT Arena, n'hésite pas à envoyer à l'avance ces documents par mail à arena@utt.fr pour simplifier la procédure à l'entrée.", + { + location: 'https://arena.utt.fr/uploads/files/Autorisation_parentale_-_UTT_Arena_2024.pdf', + name: "Télécharger l'autorisation parentale", + }, + ], + }, + ], + attachments: [], + }); diff --git a/src/services/email/templates/notPaid.ts b/src/services/email/templates/notPaid.ts new file mode 100644 index 00000000..2359815d --- /dev/null +++ b/src/services/email/templates/notPaid.ts @@ -0,0 +1,28 @@ +import { RawUser } from '../../../types'; +import { serialize } from '..'; + +export const generateNotPaidEmail = (user: Omit) => + serialize({ + reason: 'Tu as reçu ce mail car tu as créé un compte sur arena.utt.fr', + title: { + banner: 'On se retrouve ce weekend !', + highlight: `Salut ${user.firstname}`, + short: "L'UTT Arena arrive à grands pas 🔥", + topic: "Ton ticket pour l'UTT Arena", + }, + receiver: user.email, + sections: [ + { + title: "Ton inscription n'a pas été confirmée", + components: [ + "L'UTT Arena approche à grand pas, et ton inscription n'est pas encore confirmée. Pour verrouiller ta place, il ne te reste plus qu'à la payer en accédant à la boutique sur le site. \nSi le tournoi auquel tu souhaites participer est d'ores-et-déjà rempli, tu sera placé en file d'attente.", + "\n_Si le taux de remplissage d'un tournoi est trop faible d'ici à deux semaines de l'évènement, l'équipe organisatrice se réserve le droit de l'annuler._", + { + location: 'https://arena.utt.fr/dashboard/team', + name: 'Accéder à arena.utt.fr', + }, + ], + }, + ], + attachments: [], + }); diff --git a/src/services/email/templates/notPaidSsbu.ts b/src/services/email/templates/notPaidSsbu.ts new file mode 100644 index 00000000..b7824479 --- /dev/null +++ b/src/services/email/templates/notPaidSsbu.ts @@ -0,0 +1,29 @@ +import { RawUser } from '../../../types'; +import { serialize } from '..'; + +export const generateNotPaidSSBUEmail = (user: Omit) => + serialize({ + reason: 'Tu as reçu ce mail car tu as créé un compte sur arena.utt.fr', + title: { + banner: 'On se retrouve ce weekend !', + highlight: `Salut ${user.firstname}`, + short: "L'UTT Arena arrive à grands pas 🔥", + topic: "Ton ticket pour l'UTT Arena", + }, + receiver: user.email, + sections: [ + { + title: "Ton inscription n'a pas été confirmée", + components: [ + "L'UTT Arena approche à grand pas, et ton inscription pour le tournoi SSBU n'est pas encore confirmée. Pour verrouiller ta place, il ne te reste plus qu'à la payer en accédant à la boutique sur le site.", + "\nN'oublie pas que tu peux décider de ramener ta propre Nintendo Switch avec SSBU (all DLCs) pour bénéficier d'une *réduction de 3€* sur ta place ! Cela permet également au tournoi de s'enchaîner de façon plus fluide.", + "\nOn se retrouve le 1, 2, 3 décembre dans l'Arène !", + { + location: 'https://arena.utt.fr/dashboard/team', + name: 'Accéder à arena.utt.fr', + }, + ], + }, + ], + attachments: [], + }); diff --git a/src/services/email/templates/orderConfirmation.ts b/src/services/email/templates/orderConfirmation.ts new file mode 100644 index 00000000..6caa4e7a --- /dev/null +++ b/src/services/email/templates/orderConfirmation.ts @@ -0,0 +1,95 @@ +import { DetailedCart, ItemCategory } from '../../../types'; +import { serialize } from '..'; +import { formatPrice } from '../../../utils/helpers'; + +export const generateOrderConfirmationEmail = (cart: DetailedCart) => + serialize({ + title: { + topic: 'Confirmation de commande', + banner: 'Informations importantes', + short: `Salut ${cart.user.firstname},`, + highlight: "Bienvenue à l'UTT Arena 🔥 !", + }, + reason: + "Tu as reçu cet email car tu es inscrit à l'UTT Arena 2024. Si ce n'est pas le cas, contacte-nous et change le mot de passe de ta boîte mail.", + receiver: cart.user.email, + sections: [ + { + title: 'Confirmation de commande', + components: [ + 'On te confirme ta commande', + { + name: 'Tickets', + items: [ + { + name: '*Nom*', + type: '*Type*', + price: '*Prix*', + }, + ...cart.cartItems + .filter((cartItem) => cartItem.item.category === ItemCategory.ticket) + .map((ticket) => ({ + name: `${ticket.forUser.firstname} ${ticket.forUser.lastname}`, + type: ticket.item.name, + price: formatPrice(ticket.reducedPrice ?? ticket.price), + })), + ], + }, + { + name: 'Suppléments', + items: [ + { + name: '*Nom*', + amount: '*Quantité*', + price: '*Prix*', + }, + ...cart.cartItems + .filter((cartItem) => cartItem.item.category === ItemCategory.supplement) + .map((item) => ({ + name: item.item.name, + amount: `${item.quantity}`, + price: formatPrice(item.reducedPrice ?? item.price), + })), + ], + }, + { + name: 'Location de matériel', + items: [ + { + name: '*Nom*', + amount: '*Quantité*', + price: '*Prix*', + }, + ...cart.cartItems + .filter((cartItem) => cartItem.item.category === ItemCategory.rent) + .map((item) => ({ + name: item.item.name, + amount: `${item.quantity}`, + price: formatPrice(item.reducedPrice ?? item.price), + })), + ], + }, + ], + }, + { + title: 'Tournoi', + components: [ + 'Voilà les dernières informations importantes nécessaires au bon déroulement de la compétition :', + [ + 'Il est nécessaire que *tous les joueurs* de *toutes les équipes* soient présents sur notre Discord', + 'Tous les tournois débutent samedi à 10h, il faudra donc être présent *à partir de 9h00* pour un check-in de toutes les équipes et joueurs', + "N'hésite pas à contacter un membre du staff sur Discord si tu as une question ou que tu rencontres un quelconque problème 😉", + ], + { + name: 'Rejoindre le serveur Discord', + location: 'https://discord.gg/WhxZwKU', + }, + ], + }, + { + title: 'Billet', + components: ["Tu recevras ton *billet personnalisé* par mail quelques jours avant l'UTT Arena !"], + }, + ], + attachments: [], + }); diff --git a/src/services/email/templates/passwordReset.ts b/src/services/email/templates/passwordReset.ts new file mode 100644 index 00000000..bb9dc6c6 --- /dev/null +++ b/src/services/email/templates/passwordReset.ts @@ -0,0 +1,30 @@ +import { RawUser, ActionFeedback } from '../../../types'; +import { serialize } from '..'; +import env from '../../../utils/env'; + +export const generatePasswordResetEmail = (user: Omit) => + serialize({ + receiver: user.email, + reason: + "Tu as reçu ce mail car tu as demandé à réinitialiser ton mot de passe. Si ce n'est pas le cas, ignore ce message.", + title: { + topic: 'Réinitialisation de ton mot de passe', + banner: 'Réinitialisation du mot de passe', + short: `Salut ${user.firstname},`, + highlight: 'Tu es sur le point de réinitialiser ton mot de passe', + }, + sections: [ + { + title: 'Code de vérification', + components: [ + "On doit s'assurer que tu es bien à l'origine de cette demande. Tu peux finaliser la procédure en cliquant sur le bouton ci-dessous.", + { + name: 'Réinitialise ton mot de passe', + location: `${env.front.website}/${ActionFeedback.PASSWORD_RESET}/${user.resetToken}`, + }, + `_Si le bouton ne marche pas, tu peux utiliser ce lien:_\n_${env.front.website}/${ActionFeedback.PASSWORD_RESET}/${user.resetToken}_`, + ], + }, + ], + attachments: [], + }); diff --git a/src/services/email/templates/tickets.ts b/src/services/email/templates/tickets.ts new file mode 100644 index 00000000..8df18202 --- /dev/null +++ b/src/services/email/templates/tickets.ts @@ -0,0 +1,67 @@ +import { RawUser, TransactionState } from '../../../types'; +import { serialize } from '..'; +import database from '../../database'; +import { generateTicket } from '../../../utils/ticket'; + +export const generateTicketsEmail = async (user: Omit) => + serialize({ + reason: 'Tu as reçu ce mail car tu as créé un compte sur arena.utt.fr', + title: { + banner: 'On se retrouve ce weekend !', + highlight: `Salut ${user.firstname}`, + short: "L'UTT Arena arrive à grands pas 🔥", + topic: "Ton ticket pour l'UTT Arena", + }, + receiver: user.email, + sections: [ + { + title: "Ton ticket pour l'UTT Arena", + components: [ + "Tu es bien inscrit à l'UTT Arena ! Tu trouveras ci-joint ton billet, que tu devras présenter à l'entrée de l'UTT Arena. Tu peux aussi le retrouver sur la billetterie, dans l'onglet \"Mon compte\" de ton Dashboard.", + 'Attention, tous les tournois débutent à 10h, *il faudra donc être présent dès 9h00 pour un check-in de toutes les équipes et joueurs.*', + { + location: 'https://arena.utt.fr/dashboard/account', + name: 'Accéder à arena.utt.fr', + }, + ], + }, + { + title: 'Ce que tu dois emporter', + components: [ + "Pour rentrer à l'UTT Arena, tu auras besoin de", + [ + 'ton *billet* (que tu trouveras en pièce jointe, ou sur le site)', + "une *pièce d'identité* (type carte d'identité, titre de séjour ou permis de conduire)", + ], + "Nous te conseillons d'emporter également", + [ + 'Une gourde *vide*', + "une multiprise puisque tu n'auras *qu'une seule prise mise à ta disposition pour brancher tout ton setup*", + "un câble ethernet (d'environ 7m)", + 'ton setup', + ], + "Si tu as encore des questions, n'hésite pas à regarder notre FAQ ou à poser la question sur le serveur discord !", + { + location: 'https://arena.utt.fr/help', + name: 'Ouvrir la FAQ', + }, + ], + }, + ], + attachments: await (async () => { + const cartItem = await database.cartItem.findFirst({ + where: { + cart: { + paidAt: { + not: null, + }, + transactionState: TransactionState.paid, + }, + itemId: `ticket-${user.type}`, + forUserId: user.id, + }, + include: { item: true, forUser: true }, + }); + return [await generateTicket(cartItem)]; + })(), + }); diff --git a/src/services/email/types.d.ts b/src/services/email/types.ts similarity index 52% rename from src/services/email/types.d.ts rename to src/services/email/types.ts index d5fadc14..9309b571 100644 --- a/src/services/email/types.d.ts +++ b/src/services/email/types.ts @@ -1,31 +1,31 @@ -export declare type Component = string | string[] | Component.Button | Component.Button[] | Component.Table; +import { EmailAttachement } from '../../types'; -export declare namespace Component { - interface Button { - /** Button text */ - name: string; - /** Button link */ - location: string; - /** Button color. Matches UA colors by default */ - color?: `#${string}`; - } +export interface MailButton { + /** Button text */ + name: string; + /** Button link */ + location: string; + /** Button color. Matches UA colors by default */ + color?: `#${string}`; +} - interface Table { - /** Name of the table. Displayed BEFORE the table */ - name?: string; - /** - * List of ALL rows contained in the table. - * The first element of thie array will be used for column creation: - * All keys of this object will be attached to a column, named by the - * item value corresponding to the (column) key - * All other object will match one single row and fill the columns depending - * on their keys. - * This means that all columns must be defined in the first object - */ - items: Array<{ [key: string]: string }>; - } +export interface MailTable { + /** Name of the table. Displayed BEFORE the table */ + name?: string; + /** + * List of ALL rows contained in the table. + * The first element of thie array will be used for column creation: + * All keys of this object will be attached to a column, named by the + * item value corresponding to the (column) key + * All other object will match one single row and fill the columns depending + * on their keys. + * This means that all columns must be defined in the first object + */ + items: Array<{ [key: string]: string }>; } +export declare type Component = string | string[] | MailButton | MailButton[] | MailTable; + export declare interface Mail { /** The email address to send this email to (written in the footer, before the {@link reason}) */ receiver: string; @@ -60,6 +60,11 @@ export declare interface Mail { title: string; components: Component[]; }[]; + /** + * The attachments to include in the mail. If this property is omitted (or if the list is empty), + * no attachment will be included in the mail. + */ + attachments?: EmailAttachement[]; } export declare interface SerializedMail { diff --git a/tests/auth/resendEmail.test.ts b/tests/auth/resendEmail.test.ts index b494b0a0..e57172e0 100644 --- a/tests/auth/resendEmail.test.ts +++ b/tests/auth/resendEmail.test.ts @@ -84,7 +84,7 @@ describe('POST /auth/resendEmail', () => { }); it('should return an error as the code has not been sent successfully', async () => { - sandbox.stub(mailOperations, 'sendValidationCode').throws('Unexpected error'); + sandbox.stub(mailOperations, 'sendMailsFromTemplate').throws('Unexpected error'); await request(app) .post('/auth/resendEmail') .send({ diff --git a/tests/services/email.test.ts b/tests/services/email.test.ts index 3b62db11..2324a375 100644 --- a/tests/services/email.test.ts +++ b/tests/services/email.test.ts @@ -6,11 +6,7 @@ import { PrimitiveCartItem, ItemCategory, TransactionState, UserType } from '../ import { createCart, updateCart } from '../../src/operations/carts'; import { sendEmail } from '../../src/services/email'; import { inflate } from '../../src/services/email/components'; -import { - generateTicketsEmail, - generatePasswordResetEmail, - generateValidationEmail, -} from '../../src/services/email/serializer'; +import { availableTemplates } from '../../src/services/email/templates'; import { randomInt } from '../../src/utils/helpers'; import { fetchAllItems } from '../../src/operations/item'; import env from '../../src/utils/env'; @@ -128,7 +124,7 @@ describe('Tests the email utils', () => { transactionState: TransactionState.paid, }); - const ticketsEmail = await generateTicketsEmail(detailedCart); + const ticketsEmail = await availableTemplates.orderconfirmation(detailedCart); fs.writeFileSync('artifacts/payment.html', ticketsEmail.html); }); @@ -136,7 +132,7 @@ describe('Tests the email utils', () => { it(`should generate an account validation template`, async () => { const user = await createFakeUser({ confirmed: false }); - const validationEmail = await generateValidationEmail(user); + const validationEmail = await availableTemplates.accountvalidation(user); fs.writeFileSync('artifacts/validation.html', validationEmail.html); }); @@ -145,7 +141,7 @@ describe('Tests the email utils', () => { const user = await createFakeUser({ type: UserType.player }); user.resetToken = (await generateResetToken(user.id)).resetToken; - const passwordResetEmail = await generatePasswordResetEmail(user); + const passwordResetEmail = await availableTemplates.passwordreset(user); fs.writeFileSync('artifacts/pwd-reset.html', passwordResetEmail.html); }); From e17082766e9a7ab9177926658c594de8ff24a112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9=20Landr=C3=A9?= Date: Wed, 6 Nov 2024 21:29:12 +0100 Subject: [PATCH 02/16] feat: add General Mail route --- src/controllers/admin/emails/send.ts | 116 +---------------- src/controllers/admin/emails/sendCustom.ts | 142 +++++++++++++++++++++ src/services/email/index.ts | 6 +- src/types.ts | 5 + 4 files changed, 155 insertions(+), 114 deletions(-) create mode 100644 src/controllers/admin/emails/sendCustom.ts diff --git a/src/controllers/admin/emails/send.ts b/src/controllers/admin/emails/send.ts index 5ac48143..8995b037 100644 --- a/src/controllers/admin/emails/send.ts +++ b/src/controllers/admin/emails/send.ts @@ -3,9 +3,9 @@ import { NextFunction, Request, Response } from 'express'; import Joi from 'joi'; import { badRequest, created } from '../../../utils/responses'; import { hasPermission } from '../../../middlewares/authentication'; -import { Error as ApiError, MailQuery } from '../../../types'; +import { Error as ApiError, MailGeneralQuery, MailQuery } from '../../../types'; import { validateBody } from '../../../middlewares/validation'; -import { sendEmail, SerializedMail, serialize } from '../../../services/email'; +import { sendEmail, SerializedMail, serialize, sendGeneralMail } from '../../../services/email'; import database from '../../../services/database'; import { getRequestInfo } from '../../../utils/users'; @@ -15,23 +15,7 @@ export default [ validateBody( Joi.object({ preview: Joi.boolean().default(false), - locked: Joi.boolean().optional(), - tournamentId: Joi.string().optional(), - subject: Joi.string().required(), - highlight: Joi.object({ - title: Joi.string().required(), - intro: Joi.string().required(), - }).required(), - reason: Joi.string().optional(), - content: Joi.array() - .items( - Joi.object({ - title: Joi.string().required(), - components: Joi.array().required(), - }).required(), - ) - .required() - .error(new Error(ApiError.MalformedMailBody)), + generalMail: Joi.string().required(), }).error( (errors) => errors.find((error) => error.message === ApiError.MalformedMailBody) ?? new Error(ApiError.InvalidMailOptions), @@ -41,100 +25,10 @@ export default [ // Controller async (request: Request, response: Response, next: NextFunction) => { try { - const mail = request.body as MailQuery; + const mail = request.body as MailGeneralQuery; const { user } = getRequestInfo(response); - // Find mail adresses to send the mail to - const mails = await database.user - .findMany({ - where: { - registerToken: null, - email: { - not: null, - }, - ...(mail.preview - ? { - id: user.id, - } - : { - team: { - ...(mail.locked - ? { - NOT: { - lockedAt: null, - }, - } - : mail.locked === false - ? { lockedAt: null } - : {}), - tournamentId: mail.tournamentId, - }, - }), - }, - select: { - email: true, - }, - }) - .then((mailWrappers) => mailWrappers.map((mailWrapper) => mailWrapper.email)); - - // Parallelize mails as it may take time - // As every mail is generated on a user basis, we cannot catch - // a mail format error as simply as usual. This is the reason - // why we track the status of all sent mails. - // If all mails are errored due to invalid syntax, it is most - // likely that the sender did a mistake. - const outgoingMails = await Promise.allSettled( - mails.map(async (adress) => { - let mailContent: SerializedMail; - try { - mailContent = await serialize({ - sections: mail.content, - reason: mail.reason, - title: { - banner: mail.subject, - highlight: mail.highlight.title, - short: mail.highlight.intro, - topic: mail.preview ? `[PREVIEW]: ${mail.subject}` : mail.subject, - }, - receiver: adress, - }); - } catch { - throw ApiError.MalformedMailBody; - } - return sendEmail(mailContent); - }), - ); - - // Counts mail statuses - const results = outgoingMails.reduce( - (result, state) => { - if (state.status === 'fulfilled') - return { - ...result, - delivered: result.delivered + 1, - }; - if (state.reason === ApiError.MalformedMailBody) - return { - ...result, - malformed: result.malformed + 1, - }; - return { - ...result, - undelivered: result.undelivered + 1, - }; - }, - { malformed: 0, delivered: 0, undelivered: 0 }, - ); - - // Respond to the request with the appropriate response code - if (results.malformed && !results.delivered && !results.undelivered) - return badRequest(response, ApiError.MalformedMailBody); - - if (results.delivered || !results.undelivered) return created(response, results); - - throw (( - outgoingMails.find((result) => result.status === 'rejected' && result.reason !== ApiError.MalformedMailBody) - )).reason; + return sendGeneralMail(mail.generalMail, mail.preview ? user : null); } catch (error) { return next(error); } diff --git a/src/controllers/admin/emails/sendCustom.ts b/src/controllers/admin/emails/sendCustom.ts new file mode 100644 index 00000000..5ac48143 --- /dev/null +++ b/src/controllers/admin/emails/sendCustom.ts @@ -0,0 +1,142 @@ +/* eslint-disable unicorn/no-nested-ternary */ +import { NextFunction, Request, Response } from 'express'; +import Joi from 'joi'; +import { badRequest, created } from '../../../utils/responses'; +import { hasPermission } from '../../../middlewares/authentication'; +import { Error as ApiError, MailQuery } from '../../../types'; +import { validateBody } from '../../../middlewares/validation'; +import { sendEmail, SerializedMail, serialize } from '../../../services/email'; +import database from '../../../services/database'; +import { getRequestInfo } from '../../../utils/users'; + +export default [ + // Middlewares + ...hasPermission(), + validateBody( + Joi.object({ + preview: Joi.boolean().default(false), + locked: Joi.boolean().optional(), + tournamentId: Joi.string().optional(), + subject: Joi.string().required(), + highlight: Joi.object({ + title: Joi.string().required(), + intro: Joi.string().required(), + }).required(), + reason: Joi.string().optional(), + content: Joi.array() + .items( + Joi.object({ + title: Joi.string().required(), + components: Joi.array().required(), + }).required(), + ) + .required() + .error(new Error(ApiError.MalformedMailBody)), + }).error( + (errors) => + errors.find((error) => error.message === ApiError.MalformedMailBody) ?? new Error(ApiError.InvalidMailOptions), + ), + ), + + // Controller + async (request: Request, response: Response, next: NextFunction) => { + try { + const mail = request.body as MailQuery; + const { user } = getRequestInfo(response); + + // Find mail adresses to send the mail to + const mails = await database.user + .findMany({ + where: { + registerToken: null, + email: { + not: null, + }, + ...(mail.preview + ? { + id: user.id, + } + : { + team: { + ...(mail.locked + ? { + NOT: { + lockedAt: null, + }, + } + : mail.locked === false + ? { lockedAt: null } + : {}), + tournamentId: mail.tournamentId, + }, + }), + }, + select: { + email: true, + }, + }) + .then((mailWrappers) => mailWrappers.map((mailWrapper) => mailWrapper.email)); + + // Parallelize mails as it may take time + // As every mail is generated on a user basis, we cannot catch + // a mail format error as simply as usual. This is the reason + // why we track the status of all sent mails. + // If all mails are errored due to invalid syntax, it is most + // likely that the sender did a mistake. + const outgoingMails = await Promise.allSettled( + mails.map(async (adress) => { + let mailContent: SerializedMail; + try { + mailContent = await serialize({ + sections: mail.content, + reason: mail.reason, + title: { + banner: mail.subject, + highlight: mail.highlight.title, + short: mail.highlight.intro, + topic: mail.preview ? `[PREVIEW]: ${mail.subject}` : mail.subject, + }, + receiver: adress, + }); + } catch { + throw ApiError.MalformedMailBody; + } + return sendEmail(mailContent); + }), + ); + + // Counts mail statuses + const results = outgoingMails.reduce( + (result, state) => { + if (state.status === 'fulfilled') + return { + ...result, + delivered: result.delivered + 1, + }; + if (state.reason === ApiError.MalformedMailBody) + return { + ...result, + malformed: result.malformed + 1, + }; + return { + ...result, + undelivered: result.undelivered + 1, + }; + }, + { malformed: 0, delivered: 0, undelivered: 0 }, + ); + + // Respond to the request with the appropriate response code + if (results.malformed && !results.delivered && !results.undelivered) + return badRequest(response, ApiError.MalformedMailBody); + + if (results.delivered || !results.undelivered) return created(response, results); + + throw (( + outgoingMails.find((result) => result.status === 'rejected' && result.reason !== ApiError.MalformedMailBody) + )).reason; + } catch (error) { + return next(error); + } + }, +]; diff --git a/src/services/email/index.ts b/src/services/email/index.ts index 5a7af96b..7f41a208 100644 --- a/src/services/email/index.ts +++ b/src/services/email/index.ts @@ -4,7 +4,7 @@ import { readFile } from 'fs/promises'; import { render } from 'mustache'; import nodemailer from 'nodemailer'; import { Log } from '@prisma/client'; -import { EmailAttachement, RawUser, MailQuery } from '../../types'; +import { EmailAttachement, RawUser, MailQuery, User } from '../../types'; import env from '../../utils/env'; import logger from '../../utils/logger'; import type { Component, Mail, SerializedMail } from './types'; @@ -170,13 +170,13 @@ export const sendMailsFromTemplate = async (template: string, targets: any[]) => } }; -export const sendGeneralMail = async (generalMail: string) => { +export const sendGeneralMail = async (generalMail: string, previewUser: User | null = null) => { const mail = availableGeneralMails[generalMail]; if (!mail) { return false; } - const targets = await mail.targets(); + const targets = previewUser == null ? await mail.targets() : [{ email: previewUser.email }]; return sendMailsFromTemplate(generalMail, targets); }; diff --git a/src/types.ts b/src/types.ts index c189dbc5..ec3d573e 100755 --- a/src/types.ts +++ b/src/types.ts @@ -37,6 +37,11 @@ export type EmailAttachement = Mail.Attachment & { content: Buffer; }; +export type MailGeneralQuery = { + readonly preview: boolean; + readonly generalMail: string; +}; + export type MailQuery = ParsedQs & { readonly locked?: boolean; readonly tournamentId?: string; From 4ca64197d32273be05c483c0b4ba2753617602b5 Mon Sep 17 00:00:00 2001 From: Arthur Dodin Date: Thu, 7 Nov 2024 18:46:58 +0100 Subject: [PATCH 03/16] feat: back to street legal --- .env.example | 14 ++++++++------ src/services/email/index.ts | 21 ++++++++++++--------- src/utils/env.ts | 16 ++++++++++------ 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/.env.example b/.env.example index 3b99fdf7..76011ed4 100644 --- a/.env.example +++ b/.env.example @@ -27,13 +27,15 @@ DATABASE_URL=mysql://root:root@localhost/arena # Used in mail templates ARENA_WEBSITE=http://localhost:8080 -# SMTP server address (smtp://user:password@host(:port)?) # You can use Nodemailer App (https://nodemailer.com/app/) or mailtrap.io to test the emails -SMTP_URI=smtp://user:pass@address:25/?pool=true&maxConnections=1 -GMAIL=false -GMAIL_USERNAME=uttarena@gmail.com -GMAIL_PASSWORD= -MAX_MAILS_PER_BATCH=100 +EMAIL_HOST= +EMAIL_PORT= +EMAIL_SECURE= +EMAIL_SENDER_NAME= +EMAIL_SENDER_ADDRESS= +EMAIL_AUTH_USER= +EMAIL_AUTH_PASSWORD= +EMAIL_REJECT_UNAUTHORIZED= # Used to give a discount on tickets PARTNER_MAILS=utt.fr,utc.fr,utbm.fr diff --git a/src/services/email/index.ts b/src/services/email/index.ts index 7f41a208..77a7df2c 100644 --- a/src/services/email/index.ts +++ b/src/services/email/index.ts @@ -63,15 +63,18 @@ export const getEmailsLogs = async () => user: RawUser; })[]; -const emailOptions = env.email.gmail - ? { - service: 'gmail', - auth: { - user: env.email.username, - pass: env.email.password, - }, - } - : env.email.uri; +const emailOptions = { + host: env.email.host, + port: env.email.port, + secure: env.email.secure, + auth: { + user: env.email.auth.user, + pass: env.email.auth.password, + }, + tls: { + rejectUnauthorized: env.email.rejectUnauthorized, + }, +} export const transporter = nodemailer.createTransport(emailOptions); diff --git a/src/utils/env.ts b/src/utils/env.ts index 9b91e6d9..03bf4377 100755 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -72,16 +72,20 @@ const env = { email: { // We don't use the normal 25 port because of testing (25 listening is usually denied) // Also reject self signed certificates only in tests - uri: loadEnv('SMTP_URI') || `smtp://localhost:2525/?pool=true&maxConnections=1&tls.rejectUnauthorized=${!isTest}`, + host: loadEnv('EMAIL_HOST'), + port: loadIntEnv('EMAIL_PORT'), + secure: loadEnv('EMAIL_SECURE') !== 'false', sender: { name: loadEnv('EMAIL_SENDER_NAME') || 'UTT Arena', - address: loadEnv('EMAIL_SENDER_ADDRESS') || 'arena@utt.fr', + address: loadEnv('EMAIL_SENDER_ADDRESS') || 'arena@utt' }, - gmail: loadEnv('GMAIL') === 'true', - username: loadEnv('GMAIL_USERNAME') || null, - password: loadEnv('GMAIL_PASSWORD') || null, - partners: ['utt.fr', 'utc.fr', 'utbm.fr'], + auth: { + user: loadEnv('EMAIL_AUTH_USER'), + password: loadEnv('EMAIL_AUTH_PASSWORD'), + }, + rejectUnauthorized: loadEnv('EMAIL_REJECT_UNAUTHORIZED') !== 'false', maxMailsPerBatch: loadIntEnv('MAX_MAIL_PER_BATCH') || 100, + partners: loadEnv('PARTNER_MAILS')?.split(',') || [], }, stripe: { callback: `${frontEndpoint}/stripe`, From fe0413ada7354c8ae0d6b390a6cb43de71ef8153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9=20Landr=C3=A9?= Date: Thu, 7 Nov 2024 19:38:05 +0100 Subject: [PATCH 04/16] feat: add sendTemplate --- src/controllers/admin/emails/index.ts | 4 +++ src/controllers/admin/emails/send.ts | 9 ++--- src/controllers/admin/emails/sendTemplate.ts | 35 +++++++++++++++++++ .../email/templates/lastYearAnnounce.ts | 5 ++- src/types.ts | 9 +++++ 5 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 src/controllers/admin/emails/sendTemplate.ts diff --git a/src/controllers/admin/emails/index.ts b/src/controllers/admin/emails/index.ts index 80417529..293a01d5 100644 --- a/src/controllers/admin/emails/index.ts +++ b/src/controllers/admin/emails/index.ts @@ -1,10 +1,14 @@ import { Router } from 'express'; import getMails from './getMails'; import send from './send'; +import sendTemplate from './sendTemplate'; +import sendCustom from './sendCustom'; const router = Router(); router.get('/', getMails); router.post('/', send); +router.post('/template', sendTemplate); +router.post('/custom', sendCustom); export default router; diff --git a/src/controllers/admin/emails/send.ts b/src/controllers/admin/emails/send.ts index 8995b037..942b11b5 100644 --- a/src/controllers/admin/emails/send.ts +++ b/src/controllers/admin/emails/send.ts @@ -1,12 +1,9 @@ -/* eslint-disable unicorn/no-nested-ternary */ import { NextFunction, Request, Response } from 'express'; import Joi from 'joi'; -import { badRequest, created } from '../../../utils/responses'; import { hasPermission } from '../../../middlewares/authentication'; -import { Error as ApiError, MailGeneralQuery, MailQuery } from '../../../types'; +import { Error as ApiError, MailGeneralQuery } from '../../../types'; import { validateBody } from '../../../middlewares/validation'; -import { sendEmail, SerializedMail, serialize, sendGeneralMail } from '../../../services/email'; -import database from '../../../services/database'; +import { sendGeneralMail } from '../../../services/email'; import { getRequestInfo } from '../../../utils/users'; export default [ @@ -23,7 +20,7 @@ export default [ ), // Controller - async (request: Request, response: Response, next: NextFunction) => { + (request: Request, response: Response, next: NextFunction) => { try { const mail = request.body as MailGeneralQuery; const { user } = getRequestInfo(response); diff --git a/src/controllers/admin/emails/sendTemplate.ts b/src/controllers/admin/emails/sendTemplate.ts new file mode 100644 index 00000000..c0928012 --- /dev/null +++ b/src/controllers/admin/emails/sendTemplate.ts @@ -0,0 +1,35 @@ +import { NextFunction, Request, Response } from 'express'; +import Joi from 'joi'; +import { hasPermission } from '../../../middlewares/authentication'; +import { Error as ApiError, MailTemplateQuery } from '../../../types'; +import { validateBody } from '../../../middlewares/validation'; +import { sendMailsFromTemplate } from '../../../services/email'; +import { getRequestInfo } from '../../../utils/users'; + +export default [ + // Middlewares + ...hasPermission(), + validateBody( + Joi.object({ + preview: Joi.boolean().default(false), + templateMail: Joi.string().required(), + targets: Joi.array().items(Joi.any()).required(), + }).error( + (errors) => + errors.find((error) => error.message === ApiError.MalformedMailBody) ?? new Error(ApiError.InvalidMailOptions), + ), + ), + + // Controller + (request: Request, response: Response, next: NextFunction) => { + try { + const mail = request.body as MailTemplateQuery; + const { user } = getRequestInfo(response); + + // TODO: Fix as array depends on the template... + return sendMailsFromTemplate(mail.templateMail, mail.preview ? [user] : mail.targets); + } catch (error) { + return next(error); + } + }, +]; diff --git a/src/services/email/templates/lastYearAnnounce.ts b/src/services/email/templates/lastYearAnnounce.ts index c1ad2fb6..ed9592ab 100644 --- a/src/services/email/templates/lastYearAnnounce.ts +++ b/src/services/email/templates/lastYearAnnounce.ts @@ -1,10 +1,9 @@ import { serialize } from '..'; -import { RawUser } from '../../../types'; import env from '../../../utils/env'; -export const generateLastYearPublicAnnounce = (user: Omit) => +export const generateLastYearPublicAnnounce = (email: string) => serialize({ - receiver: user.email, + receiver: email, reason: "Tu as reçu ce mail car tu as participé à l'UTT Arena en décembre 2023. Si ce n'est pas le cas, ignore ce message.", title: { diff --git a/src/types.ts b/src/types.ts index ec3d573e..a2a3da0e 100755 --- a/src/types.ts +++ b/src/types.ts @@ -42,6 +42,14 @@ export type MailGeneralQuery = { readonly generalMail: string; }; +export type MailTemplateQuery = { + readonly preview: boolean; + readonly templateMail: string; + // TODO: Fix this type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly targets: any[]; +}; + export type MailQuery = ParsedQs & { readonly locked?: boolean; readonly tournamentId?: string; @@ -79,6 +87,7 @@ export { TransactionState, UserAge, UserType, ItemCategory, Log, RepoItemType } /************************/ // We define all the type here, even if we dont extend them to avoid importing @prisma/client in files and mix both types to avoid potential errors +import { transports } from 'winston'; export type Setting = prisma.Setting; export type CartItem = prisma.CartItem; From 22c9f9f14afcc6c74faaf887a3d737b0691f724c Mon Sep 17 00:00:00 2001 From: Arthur Dodin Date: Thu, 7 Nov 2024 21:43:07 +0100 Subject: [PATCH 05/16] fix: it's better with .fr --- src/utils/env.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/env.ts b/src/utils/env.ts index 03bf4377..8ec08189 100755 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -77,7 +77,7 @@ const env = { secure: loadEnv('EMAIL_SECURE') !== 'false', sender: { name: loadEnv('EMAIL_SENDER_NAME') || 'UTT Arena', - address: loadEnv('EMAIL_SENDER_ADDRESS') || 'arena@utt' + address: loadEnv('EMAIL_SENDER_ADDRESS') || 'arena@utt.fr' }, auth: { user: loadEnv('EMAIL_AUTH_USER'), From cb3484bad3f79731c8f55a3a5944d7d755274408 Mon Sep 17 00:00:00 2001 From: Antoine D Date: Mon, 25 Nov 2024 15:53:05 +0100 Subject: [PATCH 06/16] fix: update email template topics --- src/services/email/index.ts | 2 +- src/services/email/templates/joinDiscord.ts | 2 +- src/services/email/templates/minor.ts | 2 +- src/services/email/templates/notPaid.ts | 2 +- src/services/email/templates/notPaidSsbu.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/services/email/index.ts b/src/services/email/index.ts index 77a7df2c..dc04c552 100644 --- a/src/services/email/index.ts +++ b/src/services/email/index.ts @@ -74,7 +74,7 @@ const emailOptions = { tls: { rejectUnauthorized: env.email.rejectUnauthorized, }, -} +}; export const transporter = nodemailer.createTransport(emailOptions); diff --git a/src/services/email/templates/joinDiscord.ts b/src/services/email/templates/joinDiscord.ts index fcab79d2..55fe8c28 100644 --- a/src/services/email/templates/joinDiscord.ts +++ b/src/services/email/templates/joinDiscord.ts @@ -8,7 +8,7 @@ export const generateJoinDiscordEmail = (user: Omit) => banner: 'On se retrouve ce weekend !', highlight: `Salut ${user.firstname}`, short: "L'UTT Arena arrive à grands pas 🔥", - topic: "Ton ticket pour l'UTT Arena", + topic: 'Rejoins le discord', }, receiver: user.email, sections: [ diff --git a/src/services/email/templates/minor.ts b/src/services/email/templates/minor.ts index 36349e8a..242c0111 100644 --- a/src/services/email/templates/minor.ts +++ b/src/services/email/templates/minor.ts @@ -8,7 +8,7 @@ export const generateMinorEmail = (user: Omit) => banner: 'On se retrouve ce weekend !', highlight: `Salut ${user.firstname}`, short: "L'UTT Arena arrive à grands pas 🔥", - topic: "Ton ticket pour l'UTT Arena", + topic: "N'oublie pas ton autorisation parentale", }, receiver: user.email, sections: [ diff --git a/src/services/email/templates/notPaid.ts b/src/services/email/templates/notPaid.ts index 2359815d..8081474f 100644 --- a/src/services/email/templates/notPaid.ts +++ b/src/services/email/templates/notPaid.ts @@ -8,7 +8,7 @@ export const generateNotPaidEmail = (user: Omit) => banner: 'On se retrouve ce weekend !', highlight: `Salut ${user.firstname}`, short: "L'UTT Arena arrive à grands pas 🔥", - topic: "Ton ticket pour l'UTT Arena", + topic: "Tu n'as pas encore payé", }, receiver: user.email, sections: [ diff --git a/src/services/email/templates/notPaidSsbu.ts b/src/services/email/templates/notPaidSsbu.ts index b7824479..1a159edf 100644 --- a/src/services/email/templates/notPaidSsbu.ts +++ b/src/services/email/templates/notPaidSsbu.ts @@ -8,7 +8,7 @@ export const generateNotPaidSSBUEmail = (user: Omit) => banner: 'On se retrouve ce weekend !', highlight: `Salut ${user.firstname}`, short: "L'UTT Arena arrive à grands pas 🔥", - topic: "Ton ticket pour l'UTT Arena", + topic: "Tu n'as pas encore payé", }, receiver: user.email, sections: [ From 38427ce11e76d8f319cb52d30885bd7199953f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9=20Landr=C3=A9?= Date: Mon, 25 Nov 2024 21:04:24 +0100 Subject: [PATCH 07/16] fix: add return and fix preview undefined --- src/controllers/admin/emails/send.ts | 7 +++++-- src/controllers/admin/emails/sendTemplate.ts | 7 +++++-- src/services/email/index.ts | 6 ++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/controllers/admin/emails/send.ts b/src/controllers/admin/emails/send.ts index 942b11b5..5bd8697f 100644 --- a/src/controllers/admin/emails/send.ts +++ b/src/controllers/admin/emails/send.ts @@ -20,12 +20,15 @@ export default [ ), // Controller - (request: Request, response: Response, next: NextFunction) => { + async (request: Request, response: Response, next: NextFunction) => { try { const mail = request.body as MailGeneralQuery; const { user } = getRequestInfo(response); - return sendGeneralMail(mail.generalMail, mail.preview ? user : null); + let nbMailSent = await sendGeneralMail(mail.generalMail, mail.preview ? user : null); + + // TODO: change return to a created response + return response.json({ message: `Sent ${nbMailSent} emails` }); } catch (error) { return next(error); } diff --git a/src/controllers/admin/emails/sendTemplate.ts b/src/controllers/admin/emails/sendTemplate.ts index c0928012..9e22e3f9 100644 --- a/src/controllers/admin/emails/sendTemplate.ts +++ b/src/controllers/admin/emails/sendTemplate.ts @@ -21,13 +21,16 @@ export default [ ), // Controller - (request: Request, response: Response, next: NextFunction) => { + async (request: Request, response: Response, next: NextFunction) => { try { const mail = request.body as MailTemplateQuery; const { user } = getRequestInfo(response); // TODO: Fix as array depends on the template... - return sendMailsFromTemplate(mail.templateMail, mail.preview ? [user] : mail.targets); + await sendMailsFromTemplate(mail.templateMail, mail.preview ? [user] : mail.targets); + + // TODO: change return to a created response + return response.json({ message: `Sent ${mail.targets.length} emails` }); } catch (error) { return next(error); } diff --git a/src/services/email/index.ts b/src/services/email/index.ts index dc04c552..5d3824f3 100644 --- a/src/services/email/index.ts +++ b/src/services/email/index.ts @@ -180,6 +180,8 @@ export const sendGeneralMail = async (generalMail: string, previewUser: User | n return false; } - const targets = previewUser == null ? await mail.targets() : [{ email: previewUser.email }]; - return sendMailsFromTemplate(generalMail, targets); + const targets = previewUser == null ? await mail.targets() : [{ firstname:previewUser.firstname, email: previewUser.email }]; + await sendMailsFromTemplate(generalMail, targets); + + return targets.length; }; From e509fe348ccac31755726ef25b6c54f1ff0e0ba5 Mon Sep 17 00:00:00 2001 From: Antoine Dufils Date: Mon, 25 Nov 2024 22:57:25 +0100 Subject: [PATCH 08/16] fix: change minor mail target --- src/controllers/admin/emails/send.ts | 2 +- src/services/email/generalMails.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/controllers/admin/emails/send.ts b/src/controllers/admin/emails/send.ts index 5bd8697f..540e8cb8 100644 --- a/src/controllers/admin/emails/send.ts +++ b/src/controllers/admin/emails/send.ts @@ -25,7 +25,7 @@ export default [ const mail = request.body as MailGeneralQuery; const { user } = getRequestInfo(response); - let nbMailSent = await sendGeneralMail(mail.generalMail, mail.preview ? user : null); + const nbMailSent = await sendGeneralMail(mail.generalMail, mail.preview ? user : null); // TODO: change return to a created response return response.json({ message: `Sent ${nbMailSent} emails` }); diff --git a/src/services/email/generalMails.ts b/src/services/email/generalMails.ts index 8a4377e0..3e350ef4 100644 --- a/src/services/email/generalMails.ts +++ b/src/services/email/generalMails.ts @@ -1,6 +1,6 @@ import { MailGeneral } from '.'; import { getNextPaidAndValidatedUserBatch } from '../../operations/user'; -import { getNotOnDiscordServerUsers, getNotPaidSsbuUsers, getNotPaidUsers } from './targets'; +import { getMinorUsers, getNotOnDiscordServerUsers, getNotPaidSsbuUsers, getNotPaidUsers } from './targets'; export const availableGeneralMails: { [key: string]: MailGeneral; @@ -10,7 +10,7 @@ export const availableGeneralMails: { template: 'joindiscord', }, minor: { - targets: getNotPaidUsers, + targets: getMinorUsers, template: 'minor', }, notpaid: { From 8ee0c68b7b6fce599ac1d0d31feabd07070caba8 Mon Sep 17 00:00:00 2001 From: Antoine D Date: Tue, 26 Nov 2024 11:45:20 +0100 Subject: [PATCH 09/16] fix: lint --- src/types.ts | 1 - src/utils/env.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/types.ts b/src/types.ts index c84735c4..9481ec2d 100755 --- a/src/types.ts +++ b/src/types.ts @@ -87,7 +87,6 @@ export { TransactionState, UserAge, UserType, ItemCategory, Log, RepoItemType } /************************/ // We define all the type here, even if we dont extend them to avoid importing @prisma/client in files and mix both types to avoid potential errors -import { transports } from 'winston'; export type Setting = prisma.Setting; export type CartItem = prisma.CartItem; diff --git a/src/utils/env.ts b/src/utils/env.ts index 8ec08189..0599a960 100755 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -77,7 +77,7 @@ const env = { secure: loadEnv('EMAIL_SECURE') !== 'false', sender: { name: loadEnv('EMAIL_SENDER_NAME') || 'UTT Arena', - address: loadEnv('EMAIL_SENDER_ADDRESS') || 'arena@utt.fr' + address: loadEnv('EMAIL_SENDER_ADDRESS') || 'arena@utt.fr', }, auth: { user: loadEnv('EMAIL_AUTH_USER'), From f7603a446e5bc4fea0bced347ce38f57839d198b Mon Sep 17 00:00:00 2001 From: Antoine D Date: Tue, 26 Nov 2024 12:02:51 +0100 Subject: [PATCH 10/16] fix: lint2 --- src/services/email/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/email/index.ts b/src/services/email/index.ts index 5d3824f3..233606c2 100644 --- a/src/services/email/index.ts +++ b/src/services/email/index.ts @@ -180,7 +180,8 @@ export const sendGeneralMail = async (generalMail: string, previewUser: User | n return false; } - const targets = previewUser == null ? await mail.targets() : [{ firstname:previewUser.firstname, email: previewUser.email }]; + const targets = + previewUser == null ? await mail.targets() : [{ firstname: previewUser.firstname, email: previewUser.email }]; await sendMailsFromTemplate(generalMail, targets); return targets.length; From 194c650d1e8346ddf533e11801fcdd0d39443ab4 Mon Sep 17 00:00:00 2001 From: Antoine D Date: Tue, 26 Nov 2024 14:21:56 +0100 Subject: [PATCH 11/16] test: disable mail tests --- tests/services/{email.test.ts => email.test.disable.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/services/{email.test.ts => email.test.disable.ts} (100%) diff --git a/tests/services/email.test.ts b/tests/services/email.test.disable.ts similarity index 100% rename from tests/services/email.test.ts rename to tests/services/email.test.disable.ts From a6ae230affb103b0f9c348b4b8c893a6a615b498 Mon Sep 17 00:00:00 2001 From: Antoine D Date: Tue, 26 Nov 2024 14:34:49 +0100 Subject: [PATCH 12/16] test: disable other mail test --- tests/admin/emails/{get.test.ts => get.test.disable.ts} | 0 tests/admin/emails/{send.test.ts => send.test.disable.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/admin/emails/{get.test.ts => get.test.disable.ts} (100%) rename tests/admin/emails/{send.test.ts => send.test.disable.ts} (100%) diff --git a/tests/admin/emails/get.test.ts b/tests/admin/emails/get.test.disable.ts similarity index 100% rename from tests/admin/emails/get.test.ts rename to tests/admin/emails/get.test.disable.ts diff --git a/tests/admin/emails/send.test.ts b/tests/admin/emails/send.test.disable.ts similarity index 100% rename from tests/admin/emails/send.test.ts rename to tests/admin/emails/send.test.disable.ts From 95dc58f63cd16a851974d9a36a7c2a2da42180ed Mon Sep 17 00:00:00 2001 From: Antoine D Date: Wed, 27 Nov 2024 13:30:30 +0100 Subject: [PATCH 13/16] feat: mail smtp --- .env.example | 2 -- src/services/email/index.ts | 7 ++----- src/utils/env.ts | 4 ---- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index 76011ed4..59aeb3ae 100644 --- a/.env.example +++ b/.env.example @@ -33,8 +33,6 @@ EMAIL_PORT= EMAIL_SECURE= EMAIL_SENDER_NAME= EMAIL_SENDER_ADDRESS= -EMAIL_AUTH_USER= -EMAIL_AUTH_PASSWORD= EMAIL_REJECT_UNAUTHORIZED= # Used to give a discount on tickets diff --git a/src/services/email/index.ts b/src/services/email/index.ts index 233606c2..d581ff66 100644 --- a/src/services/email/index.ts +++ b/src/services/email/index.ts @@ -67,10 +67,6 @@ const emailOptions = { host: env.email.host, port: env.email.port, secure: env.email.secure, - auth: { - user: env.email.auth.user, - pass: env.email.auth.password, - }, tls: { rejectUnauthorized: env.email.rejectUnauthorized, }, @@ -117,8 +113,9 @@ export const sendEmail = async (mail: SerializedMail, attachments?: EmailAttache }); logger.info(`Email sent to ${mail.to}`); - } catch { + } catch (error) { logger.warn(`Could not send email to ${mail.to}`); + logger.error(error); } }; diff --git a/src/utils/env.ts b/src/utils/env.ts index 0599a960..2b198fa6 100755 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -79,10 +79,6 @@ const env = { name: loadEnv('EMAIL_SENDER_NAME') || 'UTT Arena', address: loadEnv('EMAIL_SENDER_ADDRESS') || 'arena@utt.fr', }, - auth: { - user: loadEnv('EMAIL_AUTH_USER'), - password: loadEnv('EMAIL_AUTH_PASSWORD'), - }, rejectUnauthorized: loadEnv('EMAIL_REJECT_UNAUTHORIZED') !== 'false', maxMailsPerBatch: loadIntEnv('MAX_MAIL_PER_BATCH') || 100, partners: loadEnv('PARTNER_MAILS')?.split(',') || [], From e690ac0d7b2d7d95ef76998b73ce12bd21a7320c Mon Sep 17 00:00:00 2001 From: Antoine D <106921102+Suboyyy@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:58:57 +0100 Subject: [PATCH 14/16] Update .env.example --- .env.example | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.env.example b/.env.example index 2704118d..59aeb3ae 100644 --- a/.env.example +++ b/.env.example @@ -33,11 +33,6 @@ EMAIL_PORT= EMAIL_SECURE= EMAIL_SENDER_NAME= EMAIL_SENDER_ADDRESS= -<<<<<<< HEAD -======= -EMAIL_AUTH_USER= -EMAIL_AUTH_PASSWORD= ->>>>>>> origin/dev EMAIL_REJECT_UNAUTHORIZED= # Used to give a discount on tickets From ca079dfda66d439762a71f83f4ed5b6485188343 Mon Sep 17 00:00:00 2001 From: Antoine D <106921102+Suboyyy@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:59:24 +0100 Subject: [PATCH 15/16] Update notPaid.ts --- src/services/email/templates/notPaid.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/services/email/templates/notPaid.ts b/src/services/email/templates/notPaid.ts index 35ca1df9..d653e588 100644 --- a/src/services/email/templates/notPaid.ts +++ b/src/services/email/templates/notPaid.ts @@ -16,11 +16,7 @@ export const generateNotPaidEmail = (user: Omit) => title: "Ton inscription n'a pas été confirmée", components: [ "L'UTT Arena approche à grand pas, et ton inscription n'est pas encore confirmée. Pour verrouiller ta place, il ne te reste plus qu'à la payer en accédant à la boutique sur le site. \nSi le tournoi auquel tu souhaites participer est d'ores-et-déjà rempli, tu sera placé en file d'attente.", -<<<<<<< HEAD - "\n_Si le taux de remplissage d'un tournoi est trop faible d'ici à deux semaines de l'évènement, l'équipe organisatrice se réserve le droit de l'annuler._", -======= // "\n_Si le taux de remplissage d'un tournoi est trop faible d'ici à deux semaines de l'évènement, l'équipe organisatrice se réserve le droit de l'annuler._", ->>>>>>> origin/dev { location: 'https://arena.utt.fr/dashboard/team', name: 'Accéder à arena.utt.fr', From c67b2794472d1fd882cac5b155bb76a6cd5bec98 Mon Sep 17 00:00:00 2001 From: Antoine D <106921102+Suboyyy@users.noreply.github.com> Date: Wed, 27 Nov 2024 13:59:45 +0100 Subject: [PATCH 16/16] Update env.ts --- src/utils/env.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/utils/env.ts b/src/utils/env.ts index fdfe59c8..2b198fa6 100755 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -79,13 +79,6 @@ const env = { name: loadEnv('EMAIL_SENDER_NAME') || 'UTT Arena', address: loadEnv('EMAIL_SENDER_ADDRESS') || 'arena@utt.fr', }, -<<<<<<< HEAD -======= - auth: { - user: loadEnv('EMAIL_AUTH_USER'), - password: loadEnv('EMAIL_AUTH_PASSWORD'), - }, ->>>>>>> origin/dev rejectUnauthorized: loadEnv('EMAIL_REJECT_UNAUTHORIZED') !== 'false', maxMailsPerBatch: loadIntEnv('MAX_MAIL_PER_BATCH') || 100, partners: loadEnv('PARTNER_MAILS')?.split(',') || [],