Compare commits

...

3 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
15 changed files with 202 additions and 17 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"
} }
+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",
+2 -1
View File
@@ -15,8 +15,9 @@
"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"
} }
} }
@@ -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 };
};
+14 -1
View File
@@ -9,7 +9,11 @@ const user_frontend_service = "User Frontend";
const contact_form_service = "Contact Form Service"; 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( router.post(
@@ -63,4 +67,13 @@ router.post(
}, },
); );
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;
+17 -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";
@@ -19,11 +36,6 @@ 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" }));