added new feature: service config; Currently implemented in: loanMgmt and userMgmt (only Backend)

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-26 14:40:53 +02:00
parent 5442f2f1f3
commit 0964109c4b
4 changed files with 279 additions and 169 deletions
+178 -136
View File
@@ -1,5 +1,9 @@
import express from "express"; import express from "express";
import { authenticate, generateToken } from "../../services/authentication.js"; import { authenticate, generateToken } from "../../services/authentication.js";
import {
checkIfServiceIsActive,
checkIfServiceIsActive2,
} from "../../services/functions.js";
const router = express.Router(); const router = express.Router();
import dotenv from "dotenv"; import dotenv from "dotenv";
dotenv.config(); dotenv.config();
@@ -18,106 +22,129 @@ import {
} from "./database/loansMgmt.database.js"; } from "./database/loansMgmt.database.js";
import { sendMailLoan } from "./services/mailer.js"; import { sendMailLoan } from "./services/mailer.js";
router.post("/createLoan", authenticate, async (req, res) => { router.post(
try { "/createLoan",
const { items, startDate, endDate, note } = req.body || {}; checkIfServiceIsActive("Loan Service"),
authenticate,
async (req, res) => {
try {
const { items, startDate, endDate, note } = req.body || {};
if (!Array.isArray(items) || items.length === 0) { if (!Array.isArray(items) || items.length === 0) {
return res.status(400).json({ message: "Items array is required" }); return res.status(400).json({ message: "Items array is required" });
} }
// If dates are not provided, default to now .. +7 days // If dates are not provided, default to now .. +7 days
const start = const start =
startDate ?? new Date().toISOString().slice(0, 19).replace("T", " "); startDate ?? new Date().toISOString().slice(0, 19).replace("T", " ");
const end = const end =
endDate ?? endDate ??
new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
.toISOString() .toISOString()
.slice(0, 19) .slice(0, 19)
.replace("T", " "); .replace("T", " ");
// Coerce item IDs to numbers and filter invalids // Coerce item IDs to numbers and filter invalids
const itemIds = items const itemIds = items
.map((v) => Number(v)) .map((v) => Number(v))
.filter((n) => Number.isFinite(n)); .filter((n) => Number.isFinite(n));
if (itemIds.length === 0) { if (itemIds.length === 0) {
return res.status(400).json({ message: "No valid item IDs provided" }); return res.status(400).json({ message: "No valid item IDs provided" });
} }
const result = await createLoanInDatabase( const result = await createLoanInDatabase(
req.user.username, req.user.username,
start, start,
end, end,
note, note,
itemIds, 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,
mailInfo.data.note,
); );
return res.status(201).json({
message: "Loan created successfully", if (result.success) {
loanId: result.data.id, if (await checkIfServiceIsActive2("Loan Mailer")) {
loanCode: result.data.loan_code, 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,
mailInfo.data.note,
);
}
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" });
} }
},
);
if (result.code === "CONFLICT") { router.get(
return res "/loans",
.status(409) checkIfServiceIsActive("Loan Service"),
.json({ message: "Items not available in the selected period" }); authenticate,
async (req, res) => {
const result = await getLoansFromDatabase(req.user.username);
if (result.success) {
res.status(200).json(result.data);
} else if (result.status) {
res.status(200).json([]);
} else {
res.status(500).json({ message: "Failed to fetch loans" });
} }
},
);
if (result.code === "BAD_REQUEST") { router.post(
return res.status(400).json({ message: result.message }); "/set-return-date/:loan_code",
checkIfServiceIsActive("Loan Service"),
authenticate,
async (req, res) => {
const loanCode = req.params.loan_code;
const result = await setReturnDate(loanCode);
if (result.success) {
res.status(200).json({ data: result.data });
} else {
res.status(500).json({ message: "Failed to set return date" });
} }
},
);
return res.status(500).json({ message: "Failed to create loan" }); router.post(
} catch (err) { "/set-take-date/:loan_code",
console.error("createLoan error:", err); checkIfServiceIsActive("Loan Service"),
return res.status(500).json({ message: "Failed to create loan" }); authenticate,
} async (req, res) => {
}); const loanCode = req.params.loan_code;
const result = await setTakeDate(loanCode);
router.get("/loans", authenticate, async (req, res) => { if (result.success) {
const result = await getLoansFromDatabase(req.user.username); res.status(200).json({ data: result.data });
if (result.success) { } else {
res.status(200).json(result.data); res.status(500).json({ message: "Failed to set take date" });
} else if (result.status) { }
res.status(200).json([]); },
} else { );
res.status(500).json({ message: "Failed to fetch loans" });
}
});
router.post("/set-return-date/:loan_code", authenticate, async (req, res) => {
const loanCode = req.params.loan_code;
const result = await setReturnDate(loanCode);
if (result.success) {
res.status(200).json({ data: result.data });
} else {
res.status(500).json({ message: "Failed to set return date" });
}
});
router.post("/set-take-date/:loan_code", authenticate, async (req, res) => {
const loanCode = req.params.loan_code;
const result = await setTakeDate(loanCode);
if (result.success) {
res.status(200).json({ data: result.data });
} else {
res.status(500).json({ message: "Failed to set take date" });
}
});
router.get("/all-items", authenticate, async (req, res) => { router.get("/all-items", authenticate, async (req, res) => {
const result = await getItems(); const result = await getItems();
@@ -128,56 +155,71 @@ router.get("/all-items", authenticate, async (req, res) => {
} }
}); });
router.delete("/delete-loan/:id", authenticate, async (req, res) => { router.delete(
const loanId = req.params.id; "/delete-loan/:id",
const result = await SETdeleteLoanFromDatabase(loanId); checkIfServiceIsActive("Loan Service"),
if (result.success) { authenticate,
res.status(200).json({ message: "Loan deleted successfully" }); async (req, res) => {
} else { const loanId = req.params.id;
if (result.code === "LOAN_NOT_FOUND") { const result = await SETdeleteLoanFromDatabase(loanId);
res.status(404).json({ message: "Loan not found" }); if (result.success) {
res.status(200).json({ message: "Loan deleted successfully" });
} else {
if (result.code === "LOAN_NOT_FOUND") {
res.status(404).json({ message: "Loan not found" });
}
if (result.code === "LOAN_NOT_RETURNED") {
res.status(507).json({
message: "Cannot delete loan that has not been returned",
});
}
res.status(500).json({ message: "Failed to delete loan" });
}
},
);
router.get(
"/all-loans",
checkIfServiceIsActive("Loan Service"),
authenticate,
async (req, res) => {
const result = await getALLLoans();
if (result.success) {
res.status(200).json(result.data);
} else {
res.status(500).json({ message: "Failed to fetch loans" });
}
},
);
router.post(
"/borrowable-items",
checkIfServiceIsActive("Loan Service"),
authenticate,
async (req, res) => {
const { startDate, endDate } = req.body || {};
if (!startDate || !endDate) {
return res
.status(400)
.json({ message: "startDate and endDate are required" });
} }
if (result.code === "LOAN_NOT_RETURNED") { const result = await getBorrowableItemsFromDatabase(
res.status(507).json({ startDate,
message: "Cannot delete loan that has not been returned", endDate,
}); req.user.role,
);
if (result.success) {
// return the array directly for consistency with /items
return res.status(200).json(result.data);
} else {
return res
.status(500)
.json({ message: "Failed to fetch borrowable items" });
} }
},
res.status(500).json({ message: "Failed to delete loan" }); );
}
});
router.get("/all-loans", authenticate, async (req, res) => {
const result = await getALLLoans();
if (result.success) {
res.status(200).json(result.data);
} else {
res.status(500).json({ message: "Failed to fetch loans" });
}
});
router.post("/borrowable-items", authenticate, async (req, res) => {
const { startDate, endDate } = req.body || {};
if (!startDate || !endDate) {
return res
.status(400)
.json({ message: "startDate and endDate are required" });
}
const result = await getBorrowableItemsFromDatabase(
startDate,
endDate,
req.user.role,
);
if (result.success) {
// return the array directly for consistency with /items
return res.status(200).json(result.data);
} else {
return res
.status(500)
.json({ message: "Failed to fetch borrowable items" });
}
});
export default router; export default router;
+47 -32
View File
@@ -1,5 +1,6 @@
import express from "express"; import express from "express";
import { authenticate, generateToken } from "../../services/authentication.js"; import { authenticate, generateToken } from "../../services/authentication.js";
import { checkIfServiceIsActive } from "../../services/functions.js";
const router = express.Router(); const router = express.Router();
import dotenv from "dotenv"; import dotenv from "dotenv";
dotenv.config(); dotenv.config();
@@ -8,41 +9,55 @@ dotenv.config();
import { loginFunc, changePassword } from "./database/userMgmt.database.js"; import { loginFunc, changePassword } from "./database/userMgmt.database.js";
import { sendMail } from "./services/mailer_v2.js"; import { sendMail } from "./services/mailer_v2.js";
router.post("/login", async (req, res) => { router.post(
const result = await loginFunc(req.body.username, req.body.password); "/login",
if (result.success) { checkIfServiceIsActive("User Frontend"),
const token = await generateToken({ async (req, res) => {
username: result.data.username, const result = await loginFunc(req.body.username, req.body.password);
is_admin: result.data.is_admin, if (result.success) {
first_name: result.data.first_name, const token = await generateToken({
last_name: result.data.last_name, username: result.data.username,
role: result.data.role, is_admin: result.data.is_admin,
}); first_name: result.data.first_name,
res.status(200).json({ message: "Login successful", token }); last_name: result.data.last_name,
} else { role: result.data.role,
res.status(401).json({ message: "Invalid credentials" }); });
} res.status(200).json({ message: "Login successful", token });
}); } else {
res.status(401).json({ message: "Invalid credentials" });
}
},
);
router.post("/change-password", authenticate, async (req, res) => { router.post(
const oldPassword = req.body.oldPassword; "/change-password",
const newPassword = req.body.newPassword; checkIfServiceIsActive("User Frontend"),
const username = req.user.username; authenticate,
const result = await changePassword(username, oldPassword, newPassword); async (req, res) => {
if (result.success) { const oldPassword = req.body.oldPassword;
res.status(200).json({ message: "Password changed successfully" }); const newPassword = req.body.newPassword;
} else { const username = req.user.username;
res.status(500).json({ message: "Failed to change password" }); 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" });
}
},
);
router.post("/contact", authenticate, async (req, res) => { router.post(
const message = req.body.message; "/contact",
const username = req.user.username; checkIfServiceIsActive("Contact Form Service"),
authenticate,
async (req, res) => {
const message = req.body.message;
const username = req.user.username;
sendMail(username, message); sendMail(username, message);
res.status(200).json({ message: "Contact message sent successfully" }); res.status(200).json({ message: "Contact message sent successfully" });
}); },
);
export default router; export default router;
+12 -1
View File
@@ -54,4 +54,15 @@ CREATE TABLE apiKeys (
entry_created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, entry_created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id), PRIMARY KEY (id),
CHECK (api_key REGEXP '^[0-9]{8}$') CHECK (api_key REGEXP '^[0-9]{8}$')
) ENGINE=InnoDB; ) ENGINE=InnoDB;
CREATE TABLE functions (
id INT NOT NULL AUTO_INCREMENT,
function_name VARCHAR(500) NOT NULL UNIQUE,
active BOOLEAN NOT NULL DEFAULT true,
entry_updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
entry_created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB;
INSERT INTO functions (function_name) VALUES ("Loan Mailer"), ("Loan Service"), ("Contact Form Service"), ("User Frontend"), ("API")
+42
View File
@@ -0,0 +1,42 @@
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 function checkIfServiceIsActive(service) {
return async (req, res, next) => {
const [result] = await pool.query(
"SELECT * FROM functions WHERE function_name = ? AND active = 1;",
[service],
);
if (result.length > 0) {
return next();
}
return res
.status(503)
.json({ message: `-${service}- is currently unavailable.` });
};
}
export async function checkIfServiceIsActive2(service) {
const [result] = await pool.query(
"SELECT * FROM functions WHERE function_name = ? AND active = 1;",
[service],
);
if (result.length > 0) {
return true;
}
return false;
}