diff --git a/FrontendV2/src/pages/ContactPage.tsx b/FrontendV2/src/pages/ContactPage.tsx index 6e62c29..6c27775 100644 --- a/FrontendV2/src/pages/ContactPage.tsx +++ b/FrontendV2/src/pages/ContactPage.tsx @@ -5,6 +5,8 @@ import { Alert, Container, Text, + VStack, + Spinner, } from "@chakra-ui/react"; import { useTranslation } from "react-i18next"; import { useState } from "react"; @@ -21,9 +23,21 @@ interface Alert { export const ContactPage = () => { const { t } = useTranslation(); const [message, setMessage] = useState(""); + const [isSending, setIsSending] = useState(false); const [alert, setAlert] = useState(null); const sendMessage = async () => { + setIsSending(true); + if (message.trim() === "") { + setAlert({ + type: "error", + headline: t("contactPage_messageErrorHeadline"), + text: t("contactPage_messageErrorText2"), + }); + setIsSending(false); + return; + } + // Logic to send the message const result = await fetch(`${API_BASE}/api/users/contact`, { method: "POST", @@ -55,6 +69,7 @@ export const ContactPage = () => { text: t("contactPage_errorText"), }); } + setIsSending(false); }; return ( @@ -84,6 +99,12 @@ export const ContactPage = () => { )} + {isSending && ( + + + {t("loading")} + + )} ); diff --git a/FrontendV2/src/pages/HomePage.tsx b/FrontendV2/src/pages/HomePage.tsx index 17afd4d..835006d 100644 --- a/FrontendV2/src/pages/HomePage.tsx +++ b/FrontendV2/src/pages/HomePage.tsx @@ -133,12 +133,6 @@ export const HomePage = () => { > {t("get-borrowable-items")} - {isLoadingA && ( - - - {t("loading")} - - )} {borrowableItems.length > 0 && ( @@ -191,9 +185,11 @@ export const HomePage = () => { )} {selectedItems.length >= 1 && ( )} + {isLoadingA && ( + + + {t("loading")} + + )} diff --git a/FrontendV2/src/utils/i18n/locales/de/de.json b/FrontendV2/src/utils/i18n/locales/de/de.json index 0de2e7e..e385379 100644 --- a/FrontendV2/src/utils/i18n/locales/de/de.json +++ b/FrontendV2/src/utils/i18n/locales/de/de.json @@ -99,5 +99,7 @@ "contactPage_serviceDeactivatedText": "Der Kontaktservice ist derzeit deaktiviert. Bitte versuchen Sie es später erneut.", "loan_page_serviceDeactivatedText": "Der Ausleihservice ist derzeit deaktiviert. Bitte versuchen Sie es später erneut.", "is-deactivated": "ist deaktiviert.", - "deactivated-services": "Deaktivierte Services" + "deactivated-services": "Deaktivierte Services", + "contactPage_messageErrorHeadline": "Fehler bei der Nachrichteneingabe", + "contactPage_messageErrorText2": "Bitte geben Sie eine Nachricht ein, bevor Sie sie senden." } \ No newline at end of file diff --git a/FrontendV2/src/utils/i18n/locales/en/en.json b/FrontendV2/src/utils/i18n/locales/en/en.json index c44d4fa..33a99d9 100644 --- a/FrontendV2/src/utils/i18n/locales/en/en.json +++ b/FrontendV2/src/utils/i18n/locales/en/en.json @@ -99,5 +99,7 @@ "contactPage_serviceDeactivatedText": "The contact service is currently deactivated. Please try again later.", "loan_page_serviceDeactivatedText": "The loan service is currently deactivated. Please try again later.", "is-deactivated": "is deactivated.", - "deactivated-services": "Deactivated services" + "deactivated-services": "Deactivated services", + "contactPage_messageErrorHeadline": "Error submitting message", + "contactPage_messageErrorText2": "Please enter a message before sending it." } \ No newline at end of file diff --git a/backendV2/package-lock.json b/backendV2/package-lock.json index aeb4f7d..67aff2b 100644 --- a/backendV2/package-lock.json +++ b/backendV2/package-lock.json @@ -16,7 +16,7 @@ "express-rate-limit": "^8.4.1", "jose": "^6.0.12", "mysql2": "^3.14.3", - "nodemailer": "^7.0.6" + "nodemailer": "^8.0.6" } }, "node_modules/accepts": { @@ -54,29 +54,49 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -528,18 +548,6 @@ "node": ">= 0.8" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -684,9 +692,9 @@ } }, "node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -759,9 +767,9 @@ } }, "node_modules/nodemailer": { - "version": "7.0.10", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.10.tgz", - "integrity": "sha512-Us/Se1WtT0ylXgNFfyFSx4LElllVLJXQjWi2Xz17xWw7amDKO2MLtFnVp1WACy7GkVGs+oBlRopVNUzlrGSw1w==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.6.tgz", + "integrity": "sha512-Nm2XeuDwwy2wi5A+8jPWwQwNzcjNjhWdE3pVLoXEusxJqCnAPAgnBGkSmiLknbnWuOF9qraRpYZjfxqtKZ4tPw==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -819,9 +827,9 @@ } }, "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", "funding": { "type": "opencollective", @@ -848,9 +856,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" diff --git a/backendV2/package.json b/backendV2/package.json index c16054e..de1003c 100644 --- a/backendV2/package.json +++ b/backendV2/package.json @@ -18,6 +18,6 @@ "express-rate-limit": "^8.4.1", "jose": "^6.0.12", "mysql2": "^3.14.3", - "nodemailer": "^7.0.6" + "nodemailer": "^8.0.6" } } diff --git a/backendV2/routes/app/loanMgmt.route.js b/backendV2/routes/app/loanMgmt.route.js index ad1126a..ef1d63c 100644 --- a/backendV2/routes/app/loanMgmt.route.js +++ b/backendV2/routes/app/loanMgmt.route.js @@ -4,9 +4,14 @@ import { checkIfServiceIsActive, checkIfServiceIsActive2, } from "../../services/functions.js"; -const router = express.Router(); + +// mailer imports +import { sendMail } from "../../services/mailer/send.js"; +import { loanMail } from "../../services/mailer/templates/loan_created.js"; + import dotenv from "dotenv"; dotenv.config(); +const router = express.Router(); const loan_service = "Loan Service"; const loan_mailer_service = "Loan Mailer"; @@ -23,7 +28,6 @@ import { setReturnDate, setTakeDate, } from "./database/loansMgmt.database.js"; -import { sendMailLoan } from "./services/mailer.js"; router.post( "/createLoan", @@ -63,19 +67,24 @@ router.post( note, itemIds, ); - if (result.success) { if (await checkIfServiceIsActive2(loan_mailer_service)) { const mailInfo = await getLoanInfoWithID(result.data.id); console.log(mailInfo); - sendMailLoan( - mailInfo.data.username, + const { html, text } = loanMail( + req.user.first_name + " " + req.user.last_name, mailInfo.data.loaned_items_name, mailInfo.data.start_date, mailInfo.data.end_date, mailInfo.data.created_at, mailInfo.data.note, ); + await sendMail({ + to: process.env.MAIL_SENDEES, + subject: "Neue Ausleihe erstellt!", + html, + text, + }); } return res.status(201).json({ diff --git a/backendV2/routes/app/services/mailer.js b/backendV2/routes/app/services/mailer.js deleted file mode 100644 index 3a081d6..0000000 --- a/backendV2/routes/app/services/mailer.js +++ /dev/null @@ -1,215 +0,0 @@ -import nodemailer from "nodemailer"; -import dotenv from "dotenv"; -dotenv.config(); - -const formatDateTime = (value) => { - if (value == null) return "N/A"; - - const toOut = (d) => { - if (!(d instanceof Date) || isNaN(d.getTime())) return "N/A"; - const dd = String(d.getDate()).padStart(2, "0"); - const mm = String(d.getMonth() + 1).padStart(2, "0"); - const yyyy = d.getFullYear(); - const hh = String(d.getHours()).padStart(2, "0"); - const mi = String(d.getMinutes()).padStart(2, "0"); - return `${dd}.${mm}.${yyyy} ${hh}:${mi} Uhr`; - }; - - if (value instanceof Date) return toOut(value); - if (typeof value === "number") return toOut(new Date(value)); - - const s = String(value).trim(); - - // Direct pattern: "YYYY-MM-DD[ T]HH:mm[:ss]" - const m = s.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::\d{2})?/); - if (m) { - const [, y, M, d, h, min] = m; - return `${d}.${M}.${y} ${h}:${min} Uhr`; - } - - // ISO or other parseable formats - const dObj = new Date(s); - if (!isNaN(dObj.getTime())) return toOut(dObj); - - return "N/A"; -}; - -function buildLoanEmail({ - user, - items, - startDate, - endDate, - createdDate, - note, -}) { - const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9"; - const itemsList = - Array.isArray(items) && items.length - ? `
    ${items - .map( - (i) => - `
  • ${i}
  • `, - ) - .join("")}
` - : "N/A"; - - return ` - - - - - - - - - - - -
- Neue Ausleihe erstellt – Übersicht der Buchung. -
-
- - - - - - - - -
- -`; -} - -function buildLoanEmailText({ - user, - items, - startDate, - endDate, - createdDate, - note, -}) { - const itemsText = - Array.isArray(items) && items.length ? items.join(", ") : "N/A"; - return [ - "Neue Ausleihe erstellt", - "", - `Benutzer: ${user || "N/A"}`, - `Gegenstände: ${itemsText}`, - `Start: ${formatDateTime(startDate)}`, - `Ende: ${formatDateTime(endDate)}`, - `Erstellt am: ${formatDateTime(createdDate)}`, - `Notiz: ${note || "Keine Notiz"}`, - ].join("\n"); -} - -export function sendMailLoan( - user, - items, - startDate, - endDate, - createdDate, - note, -) { - const transporter = nodemailer.createTransport({ - host: process.env.MAIL_HOST, - port: process.env.MAIL_PORT, - secure: true, - auth: { - user: process.env.MAIL_USER, - pass: process.env.MAIL_PASSWORD, - }, - }); - - (async () => { - const info = await transporter.sendMail({ - from: '"Ausleihsystem" ', - to: process.env.MAIL_SENDEES, - subject: "Eine neue Ausleihe wurde erstellt!", - text: buildLoanEmailText({ - user, - items, - startDate, - endDate, - createdDate, - note, - }), - html: buildLoanEmail({ - user, - items, - startDate, - endDate, - createdDate, - note, - }), - }); - - console.log("Loan message sent:", info.messageId); - })(); -} diff --git a/backendV2/routes/app/services/mailer_v2.js b/backendV2/routes/app/services/mailer_v2.js deleted file mode 100644 index 4a3ed70..0000000 --- a/backendV2/routes/app/services/mailer_v2.js +++ /dev/null @@ -1,43 +0,0 @@ -import nodemailer from "nodemailer"; -import dotenv from "dotenv"; -dotenv.config(); - -export function sendMail(username, message) { - const transporter = nodemailer.createTransport({ - host: process.env.MAIL_HOST, - port: process.env.MAIL_PORT, - secure: true, - auth: { - user: process.env.MAIL_USER, - pass: process.env.MAIL_PASSWORD, - }, - }); - - (async () => { - const mailText = `Neue Kontaktanfrage im Ausleihsystem.\n\nBenutzername: ${username}\n\nNachricht:\n${message}`; - - const mailHtml = ` - - - - Neue Nachricht im Ausleihsystem - - -

Neue Nachricht im Ausleihsystem

-

Benutzername: ${username}

-

Nachricht:

-

${message}

- -`; - - const info = await transporter.sendMail({ - from: '"Ausleihsystem" ', - to: process.env.MAIL_SENDEES_CONTACT, - subject: "Sie haben eine neue Nachricht!", - text: mailText, - html: mailHtml, - }); - - console.log("Contact message sent: %s", info.messageId); - })(); -} diff --git a/backendV2/routes/app/userMgmt.route.js b/backendV2/routes/app/userMgmt.route.js index e1eec7a..b05a0ec 100644 --- a/backendV2/routes/app/userMgmt.route.js +++ b/backendV2/routes/app/userMgmt.route.js @@ -14,7 +14,10 @@ import { changePassword, getDeactivatedServices, } from "./database/userMgmt.database.js"; -import { sendMail } from "./services/mailer_v2.js"; + +// mailer imports +import { sendMail } from "../../services/mailer/send.js"; +import { contactMail } from "../../services/mailer/templates/contact.js"; router.post( "/login", @@ -58,12 +61,29 @@ router.post( checkIfServiceIsActive(contact_form_service), authenticate, async (req, res) => { - const message = req.body.message; - const username = req.user.username; + try { + const message = req.body?.message; + const username = req.user?.first_name + " " + req.user?.last_name; - sendMail(username, message); + if (!username || !message) { + return res + .status(400) + .json({ message: "Username and message are required" }); + } - res.status(200).json({ message: "Contact message sent successfully" }); + const { html, text } = contactMail({ username, message }); + await sendMail({ + to: process.env.MAIL_SENDEES_CONTACT, + subject: "Neue Nachricht!", + html, + text, + }); + + res.status(200).json({ message: "Contact message sent successfully" }); + } catch (error) { + console.error("Failed to send contact mail:", error); + res.status(500).json({ message: "Failed to send contact message" }); + } }, ); diff --git a/backendV2/services/mailer/send.js b/backendV2/services/mailer/send.js new file mode 100644 index 0000000..1de4643 --- /dev/null +++ b/backendV2/services/mailer/send.js @@ -0,0 +1,13 @@ +import { transporter } from "./transporter.js"; + +export async function sendMail({ to, subject, text, html }) { + const info = await transporter.sendMail({ + from: '"Ausleihsystem" ', + to, + subject, + text, + html, + }); + console.log("Mail sent:", info.messageId); + return info; +} diff --git a/backendV2/services/mailer/templates/contact.js b/backendV2/services/mailer/templates/contact.js new file mode 100644 index 0000000..7977a1c --- /dev/null +++ b/backendV2/services/mailer/templates/contact.js @@ -0,0 +1,76 @@ +export function contactMail({ username, message }) { + const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9"; + + const html = ` + + + + + + Neue Nachricht + + +
Neue Kontaktanfrage im Ausleihsystem.
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+

Neue Nachricht

+

Eine neue Kontaktanfrage ist eingegangen.

+
 
+ + + + + + + + + +
+ Benutzername + + ${username || "N/A"} +
+ Nachricht + + ${message || "N/A"} +
+
+

+ Diese E-Mail wurde automatisch vom Ausleihsystem gesendet.
+ Bitte antworten Sie nicht auf diese Nachricht. +

+
+
+ +`; + + const text = `Neue Kontaktanfrage\n\nBenutzername: ${username}\nNachricht:\n${message}`; + + return { html, text }; +} diff --git a/backendV2/services/mailer/templates/loan_created.js b/backendV2/services/mailer/templates/loan_created.js new file mode 100644 index 0000000..a75a71e --- /dev/null +++ b/backendV2/services/mailer/templates/loan_created.js @@ -0,0 +1,124 @@ +const formatDateTime = (value) => { + if (value == null) return "N/A"; + const d = value instanceof Date ? value : new Date(value); + if (isNaN(d.getTime())) return "N/A"; + return ( + d.toLocaleString("de-DE", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }) + " Uhr" + ); +}; + +export function loanMail(user, items, startDate, endDate, createdDate, note) { + const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9"; + + const itemsHtml = + Array.isArray(items) && items.length + ? items + .map( + (i) => + `${i}`, + ) + .join(" ") + : 'Keine Gegenstände'; + + const row = (label, value, isLast = false) => ` + + + ${label} + + + ${value} + + `; + + const html = ` + + + + + + Neue Ausleihe + + +
Neue Ausleihe erstellt – Details zur Buchung.
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Neue Ausleihe

+

Es wurde soeben eine neue Ausleihe im System angelegt.

+
 
+

Details zur Ausleihe

+ + ${row("Benutzer", user || "N/A")} + ${row("Gegenstände", itemsHtml)} + ${row("Startdatum", formatDateTime(startDate))} + ${row("Enddatum", formatDateTime(endDate))} + ${row("Erstellt am", formatDateTime(createdDate))} + ${row("Notiz", note || 'Keine Notiz', true)} +
+
+ + Ausleihe ansehen → + +
+

+ Diese E-Mail wurde automatisch vom Ausleihsystem gesendet.
+ Bitte antworten Sie nicht auf diese Nachricht. +

+
+
+ +`; + + const itemsText = Array.isArray(items) ? items.join(", ") : "N/A"; + const text = [ + "Neue Ausleihe erstellt", + "-".repeat(30), + `Benutzer: ${user || "N/A"}`, + `Gegenstaende: ${itemsText}`, + `Start: ${formatDateTime(startDate)}`, + `Ende: ${formatDateTime(endDate)}`, + `Erstellt: ${formatDateTime(createdDate)}`, + `Notiz: ${note || "Keine Notiz"}`, + "", + "-> https://admin.insta.the1s.de/api", + ].join("\n"); + + return { html, text }; +} diff --git a/backendV2/services/mailer/transporter.js b/backendV2/services/mailer/transporter.js new file mode 100644 index 0000000..47d9f4f --- /dev/null +++ b/backendV2/services/mailer/transporter.js @@ -0,0 +1,13 @@ +import nodemailer from "nodemailer"; +import dotenv from "dotenv"; +dotenv.config(); + +export const transporter = nodemailer.createTransport({ + host: process.env.MAIL_HOST, + port: process.env.MAIL_PORT, + secure: true, + auth: { + user: process.env.MAIL_USER, + pass: process.env.MAIL_PASSWORD, + }, +});