Refactor loan and user management components and backend routes

- Updated LoanTable component to fetch loan data from new API endpoint and display notes.
- Enhanced UserTable component to include additional user fields (first name, last name, email, admin status) and updated input handling.
- Modified fetcher utility to use new user data API endpoint.
- Adjusted login functionality to point to the new admin login endpoint and handle unauthorized access.
- Refactored user actions utility to align with updated API endpoints for user management.
- Updated backend routes for user and loan data management to reflect new structure and naming conventions.
- Revised SQL schema and mock data to accommodate new fields and constraints.
- Changed Docker configuration to use the new database name.
This commit is contained in:
2025-11-11 17:08:45 +01:00
parent 974a5a75d8
commit a8b4ac3d60
26 changed files with 605 additions and 347 deletions

View File

@@ -8,21 +8,21 @@ dotenv.config();
import {
getAllApiKeys,
createAPIentry,
deleteAPIKey,
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({ apiKeys: result.data });
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 username = req.body.username;
const result = await createAPIentry(apiKey, username);
const entryName = req.body.entryName;
const result = await createAPIentry(apiKey, entryName);
if (result.success) {
return res.status(201).json({ message: "API key created successfully" });
}
@@ -31,7 +31,7 @@ router.post("/create-api-key", authenticateAdmin, async (req, res) => {
router.delete("/delete-api-key/:id", authenticateAdmin, async (req, res) => {
const apiKeyId = req.params.id;
const result = await deleteAPIKey(apiKeyId);
const result = await deleteAPKey(apiKeyId);
if (result.success) {
return res.status(200).json({ message: "API key deleted successfully" });
}

View File

@@ -19,10 +19,10 @@ export const getAllApiKeys = async () => {
return { success: false };
};
export const createAPIentry = async (apiKey, user) => {
export const createAPIentry = async (apiKey, entryName) => {
const [result] = await pool.query(
"INSERT INTO apiKeys (api_key, username) VALUES (?, ?)",
[apiKey, user]
"INSERT INTO apiKeys (api_key, entry_name) VALUES (?, ?)",
[apiKey, entryName]
);
if (result.affectedRows > 0) return { success: true };
return { success: false };

View File

@@ -26,7 +26,7 @@ export const deleteItemById = async (itemId) => {
export const createItem = async (item_name, can_borrow_role, in_safe) => {
const [result] = await pool.query(
"INSERT INTO items (item_name, can_borrow_role, in_safe) VALUES (?, ?, ?)",
[item_name, can_borrow_role, in_safe]
[item_name, can_borrow_role, true]
);
if (result.affectedRows > 0) return { success: true };
return { success: false };
@@ -34,9 +34,37 @@ export const createItem = async (item_name, can_borrow_role, in_safe) => {
export const editItemById = async (itemId, item_name, can_borrow_role) => {
const [result] = await pool.query(
"UPDATE items SET item_name = ?, can_borrow_role = ? WHERE id = ?",
"UPDATE items SET item_name = ?, can_borrow_role = ?, entry_updated_at = NOW() WHERE id = ?",
[item_name, can_borrow_role, 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 };
};

View File

@@ -61,7 +61,7 @@ export const editUserById = async (
export const getAllUsers = async () => {
const [result] = await pool.query(
"SELECT id, username, first_name, last_name, role, email, is_admin FROM users"
"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 };

View File

@@ -16,7 +16,7 @@ import {
router.get("/all-items", authenticateAdmin, async (req, res) => {
const result = await getAllItems();
if (result.success) {
return res.status(200).json({ items: result.data });
return res.status(200).json(result.data);
}
return res.status(500).json({ message: "Failed to retrieve items" });
});
@@ -31,8 +31,8 @@ router.delete("/delete-item/:id", authenticateAdmin, async (req, res) => {
});
router.post("/create-item", authenticateAdmin, async (req, res) => {
const { item_name, can_borrow_role, in_safe } = req.body;
const result = await createItem(item_name, can_borrow_role, in_safe);
const { item_name, can_borrow_role } = req.body;
const result = await createItem(item_name, can_borrow_role);
if (result.success) {
return res.status(201).json({ message: "Item created successfully" });
}
@@ -55,8 +55,7 @@ router.post("/edit-item/:id", authenticateAdmin, async (req, res) => {
router.post("/change-safe-state/:id", authenticateAdmin, async (req, res) => {
const itemId = req.params.id;
const { in_safe } = req.body;
const result = await changeSafeState(itemId, in_safe);
const result = await changeSafeState(itemId);
if (result.success) {
return res.status(200).json({ message: "Safe state changed successfully" });
}

View File

@@ -13,7 +13,7 @@ import {
router.get("/all-loans", authenticateAdmin, async (req, res) => {
const result = await getAllLoans();
if (result.success) {
return res.status(200).json({ loans: result.data });
return res.status(200).json(result.data);
}
return res.status(500).json({ message: "Failed to retrieve loans" });
});

View File

@@ -47,7 +47,6 @@ router.delete("/delete-user/:id", authenticateAdmin, async (req, res) => {
});
router.post("/edit-user/:id", authenticateAdmin, async (req, res) => {
const password = req.body.password;
const first_name = req.body.first_name;
const last_name = req.body.last_name;
const role = req.body.role;
@@ -57,7 +56,6 @@ router.post("/edit-user/:id", authenticateAdmin, async (req, res) => {
const result = await editUserById(
userId,
password,
first_name,
last_name,
role,
@@ -109,7 +107,7 @@ router.post("/edit-user/:id", authenticateAdmin, async (req, res) => {
router.get("/users", authenticateAdmin, async (req, res) => {
const result = await getAllUsers();
if (result.success) {
return res.status(200).json({ users: result.data });
return res.status(200).json(result.data);
}
return res.status(500).json({ message: "Failed to retrieve users" });
});

View File

@@ -1,7 +1,9 @@
import express from "express";
import { authenticate, generateToken } from "../../services/authentication.js";
import {
generateToken,
authenticateAdmin,
} from "../../services/authentication.js";
const router = express.Router();
import nodemailer from "nodemailer";
import dotenv from "dotenv";
dotenv.config();
@@ -9,7 +11,12 @@ dotenv.config();
import { loginAdmin } from "./database/userMgmt.database.js";
router.post("/login", async (req, res) => {
const result = await loginAdmin(req.body.username, req.body.password);
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({
@@ -18,7 +25,11 @@ router.post("/login", async (req, res) => {
last_name: result.data.last_name,
admin: result.data.is_admin,
});
return res.status(200).json({ message: "Login erfolgreich", token });
return res.status(200).json({
message: "Login erfolgreich",
token,
first_name: result.data.first_name,
});
}
if (result.reason === "not_admin") {
@@ -27,3 +38,9 @@ router.post("/login", async (req, res) => {
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" });
});
export default router;

View File

@@ -1,3 +1,5 @@
import express from "express";
const router = express.Router();
const router = express.Router();
export default router;

View File

@@ -1,5 +1,5 @@
import express from "express";
import { authenticate, generateToken } from "../services/tokenService.js";
import { authenticate, generateToken } from "../../services/authentication.js";
const router = express.Router();
import nodemailer from "nodemailer";
import dotenv from "dotenv";
@@ -21,3 +21,4 @@ router.post("/login", async (req, res) => {
}
});
export default router;

View File

@@ -1,91 +1,39 @@
-- MUST BE UPDATED BEFORE USE
-- Mock data for borrow_system_new
USE borrow_system_new;
-- Optional: keep insert order predictable
SET time_zone = '+00:00';
START TRANSACTION;
-- Users
INSERT INTO users (username, password, first_name, last_name, role, is_admin)
INSERT INTO users (username, password, email, first_name, last_name, role, is_admin, entry_created_at)
VALUES
('alice', 'password123', 'Alice', 'Andersen', 1, false),
('bob', 'password123', 'Bob', 'Berg', 2, false),
('carol', 'password123', 'Carol', 'Christie', 2, false),
('dave', 'password123', 'Dave', 'Dawson', 1, false),
('eve', 'password123', 'Eve', 'Evans', 1, false),
('admin', 'password123', 'Admin', 'User', 3, true);
('admin', '$2b$12$adminhashedpasswordplaceholder0000000000', 'admin@example.com', 'System', 'Admin', 99, TRUE, '2025-01-01 08:00:00'),
('alice', '$2b$12$alicehashedpasswordplaceholder0000000000', 'alice@example.com', 'Alice', 'Anderson', 1, FALSE, '2025-06-01 09:10:00'),
('bob', '$2b$12$bobhashedpasswordplaceholder000000000000', 'bob@example.com', 'Bob', 'Brown', 2, FALSE, '2025-06-02 10:15:00'),
('carol', '$2b$12$carolhashedpasswordplaceholder00000000000', 'carol@example.com', 'Carol', 'Clark', 0, FALSE, '2025-06-03 11:20:00');
-- Items
INSERT INTO items (item_name, can_borrow_role, in_safe, last_borrowed_person, currently_borrowing)
-- Items (ids will start at 1)
INSERT INTO items (item_name, can_borrow_role, in_safe, entry_created_at, last_borrowed_person, currently_borrowing)
VALUES
('Canon EOS 90D Camera', 1, false, 'bob', 'alice'),
('Rode NT1 Microphone', 1, true, 'dave', NULL),
('MacBook Pro 13', 2, false, 'bob', 'carol'),
('Tripod Manfrotto', 1, false, 'carol', 'alice'),
('LED Panel Aputure', 1, true, NULL, NULL),
('Zoom H6 Recorder', 1, true, 'dave', NULL),
('Wacom Intuos Tablet', 1, true, NULL, NULL),
('DJI Ronin-S Gimbal', 2, true, NULL, NULL),
('Sony A7 III Body', 2, false, 'carol', 'eve'),
('Sigma 24-70mm Lens', 2, false, 'carol', 'eve');
-- Capture item IDs for JSON arrays
SET @id_canon = (SELECT id FROM items WHERE item_name='Canon EOS 90D Camera');
SET @id_rode = (SELECT id FROM items WHERE item_name='Rode NT1 Microphone');
SET @id_mac13 = (SELECT id FROM items WHERE item_name='MacBook Pro 13');
SET @id_tripod = (SELECT id FROM items WHERE item_name='Tripod Manfrotto');
SET @id_led = (SELECT id FROM items WHERE item_name='LED Panel Aputure');
SET @id_zoom = (SELECT id FROM items WHERE item_name='Zoom H6 Recorder');
SET @id_tablet = (SELECT id FROM items WHERE item_name='Wacom Intuos Tablet');
SET @id_ronin = (SELECT id FROM items WHERE item_name='DJI Ronin-S Gimbal');
SET @id_sony = (SELECT id FROM items WHERE item_name='Sony A7 III Body');
SET @id_sigma = (SELECT id FROM items WHERE item_name='Sigma 24-70mm Lens');
('MacBook Pro 16\"', 1, TRUE, '2025-05-01 09:00:00', 'alice', NULL),
('Projector Epson X200', 2, TRUE, '2025-04-20 10:00:00', 'bob', NULL),
('Canon EOS R6', 1, TRUE, '2025-03-15 14:30:00', NULL, NULL),
('Wireless Microphone', 0, TRUE,'2025-05-10 12:00:00', 'carol', NULL),
('USB-C Charger', 0, FALSE, '2025-05-11 12:30:00', 'alice', 'alice');
-- Loans
INSERT INTO loans (
username, loan_code, start_date, end_date, take_date, returned_date, loaned_items_id, loaned_items_name, deleted
) VALUES
-- Ongoing loan: Alice has Canon + Tripod
('alice', 100001, '2025-10-01 09:00:00', '2025-10-08 17:00:00', '2025-10-01 09:15:00', NULL,
JSON_ARRAY(@id_canon, @id_tripod),
JSON_ARRAY('Canon EOS 90D Camera','Tripod Manfrotto'),
false
),
-- Ongoing loan: Carol has MacBook Pro 13
('carol', 100002, '2025-10-03 10:00:00', '2025-10-10 16:00:00', '2025-10-03 10:05:00', NULL,
JSON_ARRAY(@id_mac13),
JSON_ARRAY('MacBook Pro 13'),
false
),
-- Returned loan: Dave had Zoom + Rode
('dave', 100003, '2025-09-10 08:30:00', '2025-09-12 16:00:00', '2025-09-10 08:45:00', '2025-09-12 15:40:00',
JSON_ARRAY(@id_zoom, @id_rode),
JSON_ARRAY('Zoom H6 Recorder','Rode NT1 Microphone'),
false
),
-- Cancelled/deleted booking (never taken): Bob reserved Tablet
('bob', 100004, '2025-10-05 09:00:00', '2025-10-06 09:00:00', NULL, NULL,
JSON_ARRAY(@id_tablet),
JSON_ARRAY('Wacom Intuos Tablet'),
true
),
-- Ongoing loan, likely overdue: Eve has Sony + Sigma
('eve', 100005, '2025-10-15 11:00:00', '2025-10-20 12:00:00', '2025-10-15 11:10:00', NULL,
JSON_ARRAY(@id_sony, @id_sigma),
JSON_ARRAY('Sony A7 III Body','Sigma 24-70mm Lens'),
false
),
-- Completed single-day loan: Bob used LED panel
('bob', 100006, '2025-09-20 13:00:00', '2025-09-20 18:00:00', '2025-09-20 13:05:00', '2025-09-20 17:30:00',
JSON_ARRAY(@id_led),
JSON_ARRAY('LED Panel Aputure'),
false
);
-- API keys
INSERT INTO apiKeys (api_key, username)
INSERT INTO loans (username, loan_code, start_date, end_date, take_date, returned_date, created_at, loaned_items_id, loaned_items_name, deleted, note)
VALUES
(71002123, 'alice'),
(71002124, 'bob'),
(71002125, 'carol'),
(99999999, 'admin');
('alice', '000101', '2025-06-10 09:00:00', '2025-06-17 09:00:00', '2025-06-10 09:05:00', NULL, '2025-06-10 09:00:00',
JSON_ARRAY(1,5), JSON_ARRAY('MacBook Pro 16\"','USB-C Charger'), FALSE, 'For project work'),
('bob', '000102', '2025-06-01 14:00:00', '2025-06-04 12:00:00', '2025-06-01 14:10:00', '2025-06-04 11:50:00', '2025-06-01 14:00:00',
JSON_ARRAY(2), JSON_ARRAY('Projector Epson X200'), FALSE, NULL),
('carol', '000103', '2025-06-05 08:00:00', '2025-06-06 18:00:00', NULL, NULL, '2025-06-05 08:00:00',
JSON_ARRAY(4), JSON_ARRAY('Wireless Microphone'), FALSE, 'Reserved for event');
-- API keys (15 digits)
INSERT INTO apiKeys (api_key, entry_name, entry_created_at, last_used_at)
VALUES
('000000000000001', 'internal-service-key', '2025-01-02 07:00:00', NULL),
('123456789012345', 'ci-pipeline', '2025-02-15 08:30:00', '2025-06-10 09:00:00');
COMMIT;

View File

@@ -17,7 +17,7 @@ CREATE TABLE users (
CREATE TABLE loans (
id int NOT NULL AUTO_INCREMENT,
username varchar(100) NOT NULL,
loan_code int NOT NULL UNIQUE,
loan_code Char(6) NOT NULL UNIQUE,
start_date timestamp NOT NULL,
end_date timestamp NOT NULL,
take_date timestamp NULL DEFAULT NULL,
@@ -28,10 +28,7 @@ CREATE TABLE loans (
deleted bool NOT NULL DEFAULT false,
note varchar(500) DEFAULT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_loans_username
FOREIGN KEY (username) REFERENCES users(username)
ON UPDATE CASCADE
ON DELETE RESTRICT
CHECK (loan_code REGEXP '^[0-9]{6}$')
) ENGINE=InnoDB;
CREATE TABLE items (
@@ -47,15 +44,11 @@ CREATE TABLE items (
);
CREATE TABLE apiKeys (
id int NOT NULL AUTO_INCREMENT,
id INT NOT NULL AUTO_INCREMENT,
api_key CHAR(15) NOT NULL UNIQUE,
username VARCHAR(100) NOT NULL,
last_used_at timestamp DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
entry_created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
entry_name VARCHAR(100) NOT NULL,
last_used_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
entry_created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
CONSTRAINT chk_api_key_len CHECK (CHAR_LENGTH(api_key) = 15),
CONSTRAINT fk_apikeys_username
FOREIGN KEY (username) REFERENCES users(username)
ON UPDATE CASCADE
ON DELETE RESTRICT
CHECK (api_key REGEXP '^[0-9]{15}$')
) ENGINE=InnoDB;

View File

@@ -1,43 +1,51 @@
import express from "express";
import cors from "cors";
import env from "dotenv";
import info from "./info.json" assert { type: "json" };
// frontend routes
import loansMgmtRouter from "./routes/app/loanMgmt.route.js";
import userMgmtRouter from "./routes/app/userMgmt.route.js";
import userMgmtRouterAPP from "./routes/app/userMgmt.route.js";
// admin routes
import userDataMgmtRouter from "./routes/admin/userDataMgmt.route.js";
import loanDataMgmtRouter from "./routes/admin/loanDataMgmt.route.js";
import itemDataMgmtRouter from "./routes/admin/itemDataMgmt.route.js";
import apiDataMgmtRouter from "./routes/admin/apiDataMgmt.route.js";
import userMgmtRouterADMIN from "./routes/admin/userMgmt.route.js";
env.config();
const app = express();
const port = 8002;
const port = 8004;
app.use(cors());
// Body-Parser VOR den Routen registrieren
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
// frontend routes
app.use("/api/loans", loansMgmtRouter);
app.use("/api/users", userMgmtRouter);
app.use("/api/users", userMgmtRouterAPP);
// admin routes
app.use("/api/admin/loan-data", loanDataMgmtRouter);
app.use("/api/admin/user-data", userDataMgmtRouter);
app.use("/api/admin/item-data", itemDataMgmtRouter);
app.use("/api/admin/api-data", apiDataMgmtRouter);
app.use("/api/admin/user-mgmt", userMgmtRouterADMIN);
// Increase body size limits to support large CSV JSON payloads
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
app.set("view engine", "ejs");
app.use(express.json({ limit: "10mb" }));
app.listen(port, () => {
console.log(`Server is running on port: ${port}`);
});
app.get("/", (req, res) => {
res.send(info);
});
// error handling code
app.use((err, req, res, next) => {
// Log the error stack and send a generic error response
console.error(err.stack);
res.status(500).send("Something broke!");
});

View File

@@ -1,6 +1,6 @@
import { SignJWT, jwtVerify } from "jose";
import env from "dotenv";
import { getAllApiKeys } from "./database";
import { getAllApiKeys } from "./database.js";
env.config();
const secretKey = process.env.SECRET_KEY;