major changes to docker and npm config
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
FROM node:20-alpine
|
||||
|
||||
ENV NODE_ENV=production
|
||||
WORKDIR /backend
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8004
|
||||
CMD ["npm", "start"]
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"backend-info": {
|
||||
"version": "v2.2 (dev)"
|
||||
},
|
||||
"frontend-info": {
|
||||
"version": "v2.2 (dev)"
|
||||
},
|
||||
"admin-panel-info": {
|
||||
"version": "v1.4 (dev)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "2.1.1-dev",
|
||||
"private": true,
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"dev": "node --watch server.js",
|
||||
"start": "NODE_ENV=production node server.js\""
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.1",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^8.4.1",
|
||||
"jose": "^6.0.12",
|
||||
"mysql2": "^3.14.3",
|
||||
"nodemailer": "^10.2.1"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,69 @@
|
||||
use borrow_system_new;
|
||||
|
||||
CREATE TABLE users (
|
||||
id int NOT NULL AUTO_INCREMENT,
|
||||
username varchar(100) NOT NULL UNIQUE,
|
||||
password varchar(255) NOT NULL,
|
||||
email varchar(255) NOT NULL,
|
||||
first_name varchar(255) NOT NULL,
|
||||
last_name varchar(255) NOT NULL,
|
||||
role int NOT NULL,
|
||||
is_admin bool NOT NULL DEFAULT false,
|
||||
entry_created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
entry_updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE loans (
|
||||
id int NOT NULL AUTO_INCREMENT,
|
||||
username varchar(100) NOT NULL,
|
||||
lockers json NOT NULL DEFAULT ('[]'),
|
||||
loan_code Char(6) NOT NULL UNIQUE,
|
||||
start_date timestamp NOT NULL,
|
||||
end_date timestamp NOT NULL,
|
||||
take_date timestamp NULL DEFAULT NULL,
|
||||
returned_date timestamp NULL DEFAULT NULL,
|
||||
created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
loaned_items_id json NOT NULL DEFAULT ('[]'),
|
||||
loaned_items_name json NOT NULL DEFAULT ('[]'),
|
||||
deleted bool NOT NULL DEFAULT false,
|
||||
deleted_admin bool NOT NULL DEFAULT false,
|
||||
note varchar(500) DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CHECK (loan_code REGEXP '^[0-9]{6}$')
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE items (
|
||||
id int NOT NULL AUTO_INCREMENT,
|
||||
item_name varchar(255) NOT NULL UNIQUE,
|
||||
can_borrow_role INT NOT NULL,
|
||||
in_safe bool NOT NULL DEFAULT true,
|
||||
safe_nr INT DEFAULT NULL UNIQUE,
|
||||
door_key INT DEFAULT NULL UNIQUE,
|
||||
entry_created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
entry_updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
last_borrowed_person varchar(255) DEFAULT NULL,
|
||||
currently_borrowing varchar(255) DEFAULT NULL,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE apiKeys (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
api_key CHAR(8) NOT NULL UNIQUE,
|
||||
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),
|
||||
CHECK (api_key REGEXP '^[0-9]{8}$')
|
||||
) 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")
|
||||
@@ -0,0 +1,91 @@
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import dotenv from "dotenv";
|
||||
import info from "./info.json" with { type: "json" };
|
||||
import { authenticate } from "./services/authentication.js";
|
||||
import { rateLimit } from "express-rate-limit";
|
||||
|
||||
dotenv.config();
|
||||
const app = express();
|
||||
const port = 8004;
|
||||
const naasURL = process.env.NAAS_URL;
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: 1 * 60 * 1000, // 1 minute
|
||||
limit: 50, // Limit each IP to 50 requests per `window` (here, per 1 minute).
|
||||
standardHeaders: "draft-8", // draft-6: `RateLimit-*` headers; draft-7 & draft-8: combined `RateLimit` header
|
||||
legacyHeaders: false, // Disable the `X-RateLimit-*` headers.
|
||||
ipv6Subnet: 56, // Set to 60 or 64 to be less aggressive, or 52 or 48 to be more aggressive
|
||||
// store: ... , // Redis, Memcached, etc. See below.
|
||||
});
|
||||
|
||||
app.use(limiter);
|
||||
|
||||
// frontend routes
|
||||
import loansMgmtRouter from "./routes/app/loanMgmt.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";
|
||||
import serverConfMgmtRouter from "./routes/admin/serverConfMgmt.route.js";
|
||||
|
||||
// API routes
|
||||
import apiRouter from "./routes/api/api.route.js";
|
||||
|
||||
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", 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);
|
||||
app.use("/api/admin/server-config", serverConfMgmtRouter);
|
||||
|
||||
// API routes
|
||||
app.use("/api", apiRouter);
|
||||
|
||||
app.set("view engine", "ejs");
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server is running on port: ${port}`);
|
||||
});
|
||||
|
||||
app.get("/no", async (req, res) => {
|
||||
try {
|
||||
const response = await fetch(naasURL);
|
||||
if (!response.ok) {
|
||||
res.status(500).send("Request to no-as-a-service went wrong.");
|
||||
}
|
||||
const data = await response.json();
|
||||
res.json(data);
|
||||
} catch (error) {
|
||||
console.error("Error communicating with no-as-a-service:", error);
|
||||
res.status(500).send("Error communicating with no-as-a-service.");
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/verify", authenticate, async (req, res) => {
|
||||
res.status(200).json({ message: "Token is valid", user: req.user });
|
||||
});
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
res.send(info);
|
||||
});
|
||||
|
||||
// error handling code
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err.stack);
|
||||
res.status(500).send("Something broke!");
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import { SignJWT, jwtVerify } from "jose";
|
||||
import env from "dotenv";
|
||||
import { verifyAPIKeyDB } from "./database.js";
|
||||
import { checkIfServiceIsActive2 } from "./functions.js";
|
||||
env.config();
|
||||
|
||||
const api_service = "API";
|
||||
const user_frontend_service = "User Frontend";
|
||||
|
||||
const secretKey = process.env.SECRET_KEY;
|
||||
if (!secretKey) {
|
||||
throw new Error("Missing SECRET_KEY environment variable");
|
||||
}
|
||||
const secret = new TextEncoder().encode(secretKey);
|
||||
|
||||
export async function generateToken(payload) {
|
||||
return await new SignJWT(payload)
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setIssuedAt()
|
||||
.setExpirationTime("2h")
|
||||
.sign(secret);
|
||||
}
|
||||
|
||||
export async function authenticateAdmin(req, res, next) {
|
||||
const authHeader = req.headers["authorization"];
|
||||
if (!authHeader) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
const [scheme, token] = authHeader.split(" ");
|
||||
if (!/^Bearer$/i.test(scheme) || !token) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await verifyToken(token);
|
||||
if (!payload?.admin) {
|
||||
return res.status(403).json({ message: "Forbidden: admin only" });
|
||||
}
|
||||
req.user = payload;
|
||||
return next();
|
||||
} catch {
|
||||
return res.status(403).json({ message: "Forbidden 403" });
|
||||
}
|
||||
}
|
||||
|
||||
export async function authenticate(req, res, next) {
|
||||
const authHeader = req.headers["authorization"];
|
||||
const apiKey = req.params.key;
|
||||
|
||||
if (authHeader) {
|
||||
const serviceActive = await checkIfServiceIsActive2(user_frontend_service);
|
||||
if (!serviceActive) {
|
||||
return res
|
||||
.status(503)
|
||||
.json({ message: "User Frontend is currently unavailable." });
|
||||
}
|
||||
|
||||
const parts = authHeader.split(" ");
|
||||
const scheme = parts[0];
|
||||
const token = parts[1];
|
||||
|
||||
if (!/^Bearer$/i.test(scheme) || !token) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await verifyToken(token);
|
||||
req.user = payload;
|
||||
return next();
|
||||
} catch {
|
||||
return res.status(403).json({ message: "Present token invalid" }); // present token invalid
|
||||
}
|
||||
} else if (apiKey) {
|
||||
const serviceActive = await checkIfServiceIsActive2(api_service);
|
||||
if (!serviceActive) {
|
||||
return res
|
||||
.status(503)
|
||||
.json({ message: "API Service is currently unavailable." });
|
||||
}
|
||||
|
||||
try {
|
||||
await verifyAPIKey(apiKey);
|
||||
return next();
|
||||
} catch {
|
||||
return res.status(403).json({ message: "API Key invalid" }); // fix: don't chain after sendStatus
|
||||
}
|
||||
} else {
|
||||
return res.status(401).json({ message: "Unauthorized" }); // no credentials
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyAPIKey(apiKey) {
|
||||
const result = await verifyAPIKeyDB(apiKey);
|
||||
|
||||
if (result.valid) {
|
||||
return;
|
||||
} else {
|
||||
throw new Error("Invalid API Key");
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyToken(token) {
|
||||
const { payload } = await jwtVerify(token, secret, {
|
||||
algorithms: ["HS256"],
|
||||
});
|
||||
return payload;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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 verifyAPIKeyDB = async (apiKey) => {
|
||||
const [result] = await pool.query(
|
||||
"SELECT * FROM apiKeys WHERE api_key = ?;",
|
||||
[apiKey]
|
||||
);
|
||||
if (result.length > 0) {
|
||||
const [lastUsed] = await pool.query(
|
||||
"UPDATE apiKeys SET last_used_at = NOW() WHERE api_key = ?;",
|
||||
[apiKey]
|
||||
);
|
||||
if (lastUsed.affectedRows > 0) {
|
||||
return { valid: true };
|
||||
} else {
|
||||
return { valid: false };
|
||||
}
|
||||
} else {
|
||||
return { valid: false };
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { transporter } from "./transporter.js";
|
||||
|
||||
export async function sendMail({ to, subject, text, html }) {
|
||||
const info = await transporter.sendMail({
|
||||
from: '"Ausleihsystem" <noreply@mcs-medien.de>',
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
});
|
||||
console.log("Mail sent:", info.messageId);
|
||||
return info;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
export function contactMail({ username, message }) {
|
||||
const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9";
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title>Neue Nachricht</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f2f4f7;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;color:#111827;-webkit-text-size-adjust:100%;">
|
||||
<div style="display:none;max-height:0;overflow:hidden;opacity:0;">Neue Kontaktanfrage im Ausleihsystem.</div>
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#f2f4f7;">
|
||||
<tr>
|
||||
<td align="center" style="padding:32px 16px;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" width="600" style="max-width:600px;width:100%;border-radius:14px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.06);">
|
||||
|
||||
<!-- Header -->
|
||||
<tr>
|
||||
<td style="background:${brand};padding:32px 30px 28px;">
|
||||
<h1 style="margin:0;font-size:24px;color:#ffffff;font-weight:700;">Neue Nachricht</h1>
|
||||
<p style="margin:8px 0 0;font-size:14px;color:rgba(255,255,255,0.85);">Eine neue Kontaktanfrage ist eingegangen.</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Accent line -->
|
||||
<tr>
|
||||
<td style="height:3px;line-height:3px;font-size:1px;background:${brand};"> </td>
|
||||
</tr>
|
||||
|
||||
<!-- Content -->
|
||||
<tr>
|
||||
<td style="background:#ffffff;padding:28px 30px;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#fafbfc;border:1px solid #f3f4f6;border-radius:10px;border-collapse:collapse;">
|
||||
<tr>
|
||||
<td style="padding:14px 16px;color:#6b7280;font-size:13px;white-space:nowrap;vertical-align:top;border-bottom:1px solid #f3f4f6;">
|
||||
Benutzername
|
||||
</td>
|
||||
<td style="padding:14px 16px;font-weight:600;color:#111827;font-size:14px;vertical-align:top;border-bottom:1px solid #f3f4f6;">
|
||||
${username || "N/A"}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:14px 16px;color:#6b7280;font-size:13px;white-space:nowrap;vertical-align:top;">
|
||||
Nachricht
|
||||
</td>
|
||||
<td style="padding:14px 16px;font-weight:600;color:#111827;font-size:14px;vertical-align:top;white-space:pre-line;">
|
||||
${message || "N/A"}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td style="background:#f9fafb;padding:20px 30px;border-top:1px solid #e5e7eb;">
|
||||
<p style="margin:0;font-size:12px;color:#9ca3af;text-align:center;line-height:1.6;">
|
||||
Diese E-Mail wurde automatisch vom Ausleihsystem gesendet.<br>
|
||||
Bitte antworten Sie nicht auf diese Nachricht.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const text = `Neue Kontaktanfrage\n\nBenutzername: ${username}\nNachricht:\n${message}`;
|
||||
|
||||
return { html, text };
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
const formatDateTime = (value) => {
|
||||
if (value == null) return "N/A";
|
||||
const d = value instanceof Date ? value : new Date(value);
|
||||
if (isNaN(d.getTime())) return "N/A";
|
||||
return (
|
||||
d.toLocaleString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}) + " Uhr"
|
||||
);
|
||||
};
|
||||
|
||||
export function loanMail(user, items, startDate, endDate, createdDate, note) {
|
||||
const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9";
|
||||
|
||||
const itemsHtml =
|
||||
Array.isArray(items) && items.length
|
||||
? items
|
||||
.map(
|
||||
(i) =>
|
||||
`<span style="display:inline-block;background:#f0f9ff;color:#0369a1;padding:4px 12px;margin:2px 4px 2px 0;border-radius:20px;font-size:13px;font-weight:500;">${i}</span>`,
|
||||
)
|
||||
.join(" ")
|
||||
: '<span style="color:#9ca3af;">Keine Gegenstände</span>';
|
||||
|
||||
const row = (label, value, isLast = false) => `
|
||||
<tr>
|
||||
<td style="padding:14px 16px;color:#6b7280;font-size:13px;white-space:nowrap;vertical-align:top;${isLast ? "" : "border-bottom:1px solid #f3f4f6;"}">
|
||||
${label}
|
||||
</td>
|
||||
<td style="padding:14px 16px;font-weight:600;color:#111827;font-size:14px;vertical-align:top;${isLast ? "" : "border-bottom:1px solid #f3f4f6;"}">
|
||||
${value}
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title>Neue Ausleihe</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f2f4f7;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;color:#111827;-webkit-text-size-adjust:100%;">
|
||||
<div style="display:none;max-height:0;overflow:hidden;opacity:0;">Neue Ausleihe erstellt – Details zur Buchung.</div>
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#f2f4f7;">
|
||||
<tr>
|
||||
<td align="center" style="padding:32px 16px;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" width="600" style="max-width:600px;width:100%;border-radius:14px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.06);">
|
||||
|
||||
<!-- Header -->
|
||||
<tr>
|
||||
<td style="background:${brand};padding:32px 30px 28px;">
|
||||
<h1 style="margin:0;font-size:24px;color:#ffffff;font-weight:700;">Neue Ausleihe</h1>
|
||||
<p style="margin:8px 0 0;font-size:14px;color:rgba(255,255,255,0.85);">Es wurde soeben eine neue Ausleihe im System angelegt.</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Accent line -->
|
||||
<tr>
|
||||
<td style="background:#ffffff;padding:0;height:3px;line-height:3px;font-size:1px;background:${brand};"> </td>
|
||||
</tr>
|
||||
|
||||
<!-- Details -->
|
||||
<tr>
|
||||
<td style="background:#ffffff;padding:28px 30px 10px;">
|
||||
<p style="margin:0 0 14px;font-size:11px;font-weight:700;color:#9ca3af;letter-spacing:1.5px;text-transform:uppercase;">Details zur Ausleihe</p>
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#fafbfc;border:1px solid #f3f4f6;border-radius:10px;border-collapse:collapse;">
|
||||
${row("Benutzer", user || "N/A")}
|
||||
${row("Gegenstände", itemsHtml)}
|
||||
${row("Startdatum", formatDateTime(startDate))}
|
||||
${row("Enddatum", formatDateTime(endDate))}
|
||||
${row("Erstellt am", formatDateTime(createdDate))}
|
||||
${row("Notiz", note || '<span style="color:#9ca3af;">Keine Notiz</span>', true)}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Button -->
|
||||
<tr>
|
||||
<td style="background:#ffffff;padding:20px 30px 32px;" align="center">
|
||||
<a href="https://admin.insta.the1s.de/api" target="_blank" rel="noopener noreferrer"
|
||||
style="display:inline-block;background:${brand};color:#ffffff;text-decoration:none;padding:13px 28px;border-radius:8px;font-weight:600;font-size:15px;">
|
||||
Ausleihe ansehen →
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td style="background:#f9fafb;padding:20px 30px;border-top:1px solid #e5e7eb;">
|
||||
<p style="margin:0;font-size:12px;color:#9ca3af;text-align:center;line-height:1.6;">
|
||||
Diese E-Mail wurde automatisch vom Ausleihsystem gesendet.<br>
|
||||
Bitte antworten Sie nicht auf diese Nachricht.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const itemsText = Array.isArray(items) ? items.join(", ") : "N/A";
|
||||
const text = [
|
||||
"Neue Ausleihe erstellt",
|
||||
"-".repeat(30),
|
||||
`Benutzer: ${user || "N/A"}`,
|
||||
`Gegenstaende: ${itemsText}`,
|
||||
`Start: ${formatDateTime(startDate)}`,
|
||||
`Ende: ${formatDateTime(endDate)}`,
|
||||
`Erstellt: ${formatDateTime(createdDate)}`,
|
||||
`Notiz: ${note || "Keine Notiz"}`,
|
||||
"",
|
||||
"-> https://admin.insta.the1s.de/api",
|
||||
].join("\n");
|
||||
|
||||
return { html, text };
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import nodemailer from "nodemailer";
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
export 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,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>backend</title>
|
||||
</head>
|
||||
<body>
|
||||
backend
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user