diff --git a/backendV2/routes/app/database/loansMgmt.database.js b/backendV2/routes/app/database/loansMgmt.database.js index e69de29..2ae888c 100644 --- a/backendV2/routes/app/database/loansMgmt.database.js +++ b/backendV2/routes/app/database/loansMgmt.database.js @@ -0,0 +1,179 @@ +import mysql from "mysql2"; +import dotenv from "dotenv"; +dotenv.config(); + +const pool = mysql + .createPool({ + host: process.env.DB_HOST, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + }) + .promise(); + +export const createLoanInDatabase = async ( + username, + startDate, + endDate, + note, + itemIds +) => { + if (!username) + return { success: false, code: "BAD_REQUEST", message: "Missing username" }; + if (!Array.isArray(itemIds) || itemIds.length === 0) + return { + success: false, + code: "BAD_REQUEST", + message: "No items provided", + }; + if (!startDate || !endDate) + return { success: false, code: "BAD_REQUEST", message: "Missing dates" }; + + const start = new Date(startDate); + const end = new Date(endDate); + if ( + !(start instanceof Date) || + isNaN(start.getTime()) || + !(end instanceof Date) || + isNaN(end.getTime()) || + start >= end + ) { + return { + success: false, + code: "BAD_REQUEST", + message: "Invalid date range", + }; + } + + const conn = await pool.getConnection(); + try { + await conn.beginTransaction(); + + // Ensure all items exist and collect names + const [itemsRows] = await conn.query( + "SELECT id, item_name FROM items WHERE id IN (?)", + [itemIds] + ); + if (!itemsRows || itemsRows.length !== itemIds.length) { + await conn.rollback(); + return { + success: false, + code: "BAD_REQUEST", + message: "One or more items not found", + }; + } + const itemNames = itemIds + .map( + (id) => itemsRows.find((r) => Number(r.id) === Number(id))?.item_name + ) + .filter(Boolean); + + // Check availability (no overlap with existing loans) + const [confRows] = await conn.query( + ` + SELECT COUNT(*) AS conflicts + FROM loans l + JOIN JSON_TABLE(l.loaned_items_id, '$[*]' COLUMNS (item_id INT PATH '$')) jt + ON TRUE + WHERE jt.item_id IN (?) + AND l.deleted = 0 + AND l.start_date < ? + AND COALESCE(l.returned_date, l.end_date) > ? + `, + [itemIds, end, start] + ); + if (confRows?.[0]?.conflicts > 0) { + await conn.rollback(); + return { + success: false, + code: "CONFLICT", + message: "One or more items are not available in the selected period", + }; + } + + // Generate unique loan_code (retry a few times) + let loanCode = null; + for (let i = 0; i < 6; i++) { + const candidate = Math.floor(100000 + Math.random() * 899999); // 6 digits + const [exists] = await conn.query( + "SELECT 1 FROM loans WHERE loan_code = ? LIMIT 1", + [candidate] + ); + if (exists.length === 0) { + loanCode = candidate; + break; + } + } + if (!loanCode) { + await conn.rollback(); + return { + success: false, + code: "SERVER_ERROR", + message: "Failed to generate unique loan code", + }; + } + + // Insert loan + const [insertRes] = await conn.query( + ` + INSERT INTO loans (username, loan_code, start_date, end_date, loaned_items_id, loaned_items_name, note) + VALUES (?, ?, ?, ?, CAST(? AS JSON), CAST(? AS JSON), ?) + `, + [ + username, + loanCode, + // Use DATETIME/TIMESTAMP friendly format + new Date(start).toISOString().slice(0, 19).replace("T", " "), + new Date(end).toISOString().slice(0, 19).replace("T", " "), + JSON.stringify(itemIds.map((n) => Number(n))), + JSON.stringify(itemNames), + note, + ] + ); + + await conn.commit(); + return { + success: true, + data: { + id: insertRes.insertId, + loan_code: loanCode, + username, + start_date: start, + end_date: end, + items: itemIds, + item_names: itemNames, + }, + }; + } catch (err) { + await conn.rollback(); + console.error("createLoanInDatabase error:", err); + return { + success: false, + code: "SERVER_ERROR", + message: "Failed to create loan", + }; + } finally { + conn.release(); + } +}; + +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 getLoansFromDatabase = async (username) => { + const [result] = await pool.query( + "SELECT * FROM loans WHERE username = ? AND deleted = 0;", + [username] + ); + if (result.length > 0) { + return { success: true, data: result }; + } + return { success: false }; +}; diff --git a/backendV2/routes/app/database/userMgmt.database.js b/backendV2/routes/app/database/userMgmt.database.js index 1a5640c..d658de4 100644 --- a/backendV2/routes/app/database/userMgmt.database.js +++ b/backendV2/routes/app/database/userMgmt.database.js @@ -19,3 +19,21 @@ export const loginFunc = async (username, password) => { if (result.length > 0) return { success: true, data: result[0] }; return { success: false }; }; + +export const changePassword = async (username, oldPassword, newPassword) => { + // get user current password + const [user] = await pool.query( + "SELECT * FROM users WHERE username = ? AND password = ?", + [username, oldPassword] + ); + if (user.length === 0) return { success: false }; + + // update password + + const [result] = await pool.query( + "UPDATE users SET password = ? WHERE username = ?", + [newPassword, username] + ); + if (result.affectedRows > 0) return { success: true }; + return { success: false }; +}; diff --git a/backendV2/routes/app/loanMgmt.route.js b/backendV2/routes/app/loanMgmt.route.js index 9da7196..30e6809 100644 --- a/backendV2/routes/app/loanMgmt.route.js +++ b/backendV2/routes/app/loanMgmt.route.js @@ -1,5 +1,93 @@ import express from "express"; - +import { authenticate, generateToken } from "../../services/authentication.js"; const router = express.Router(); +import dotenv from "dotenv"; +dotenv.config(); + +// database funcs import +import { + createLoanInDatabase, + getLoanInfoWithID, + getLoansFromDatabase, +} from "./database/loansMgmt.database.js"; +import { sendMailLoan } from "./services/mailer.js"; + +router.post("/createLoan", authenticate, async (req, res) => { + try { + const { items, startDate, endDate, note } = req.body || {}; + + if (!Array.isArray(items) || items.length === 0) { + return res.status(400).json({ message: "Items array is required" }); + } + + // If dates are not provided, default to now .. +7 days + const start = + startDate ?? new Date().toISOString().slice(0, 19).replace("T", " "); + const end = + endDate ?? + new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) + .toISOString() + .slice(0, 19) + .replace("T", " "); + + // Coerce item IDs to numbers and filter invalids + const itemIds = items + .map((v) => Number(v)) + .filter((n) => Number.isFinite(n)); + + if (itemIds.length === 0) { + return res.status(400).json({ message: "No valid item IDs provided" }); + } + + const result = await createLoanInDatabase( + req.user.username, + start, + end, + note, + itemIds + ); + + 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, + loanCode: result.data.loan_code, + }); + } + + if (result.code === "CONFLICT") { + return res + .status(409) + .json({ message: "Items not available in the selected period" }); + } + + if (result.code === "BAD_REQUEST") { + return res.status(400).json({ message: result.message }); + } + + return res.status(500).json({ message: "Failed to create loan" }); + } catch (err) { + console.error("createLoan error:", err); + return res.status(500).json({ message: "Failed to create loan" }); + } +}); + +router.get("/loans", authenticate, async (req, res) => { + const result = await getLoansFromDatabase(req.user.username); + if (result.success) { + res.status(200).json(result.data); + } else { + res.status(500).json({ message: "Failed to fetch loans" }); + } +}); export default router; diff --git a/backendV2/routes/app/services/mailer.js b/backendV2/routes/app/services/mailer.js new file mode 100644 index 0000000..c6533a6 --- /dev/null +++ b/backendV2/routes/app/services/mailer.js @@ -0,0 +1,144 @@ +function buildLoanEmail({ user, items, startDate, endDate, createdDate }) { + const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9"; + const itemsList = + Array.isArray(items) && items.length + ? `` + : "N/A"; + + return ` + + + + + + + + + + + +
+ Neue Ausleihe erstellt – Übersicht der Buchung. +
+
+ + + + + + + + +
+ +`; +} + +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"); +} diff --git a/backendV2/routes/app/userMgmt.route.js b/backendV2/routes/app/userMgmt.route.js index 56acbb8..ae2c214 100644 --- a/backendV2/routes/app/userMgmt.route.js +++ b/backendV2/routes/app/userMgmt.route.js @@ -5,7 +5,7 @@ import dotenv from "dotenv"; dotenv.config(); // database funcs import -import { loginFunc } from "./database/userMgmt.database.js"; +import { loginFunc, changePassword } from "./database/userMgmt.database.js"; router.post("/login", async (req, res) => { const result = await loginFunc(req.body.username, req.body.password); @@ -20,4 +20,16 @@ router.post("/login", async (req, res) => { } }); +router.post("/change-password", authenticate, async (req, res) => { + const oldPassword = req.body.oldPassword; + const newPassword = req.body.newPassword; + const username = req.user.username; + const result = await changePassword(username, oldPassword, newPassword); + if (result.success) { + res.status(200).json({ message: "Password changed successfully" }); + } else { + res.status(500).json({ message: "Failed to change password" }); + } +}); + export default router;