major changes to docker and npm config

This commit is contained in:
2026-10-26 16:29:17 +01:00
parent 929ec5ef10
commit 9c17b2a495
85 changed files with 3146 additions and 2871 deletions
+41
View File
@@ -0,0 +1,41 @@
import express from "express";
import { authenticateAdmin } from "../../services/authentication.js";
const router = express.Router();
import dotenv from "dotenv";
dotenv.config();
// database funcs import
import {
getAllApiKeys,
createAPIentry,
deleteAPKey,
} from "./database/apiDataMgmt.database.js";
router.get("/get-api-keys", authenticateAdmin, async (req, res) => {
const result = await getAllApiKeys();
if (result.success) {
return res.status(200).json(result.data);
}
return res.status(500).json({ message: "Failed to retrieve API keys" });
});
router.post("/create-api-key", authenticateAdmin, async (req, res) => {
const apiKey = req.body.apiKey;
const entryName = req.body.entryName;
const result = await createAPIentry(apiKey, entryName);
if (result.success) {
return res.status(201).json({ message: "API key created successfully" });
}
return res.status(500).json({ message: "Failed to create API key" });
});
router.delete("/delete-api-key/:id", authenticateAdmin, async (req, res) => {
const apiKeyId = req.params.id;
const result = await deleteAPKey(apiKeyId);
if (result.success) {
return res.status(200).json({ message: "API key deleted successfully" });
}
return res.status(500).json({ message: "Failed to delete API key" });
});
export default router;
@@ -0,0 +1,37 @@
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 getAllApiKeys = async () => {
const [rows] = await pool.query("SELECT * FROM apiKeys");
if (rows.length > 0) {
return { success: true, data: rows };
}
return { success: false };
};
export const createAPIentry = async (apiKey, entryName) => {
const [result] = await pool.query(
"INSERT INTO apiKeys (api_key, entry_name) VALUES (?, ?)",
[apiKey, entryName]
);
if (result.affectedRows > 0) return { success: true };
return { success: false };
};
export const deleteAPKey = async (apiKeyId) => {
const [result] = await pool.query("DELETE FROM apiKeys WHERE id = ?", [
apiKeyId,
]);
if (result.affectedRows > 0) return { success: true };
return { success: false };
};
@@ -0,0 +1,82 @@
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 getAllItems = async () => {
const [result] = await pool.query("SELECT * FROM items");
if (result.length > 0) return { success: true, data: result };
return { success: false };
};
export const deleteItemById = async (itemId) => {
const [result] = await pool.query("DELETE FROM items WHERE id = ?", [itemId]);
if (result.affectedRows > 0) return { success: true };
return { success: false };
};
export const createItem = async (item_name, can_borrow_role, lockerNumber) => {
const [result] = await pool.query(
"INSERT INTO items (item_name, can_borrow_role, in_safe, safe_nr) VALUES (?, ?, ?, ?)",
[item_name, can_borrow_role, true, lockerNumber]
);
if (result.affectedRows > 0) return { success: true };
return { success: false };
};
export const editItemById = async (
itemId,
item_name,
can_borrow_role,
safe_nr,
door_key
) => {
let newSafeNr;
if (safe_nr === null || safe_nr === "") {
newSafeNr = null;
} else {
newSafeNr = safe_nr;
}
const [result] = await pool.query(
"UPDATE items SET item_name = ?, can_borrow_role = ?, safe_nr = ?, door_key = ?, entry_updated_at = NOW() WHERE id = ?",
[item_name, can_borrow_role, newSafeNr, door_key, itemId]
);
if (result.affectedRows > 0) return { success: true };
return { success: false };
};
export const changeSafeState = async (itemId) => {
const currentState = await pool.query(
"SELECT in_safe FROM items WHERE id = ?",
[itemId]
);
if (currentState[0].length === 0) {
return { success: false };
}
if (currentState[0][0].in_safe) {
const [result] = await pool.query(
"UPDATE items SET in_safe = false WHERE id = ?",
[itemId]
);
if (result.affectedRows > 0) return { success: true };
}
if (!currentState[0][0].in_safe) {
const [result] = await pool.query(
"UPDATE items SET in_safe = true WHERE id = ?",
[itemId]
);
if (result.affectedRows > 0) return { success: true };
}
return { success: false };
};
@@ -0,0 +1,26 @@
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 getAllLoans = async () => {
const [rows] = await pool.query("SELECT * FROM loans WHERE deleted_admin = 0;");
return { success: true, data: rows };
};
export const deleteLoanById = async (loanId) => {
const [result] = await pool.query(
"UPDATE loans SET deleted = true, deleted_admin = true WHERE id = ?",
[loanId],
);
if (result.affectedRows > 0) return { success: true };
return { success: false };
};
@@ -0,0 +1,26 @@
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 getAllFunctions = async () => {
const [rows] = await pool.query("SELECT * FROM functions");
return { success: true, data: rows };
};
export const updateFunctionStatus = async (functionName, active) => {
const [result] = await pool.query(
"UPDATE functions SET active = ? WHERE function_name = ?",
[active, functionName],
);
if (result.affectedRows > 0) return { success: true };
return { success: false };
};
@@ -0,0 +1,79 @@
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 createUser = async (
username,
role,
password,
isAdmin,
email,
first_name,
last_name,
) => {
const [result] = await pool.query(
"INSERT INTO users (username, role, password, is_admin, email, first_name, last_name) VALUES (?, ?, ?, ?, ?, ?, ?)",
[username, role, password, isAdmin, email, first_name, last_name],
);
if (result.affectedRows > 0) return { success: true };
return { success: false };
};
export const deleteUserById = async (userId) => {
const [result] = await pool.query("DELETE FROM users WHERE id = ?", [userId]);
if (result.affectedRows > 0) return { success: true };
return { success: false };
};
export const changePassword = async (username, newPassword) => {
const [result] = await pool.query(
"UPDATE users SET password = ?, entry_updated_at = NOW() WHERE username = ?",
[newPassword, username],
);
if (result.affectedRows > 0) return { success: true };
return { success: false };
};
export const editUserById = async (
userId,
first_name,
last_name,
role,
email,
is_admin,
) => {
const [result] = await pool.query(
"UPDATE users SET first_name = ?, last_name = ?, role = ?, email = ?, is_admin = ?, entry_updated_at = NOW() WHERE id = ?",
[first_name, last_name, role, email, is_admin, userId],
);
if (result.affectedRows > 0) return { success: true };
return { success: false };
};
export const getAllUsers = async () => {
const [result] = await pool.query(
"SELECT id, username, first_name, last_name, role, email, is_admin, entry_created_at, entry_updated_at FROM users",
);
if (result.length > 0) return { success: true, data: result };
return { success: false };
};
export const getUserById = async (userId) => {
const [rows] = await pool.query(
"SELECT id, username, first_name, last_name, role, email, is_admin FROM users WHERE id = ?",
[userId],
);
if (rows.length === 0) {
return { success: false };
}
return { success: true, data: rows[0] };
};
@@ -0,0 +1,47 @@
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 loginAdmin = async (username, password) => {
const [rows] = await pool.query(
"SELECT id, username, first_name, last_name, role, is_admin FROM users WHERE username = ? AND password = ?",
[username, password]
);
if (rows.length === 0) {
return { success: false, reason: "invalid_credentials" };
}
const user = rows[0];
if (!user.is_admin) {
return { success: false, reason: "not_admin" };
}
return { success: true, data: user };
};
export const executeQuery = async (query, password, username) => {
let verified = false;
const [user] = await pool.query(
"SELECT * FROM users WHERE username = ? AND password = ?",
[username, password]
);
if (user.length > 0 && user[0].is_admin) {
verified = true;
}
if (!verified) {
return { success: false, message: "Unauthorized" };
}
const [result] = await pool.query(`${query}`);
return { success: true, data: result };
};
@@ -0,0 +1,68 @@
import express from "express";
import { authenticateAdmin } from "../../services/authentication.js";
const router = express.Router();
import dotenv from "dotenv";
dotenv.config();
// database funcs import
import {
editItemById,
getAllItems,
deleteItemById,
createItem,
changeSafeState,
} from "./database/itemDataMgmt.database.js";
router.get("/all-items", authenticateAdmin, async (req, res) => {
const result = await getAllItems();
if (result.success) {
return res.status(200).json(result.data);
}
return res.status(500).json({ message: "Failed to retrieve items" });
});
router.delete("/delete-item/:id", authenticateAdmin, async (req, res) => {
const itemId = req.params.id;
const result = await deleteItemById(itemId);
if (result.success) {
return res.status(200).json({ message: "Item deleted successfully" });
}
return res.status(500).json({ message: "Failed to delete item" });
});
router.post("/create-item", authenticateAdmin, async (req, res) => {
const { item_name, can_borrow_role, lockerNumber } = req.body;
const result = await createItem(item_name, can_borrow_role, lockerNumber);
if (result.success) {
return res.status(201).json({ message: "Item created successfully" });
}
return res.status(500).json({ message: "Failed to create item" });
});
router.post("/edit-item/:id", authenticateAdmin, async (req, res) => {
const itemId = req.params.id;
const { item_name, can_borrow_role, safe_nr, door_key } = req.body;
const result = await editItemById(
itemId,
item_name,
can_borrow_role,
safe_nr,
door_key
);
if (result.success) {
return res.status(200).json({ message: "Item edited successfully" });
}
return res.status(500).json({ message: "Failed to edit item" });
});
router.post("/change-safe-state/:id", authenticateAdmin, async (req, res) => {
const itemId = req.params.id;
const result = await changeSafeState(itemId);
if (result.success) {
return res.status(200).json({ message: "Safe state changed successfully" });
}
return res.status(500).json({ message: "Failed to change safe state" });
});
export default router;
@@ -0,0 +1,30 @@
import express from "express";
import { authenticateAdmin } from "../../services/authentication.js";
const router = express.Router();
import dotenv from "dotenv";
dotenv.config();
// database funcs import
import {
deleteLoanById,
getAllLoans,
} from "./database/loanDataMgmt.database.js";
router.get("/all-loans", authenticateAdmin, async (req, res) => {
const result = await getAllLoans();
if (result.success) {
return res.status(200).json(result.data);
}
return res.status(500).json({ message: "Failed to retrieve loans" });
});
router.delete("/delete-loan/:id", authenticateAdmin, async (req, res) => {
const loanId = req.params.id;
const result = await deleteLoanById(loanId);
if (result.success) {
return res.status(200).json({ message: "Loan deleted successfully" });
}
return res.status(500).json({ message: "Failed to delete loan" });
});
export default router;
@@ -0,0 +1,50 @@
import express from "express";
import { authenticateAdmin } from "../../services/authentication.js";
const router = express.Router();
import dotenv from "dotenv";
dotenv.config();
// database funcs import
import {
getAllFunctions,
updateFunctionStatus,
} from "./database/serverConfMgmt.database.js";
// Route to get all functions and their statuses
router.get("/all", async (req, res) => {
try {
const result = await getAllFunctions();
if (result.success) {
res.status(200).json({ data: result.data });
} else {
res.status(500).json({ message: "Failed to fetch functions" });
}
} catch (error) {
res
.status(500)
.json({ message: "An error occurred", error: error.message });
}
});
// Route to update the status of a function
router.post("/update", async (req, res) => {
const functionName = req.query.functionName;
let active = req.query.active;
if (active === "false") {
active = 0;
} else if (active === "true") {
active = 1;
} else {
res.status(406).json({ message: "Got unexpected format" });
}
const result = await updateFunctionStatus(functionName, active);
if (result.success) {
res.status(200).json({ message: "Function status updated successfully" });
} else {
res.status(500).json({ message: "Failed to update function status" });
}
});
export default router;
+123
View File
@@ -0,0 +1,123 @@
import express from "express";
import { authenticateAdmin } from "../../services/authentication.js";
const router = express.Router();
import dotenv from "dotenv";
dotenv.config();
// database funcs import
import {
createUser,
deleteUserById,
editUserById,
changePassword,
getAllUsers,
getUserById,
} from "./database/userDataMgmt.database.js";
router.post("/create-user", authenticateAdmin, async (req, res) => {
const username = req.body.username;
const role = req.body.role;
const password = req.body.password;
const isAdmin = req.body.isAdmin;
const email = req.body.email;
const first_name = req.body.first_name;
const last_name = req.body.last_name;
const result = await createUser(
username,
role,
password,
isAdmin,
email,
first_name,
last_name
);
if (result.success) {
return res.status(201).json({ message: "User created successfully" });
}
return res.status(500).json({ message: "Failed to create user" });
});
router.delete("/delete-user/:id", authenticateAdmin, async (req, res) => {
const userId = req.params.id;
const result = await deleteUserById(userId);
if (result.success) {
return res.status(200).json({ message: "User deleted successfully" });
}
return res.status(500).json({ message: "Failed to delete user" });
});
router.post("/edit-user/:id", authenticateAdmin, async (req, res) => {
const first_name = req.body.first_name;
const last_name = req.body.last_name;
const role = req.body.role;
const email = req.body.email;
const userId = req.params.id;
const is_admin = req.body.is_admin;
const result = await editUserById(
userId,
first_name,
last_name,
role,
email,
is_admin
);
if (result.success) {
return res.status(200).json({ message: "User edited successfully" });
}
return res.status(500).json({ message: "Failed to edit user" });
});
router.post("/change-password", authenticateAdmin, async (req, res) => {
const username = req.body.username;
const password = req.body.password;
const result = await changePassword(username, password);
if (result.success) {
return res.status(200).json({ message: "Password reset successfully" });
}
return res.status(500).json({ message: "Failed to reset password" });
});
router.post("/edit-user/:id", authenticateAdmin, async (req, res) => {
const userId = req.params.id;
const first_name = req.body.first_name;
const last_name = req.body.last_name;
const role = req.body.role;
const email = req.body.email;
const is_admin = req.body.is_admin;
const result = await editUserById(
userId,
first_name,
last_name,
role,
email,
is_admin
);
if (result.success) {
return res.status(200).json({ message: "User edited successfully" });
}
return res.status(500).json({ message: "Failed to edit user" });
});
router.get("/users", authenticateAdmin, async (req, res) => {
const result = await getAllUsers();
if (result.success) {
return res.status(200).json(result.data);
}
return res.status(500).json({ message: "Failed to retrieve users" });
});
router.get("/user/:id", authenticateAdmin, async (req, res) => {
const result = await getUserById(req.params.id);
if (result.success) {
return res.status(200).json({ user: result.data });
}
return res.status(500).json({ message: "Failed to retrieve user" });
});
export default router;
+54
View File
@@ -0,0 +1,54 @@
import express from "express";
import {
generateToken,
authenticateAdmin,
} from "../../services/authentication.js";
const router = express.Router();
import dotenv from "dotenv";
dotenv.config();
// database funcs import
import { loginAdmin, executeQuery } from "./database/userMgmt.database.js";
router.post("/login", async (req, res) => {
const { username, password } = req.body || {};
if (!username || !password) {
return res.status(400).json({ message: "Missing username or password" });
}
const result = await loginAdmin(username, password);
if (result.success) {
const token = await generateToken({
username: result.data.username,
first_name: result.data.first_name,
last_name: result.data.last_name,
admin: result.data.is_admin,
});
return res.status(200).json({
message: "Login erfolgreich",
token,
first_name: result.data.first_name,
});
}
if (result.reason === "not_admin") {
return res.status(403).json({ message: "Du bist kein Admin" });
}
return res.status(401).json({ message: "Ungültige Anmeldedaten" });
});
router.get("/verify-token", authenticateAdmin, async (req, res) => {
return res.status(200).json({ message: "Token is valid" });
});
router.post("/database-query", authenticateAdmin, async (req, res) => {
const query = req.body.query;
const password = req.body.password;
const username = req.body.username;
const result = await executeQuery(query, password, username);
});
export default router;
+139
View File
@@ -0,0 +1,139 @@
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 getItemsFromDatabaseV2 = async () => {
const [rows] = await pool.query("SELECT * FROM items;");
if (rows.length > 0) {
return { success: true, data: rows };
}
return { success: false };
};
export const getLoanByCodeV2 = async (loan_code) => {
const [result] = await pool.query(
"SELECT username, returned_date, take_date, lockers FROM loans WHERE loan_code = ?;",
[loan_code],
);
if (result.length > 0) {
return { success: true, data: result[0] };
}
return { success: false };
};
export const changeInSafeStateV2 = async (itemId) => {
const [result] = await pool.query(
"UPDATE items SET in_safe = NOT in_safe WHERE id = ?",
[itemId],
);
if (result.affectedRows > 0) {
return { success: true };
}
return { success: false };
};
export const setReturnDateV2 = async (loanCode) => {
try {
const [items] = await pool.query(
"SELECT loaned_items_id, username FROM loans WHERE loan_code = ?",
[loanCode],
);
if (items.length === 0)
return { success: false, message: "No items found for loan" };
const itemIds = Array.isArray(items[0].loaned_items_id)
? items[0].loaned_items_id
: JSON.parse(items[0].loaned_items_id || "[]");
const [result] = await pool.query(
"UPDATE loans SET returned_date = NOW() WHERE loan_code = ? AND returned_date IS NULL",
[loanCode],
);
if (result.affectedRows === 0) return { success: false };
if (itemIds.length > 0) {
await pool.query(
"UPDATE items SET in_safe = 1, currently_borrowing = NULL, last_borrowed_person = ? WHERE id IN (?)",
[items[0].username, itemIds],
);
}
return { success: true, data: { returned: true } };
} catch (error) {
console.error("setReturnDateV2 error:", error);
return { success: false, message: "Failed to set return date" };
}
};
export const setTakeDateV2 = async (loanCode) => {
const [isTaken] = await pool.query(
"SELECT take_date FROM loans WHERE loan_code = ?",
[loanCode],
);
if (isTaken.length === 0 || isTaken[0].take_date !== null) {
return { success: false, message: "Loan not found or already taken" };
}
const [items] = await pool.query(
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
[loanCode],
);
const [owner] = await pool.query(
"SELECT username FROM loans WHERE loan_code = ?",
[loanCode],
);
if (items.length === 0)
return { success: false, message: "No items found for loan" };
const itemIds = Array.isArray(items[0].loaned_items_id)
? items[0].loaned_items_id
: JSON.parse(items[0].loaned_items_id || "[]");
const [setItemStates] = await pool.query(
"UPDATE items SET in_safe = 0, currently_borrowing = (?) WHERE id IN (?)",
[owner[0].username, itemIds],
);
const [result] = await pool.query(
"UPDATE loans SET take_date = NOW() WHERE loan_code = ? AND take_date IS NULL",
[loanCode],
);
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
return { success: true };
}
return { message: "Failed to set take date", success: false };
};
export const openDoor = async (doorKey) => {
const [result] = await pool.query(
"SELECT safe_nr, id FROM items WHERE door_key = ?;",
[doorKey],
);
if (result.length > 0) {
const [changeItemSate] = await pool.query(
"UPDATE items SET in_safe = NOT in_safe WHERE id = ?",
[result[0].id],
);
if (changeItemSate.affectedRows > 0) {
return { success: true, data: result[0] };
} else {
return { success: false };
}
}
return { success: false };
};
+101
View File
@@ -0,0 +1,101 @@
import express from "express";
import { authenticate } from "../../services/authentication.js";
import { checkIfServiceIsActive } from "../../services/functions.js";
const router = express.Router();
import dotenv from "dotenv";
dotenv.config();
const loan_service = "Loan Service";
import {
getItemsFromDatabaseV2,
changeInSafeStateV2,
setTakeDateV2,
setReturnDateV2,
getLoanByCodeV2,
openDoor,
} from "./api.database.js";
// Route for API to get all items from the database
router.get("/items/:key", authenticate, async (req, res) => {
const result = await getItemsFromDatabaseV2();
if (result.success) {
res.status(200).json({ data: result.data });
} else {
res.status(500).json({ message: "Failed to fetch items" });
}
});
// Route for API to control the safe state of an item
router.post("/change-state/:key/:itemId", authenticate, async (req, res) => {
const itemId = req.params.itemId;
const result = await changeInSafeStateV2(itemId);
if (result.success) {
res.status(200).json({ data: result.data });
} else {
res.status(500).json({ message: "Failed to update item state" });
}
});
// Route for API to get a loan by its code
router.get(
"/get-loan-by-code/:key/:loan_code",
authenticate,
checkIfServiceIsActive(loan_service),
async (req, res) => {
const loan_code = req.params.loan_code;
const result = await getLoanByCodeV2(loan_code);
if (result.success) {
res.status(200).json({ data: result.data });
} else {
res.status(404).json({ message: "Loan not found" });
}
},
);
// Route for API to set the return date by the loan code
router.post(
"/set-return-date/:key/:loan_code",
authenticate,
checkIfServiceIsActive(loan_service),
async (req, res) => {
const loanCode = req.params.loan_code;
const result = await setReturnDateV2(loanCode);
if (result.success) {
res.status(200).json({});
} else {
res.status(500).json({ message: "Failed to set return date" });
}
},
);
// Route for API to set the take away date by the loan code
router.post(
"/set-take-date/:key/:loan_code",
authenticate,
checkIfServiceIsActive(loan_service),
async (req, res) => {
const loanCode = req.params.loan_code;
const result = await setTakeDateV2(loanCode);
if (result.success) {
res.status(200).json({});
} else {
res.status(500).json({ message: result.message });
}
},
);
// Route for API to open a door
router.get("/open-door/:key/:doorKey", authenticate, async (req, res) => {
const doorKey = req.params.doorKey;
const result = await openDoor(doorKey);
if (result.success) {
res.status(200).json({ data: result.data });
} else {
res.status(500).json({ message: "Failed to open door" });
}
});
export default router;
@@ -0,0 +1,340 @@
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 + lockers
const [itemsRows] = await conn.query(
"SELECT id, item_name, safe_nr 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,
);
// Build lockers array (unique, only 2-digit numbers from safe_nr)
const lockers = [
...new Set(
itemsRows
.map((r) => r.safe_nr)
.filter(
(sn) =>
sn !== null &&
sn !== undefined &&
Number.isInteger(Number(sn)) &&
Number(sn) >= 0 &&
Number(sn) <= 99,
)
.map((sn) => Number(sn)),
),
];
// 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",
};
}
let index = false;
let candidate;
let loanCode;
// Generates 6-digit loan code
do {
candidate = Math.floor(100000 + Math.random() * 899999);
const [rows] = await conn.query(
"SELECT 1 FROM loans where loan_code = ? LIMIT 1",
[candidate],
);
if (rows.length == 0) {
index = true;
loanCode = candidate;
}
} while (index === false);
// Insert loan (now includes lockers)
const [insertRes] = await conn.query(
`
INSERT INTO loans (username, loan_code, start_date, end_date, lockers, loaned_items_id, loaned_items_name, note)
VALUES (?, ?, ?, ?, CAST(? AS JSON), CAST(? AS JSON), CAST(? AS JSON), ?)
`,
[
username,
loanCode,
new Date(start).toISOString().slice(0, 19).replace("T", " "),
new Date(end).toISOString().slice(0, 19).replace("T", " "),
JSON.stringify(lockers),
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,
lockers,
},
};
} 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, status: true, data: result };
} else if (result.length === 0) {
return { success: true, status: true, data: [] };
}
return { success: false };
};
export const getBorrowableItemsFromDatabase = async (
startDate,
endDate,
role = 0,
) => {
// Overlap if: loan.start < end AND effective_end > start
// effective_end is returned_date if set, otherwise end_date
const hasRoleFilter = Number(role) > 0;
const sql = `
SELECT i.*
FROM items i
WHERE ${hasRoleFilter ? "i.can_borrow_role >= ? AND " : ""}NOT EXISTS (
SELECT 1
FROM loans l
JOIN JSON_TABLE(l.loaned_items_id, '$[*]' COLUMNS (item_id INT PATH '$')) jt
WHERE jt.item_id = i.id
AND l.deleted = 0
AND l.start_date < ?
AND COALESCE(l.returned_date, l.end_date) > ?
);
`;
const params = hasRoleFilter
? [role, endDate, startDate]
: [endDate, startDate];
const [rows] = await pool.query(sql, params);
if (rows.length > 0) {
return { success: true, data: rows };
}
return { success: false };
};
export const SETdeleteLoanFromDatabase = async (loanId) => {
const [checkIfdatesReturned] = await pool.query(
"SELECT take_date, returned_date FROM loans WHERE id = ? AND deleted = 0",
[loanId],
);
if (checkIfdatesReturned.length === 0) {
return { success: false, code: "LOAN_NOT_FOUND" };
}
const { take_date, returned_date } = checkIfdatesReturned[0];
const bothNull = take_date === null && returned_date === null;
const bothSet = take_date !== null && returned_date !== null;
if (!(bothNull || bothSet)) {
return { success: false, code: "LOAN_NOT_RETURNED" };
}
const [result] = await pool.query(
"UPDATE loans SET deleted = 1 WHERE id = ?;",
[loanId],
);
if (result.affectedRows > 0) {
return { success: true };
} else {
return { success: false };
}
};
export const getALLLoans = async () => {
const [result] = await pool.query("SELECT * FROM loans WHERE deleted = 0;");
if (result.length > 0) {
return { success: true, data: result };
}
return { success: false };
};
export const getItems = async () => {
const [result] = await pool.query("SELECT * FROM items;");
if (result.length > 0) {
return { success: true, data: result };
}
return { success: false };
};
export const setReturnDate = async (loanCode) => {
const [items] = await pool.query(
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
[loanCode],
);
const [owner] = await pool.query(
"SELECT username FROM loans WHERE loan_code = ?",
[loanCode],
);
if (items.length === 0) return { success: false };
const itemIds = Array.isArray(items[0].loaned_items_id)
? items[0].loaned_items_id
: JSON.parse(items[0].loaned_items_id || "[]");
const [setItemStates] = await pool.query(
"UPDATE items SET in_safe = 1, currently_borrowing = NULL, last_borrowed_person = (?) WHERE id IN (?)",
[owner[0].username, itemIds],
);
const [result] = await pool.query(
"UPDATE loans SET returned_date = NOW() WHERE loan_code = ?",
[loanCode],
);
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
return { success: true };
}
return { success: false };
};
export const setTakeDate = async (loanCode) => {
const [items] = await pool.query(
"SELECT loaned_items_id FROM loans WHERE loan_code = ?",
[loanCode],
);
const [owner] = await pool.query(
"SELECT username FROM loans WHERE loan_code = ?",
[loanCode],
);
if (items.length === 0) return { success: false };
const itemIds = Array.isArray(items[0].loaned_items_id)
? items[0].loaned_items_id
: JSON.parse(items[0].loaned_items_id || "[]");
const [setItemStates] = await pool.query(
"UPDATE items SET in_safe = 0, currently_borrowing = (?) WHERE id IN (?)",
[owner[0].username, itemIds],
);
const [result] = await pool.query(
"UPDATE loans SET take_date = NOW() WHERE loan_code = ?",
[loanCode],
);
if (result.affectedRows > 0 && setItemStates.affectedRows > 0) {
return { success: true };
}
return { success: false };
};
@@ -0,0 +1,63 @@
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 loginFunc = async (username, password) => {
const [result] = await pool.query(
"SELECT * FROM users WHERE username = ? AND password = ?",
[username, password],
);
if (result.length > 0) return { success: true, data: result[0] };
return { success: false };
};
export const getItems = async () => {
const [rows] = await pool.query("SELECT * FROM items;");
if (rows.length > 0) {
return { success: true, data: rows };
}
return { success: false };
};
export const getALLLoans = async () => {
const [rows] = await pool.query("SELECT * FROM loans;");
if (rows.length > 0) {
return { success: true, data: rows };
}
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 };
};
export const getDeactivatedServices = async () => {
const [rows] = await pool.query("SELECT function_name FROM functions WHERE active = 0;");
if (rows.length > 0) {
return { success: true, data: rows };
}
return { success: false };
};
+237
View File
@@ -0,0 +1,237 @@
import express from "express";
import { authenticate, generateToken } from "../../services/authentication.js";
import {
checkIfServiceIsActive,
checkIfServiceIsActive2,
} from "../../services/functions.js";
// 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";
// database funcs import
import {
createLoanInDatabase,
getLoanInfoWithID,
getLoansFromDatabase,
getBorrowableItemsFromDatabase,
getALLLoans,
getItems,
SETdeleteLoanFromDatabase,
setReturnDate,
setTakeDate,
} from "./database/loansMgmt.database.js";
router.post(
"/createLoan",
checkIfServiceIsActive(loan_service),
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) {
if (await checkIfServiceIsActive2(loan_mailer_service)) {
const mailInfo = await getLoanInfoWithID(result.data.id);
console.log(mailInfo);
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({
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",
checkIfServiceIsActive(loan_service),
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" });
}
},
);
router.post(
"/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" });
}
},
);
router.post(
"/set-take-date/:loan_code",
checkIfServiceIsActive(loan_service),
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) => {
const result = await getItems();
if (result.success) {
res.status(200).json(result.data);
} else {
res.status(500).json({ message: "Failed to fetch items" });
}
});
router.delete(
"/delete-loan/:id",
checkIfServiceIsActive(loan_service),
authenticate,
async (req, res) => {
const loanId = req.params.id;
const result = await SETdeleteLoanFromDatabase(loanId);
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" });
}
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;
+99
View File
@@ -0,0 +1,99 @@
import express from "express";
import { authenticate, generateToken } from "../../services/authentication.js";
import { checkIfServiceIsActive } from "../../services/functions.js";
const router = express.Router();
import dotenv from "dotenv";
dotenv.config();
const user_frontend_service = "User Frontend";
const contact_form_service = "Contact Form Service";
// database funcs import
import {
loginFunc,
changePassword,
getDeactivatedServices,
} from "./database/userMgmt.database.js";
// mailer imports
import { sendMail } from "../../services/mailer/send.js";
import { contactMail } from "../../services/mailer/templates/contact.js";
router.post(
"/login",
checkIfServiceIsActive(user_frontend_service),
async (req, res) => {
const result = await loginFunc(req.body.username, req.body.password);
if (result.success) {
const token = await generateToken({
username: result.data.username,
is_admin: result.data.is_admin,
first_name: result.data.first_name,
last_name: result.data.last_name,
role: result.data.role,
});
res.status(200).json({ message: "Login successful", token });
} else {
res.status(401).json({ message: "Invalid credentials" });
}
},
);
router.post(
"/change-password",
checkIfServiceIsActive(user_frontend_service),
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" });
}
},
);
router.post(
"/contact",
checkIfServiceIsActive(contact_form_service),
authenticate,
async (req, res) => {
try {
const message = req.body?.message;
const username = req.user?.first_name + " " + req.user?.last_name;
if (!username || !message) {
return res
.status(400)
.json({ message: "Username and message are required" });
}
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" });
}
},
);
router.get("/deactivated-services", authenticate, async (req, res) => {
const result = await getDeactivatedServices();
if (result.success) {
res.status(200).json(result.data);
} else {
res.status(500).json({ message: "Failed to fetch deactivated services" });
}
});
export default router;