From 880029a0cf8be2f22009e07a3911af1f09fecc89 Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Mon, 29 Sep 2025 10:53:50 +0200 Subject: [PATCH 1/6] changed docs --- Docs/backend_API_docs/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Docs/backend_API_docs/README.md b/Docs/backend_API_docs/README.md index ee866cf..0d22b10 100644 --- a/Docs/backend_API_docs/README.md +++ b/Docs/backend_API_docs/README.md @@ -152,6 +152,10 @@ POST `/apiV2/setReturnDate/:key/:loan_code` Sets the `returned_date` to the current server time. +**Note:** I have updated this API route, so that everytime you return or take a loan, the state of the loaned items is automatically updated. + +**DO NOT UPDATE THE STATE MANUALLY! (only if the item was taken with an admin key)** + Example request: ``` @@ -174,6 +178,10 @@ POST `/apiV2/setTakeDate/:key/:loan_code` Sets the `take_date` to the current server time. +**Note:** I have updated this API route, so that everytime you return or take a loan, the state of the loaned items is automatically updated. + +**DO NOT UPDATE THE STATE MANUALLY! (only if the item was taken with an admin key)** + Example request: ``` From 9cad1e8b6b0f917cadf8d57c7b41a6d96cbca4de Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Tue, 30 Sep 2025 12:53:58 +0200 Subject: [PATCH 2/6] fixed minor display bugs --- admin/src/components/API/Landingpage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin/src/components/API/Landingpage.tsx b/admin/src/components/API/Landingpage.tsx index 3c61d6a..6d91e3f 100644 --- a/admin/src/components/API/Landingpage.tsx +++ b/admin/src/components/API/Landingpage.tsx @@ -208,7 +208,7 @@ const Landingpage: React.FC = () => { borderRadius="full" > - + Im Schließfach @@ -221,7 +221,7 @@ const Landingpage: React.FC = () => { borderRadius="full" > - + Nicht im Schließfach From 8f9696991f194c1a805a6945f71cb766e93f7efa Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Tue, 30 Sep 2025 12:59:38 +0200 Subject: [PATCH 3/6] improved error handling --- admin/src/components/AddAPIKey.tsx | 8 ++++++++ admin/src/utils/userActions.ts | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/admin/src/components/AddAPIKey.tsx b/admin/src/components/AddAPIKey.tsx index c710397..6b8ff9c 100644 --- a/admin/src/components/AddAPIKey.tsx +++ b/admin/src/components/AddAPIKey.tsx @@ -59,6 +59,14 @@ const AddAPIKey: React.FC = ({ onClose, alert }) => { "Der API Key wurde erfolgreich erstellt." ); onClose(); + } else { + alert( + "error", + "Fehler beim Erstellen des API Keys", + res.message || + "Beim Erstellen des API Keys ist ein Fehler aufgetreten. (frontend bug)" + ); + onClose(); } }} > diff --git a/admin/src/utils/userActions.ts b/admin/src/utils/userActions.ts index 8cf8e1e..d8417e3 100644 --- a/admin/src/utils/userActions.ts +++ b/admin/src/utils/userActions.ts @@ -213,7 +213,11 @@ export const createAPIentry = async (apiKey: string, user: string) => { body: JSON.stringify({ apiKey, user }), }); if (!response.ok) { - throw new Error("Failed to create API entry"); + return { + success: false, + message: + "Fehler beim Erstellen des API Keys. Achten Sie darauf, dass alle Felder ausgefüllt sind und der API Key nicht doppelt vergeben wird.", + }; } return { success: true }; } catch (error) { From bf36a6605fbb53efa817cd1fbef4e95c39323c93 Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Tue, 30 Sep 2025 13:03:00 +0200 Subject: [PATCH 4/6] improved error handling for adding an item --- admin/src/components/AddItemForm.tsx | 4 +++- admin/src/utils/userActions.ts | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/admin/src/components/AddItemForm.tsx b/admin/src/components/AddItemForm.tsx index 76ec615..5780a94 100644 --- a/admin/src/components/AddItemForm.tsx +++ b/admin/src/components/AddItemForm.tsx @@ -68,8 +68,10 @@ const AddItemForm: React.FC = ({ onClose, alert }) => { alert( "error", "Fehler", - "Der Gegenstand konnte nicht erstellt werden." + res.message || + "Der Gegenstand konnte nicht erstellt werden. (frontend bug)" ); + onClose(); } }} > diff --git a/admin/src/utils/userActions.ts b/admin/src/utils/userActions.ts index d8417e3..38ef767 100644 --- a/admin/src/utils/userActions.ts +++ b/admin/src/utils/userActions.ts @@ -148,7 +148,11 @@ export const createItem = async ( body: JSON.stringify({ item_name, can_borrow_role }), }); if (!response.ok) { - throw new Error("Failed to create item"); + return { + success: false, + message: + "Fehler beim Erstellen des Gegenstands. Der Name des Gegenstandes darf nicht mehrmals vergeben werden.", + }; } return { success: true }; } catch (error) { From 04453fd885158d36320a63e9ed378b8778f233d1 Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Tue, 30 Sep 2025 13:06:49 +0200 Subject: [PATCH 5/6] improved design of the error message --- admin/src/components/ChangePWform.tsx | 105 ++++++++++++++------------ 1 file changed, 56 insertions(+), 49 deletions(-) diff --git a/admin/src/components/ChangePWform.tsx b/admin/src/components/ChangePWform.tsx index aeddefe..001e5cf 100644 --- a/admin/src/components/ChangePWform.tsx +++ b/admin/src/components/ChangePWform.tsx @@ -55,57 +55,64 @@ const ChangePWform: React.FC = ({ - - - + - {showSubAlert && ( - - - - {subAlertMessage} - - - )} + const res = await changePW(newPassword, username); + if (res.success) { + alert( + "success", + "Passwort geändert", + "Das Passwort wurde erfolgreich geändert." + ); + onClose(); + } else { + alert( + "error", + "Fehler", + "Das Passwort konnte nicht geändert werden." + ); + onClose(); + } + }} + > + Ändern + + + + {showSubAlert && ( + + + + {subAlertMessage} + + + )} + From af7d15c97a0c1339e33f987b9c278a69ef66f06e Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Thu, 2 Oct 2025 22:32:26 +0200 Subject: [PATCH 6/6] add nodemailer integration for loan email notifications and implement loan info retrieval --- admin/tsconfig.app.json | 3 +- backend/package-lock.json | 12 ++- backend/package.json | 3 +- backend/routes/api.js | 159 +++++++++++++++++++++++++++++++++++ backend/services/database.js | 10 +++ 5 files changed, 184 insertions(+), 3 deletions(-) diff --git a/admin/tsconfig.app.json b/admin/tsconfig.app.json index 8ad072c..c433433 100644 --- a/admin/tsconfig.app.json +++ b/admin/tsconfig.app.json @@ -29,7 +29,8 @@ "@/*": ["./src/*"] }, - "forceConsistentCasingInFileNames": true + "forceConsistentCasingInFileNames": true, + "ignoreDeprecations": "6.0" }, "include": ["src"] } diff --git a/backend/package-lock.json b/backend/package-lock.json index 4453be8..5ea46a4 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -14,7 +14,8 @@ "ejs": "^3.1.10", "express": "^5.1.0", "jose": "^6.0.12", - "mysql2": "^3.14.3" + "mysql2": "^3.14.3", + "nodemailer": "^7.0.6" } }, "node_modules/accepts": { @@ -713,6 +714,15 @@ "node": ">= 0.6" } }, + "node_modules/nodemailer": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.6.tgz", + "integrity": "sha512-F44uVzgwo49xboqbFgBGkRaiMgtoBrBEWCVincJPK9+S9Adkzt/wXCLKbf7dxucmxfTI5gHGB+bEmdyzN6QKjw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", diff --git a/backend/package.json b/backend/package.json index 3614208..2215eb9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -16,6 +16,7 @@ "ejs": "^3.1.10", "express": "^5.1.0", "jose": "^6.0.12", - "mysql2": "^3.14.3" + "mysql2": "^3.14.3", + "nodemailer": "^7.0.6" } } diff --git a/backend/routes/api.js b/backend/routes/api.js index 05c6536..103eeef 100644 --- a/backend/routes/api.js +++ b/backend/routes/api.js @@ -25,9 +25,159 @@ import { getAllApiKeys, createAPIentry, deleteAPKey, + getLoanInfoWithID, } from "../services/database.js"; import { authenticate, generateToken } from "../services/tokenService.js"; const router = express.Router(); +import nodemailer from "nodemailer"; +import dotenv from "dotenv"; +dotenv.config(); + +// Nice HTML + text templates for the loan email +function buildLoanEmail({ user, items, startDate, endDate, createdDate }) { + 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

