Compare commits

...

7 Commits

Author SHA1 Message Date
theis.gaedigk 4a3c948386 implemented deactivated services banner
Co-authored-by: Copilot <copilot@github.com>
2026-04-26 16:49:32 +02:00
theis.gaedigk 6fb03530df improved error logging
Co-authored-by: Copilot <copilot@github.com>
2026-04-26 16:24:11 +02:00
theis.gaedigk d2c36e71be added request limiter to backend 2026-04-26 16:10:34 +02:00
theis.gaedigk 40d784ab36 implemented service configuration to admin panel
Co-authored-by: Copilot <copilot@github.com>
2026-04-26 15:59:57 +02:00
theis.gaedigk 60c85efd37 refactored backend
Co-authored-by: Copilot <copilot@github.com>
2026-04-26 15:03:37 +02:00
theis.gaedigk 747932cf03 implemented service configuration to API service
Co-authored-by: Copilot <copilot@github.com>
2026-04-26 14:57:59 +02:00
theis.gaedigk 0964109c4b added new feature: service config; Currently implemented in: loanMgmt and userMgmt (only Backend)
Co-authored-by: Copilot <copilot@github.com>
2026-04-26 14:40:53 +02:00
25 changed files with 778 additions and 186 deletions
+3 -3
View File
@@ -5224,9 +5224,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.6", "version": "8.5.11",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.11.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "integrity": "sha512-5dDj8+lmvA8XB78SmzGI8NlQoksv7IfutGWeVZxiixHbO+p4LDPT3wuG/D9sM/wrjZZ9I+Siy/e117vbFPxSZg==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -0,0 +1,67 @@
import { Alert, Stack, VStack, Spinner, Text, Heading } from "@chakra-ui/react";
import { useEffect, useState } from "react";
import { API_BASE } from "@/config/api.config";
import Cookies from "js-cookie";
import { useTranslation } from "react-i18next";
export const DeactivatedServices = () => {
const { t } = useTranslation();
const [deactivatedServices, setDeactivatedServices] = useState<
{ function_name: string }[]
>([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchDeactivatedServices = async () => {
setIsLoading(true);
try {
const response = await fetch(
`${API_BASE}/api/users/deactivated-services`,
{
headers: {
Authorization: `Bearer ${Cookies.get("token") || ""}`,
},
},
);
if (response.ok) {
const data = await response.json();
setDeactivatedServices(data);
} else {
console.error("Failed to fetch deactivated services");
}
} catch (error) {
console.error("Error fetching deactivated services:", error);
}
setIsLoading(false);
};
fetchDeactivatedServices();
}, []);
return (
<>
{deactivatedServices.length >= 1 && (
<Stack gap="2">
<Heading size={"xl"}>{t("deactivated-services")}</Heading>
{isLoading && (
<VStack colorPalette="teal">
<Spinner color="colorPalette.600" />
<Text color="colorPalette.600">{t("loading")}</Text>
</VStack>
)}
{deactivatedServices.length >= 1 &&
deactivatedServices.map((item) => (
<Alert.Root key={item.function_name} status="warning">
<Alert.Indicator />
<Alert.Title>
{item.function_name} {t("is-deactivated")}
</Alert.Title>
</Alert.Root>
))}
</Stack>
)}
</>
);
};
+1
View File
@@ -69,6 +69,7 @@ export const Header = () => {
className="mb-6" className="mb-6"
position="relative" position="relative"
pr={{ base: 10, md: 0 }} // Platz für den Mobile-Button rechts pr={{ base: 10, md: 0 }} // Platz für den Mobile-Button rechts
marginBottom={1}
> >
{/* Mobile: Drei-Punkte-Button, vertikal zentriert im Header */} {/* Mobile: Drei-Punkte-Button, vertikal zentriert im Header */}
<Box <Box
+6
View File
@@ -42,6 +42,12 @@ export const ContactPage = () => {
text: t("contactPage_successText"), text: t("contactPage_successText"),
}); });
setMessage(""); setMessage("");
} else if (result.status === 503) {
setAlert({
type: "error",
headline: t("serviceDeactivatedHeadline"),
text: t("contactPage_serviceDeactivatedText"),
});
} else { } else {
setAlert({ setAlert({
type: "error", type: "error",
+2
View File
@@ -19,6 +19,7 @@ import { createLoan } from "@/utils/Fetcher";
import { Header } from "@/components/Header"; import { Header } from "@/components/Header";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { approvalAnimation } from "@/components/dotLottie"; import { approvalAnimation } from "@/components/dotLottie";
import { DeactivatedServices } from "@/components/DeactivatedServices";
export interface User { export interface User {
username: string; username: string;
@@ -71,6 +72,7 @@ export const HomePage = () => {
)} )}
<Container className="px-6 sm:px-8 pt-10"> <Container className="px-6 sm:px-8 pt-10">
<Header /> <Header />
<DeactivatedServices />
{isMsg && ( {isMsg && (
<MyAlert <MyAlert
status={msgStatus} status={msgStatus}
+10
View File
@@ -79,6 +79,16 @@ const Landingpage: React.FC = () => {
Authorization: `Bearer ${Cookies.get("token")}`, Authorization: `Bearer ${Cookies.get("token")}`,
}, },
}); });
if (loanRes.status === 503) {
setError(
"error",
t("serviceDeactivatedHeadline"),
t("loan_page_serviceDeactivatedText"),
);
setIsLoading(false);
return;
}
const loanData = await loanRes.json(); const loanData = await loanRes.json();
if (Array.isArray(loanData)) { if (Array.isArray(loanData)) {
setLoans(loanData); setLoans(loanData);
+7
View File
@@ -52,6 +52,13 @@ export const MyLoansPage = () => {
}); });
if (!res.ok) { if (!res.ok) {
if (res.status === 503) {
setMsgStatus("error");
setMsgTitle(t("serviceDeactivatedHeadline"));
setMsgDescription(t("loan_page_serviceDeactivatedText"));
setIsMsg(true);
return;
}
setMsgStatus("error"); setMsgStatus("error");
setMsgTitle(t("error")); setMsgTitle(t("error"));
setMsgDescription(t("error-fetching-loans")); setMsgDescription(t("error-fetching-loans"));
+20
View File
@@ -17,6 +17,16 @@ export const getBorrowableItems = async (
}); });
if (!response.ok) { if (!response.ok) {
if (response.status === 503) {
return {
data: null,
status: "error",
title: "Service deactivated",
description:
"The loan service is currently deactivated. Please try again later.",
};
}
return { return {
data: null, data: null,
status: "error", status: "error",
@@ -60,6 +70,16 @@ export const createLoan = async (
}); });
if (!response.ok) { if (!response.ok) {
if (response.status === 503) {
return {
data: null,
status: "error",
title: "Service deactivated",
description:
"The loan service is currently deactivated. Please try again later.",
};
}
return { return {
data: null, data: null,
status: "error", status: "error",
+6 -1
View File
@@ -94,5 +94,10 @@
"naas-error": "Fehler mit no-as-a-service", "naas-error": "Fehler mit no-as-a-service",
"naas-error-desc": "Ein Fehler ist beim Kommunizieren mit no-as-a-service aufgetreten.", "naas-error-desc": "Ein Fehler ist beim Kommunizieren mit no-as-a-service aufgetreten.",
"naas-header": "Eine gute Möglichkeit, nein zu sagen...", "naas-header": "Eine gute Möglichkeit, nein zu sagen...",
"error-deleting-loan-507": "Die Ausleihe kann nicht gelöscht werden, da sie noch nicht zurückgegeben wurde." "error-deleting-loan-507": "Die Ausleihe kann nicht gelöscht werden, da sie noch nicht zurückgegeben wurde.",
"serviceDeactivatedHeadline": "Service deaktiviert",
"contactPage_serviceDeactivatedText": "Der Kontaktservice ist derzeit deaktiviert. Bitte versuchen Sie es später erneut.",
"loan_page_serviceDeactivatedText": "Der Ausleihservice ist derzeit deaktiviert. Bitte versuchen Sie es später erneut.",
"is-deactivated": "ist deaktiviert.",
"deactivated-services": "Deaktivierte Services"
} }
+6 -1
View File
@@ -94,5 +94,10 @@
"naas-error": "Error with no-as-a-service", "naas-error": "Error with no-as-a-service",
"naas-error-desc": "An error occurred while communicating with no-as-a-service.", "naas-error-desc": "An error occurred while communicating with no-as-a-service.",
"naas-header": "A good way to say no...", "naas-header": "A good way to say no...",
"error-deleting-loan-507": "The loan cannot be deleted because it has not been returned yet." "error-deleting-loan-507": "The loan cannot be deleted because it has not been returned yet.",
"serviceDeactivatedHeadline": "Service deactivated",
"contactPage_serviceDeactivatedText": "The contact service is currently deactivated. Please try again later.",
"loan_page_serviceDeactivatedText": "The loan service is currently deactivated. Please try again later.",
"is-deactivated": "is deactivated.",
"deactivated-services": "Deactivated services"
} }
+3
View File
@@ -7,6 +7,7 @@ import UserTable from "../components/UserTable";
import ItemTable from "../components/ItemTable"; import ItemTable from "../components/ItemTable";
import LoanTable from "../components/LoanTable"; import LoanTable from "../components/LoanTable";
import APIKeyTable from "@/components/APIKeyTable"; import APIKeyTable from "@/components/APIKeyTable";
import ServerConfig from "@/components/ServerConfig";
import { MoveLeft } from "lucide-react"; import { MoveLeft } from "lucide-react";
type DashboardProps = { type DashboardProps = {
@@ -44,6 +45,7 @@ const Dashboard: React.FC<DashboardProps> = ({ onLogout }) => {
viewSchliessfaecher={() => setActiveView("Schließfächer")} viewSchliessfaecher={() => setActiveView("Schließfächer")}
viewUser={() => setActiveView("User")} viewUser={() => setActiveView("User")}
viewAPI={() => setActiveView("API")} viewAPI={() => setActiveView("API")}
viewConfig={() => setActiveView("Server Konfiguration")}
/> />
<Box flex="1" display="flex" flexDirection="column"> <Box flex="1" display="flex" flexDirection="column">
<Flex <Flex
@@ -88,6 +90,7 @@ const Dashboard: React.FC<DashboardProps> = ({ onLogout }) => {
{activeView === "Ausleihen" && <LoanTable />} {activeView === "Ausleihen" && <LoanTable />}
{activeView === "Gegenstände" && <ItemTable />} {activeView === "Gegenstände" && <ItemTable />}
{activeView === "API" && <APIKeyTable />} {activeView === "API" && <APIKeyTable />}
{activeView === "Server Konfiguration" && <ServerConfig />}
</Box> </Box>
</Box> </Box>
</Flex> </Flex>
+11
View File
@@ -9,6 +9,7 @@ type SidebarProps = {
viewSchliessfaecher: () => void; viewSchliessfaecher: () => void;
viewUser: () => void; viewUser: () => void;
viewAPI: () => void; viewAPI: () => void;
viewConfig: () => void;
}; };
const Sidebar: React.FC<SidebarProps> = ({ const Sidebar: React.FC<SidebarProps> = ({
@@ -16,6 +17,7 @@ const Sidebar: React.FC<SidebarProps> = ({
viewGegenstaende, viewGegenstaende,
viewUser, viewUser,
viewAPI, viewAPI,
viewConfig
}) => { }) => {
const [info, setInfo] = useState<any>(null); const [info, setInfo] = useState<any>(null);
@@ -83,6 +85,15 @@ const Sidebar: React.FC<SidebarProps> = ({
> >
API Keys API Keys
</Link> </Link>
<Link
px={3}
py={2}
rounded="md"
_hover={{ bg: "gray.700", textDecoration: "none" }}
onClick={viewConfig}
>
Server Konfiguration
</Link>
</VStack> </VStack>
<Box mt="auto" pt={8} fontSize="xs" color="gray.500"> <Box mt="auto" pt={8} fontSize="xs" color="gray.500">
+175
View File
@@ -0,0 +1,175 @@
import React from "react";
import {
Table,
Spinner,
Text,
VStack,
Heading,
Switch,
} from "@chakra-ui/react";
import MyAlert from "./myChakra/MyAlert";
import Cookies from "js-cookie";
import { useState, useEffect } from "react";
import { formatDateTime } from "@/utils/userFuncs";
import { API_BASE } from "@/config/api.config";
type Items = {
id: number;
function_name: string;
active: boolean;
entry_created_at: string;
updated_at: string | null;
};
const ServerConfig: React.FC = () => {
const [items, setItems] = useState<Items[]>([]);
const [errorStatus, setErrorStatus] = useState<"error" | "success">("error");
const [errorMessage, setErrorMessage] = useState("");
const [errorDsc, setErrorDsc] = useState("");
const [isError, setIsError] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [reload, setReload] = useState(false);
const handleSwitchChange = async (id: number, newState: boolean) => {
try {
const response = await fetch(
`${API_BASE}/api/admin/server-config/update?functionName=${encodeURIComponent(
items.find((item) => item.id === id)?.function_name || "",
)}&active=${newState}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,
},
},
);
if (response.ok) {
setReload((prev) => !prev);
setError(
"success",
"Status updated",
"The function status was updated successfully.",
);
} else {
setError(
"error",
"Failed to update status",
"There is an error updating the function status.",
);
}
} catch (error) {
setError(
"error",
"Failed to update status",
"There is an error updating the function status.",
);
}
};
const setError = (
status: "error" | "success",
message: string,
description: string,
) => {
setIsError(false);
setErrorStatus(status);
setErrorMessage(message);
setErrorDsc(description);
setIsError(true);
};
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const response = await fetch(
`${API_BASE}/api/admin/server-config/all`,
{
method: "GET",
headers: {
Authorization: `Bearer ${Cookies.get("token")}`,
},
},
);
const data = await response.json();
return data.data;
} catch (error) {
setError("error", "Failed to fetch items", "There is an error");
} finally {
setIsLoading(false);
}
};
fetchData().then((data) => {
if (Array.isArray(data)) {
setItems(data);
}
});
}, [reload]);
return (
<>
<Heading marginBottom={4} size="2xl">
Server Konfiguration
</Heading>
{isError && (
<MyAlert
status={errorStatus}
description={errorDsc}
title={errorMessage}
/>
)}
{isLoading && (
<VStack colorPalette="teal">
<Spinner color="colorPalette.600" />
<Text color="colorPalette.600">Loading...</Text>
</VStack>
)}
<Table.Root size="sm" striped w="100%" style={{ tableLayout: "auto" }}>
<Table.Header>
<Table.Row>
<Table.ColumnHeader width="1%" whiteSpace="nowrap">
<strong>#</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Service Name</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Toggle</strong>
</Table.ColumnHeader>
<Table.ColumnHeader>
<strong>Eintrag erstellt am</strong>
</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
{items.map((item) => (
<Table.Row key={item.id}>
<Table.Cell whiteSpace="nowrap">{item.id}</Table.Cell>
<Table.Cell fontFamily="mono">{item.function_name}</Table.Cell>
<Table.Cell>
<Switch.Root
checked={item.active}
onCheckedChange={() =>
handleSwitchChange(item.id, !item.active)
}
>
<Switch.HiddenInput />
<Switch.Control>
<Switch.Thumb />
</Switch.Control>
<Switch.Label />
</Switch.Root>
</Table.Cell>
<Table.Cell whiteSpace="nowrap">
{formatDateTime(item.entry_created_at)}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
</>
);
};
export default ServerConfig;
+30 -2
View File
@@ -1,18 +1,19 @@
{ {
"name": "backendv2", "name": "backendv2",
"version": "1.0.0", "version": "v2.1.1 (dev)",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "backendv2", "name": "backendv2",
"version": "1.0.0", "version": "v2.1.1 (dev)",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^17.2.1", "dotenv": "^17.2.1",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"express": "^5.1.0", "express": "^5.1.0",
"express-rate-limit": "^8.4.1",
"jose": "^6.0.12", "jose": "^6.0.12",
"mysql2": "^3.14.3", "mysql2": "^3.14.3",
"nodemailer": "^7.0.6" "nodemailer": "^7.0.6"
@@ -349,6 +350,24 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/express-rate-limit": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz",
"integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==",
"license": "MIT",
"dependencies": {
"ip-address": "10.1.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/express-rate-limit"
},
"peerDependencies": {
"express": ">= 4.11"
}
},
"node_modules/filelist": { "node_modules/filelist": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@@ -527,6 +546,15 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/ip-address": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+1
View File
@@ -15,6 +15,7 @@
"dotenv": "^17.2.1", "dotenv": "^17.2.1",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"express": "^5.1.0", "express": "^5.1.0",
"express-rate-limit": "^8.4.1",
"jose": "^6.0.12", "jose": "^6.0.12",
"mysql2": "^3.14.3", "mysql2": "^3.14.3",
"nodemailer": "^7.0.6" "nodemailer": "^7.0.6"
@@ -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,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;
+6
View File
@@ -1,9 +1,12 @@
import express from "express"; import express from "express";
import { authenticate } from "../../services/authentication.js"; import { authenticate } from "../../services/authentication.js";
import { checkIfServiceIsActive } from "../../services/functions.js";
const router = express.Router(); const router = express.Router();
import dotenv from "dotenv"; import dotenv from "dotenv";
dotenv.config(); dotenv.config();
const loan_service = "Loan Service";
import { import {
getItemsFromDatabaseV2, getItemsFromDatabaseV2,
changeInSafeStateV2, changeInSafeStateV2,
@@ -39,6 +42,7 @@ router.post("/change-state/:key/:itemId", authenticate, async (req, res) => {
router.get( router.get(
"/get-loan-by-code/:key/:loan_code", "/get-loan-by-code/:key/:loan_code",
authenticate, authenticate,
checkIfServiceIsActive(loan_service),
async (req, res) => { async (req, res) => {
const loan_code = req.params.loan_code; const loan_code = req.params.loan_code;
const result = await getLoanByCodeV2(loan_code); const result = await getLoanByCodeV2(loan_code);
@@ -54,6 +58,7 @@ router.get(
router.post( router.post(
"/set-return-date/:key/:loan_code", "/set-return-date/:key/:loan_code",
authenticate, authenticate,
checkIfServiceIsActive(loan_service),
async (req, res) => { async (req, res) => {
const loanCode = req.params.loan_code; const loanCode = req.params.loan_code;
const result = await setReturnDateV2(loanCode); const result = await setReturnDateV2(loanCode);
@@ -69,6 +74,7 @@ router.post(
router.post( router.post(
"/set-take-date/:key/:loan_code", "/set-take-date/:key/:loan_code",
authenticate, authenticate,
checkIfServiceIsActive(loan_service),
async (req, res) => { async (req, res) => {
const loanCode = req.params.loan_code; const loanCode = req.params.loan_code;
const result = await setTakeDateV2(loanCode); const result = await setTakeDateV2(loanCode);
@@ -14,7 +14,7 @@ const pool = mysql
export const loginFunc = async (username, password) => { export const loginFunc = async (username, password) => {
const [result] = await pool.query( const [result] = await pool.query(
"SELECT * FROM users WHERE username = ? AND password = ?", "SELECT * FROM users WHERE username = ? AND password = ?",
[username, password] [username, password],
); );
if (result.length > 0) return { success: true, data: result[0] }; if (result.length > 0) return { success: true, data: result[0] };
return { success: false }; return { success: false };
@@ -40,7 +40,7 @@ export const changePassword = async (username, oldPassword, newPassword) => {
// get user current password // get user current password
const [user] = await pool.query( const [user] = await pool.query(
"SELECT * FROM users WHERE username = ? AND password = ?", "SELECT * FROM users WHERE username = ? AND password = ?",
[username, oldPassword] [username, oldPassword],
); );
if (user.length === 0) return { success: false }; if (user.length === 0) return { success: false };
@@ -48,8 +48,16 @@ export const changePassword = async (username, oldPassword, newPassword) => {
const [result] = await pool.query( const [result] = await pool.query(
"UPDATE users SET password = ? WHERE username = ?", "UPDATE users SET password = ? WHERE username = ?",
[newPassword, username] [newPassword, username],
); );
if (result.affectedRows > 0) return { success: true }; if (result.affectedRows > 0) return { success: true };
return { success: false }; 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 };
};
+59 -14
View File
@@ -1,9 +1,16 @@
import express from "express"; import express from "express";
import { authenticate, generateToken } from "../../services/authentication.js"; import { authenticate, generateToken } from "../../services/authentication.js";
import {
checkIfServiceIsActive,
checkIfServiceIsActive2,
} from "../../services/functions.js";
const router = express.Router(); const router = express.Router();
import dotenv from "dotenv"; import dotenv from "dotenv";
dotenv.config(); dotenv.config();
const loan_service = "Loan Service";
const loan_mailer_service = "Loan Mailer";
// database funcs import // database funcs import
import { import {
createLoanInDatabase, createLoanInDatabase,
@@ -18,7 +25,11 @@ import {
} from "./database/loansMgmt.database.js"; } from "./database/loansMgmt.database.js";
import { sendMailLoan } from "./services/mailer.js"; import { sendMailLoan } from "./services/mailer.js";
router.post("/createLoan", authenticate, async (req, res) => { router.post(
"/createLoan",
checkIfServiceIsActive(loan_service),
authenticate,
async (req, res) => {
try { try {
const { items, startDate, endDate, note } = req.body || {}; const { items, startDate, endDate, note } = req.body || {};
@@ -54,6 +65,7 @@ router.post("/createLoan", authenticate, async (req, res) => {
); );
if (result.success) { if (result.success) {
if (await checkIfServiceIsActive2(loan_mailer_service)) {
const mailInfo = await getLoanInfoWithID(result.data.id); const mailInfo = await getLoanInfoWithID(result.data.id);
console.log(mailInfo); console.log(mailInfo);
sendMailLoan( sendMailLoan(
@@ -64,6 +76,8 @@ router.post("/createLoan", authenticate, async (req, res) => {
mailInfo.data.created_at, mailInfo.data.created_at,
mailInfo.data.note, mailInfo.data.note,
); );
}
return res.status(201).json({ return res.status(201).json({
message: "Loan created successfully", message: "Loan created successfully",
loanId: result.data.id, loanId: result.data.id,
@@ -86,9 +100,14 @@ router.post("/createLoan", authenticate, async (req, res) => {
console.error("createLoan error:", err); console.error("createLoan error:", err);
return res.status(500).json({ message: "Failed to create loan" }); return res.status(500).json({ message: "Failed to create loan" });
} }
}); },
);
router.get("/loans", authenticate, async (req, res) => { router.get(
"/loans",
checkIfServiceIsActive(loan_service),
authenticate,
async (req, res) => {
const result = await getLoansFromDatabase(req.user.username); const result = await getLoansFromDatabase(req.user.username);
if (result.success) { if (result.success) {
res.status(200).json(result.data); res.status(200).json(result.data);
@@ -97,9 +116,14 @@ router.get("/loans", authenticate, async (req, res) => {
} else { } else {
res.status(500).json({ message: "Failed to fetch loans" }); res.status(500).json({ message: "Failed to fetch loans" });
} }
}); },
);
router.post("/set-return-date/:loan_code", authenticate, async (req, res) => { router.post(
"/set-return-date/:loan_code",
checkIfServiceIsActive(loan_service),
authenticate,
async (req, res) => {
const loanCode = req.params.loan_code; const loanCode = req.params.loan_code;
const result = await setReturnDate(loanCode); const result = await setReturnDate(loanCode);
if (result.success) { if (result.success) {
@@ -107,9 +131,14 @@ router.post("/set-return-date/:loan_code", authenticate, async (req, res) => {
} else { } else {
res.status(500).json({ message: "Failed to set return date" }); res.status(500).json({ message: "Failed to set return date" });
} }
}); },
);
router.post("/set-take-date/:loan_code", authenticate, async (req, res) => { router.post(
"/set-take-date/:loan_code",
checkIfServiceIsActive(loan_service),
authenticate,
async (req, res) => {
const loanCode = req.params.loan_code; const loanCode = req.params.loan_code;
const result = await setTakeDate(loanCode); const result = await setTakeDate(loanCode);
if (result.success) { if (result.success) {
@@ -117,7 +146,8 @@ router.post("/set-take-date/:loan_code", authenticate, async (req, res) => {
} else { } else {
res.status(500).json({ message: "Failed to set take date" }); res.status(500).json({ message: "Failed to set take date" });
} }
}); },
);
router.get("/all-items", authenticate, async (req, res) => { router.get("/all-items", authenticate, async (req, res) => {
const result = await getItems(); const result = await getItems();
@@ -128,7 +158,11 @@ router.get("/all-items", authenticate, async (req, res) => {
} }
}); });
router.delete("/delete-loan/:id", authenticate, async (req, res) => { router.delete(
"/delete-loan/:id",
checkIfServiceIsActive(loan_service),
authenticate,
async (req, res) => {
const loanId = req.params.id; const loanId = req.params.id;
const result = await SETdeleteLoanFromDatabase(loanId); const result = await SETdeleteLoanFromDatabase(loanId);
if (result.success) { if (result.success) {
@@ -146,18 +180,28 @@ router.delete("/delete-loan/:id", authenticate, async (req, res) => {
res.status(500).json({ message: "Failed to delete loan" }); res.status(500).json({ message: "Failed to delete loan" });
} }
}); },
);
router.get("/all-loans", authenticate, async (req, res) => { router.get(
"/all-loans",
checkIfServiceIsActive(loan_service),
authenticate,
async (req, res) => {
const result = await getALLLoans(); const result = await getALLLoans();
if (result.success) { if (result.success) {
res.status(200).json(result.data); res.status(200).json(result.data);
} else { } else {
res.status(500).json({ message: "Failed to fetch loans" }); res.status(500).json({ message: "Failed to fetch loans" });
} }
}); },
);
router.post("/borrowable-items", authenticate, async (req, res) => { router.post(
"/borrowable-items",
checkIfServiceIsActive(loan_service),
authenticate,
async (req, res) => {
const { startDate, endDate } = req.body || {}; const { startDate, endDate } = req.body || {};
if (!startDate || !endDate) { if (!startDate || !endDate) {
return res return res
@@ -178,6 +222,7 @@ router.post("/borrowable-items", authenticate, async (req, res) => {
.status(500) .status(500)
.json({ message: "Failed to fetch borrowable items" }); .json({ message: "Failed to fetch borrowable items" });
} }
}); },
);
export default router; export default router;
+37 -6
View File
@@ -1,14 +1,25 @@
import express from "express"; import express from "express";
import { authenticate, generateToken } from "../../services/authentication.js"; import { authenticate, generateToken } from "../../services/authentication.js";
import { checkIfServiceIsActive } from "../../services/functions.js";
const router = express.Router(); const router = express.Router();
import dotenv from "dotenv"; import dotenv from "dotenv";
dotenv.config(); dotenv.config();
const user_frontend_service = "User Frontend";
const contact_form_service = "Contact Form Service";
// database funcs import // database funcs import
import { loginFunc, changePassword } from "./database/userMgmt.database.js"; import {
loginFunc,
changePassword,
getDeactivatedServices,
} from "./database/userMgmt.database.js";
import { sendMail } from "./services/mailer_v2.js"; import { sendMail } from "./services/mailer_v2.js";
router.post("/login", async (req, res) => { router.post(
"/login",
checkIfServiceIsActive(user_frontend_service),
async (req, res) => {
const result = await loginFunc(req.body.username, req.body.password); const result = await loginFunc(req.body.username, req.body.password);
if (result.success) { if (result.success) {
const token = await generateToken({ const token = await generateToken({
@@ -22,9 +33,14 @@ router.post("/login", async (req, res) => {
} else { } else {
res.status(401).json({ message: "Invalid credentials" }); res.status(401).json({ message: "Invalid credentials" });
} }
}); },
);
router.post("/change-password", authenticate, async (req, res) => { router.post(
"/change-password",
checkIfServiceIsActive(user_frontend_service),
authenticate,
async (req, res) => {
const oldPassword = req.body.oldPassword; const oldPassword = req.body.oldPassword;
const newPassword = req.body.newPassword; const newPassword = req.body.newPassword;
const username = req.user.username; const username = req.user.username;
@@ -34,15 +50,30 @@ router.post("/change-password", authenticate, async (req, res) => {
} else { } else {
res.status(500).json({ message: "Failed to change password" }); res.status(500).json({ message: "Failed to change password" });
} }
}); },
);
router.post("/contact", authenticate, async (req, res) => { router.post(
"/contact",
checkIfServiceIsActive(contact_form_service),
authenticate,
async (req, res) => {
const message = req.body.message; const message = req.body.message;
const username = req.user.username; const username = req.user.username;
sendMail(username, message); sendMail(username, message);
res.status(200).json({ message: "Contact message sent successfully" }); res.status(200).json({ message: "Contact message sent successfully" });
},
);
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; export default router;
+11
View File
@@ -55,3 +55,14 @@ CREATE TABLE apiKeys (
PRIMARY KEY (id), PRIMARY KEY (id),
CHECK (api_key REGEXP '^[0-9]{8}$') CHECK (api_key REGEXP '^[0-9]{8}$')
) ENGINE=InnoDB; ) ENGINE=InnoDB;
CREATE TABLE functions (
id INT NOT NULL AUTO_INCREMENT,
function_name VARCHAR(500) NOT NULL UNIQUE,
active BOOLEAN NOT NULL DEFAULT true,
entry_updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
entry_created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB;
INSERT INTO functions (function_name) VALUES ("Loan Mailer"), ("Loan Service"), ("Contact Form Service"), ("User Frontend"), ("API")
+19 -5
View File
@@ -3,6 +3,23 @@ import cors from "cors";
import dotenv from "dotenv"; import dotenv from "dotenv";
import info from "./info.json" assert { type: "json" }; import info from "./info.json" assert { type: "json" };
import { authenticate } from "./services/authentication.js"; 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 // frontend routes
import loansMgmtRouter from "./routes/app/loanMgmt.route.js"; import loansMgmtRouter from "./routes/app/loanMgmt.route.js";
@@ -14,15 +31,11 @@ import loanDataMgmtRouter from "./routes/admin/loanDataMgmt.route.js";
import itemDataMgmtRouter from "./routes/admin/itemDataMgmt.route.js"; import itemDataMgmtRouter from "./routes/admin/itemDataMgmt.route.js";
import apiDataMgmtRouter from "./routes/admin/apiDataMgmt.route.js"; import apiDataMgmtRouter from "./routes/admin/apiDataMgmt.route.js";
import userMgmtRouterADMIN from "./routes/admin/userMgmt.route.js"; import userMgmtRouterADMIN from "./routes/admin/userMgmt.route.js";
import serverConfMgmtRouter from "./routes/admin/serverConfMgmt.route.js";
// API routes // API routes
import apiRouter from "./routes/api/api.route.js"; import apiRouter from "./routes/api/api.route.js";
dotenv.config();
const app = express();
const port = 8004;
const naasURL = process.env.NAAS_URL;
app.use(cors()); app.use(cors());
// Body-Parser VOR den Routen registrieren // Body-Parser VOR den Routen registrieren
app.use(express.json({ limit: "10mb" })); app.use(express.json({ limit: "10mb" }));
@@ -38,6 +51,7 @@ app.use("/api/admin/user-data", userDataMgmtRouter);
app.use("/api/admin/item-data", itemDataMgmtRouter); app.use("/api/admin/item-data", itemDataMgmtRouter);
app.use("/api/admin/api-data", apiDataMgmtRouter); app.use("/api/admin/api-data", apiDataMgmtRouter);
app.use("/api/admin/user-mgmt", userMgmtRouterADMIN); app.use("/api/admin/user-mgmt", userMgmtRouterADMIN);
app.use("/api/admin/server-config", serverConfMgmtRouter);
// API routes // API routes
app.use("/api", apiRouter); app.use("/api", apiRouter);
+18
View File
@@ -1,8 +1,12 @@
import { SignJWT, jwtVerify } from "jose"; import { SignJWT, jwtVerify } from "jose";
import env from "dotenv"; import env from "dotenv";
import { verifyAPIKeyDB } from "./database.js"; import { verifyAPIKeyDB } from "./database.js";
import { checkIfServiceIsActive2 } from "./functions.js";
env.config(); env.config();
const api_service = "API";
const user_frontend_service = "User Frontend";
const secretKey = process.env.SECRET_KEY; const secretKey = process.env.SECRET_KEY;
if (!secretKey) { if (!secretKey) {
throw new Error("Missing SECRET_KEY environment variable"); throw new Error("Missing SECRET_KEY environment variable");
@@ -45,6 +49,13 @@ export async function authenticate(req, res, next) {
const apiKey = req.params.key; const apiKey = req.params.key;
if (authHeader) { 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 parts = authHeader.split(" ");
const scheme = parts[0]; const scheme = parts[0];
const token = parts[1]; const token = parts[1];
@@ -61,6 +72,13 @@ export async function authenticate(req, res, next) {
return res.status(403).json({ message: "Present token invalid" }); // present token invalid return res.status(403).json({ message: "Present token invalid" }); // present token invalid
} }
} else if (apiKey) { } else if (apiKey) {
const serviceActive = await checkIfServiceIsActive2(api_service);
if (!serviceActive) {
return res
.status(503)
.json({ message: "API Service is currently unavailable." });
}
try { try {
await verifyAPIKey(apiKey); await verifyAPIKey(apiKey);
return next(); return next();
+42
View File
@@ -0,0 +1,42 @@
import mysql from "mysql2";
import dotenv from "dotenv";
dotenv.config();
const pool = mysql
.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
})
.promise();
export function checkIfServiceIsActive(service) {
return async (req, res, next) => {
const [result] = await pool.query(
"SELECT * FROM functions WHERE function_name = ? AND active = 1;",
[service],
);
if (result.length > 0) {
return next();
}
return res
.status(503)
.json({ message: `-${service}- is currently unavailable.` });
};
}
export async function checkIfServiceIsActive2(service) {
const [result] = await pool.query(
"SELECT * FROM functions WHERE function_name = ? AND active = 1;",
[service],
);
if (result.length > 0) {
return true;
}
return false;
}