adjusted new backend with new routes
This commit is contained in:
@@ -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 };
|
||||||
|
};
|
||||||
|
|||||||
@@ -19,3 +19,21 @@ export const loginFunc = async (username, password) => {
|
|||||||
if (result.length > 0) return { success: true, data: result[0] };
|
if (result.length > 0) return { success: true, data: result[0] };
|
||||||
return { success: false };
|
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 };
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,93 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
|
import { authenticate, generateToken } from "../../services/authentication.js";
|
||||||
const router = express.Router();
|
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;
|
export default router;
|
||||||
|
|||||||
144
backendV2/routes/app/services/mailer.js
Normal file
144
backendV2/routes/app/services/mailer.js
Normal file
@@ -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
|
||||||
|
? `<ul style="margin:4px 0 0 18px; padding:0;">${items
|
||||||
|
.map(
|
||||||
|
(i) =>
|
||||||
|
`<li style="margin:2px 0; color:#111827; line-height:1.3;">${i}</li>`
|
||||||
|
)
|
||||||
|
.join("")}</ul>`
|
||||||
|
: "<span style='color:#111827;'>N/A</span>";
|
||||||
|
|
||||||
|
return `<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="color-scheme" content="light">
|
||||||
|
<meta name="supported-color-schemes" content="light">
|
||||||
|
<meta name="x-apple-disable-message-reformatting">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<style>
|
||||||
|
:root { color-scheme: light; supported-color-schemes: light; }
|
||||||
|
body { margin:0; padding:0; }
|
||||||
|
/* Mobile stacking */
|
||||||
|
@media (max-width:480px) {
|
||||||
|
.outer { width:100% !important; }
|
||||||
|
.pad-sm { padding:16px !important; }
|
||||||
|
.w-label { width:120px !important; }
|
||||||
|
}
|
||||||
|
/* Dark-mode override safety */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body, table, td, p, a, h1, h2, h3 { background:#ffffff !important; color:#111827 !important; }
|
||||||
|
.brand-header { background:${brand} !important; color:#ffffff !important; }
|
||||||
|
a { color:${brand} !important; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body bgcolor="#ffffff" style="background:#ffffff; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif; color:#111827; -webkit-text-size-adjust:100%;">
|
||||||
|
<!-- Preheader (hidden) -->
|
||||||
|
<div style="display:none; max-height:0; overflow:hidden; opacity:0; mso-hide:all;">
|
||||||
|
Neue Ausleihe erstellt – Übersicht der Buchung.
|
||||||
|
</div>
|
||||||
|
<div role="article" aria-roledescription="email" lang="de" style="padding:24px; background:#f2f4f7;">
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" class="outer" style="max-width:600px; margin:0 auto; background:#ffffff; border:1px solid #e5e7eb; border-radius:14px; overflow:hidden;">
|
||||||
|
<tr>
|
||||||
|
<td class="brand-header" style="padding:22px 26px; background:${brand}; color:#ffffff;">
|
||||||
|
<h1 style="margin:0; font-size:18px; line-height:1.35; font-weight:600;">Neue Ausleihe erstellt</h1>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="pad-sm" style="padding:24px 26px; color:#111827;">
|
||||||
|
<p style="margin:0 0 14px 0; line-height:1.4;">Es wurde eine neue Ausleihe angelegt. Hier sind die Details:</p>
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="border-collapse:collapse; font-size:14px; line-height:1.3; background:#fcfcfd; border:1px solid #e5e7eb; border-radius:10px; overflow:hidden;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="w-label" style="padding:10px 14px; color:#6b7280; width:170px; border-bottom:1px solid #ececec;">Benutzer</td>
|
||||||
|
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${
|
||||||
|
user || "N/A"
|
||||||
|
}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:10px 14px; color:#6b7280; vertical-align:top; border-bottom:1px solid #ececec;">Ausgeliehene Gegenstände</td>
|
||||||
|
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${itemsList}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:10px 14px; color:#6b7280; border-bottom:1px solid #ececec;">Startdatum</td>
|
||||||
|
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${formatDateTime(
|
||||||
|
startDate
|
||||||
|
)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:10px 14px; color:#6b7280; border-bottom:1px solid #ececec;">Enddatum</td>
|
||||||
|
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${formatDateTime(
|
||||||
|
endDate
|
||||||
|
)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding:10px 14px; color:#6b7280;">Erstellt am</td>
|
||||||
|
<td style="padding:10px 14px; font-weight:600; color:#111827;">${formatDateTime(
|
||||||
|
createdDate
|
||||||
|
)}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p style="margin:22px 0 0 0; font-size:14px;">
|
||||||
|
<a href="https://admin.insta.the1s.de/api" style="display:inline-block; background:${brand}; color:#ffffff; text-decoration:none; padding:10px 16px; border-radius:6px; font-weight:600; font-size:14px;" target="_blank" rel="noopener noreferrer">
|
||||||
|
Übersicht öffnen
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p style="margin:18px 0 0 0; font-size:12px; color:#6b7280; line-height:1.4;">
|
||||||
|
Diese E-Mail wurde automatisch vom Ausleihsystem gesendet. Bitte nicht antworten.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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" <noreply@mcs-medien.de>',
|
||||||
|
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");
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import dotenv from "dotenv";
|
|||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
// database funcs import
|
// database funcs import
|
||||||
import { loginFunc } from "./database/userMgmt.database.js";
|
import { loginFunc, changePassword } from "./database/userMgmt.database.js";
|
||||||
|
|
||||||
router.post("/login", async (req, res) => {
|
router.post("/login", async (req, res) => {
|
||||||
const result = await loginFunc(req.body.username, req.body.password);
|
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;
|
export default router;
|
||||||
|
|||||||
Reference in New Issue
Block a user