+
+

Es wurde eine neue Ausleihe angelegt. Hier sind die Details:

+ + + + + + + + + + + + + + + + + + + + + +
Benutzer${ + user || "N/A" + }
Ausgeliehene Gegenstände${itemsList}
Startdatum${formatDateTime( + startDate + )}
Enddatum${formatDateTime( + endDate + )}
Erstellt am${formatDateTime( + createdDate + )}
+

Diese E-Mail wurde automatisch vom Ausleihsystem gesendet. Bitte nicht antworten.

+
+
+ +`; +} + +function buildLoanEmailText({ user, items, startDate, endDate, createdDate }) { + 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)}`, + ].join("\n"); +} + +function sendMailLoan(user, items, startDate, endDate, createdDate) { + 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, + }), + html: buildLoanEmail({ user, items, startDate, endDate, createdDate }), + }); + + console.log("Message sent:", info.messageId); + })(); + console.log("sendMailLoan called"); +} + +// ...existing code... +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"; +}; +// ...existing code... router.post("/login", async (req, res) => { const result = await loginFunc(req.body.username, req.body.password); @@ -158,6 +308,15 @@ router.post("/createLoan", authenticate, async (req, res) => { ); if (result.success) { + const mailInfo = await getLoanInfoWithID(result.data.id); + console.log(mailInfo); + sendMailLoan( + mailInfo.data.username, + mailInfo.data.loaned_items_name, + mailInfo.data.start_date, + mailInfo.data.end_date, + mailInfo.data.created_at + ); return res.status(201).json({ message: "Loan created successfully", loanId: result.data.id, diff --git a/backend/services/database.js b/backend/services/database.js index dacc1d1..91d0247 100644 --- a/backend/services/database.js +++ b/backend/services/database.js @@ -182,6 +182,16 @@ export const getBorrowableItemsFromDatabase = async ( return { success: false }; }; +export const getLoanInfoWithID = async (loanId) => { + const [rows] = await pool.query("SELECT * FROM loans WHERE id = ?;", [ + loanId, + ]); + if (rows.length > 0) { + return { success: true, data: rows[0] }; + } + return { success: false }; +}; + export const createLoanInDatabase = async ( username, startDate,