Compare commits

..

53 Commits

Author SHA1 Message Date
theis.gaedigk 55c44cc639 Merge branch 'dev_demo' into debian12Demo 2026-04-18 14:59:57 +02:00
theis.gaedigk 34f133f94c Merge branch 'dev' into demoDev 2026-04-18 14:58:58 +02:00
theis.gaedigk a3df8172e2 Merge branch 'demoDev' into debian12Demo 2026-02-22 23:50:32 +01:00
theis.gaedigk 98834a9270 Merge branch 'dev' into demoDev 2026-02-22 23:50:00 +01:00
theis.gaedigk c00f720af4 Merge branch 'demoDev' into debian12Demo 2026-02-20 16:34:21 +01:00
theis.gaedigk 8d04465705 Merge branch 'dev' into demoDev 2026-02-20 16:33:54 +01:00
theis.gaedigk 06cb298a38 Merge branch 'demoDev' into debian12Demo 2026-02-09 15:55:52 +01:00
theis.gaedigk 8589971dc8 added scheme to demo branch 2026-02-09 15:55:27 +01:00
theis.gaedigk 6ec8e19737 Merge branch 'dev' into demoDev 2026-02-09 15:51:25 +01:00
theis.gaedigk d29c793b6b Merge branch 'demoDev' into debian12Demo 2026-02-07 17:50:53 +01:00
theis.gaedigk 9f44a4796d Merge branch 'dev' into demoDev 2026-02-07 17:50:34 +01:00
theis.gaedigk c97cc8b538 Merge branch 'demoDev' into debian12Demo 2026-02-07 17:44:12 +01:00
theis.gaedigk dc0a68f7f1 fixed version info 2026-02-07 17:42:21 +01:00
theis.gaedigk fe3a06e5ce Merge branch 'dev' into demoDev 2026-02-07 17:41:57 +01:00
theis.gaedigk 776fab749d Merge branch 'demoDev' into debian12Demo 2026-02-04 13:48:28 +01:00
theis.gaedigk 179f5686d1 Merge branch 'dev' into demoDev 2026-02-04 13:47:33 +01:00
theis.gaedigk 83b43f4c83 fixed bug again 2026-02-01 16:30:18 +01:00
theis.gaedigk 5d9cee63ab fixed bug: cannot start container 2026-02-01 16:28:47 +01:00
theis.gaedigk 0b203d838c added sth for demo distr 2026-02-01 16:26:58 +01:00
theis.gaedigk ae1888fe90 added secret user 2026-02-01 16:20:00 +01:00
theis.gaedigk f1c02910e6 changed docker config and other 2026-02-01 16:03:16 +01:00
theis.gaedigk d33b288956 chengd nginx config 2026-02-01 16:00:12 +01:00
theis.gaedigk 5e2a426401 edited docker config 2026-02-01 15:50:31 +01:00
theis.gaedigk 022aa669e8 Merge branch 'demoDev' into debian12Demo 2026-02-01 15:49:05 +01:00
theis.gaedigk 28373e0231 changed version info 2026-02-01 15:47:51 +01:00
theis.gaedigk 2f3583ccd0 Merge branch 'dev' into debian12 2026-01-28 18:26:08 +01:00
theis.gaedigk 9da72cc5bf Merge branch 'dev' into debian12 2026-01-28 13:06:19 +01:00
theis.gaedigk c633627b7c Merge branch 'dev' into debian12 2026-01-28 12:44:25 +01:00
theis.gaedigk 5259c41b13 Merge branch 'dev' into debian12 2026-01-27 21:29:08 +01:00
theis.gaedigk 3d9e3814fe edited docker 2026-01-27 10:33:32 +01:00
theis.gaedigk b44edb2b1d chnaged config 2026-01-16 17:17:15 +01:00
theis.gaedigk a72fabc0a0 Merge branch 'dev' into debian12 2026-01-16 17:11:30 +01:00
theis.gaedigk 1406f28f86 Merge branch 'dev' into debian12 2026-01-07 15:06:51 +01:00
theis.gaedigk 38d1091e9b Merge branch 'dev' into debian12 2025-11-30 21:23:22 +01:00
theis.gaedigk f82efecb8c edited docker config 2025-11-30 21:21:21 +01:00
theis.gaedigk 1f12bc8839 t 2025-11-30 21:17:36 +01:00
theis.gaedigk f19750f6f3 edited port config 2025-11-30 21:12:14 +01:00
theis.gaedigk 808b3fd5c4 Merge branch 'dev' into debian12 2025-11-30 21:07:32 +01:00
theis.gaedigk 0891598eb9 changed version info 2025-11-25 17:30:56 +01:00
theis.gaedigk 39ff02f2e7 Merge branch 'dev' into debian12 2025-11-25 17:11:27 +01:00
theis.gaedigk cc67fb4f85 changed version info 2025-11-24 15:35:03 +01:00
theis.gaedigk 75ff4aadc1 fixed color bug 2025-11-24 14:16:55 +01:00
theis.gaedigk 6f998d07c1 Merge branch 'dev' into debian12 2025-11-23 21:52:34 +01:00
theis.gaedigk f2bb326040 Merge branch 'dev' into debian12 2025-11-23 21:40:11 +01:00
theis.gaedigk 8c701db900 changed ports 2025-11-23 21:11:23 +01:00
theis.gaedigk d1664338a6 add networks configuration for frontend and backend services in docker-compose 2025-11-23 21:06:12 +01:00
theis.gaedigk 1a2624cd9e again 2025-11-23 20:34:19 +01:00
theis.gaedigk a138190cc6 fixed bugs 2025-11-23 20:32:14 +01:00
theis.gaedigk 993e0cd74b fixed bugs 2025-11-23 20:29:31 +01:00
theis.gaedigk dab004a7b6 changed docker config 2025-11-23 20:26:27 +01:00
theis.gaedigk d039336f39 Merge branch 'dev' into debian12 2025-11-23 20:20:41 +01:00
theis.gaedigk 4c781e9325 changed ports 2025-11-23 20:12:41 +01:00
theis.gaedigk 451e6b3646 published v2 2025-11-23 20:11:36 +01:00
52 changed files with 1374 additions and 2427 deletions
-3
View File
@@ -1,3 +0,0 @@
[submodule "no-as-a-service"]
path = no-as-a-service
url = https://github.com/hotheadhacker/no-as-a-service.git
+1 -1
View File
@@ -14,7 +14,7 @@ server {
} }
location /backend/ { location /backend/ {
proxy_pass http://borrow_system-backend_v2:8004/; proxy_pass http://demo_borrow_system-backend_v2:8102/;
} }
location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
+260 -328
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -12,7 +12,6 @@
"dependencies": { "dependencies": {
"@chakra-ui/react": "^3.28.0", "@chakra-ui/react": "^3.28.0",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@lottiefiles/dotlottie-react": "^0.19.0",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.90.5", "@tanstack/react-query": "^5.90.5",
"i18next": "^25.6.0", "i18next": "^25.6.0",
-2
View File
@@ -16,7 +16,6 @@ import { Flex } from "@chakra-ui/react";
import { Footer } from "./components/footer/Footer"; import { Footer } from "./components/footer/Footer";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { API_BASE } from "@/config/api.config"; import { API_BASE } from "@/config/api.config";
import { ContactPage } from "./pages/ContactPage";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@@ -81,7 +80,6 @@ function App() {
<Route path="/" element={<HomePage />} /> <Route path="/" element={<HomePage />} />
<Route path="/my-loans" element={<MyLoansPage />} /> <Route path="/my-loans" element={<MyLoansPage />} />
<Route path="/landingpage" element={<Landingpage />} /> <Route path="/landingpage" element={<Landingpage />} />
<Route path="/contact" element={<ContactPage />} />
</Route> </Route>
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
@@ -1,67 +0,0 @@
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>
)}
</>
);
};
-31
View File
@@ -1,7 +1,6 @@
import { import {
Button, Button,
Flex, Flex,
Image,
Heading, Heading,
Stack, Stack,
Text, Text,
@@ -23,7 +22,6 @@ import {
MoreVertical, MoreVertical,
Languages, Languages,
Table, Table,
ContactRound,
} from "lucide-react"; } from "lucide-react";
import { useUserContext } from "@/states/Context"; import { useUserContext } from "@/states/Context";
import { useState } from "react"; import { useState } from "react";
@@ -69,7 +67,6 @@ 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
@@ -156,16 +153,6 @@ export const Header = () => {
</HStack> </HStack>
} }
/> />
<Menu.Item
value="contact"
onSelect={() => navigate("/contact", { replace: true })}
children={
<HStack gap={3}>
<ContactRound size={16} />
<Text as="span">{t("contact")}</Text>
</HStack>
}
/>
<Menu.Separator /> <Menu.Separator />
<Menu.Item <Menu.Item
value="logout" value="logout"
@@ -192,13 +179,6 @@ export const Header = () => {
<Stack gap={1}> <Stack gap={1}>
{/* Titelzeile ohne Mobile-Menu (wurde nach oben verlegt) */} {/* Titelzeile ohne Mobile-Menu (wurde nach oben verlegt) */}
<Flex align="center" justify="space-between" gap={2}> <Flex align="center" justify="space-between" gap={2}>
<Image
src="/icon_borrow-system-frontend_dark.png"
alt="borrow-system logo"
boxSize="10"
objectFit="contain"
flexShrink={0}
/>
<Heading <Heading
size="2xl" size="2xl"
className="tracking-tight text-slate-900 dark:text-slate-100" className="tracking-tight text-slate-900 dark:text-slate-100"
@@ -298,17 +278,6 @@ export const Header = () => {
</HStack> </HStack>
</Button> </Button>
</a> </a>
<Button
variant={"outline"}
onClick={() => navigate("/contact", { replace: true })}
>
<HStack gap={2}>
<ContactRound size={18} />
<Text as="span">{t("contact")}</Text>
</HStack>
</Button>
<Button onClick={logout} variant="outline" colorScheme="red"> <Button onClick={logout} variant="outline" colorScheme="red">
<HStack gap={2}> <HStack gap={2}>
<LogOut size={18} /> <LogOut size={18} />
+3 -76
View File
@@ -36,43 +36,12 @@ export const UserDialogue = (props: UserDialogueProps) => {
const [msgTitle, setMsgTitle] = useState(""); const [msgTitle, setMsgTitle] = useState("");
const [msgDescription, setMsgDescription] = useState(""); const [msgDescription, setMsgDescription] = useState("");
const [isMsgNAAS, setIsMsgNAAS] = useState(false);
const [msgStatusNAAS, setMsgStatusNAAS] = useState<"error" | "success">(
"error",
);
const [msgTitleNAAS, setMsgTitleNAAS] = useState("");
const [msgDescriptionNAAS, setMsgDescriptionNAAS] = useState("");
const [oldPassword, setOldPassword] = useState(""); const [oldPassword, setOldPassword] = useState("");
const [newPassword, setNewPassword] = useState(""); const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState("");
// Dialog control // Dialog control
const [isPwOpen, setPwOpen] = useState(false); const [isPwOpen, setPwOpen] = useState(false);
const [naasDialog, setNaasDialog] = useState(false);
const [naas, setNaas] = useState("");
const openNAAS = async () => {
try {
const response = await fetch(`${API_BASE}/no`, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${Cookies.get("token")}`,
},
});
const data = await response.json();
setNaas(data.reason);
setNaasDialog(true);
} catch (error) {
setMsgStatusNAAS("error");
setMsgTitleNAAS(t("naas-error"));
setMsgDescriptionNAAS(t("naas-error-desc"));
setIsMsgNAAS(true);
console.log(msgStatusNAAS, msgTitleNAAS, msgDescriptionNAAS);
}
};
const changePassword = async () => { const changePassword = async () => {
if (newPassword !== confirmPassword) { if (newPassword !== confirmPassword) {
@@ -178,31 +147,14 @@ export const UserDialogue = (props: UserDialogueProps) => {
</Button> </Button>
</Stack> </Stack>
</Card.Body> </Card.Body>
<Card.Footer> <Card.Footer justifyContent="flex-end">
<Stack w="100%" gap={3}> <Button variant="outline" onClick={() => props.setUserDialog(false)}>
{isMsgNAAS && (
<MyAlert
status={msgStatusNAAS}
title={msgTitleNAAS}
description={msgDescriptionNAAS}
/>
)}
<HStack justify="flex-end" gap={2} wrap="wrap">
<Button
variant="outline"
onClick={() => props.setUserDialog(false)}
>
{t("cancel")} {t("cancel")}
</Button> </Button>
<Button variant="outline" onClick={() => openNAAS()}>
{t("try-naas")}
</Button>
</HStack>
</Stack>
</Card.Footer> </Card.Footer>
</Card.Root> </Card.Root>
{/* Passwort-Dialog */} {/* Passwort-Dialog (kontrolliert) */}
<Dialog.Root open={isPwOpen} onOpenChange={(e: any) => setPwOpen(e.open)}> <Dialog.Root open={isPwOpen} onOpenChange={(e: any) => setPwOpen(e.open)}>
<Portal> <Portal>
<Dialog.Backdrop /> <Dialog.Backdrop />
@@ -263,31 +215,6 @@ export const UserDialogue = (props: UserDialogueProps) => {
</Dialog.Positioner> </Dialog.Positioner>
</Portal> </Portal>
</Dialog.Root> </Dialog.Root>
<HStack wrap="wrap" gap="4">
<Dialog.Root
placement={"center"}
open={naasDialog}
motionPreset="slide-in-bottom"
>
<Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>{t("naas-header")}</Dialog.Title>
</Dialog.Header>
<Dialog.Body>
<p>{naas}</p>
</Dialog.Body>
<Dialog.CloseTrigger asChild>
<CloseButton onClick={() => setNaasDialog(false)} size="sm" />
</Dialog.CloseTrigger>
</Dialog.Content>
</Dialog.Positioner>
</Portal>
</Dialog.Root>
</HStack>
</Flex> </Flex>
); );
}; };
-28
View File
@@ -1,28 +0,0 @@
import { DotLottieReact } from "@lottiefiles/dotlottie-react";
export const unlockAnimation = () => {
return (
<DotLottieReact
src="https://lottie.host/f839baa1-9c64-44c4-9386-f0e4c87ab208/2Iw1m4k86d.lottie"
autoplay
/>
);
};
export const approvalAnimation = () => {
return (
<DotLottieReact
src="https://lottie.host/b7257009-9e3f-43e2-8112-a176f4696e4c/iQxxqAVOGX.lottie"
autoplay
/>
);
};
export const logoutAnimation = () => {
return (
<DotLottieReact
src="https://lottie.host/4975758c-de38-4d15-9f74-927709751d32/v8FtKpnD1y.lottie"
autoplay
/>
);
};
+17 -9
View File
@@ -1,15 +1,23 @@
"use client" "use client";
import { ChakraProvider, defaultSystem } from "@chakra-ui/react" import { ChakraProvider, defaultSystem } from "@chakra-ui/react";
import { import * as React from "react";
ColorModeProvider, import type { ReactNode } from "react";
type ColorModeProviderProps, import { ColorModeProvider as ThemeColorModeProvider } from "./color-mode";
} from "./color-mode"
export function Provider(props: ColorModeProviderProps) { export interface ColorModeProviderProps {
children: React.ReactNode;
}
export function ColorModeProvider({ children }: ColorModeProviderProps) {
// Wrap children with the real color-mode provider
return <ThemeColorModeProvider>{children}</ThemeColorModeProvider>;
}
export function Provider({ children }: { children: ReactNode }) {
return ( return (
<ChakraProvider value={defaultSystem}> <ChakraProvider value={defaultSystem}>
<ColorModeProvider {...props} /> <ColorModeProvider>{children}</ColorModeProvider>
</ChakraProvider> </ChakraProvider>
) );
} }
-111
View File
@@ -1,111 +0,0 @@
import {
Field,
Textarea,
Button,
Alert,
Container,
Text,
VStack,
Spinner,
} from "@chakra-ui/react";
import { useTranslation } from "react-i18next";
import { useState } from "react";
import { API_BASE } from "@/config/api.config";
import Cookies from "js-cookie";
import { Header } from "@/components/Header";
interface Alert {
type: "info" | "warning" | "success" | "error" | "neutral";
headline: string;
text: string;
}
export const ContactPage = () => {
const { t } = useTranslation();
const [message, setMessage] = useState("");
const [isSending, setIsSending] = useState(false);
const [alert, setAlert] = useState<Alert | null>(null);
const sendMessage = async () => {
setIsSending(true);
if (message.trim() === "") {
setAlert({
type: "error",
headline: t("contactPage_messageErrorHeadline"),
text: t("contactPage_messageErrorText2"),
});
setIsSending(false);
return;
}
// Logic to send the message
const result = await fetch(`${API_BASE}/api/users/contact`, {
method: "POST",
headers: {
Authorization: `Bearer ${Cookies.get("token") || ""}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ message }),
});
if (result.ok) {
setAlert({
type: "success",
headline: t("contactPage_successHeadline"),
text: t("contactPage_successText"),
});
setMessage("");
} else if (result.status === 503) {
setAlert({
type: "error",
headline: t("serviceDeactivatedHeadline"),
text: t("contactPage_serviceDeactivatedText"),
});
} else {
setAlert({
type: "error",
headline: t("contactPage_errorHeadline"),
text: t("contactPage_errorText"),
});
}
setIsSending(false);
};
return (
<Container className="px-6 sm:px-8 pt-10">
<Header />
<Field.Root invalid={message === ""}>
<Field.Label>
<Text>{t("contactPage_messageDescription")}</Text>
<Field.RequiredIndicator />
</Field.Label>
<Textarea
placeholder={t("contactPage_messagePlaceholder")}
variant="subtle"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
{message === "" && (
<Field.ErrorText>{t("contactPage_messageErrorText")}</Field.ErrorText>
)}
</Field.Root>
{alert && (
<Alert.Root status={alert.type}>
<Alert.Indicator />
<Alert.Content>
<Alert.Title>{alert.headline}</Alert.Title>
<Alert.Description>{alert.text}</Alert.Description>
</Alert.Content>
</Alert.Root>
)}
{isSending && (
<VStack colorPalette="teal">
<Spinner color="colorPalette.600" />
<Text color="colorPalette.600">{t("loading")}</Text>
</VStack>
)}
<Button onClick={sendMessage}>{t("contactPage_sendButton")}</Button>
</Container>
);
};
+13 -37
View File
@@ -18,8 +18,6 @@ import { borrowAbleItemsAtom } from "@/states/Atoms";
import { createLoan } from "@/utils/Fetcher"; 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 { DeactivatedServices } from "@/components/DeactivatedServices";
export interface User { export interface User {
username: string; username: string;
@@ -29,8 +27,6 @@ export interface User {
export const HomePage = () => { export const HomePage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [showAnimation, setShowAnimation] = useState(false);
const [borrowableItems, setBorrowableItems] = useAtom(borrowAbleItemsAtom); const [borrowableItems, setBorrowableItems] = useAtom(borrowAbleItemsAtom);
const [startDate, setStartDate] = useState(""); const [startDate, setStartDate] = useState("");
const [endDate, setEndDate] = useState(""); const [endDate, setEndDate] = useState("");
@@ -50,29 +46,13 @@ export const HomePage = () => {
setSelectedItems((prevSelected) => setSelectedItems((prevSelected) =>
prevSelected.includes(itemId) prevSelected.includes(itemId)
? prevSelected.filter((id) => id !== itemId) ? prevSelected.filter((id) => id !== itemId)
: [...prevSelected, itemId], : [...prevSelected, itemId]
); );
}; };
const showApprovalAnimation = (seconds: number) => {
const milliseconds = seconds * 1000;
setShowAnimation(true);
window.setTimeout(() => {
setShowAnimation(false);
}, milliseconds);
};
return ( return (
<>
{showAnimation && (
<div className="fixed inset-0 z-9999 flex items-center justify-center pointer-events-none">
<div>{approvalAnimation()}</div>
</div>
)}
<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}
@@ -133,6 +113,12 @@ export const HomePage = () => {
> >
{t("get-borrowable-items")} {t("get-borrowable-items")}
</Button> </Button>
{isLoadingA && (
<VStack colorPalette="teal">
<Spinner color="colorPalette.600" />
<Text color="colorPalette.600">{t("loading")}</Text>
</VStack>
)}
{borrowableItems.length > 0 && ( {borrowableItems.length > 0 && (
<Table.ScrollArea borderWidth="1px" rounded="md"> <Table.ScrollArea borderWidth="1px" rounded="md">
<Table.Root size="sm" stickyHeader> <Table.Root size="sm" stickyHeader>
@@ -172,7 +158,7 @@ export const HomePage = () => {
maxLength={MAX_CHARACTERS} maxLength={MAX_CHARACTERS}
onChange={(e) => { onChange={(e) => {
setNote( setNote(
e.currentTarget.value.slice(0, MAX_CHARACTERS), e.currentTarget.value.slice(0, MAX_CHARACTERS)
); );
}} }}
/> />
@@ -185,40 +171,30 @@ export const HomePage = () => {
)} )}
{selectedItems.length >= 1 && ( {selectedItems.length >= 1 && (
<Button <Button
onClick={() => { onClick={() =>
setIsLoadingA(true);
createLoan(selectedItems, startDate, endDate, note).then( createLoan(selectedItems, startDate, endDate, note).then(
(response) => { (response) => {
setIsLoadingA(false);
if (response.status === "error") { if (response.status === "error") {
setMsgStatus("error"); setMsgStatus("error");
setMsgTitle(response.title || t("error")); setMsgTitle(response.title || t("error"));
setMsgDescription( setMsgDescription(
response.description || t("unknown-error"), response.description || t("unknown-error")
); );
setIsMsg(true); setIsMsg(true);
return; return;
} }
showApprovalAnimation(3);
setMsgStatus("success"); setMsgStatus("success");
setMsgTitle(t("success")); setMsgTitle(t("success"));
setMsgDescription(t("loan-success")); setMsgDescription(t("loan-success"));
setIsMsg(true); setIsMsg(true);
}, }
); )
}} }
> >
{t("create-loan")} {t("create-loan")}
</Button> </Button>
)} )}
{isLoadingA && (
<VStack colorPalette="teal">
<Spinner color="colorPalette.600" />
<Text color="colorPalette.600">{t("loading")}</Text>
</VStack>
)}
</Stack> </Stack>
</Container> </Container>
</>
); );
}; };
-10
View File
@@ -79,16 +79,6 @@ 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);
+11 -51
View File
@@ -4,47 +4,26 @@ import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
import { setIsLoggedInAtom, triggerLogoutAtom } from "@/states/Atoms"; import { setIsLoggedInAtom, triggerLogoutAtom } from "@/states/Atoms";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { useNavigate, useLocation } from "react-router-dom"; import { Navigate, useNavigate, useLocation } from "react-router-dom";
import { PasswordInput } from "@/components/ui/password-input"; import { PasswordInput } from "@/components/ui/password-input";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { API_BASE } from "@/config/api.config"; import { API_BASE } from "@/config/api.config";
import { unlockAnimation } from "@/components/dotLottie";
import { logoutAnimation } from "@/components/dotLottie";
export const LoginPage = () => { export const LoginPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [isLoggedIn, setIsLoggedIn] = useAtom(setIsLoggedInAtom); const [isLoggedIn, setIsLoggedIn] = useAtom(setIsLoggedInAtom);
const [triggerLogout, setTriggerLogout] = useAtom(triggerLogoutAtom); const [triggerLogout, setTriggerLogout] = useAtom(triggerLogoutAtom);
const [showAnimation, setShowAnimation] = useState(false);
const [showLogout, setShowLogout] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const from = location.state?.from?.pathname || "/"; const from = location.state?.from?.pathname || "/";
useEffect(() => { useEffect(() => {
if (triggerLogout) { if (isLoggedIn) {
setShowLogout(true);
window.setTimeout(() => {
setShowLogout(false);
}, 4500);
}
if (!isLoggedIn) return;
// Existing sessions should redirect immediately, fresh logins wait for animation.
if (!showAnimation) {
navigate(from, { replace: true }); navigate(from, { replace: true });
return; window.location.reload(); // if deleted, the user context is not updated in time
} }
}, [isLoggedIn, navigate, from]);
const timeoutId = window.setTimeout(() => {
navigate(from, { replace: true });
window.location.reload(); // keeps user context in sync after login
}, 3000);
return () => window.clearTimeout(timeoutId);
}, [isLoggedIn, showAnimation, navigate, from]);
const loginFnc = async (username: string, password: string) => { const loginFnc = async (username: string, password: string) => {
const response = await fetch(`${API_BASE}/api/users/login`, { const response = await fetch(`${API_BASE}/api/users/login`, {
@@ -63,8 +42,6 @@ export const LoginPage = () => {
}; };
} }
setShowAnimation(true);
Cookies.set("token", data.token); Cookies.set("token", data.token);
setIsLoggedIn(true); setIsLoggedIn(true);
return { success: true }; return { success: true };
@@ -85,22 +62,14 @@ export const LoginPage = () => {
return; return;
} }
setTriggerLogout(false); setTriggerLogout(false);
navigate(from, { replace: true });
}; };
if (isLoggedIn) {
return <Navigate to={from} replace />;
}
return ( return (
<>
{showAnimation && (
<div className="fixed inset-0 z-9999 flex items-center justify-center pointer-events-none">
<div>{unlockAnimation()}</div>
</div>
)}
{showLogout && (
<div className="fixed inset-0 z-9999 flex items-center justify-center pointer-events-none">
<div>{logoutAnimation()}</div>
</div>
)}
<div className="flex flex-1 items-center justify-center p-4"> <div className="flex flex-1 items-center justify-center p-4">
<form onSubmit={(e) => e.preventDefault()}> <form onSubmit={(e) => e.preventDefault()}>
<Card.Root maxW="sm"> <Card.Root maxW="sm">
@@ -128,17 +97,9 @@ export const LoginPage = () => {
</Card.Body> </Card.Body>
<Card.Footer justifyContent="flex-end"> <Card.Footer justifyContent="flex-end">
{isError && ( {isError && (
<MyAlert <MyAlert status="error" title={errorMsg} description={errorDsc} />
status="error"
title={errorMsg}
description={errorDsc}
/>
)} )}
<Button <Button type="submit" onClick={() => handleLogin()} variant="solid">
type="submit"
onClick={() => handleLogin()}
variant="solid"
>
Login Login
</Button> </Button>
</Card.Footer> </Card.Footer>
@@ -154,6 +115,5 @@ export const LoginPage = () => {
</Card.Root> </Card.Root>
</form> </form>
</div> </div>
</>
); );
}; };
-15
View File
@@ -52,13 +52,6 @@ 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"));
@@ -91,14 +84,6 @@ export const MyLoansPage = () => {
}); });
if (!res.ok) { if (!res.ok) {
if (res.status === 507) {
setMsgStatus("error");
setMsgTitle(t("error"));
setMsgDescription(t("error-deleting-loan-507"));
setIsMsg(true);
return;
}
setMsgStatus("error"); setMsgStatus("error");
setMsgTitle(t("error")); setMsgTitle(t("error"));
setMsgDescription(t("error-deleting-loan")); setMsgDescription(t("error-deleting-loan"));
-20
View File
@@ -17,16 +17,6 @@ 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",
@@ -70,16 +60,6 @@ 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",
+2 -15
View File
@@ -68,7 +68,7 @@
"admin-status": "Admin-Status", "admin-status": "Admin-Status",
"first-name": "Vorname", "first-name": "Vorname",
"last-name": "Nachname", "last-name": "Nachname",
"app-title": "Ausleihsystem", "app-title": "Ausleihsystem (demo)",
"last-borrowed-person": "Zuletzt ausgeliehen von", "last-borrowed-person": "Zuletzt ausgeliehen von",
"currently-borrowed-by": "Derzeit ausgeliehen von", "currently-borrowed-by": "Derzeit ausgeliehen von",
"back": "Zurückgehen", "back": "Zurückgehen",
@@ -88,18 +88,5 @@
"take-loan-success": "Ausleihe erfolgreich abgeholt", "take-loan-success": "Ausleihe erfolgreich abgeholt",
"return-loan-success": "Ausleihe erfolgreich zurückgegeben", "return-loan-success": "Ausleihe erfolgreich zurückgegeben",
"network-error": "Netzwerkfehler. Kontaktieren Sie den Administrator.", "network-error": "Netzwerkfehler. Kontaktieren Sie den Administrator.",
"contactPage_messageDescription": "Bitte geben Sie hier Ihre Nachricht ein. Der Systemadministrator (Theis Gaedigk) wird sich so schnell wie möglich bei Ihnen melden.", "contactPage_messageDescription": "Bitte geben Sie hier Ihre Nachricht ein. Der Systemadministrator (Theis Gaedigk) wird sich so schnell wie möglich bei Ihnen melden."
"naas": "No-as-a-service",
"try-naas": "Klick mich",
"naas-error": "Fehler mit no-as-a-service",
"naas-error-desc": "Ein Fehler ist beim Kommunizieren mit no-as-a-service aufgetreten.",
"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.",
"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",
"contactPage_messageErrorHeadline": "Fehler bei der Nachrichteneingabe",
"contactPage_messageErrorText2": "Bitte geben Sie eine Nachricht ein, bevor Sie sie senden."
} }
+2 -15
View File
@@ -68,7 +68,7 @@
"admin-status": "Admin status", "admin-status": "Admin status",
"first-name": "First name", "first-name": "First name",
"last-name": "Last name", "last-name": "Last name",
"app-title": "Borrow System", "app-title": "Borrow System (demo)",
"last-borrowed-person": "Last borrowed by", "last-borrowed-person": "Last borrowed by",
"currently-borrowed-by": "Currently borrowed by", "currently-borrowed-by": "Currently borrowed by",
"back": "Go back", "back": "Go back",
@@ -88,18 +88,5 @@
"take-loan-success": "Loan taken successfully", "take-loan-success": "Loan taken successfully",
"return-loan-success": "Loan returned successfully", "return-loan-success": "Loan returned successfully",
"network-error": "Network error. Please contact the administrator.", "network-error": "Network error. Please contact the administrator.",
"contactPage_messageDescription": "Please enter your message here. The system administrator (Theis Gaedigk) will get back to you as soon as possible.", "contactPage_messageDescription": "Please enter your message here. The system administrator (Theis Gaedigk) will get back to you as soon as possible."
"naas": "No-as-a-service",
"try-naas": "Click me",
"naas-error": "Error 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...",
"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",
"contactPage_messageErrorHeadline": "Error submitting message",
"contactPage_messageErrorText2": "Please enter a message before sending it."
} }
+14 -7
View File
@@ -1,16 +1,23 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import svgr from "vite-plugin-svgr";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import tsconfigPaths from "vite-tsconfig-paths"; import path from "node:path";
export default defineConfig({ export default defineConfig({
plugins: [react(), svgr(), tailwindcss(), tsconfigPaths()], plugins: [tailwindcss()],
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
server: { server: {
host: "0.0.0.0", host: "0.0.0.0",
port: 8001, allowedHosts: ["insta.the1s.de"],
watch: { port: 8101,
usePolling: true, watch: { usePolling: true },
hmr: {
host: "insta.the1s.de",
port: 8101,
protocol: "wss",
}, },
}, },
}); });
+1 -1
View File
@@ -14,7 +14,7 @@ server {
} }
location /backend/ { location /backend/ {
proxy_pass http://borrow_system-backend_v2:8004/; proxy_pass http://demo_borrow_system-backend_v2:8102/;
} }
location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { location ~* \.(?:js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
+2 -5
View File
@@ -7,7 +7,6 @@ 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 = {
@@ -45,9 +44,8 @@ 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" minH={0}> <Box flex="1" display="flex" flexDirection="column">
<Flex <Flex
as="header" as="header"
align="center" align="center"
@@ -68,7 +66,7 @@ const Dashboard: React.FC<DashboardProps> = ({ onLogout }) => {
</Button> </Button>
</Flex> </Flex>
</Flex> </Flex>
<Box as="main" flex="1" p={6} minH={0} overflow="hidden"> <Box as="main" flex="1" p={6}>
{activeView === "" && ( {activeView === "" && (
<Flex <Flex
align="center" align="center"
@@ -90,7 +88,6 @@ 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,7 +9,6 @@ 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> = ({
@@ -17,7 +16,6 @@ const Sidebar: React.FC<SidebarProps> = ({
viewGegenstaende, viewGegenstaende,
viewUser, viewUser,
viewAPI, viewAPI,
viewConfig
}) => { }) => {
const [info, setInfo] = useState<any>(null); const [info, setInfo] = useState<any>(null);
@@ -85,15 +83,6 @@ 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">
+13 -57
View File
@@ -57,32 +57,32 @@ const ItemTable: React.FC = () => {
const handleItemNameChange = (id: number, value: string) => { const handleItemNameChange = (id: number, value: string) => {
setItems((prev) => setItems((prev) =>
prev.map((it) => (it.id === id ? { ...it, item_name: value } : it)), prev.map((it) => (it.id === id ? { ...it, item_name: value } : it))
); );
}; };
const handleCanBorrowRoleChange = (id: number, value: string) => { const handleCanBorrowRoleChange = (id: number, value: string) => {
setItems((prev) => setItems((prev) =>
prev.map((it) => (it.id === id ? { ...it, can_borrow_role: value } : it)), prev.map((it) => (it.id === id ? { ...it, can_borrow_role: value } : it))
); );
}; };
const handleLockerNumberChange = (id: number, value: string) => { const handleLockerNumberChange = (id: number, value: string) => {
setItems((prev) => setItems((prev) =>
prev.map((it) => (it.id === id ? { ...it, safe_nr: value } : it)), prev.map((it) => (it.id === id ? { ...it, safe_nr: value } : it))
); );
}; };
const handleDoorKeyChange = (id: number, value: string) => { const handleDoorKeyChange = (id: number, value: string) => {
setItems((prev) => setItems((prev) =>
prev.map((it) => (it.id === id ? { ...it, door_key: value } : it)), prev.map((it) => (it.id === id ? { ...it, door_key: value } : it))
); );
}; };
const setError = ( const setError = (
status: "error" | "success", status: "error" | "success",
message: string, message: string,
description: string, description: string
) => { ) => {
setIsError(false); setIsError(false);
setErrorStatus(status); setErrorStatus(status);
@@ -102,7 +102,7 @@ const ItemTable: React.FC = () => {
headers: { headers: {
Authorization: `Bearer ${Cookies.get("token")}`, Authorization: `Bearer ${Cookies.get("token")}`,
}, },
}, }
); );
const data = await response.json(); const data = await response.json();
return data; return data;
@@ -193,50 +193,11 @@ const ItemTable: React.FC = () => {
{/* make table fill available width, like UserTable */} {/* make table fill available width, like UserTable */}
{!isLoading && ( {!isLoading && (
<Table.ScrollArea flex="1" minH={0} rounded="md" mt={4}>
<Table.Root <Table.Root
size="sm" size="sm"
striped striped
stickyHeader w="100%"
css={{ style={{ tableLayout: "auto" }} // Spalten nach Content
"& [data-sticky]": {
position: "sticky",
zIndex: 1,
bg: "bg",
_after: {
content: '""',
position: "absolute",
pointerEvents: "none",
top: "0",
bottom: "-1px",
width: "32px",
},
},
"& [data-sticky=end]": {
_after: {
insetInlineEnd: "0",
translate: "100% 0",
shadow: "inset 8px 0px 8px -8px rgba(0, 0, 0, 0.16)",
},
},
"& [data-sticky=start]": {
_after: {
insetInlineStart: "0",
translate: "-100% 0",
shadow: "inset -8px 0px 8px -8px rgba(0, 0, 0, 0.16)",
},
},
"& thead tr": {
shadow: "0 1px 0 0 {colors.border}",
"&:has(th[data-sticky])": {
zIndex: 2,
},
},
}}
> >
<Table.Header> <Table.Header>
<Table.Row> <Table.Row>
@@ -354,12 +315,8 @@ const ItemTable: React.FC = () => {
value={item.door_key} value={item.door_key}
/> />
</Table.Cell> </Table.Cell>
<Table.Cell> <Table.Cell>{formatDateTime(item.entry_created_at)}</Table.Cell>
{formatDateTime(item.entry_created_at)} <Table.Cell>{formatDateTime(item.entry_updated_at)}</Table.Cell>
</Table.Cell>
<Table.Cell>
{formatDateTime(item.entry_updated_at)}
</Table.Cell>
<Table.Cell>{item.last_borrowed_person}</Table.Cell> <Table.Cell>{item.last_borrowed_person}</Table.Cell>
<Table.Cell>{item.currently_borrowing}</Table.Cell> <Table.Cell>{item.currently_borrowing}</Table.Cell>
<Table.Cell whiteSpace="nowrap"> <Table.Cell whiteSpace="nowrap">
@@ -370,7 +327,7 @@ const ItemTable: React.FC = () => {
item.item_name, item.item_name,
item.safe_nr, item.safe_nr,
item.door_key, item.door_key,
item.can_borrow_role, item.can_borrow_role
).then((response) => { ).then((response) => {
if (response.success) { if (response.success) {
setError( setError(
@@ -381,7 +338,7 @@ const ItemTable: React.FC = () => {
item.item_name + item.item_name +
'" mit ID ' + '" mit ID ' +
item.id + item.id +
" bearbeitet.", " bearbeitet."
); );
} }
}) })
@@ -399,7 +356,7 @@ const ItemTable: React.FC = () => {
setError( setError(
"success", "success",
"Gegenstand gelöscht", "Gegenstand gelöscht",
"Der Gegenstand wurde erfolgreich gelöscht.", "Der Gegenstand wurde erfolgreich gelöscht."
); );
} }
}) })
@@ -415,7 +372,6 @@ const ItemTable: React.FC = () => {
))} ))}
</Table.Body> </Table.Body>
</Table.Root> </Table.Root>
</Table.ScrollArea>
)} )}
<Text>* LaP = Letzte ausleihende Person</Text> <Text>* LaP = Letzte ausleihende Person</Text>
<Text>** Dav = Derzeit ausgeliehen von</Text> <Text>** Dav = Derzeit ausgeliehen von</Text>
+6 -52
View File
@@ -1,6 +1,5 @@
import React from "react"; import React from "react";
import { import {
Box,
Table, Table,
Spinner, Spinner,
Text, Text,
@@ -32,7 +31,7 @@ const LoanTable: React.FC = () => {
const setError = ( const setError = (
status: "error" | "success", status: "error" | "success",
message: string, message: string,
description: string, description: string
) => { ) => {
setIsError(false); setIsError(false);
setErrorStatus(status); setErrorStatus(status);
@@ -66,7 +65,7 @@ const LoanTable: React.FC = () => {
headers: { headers: {
Authorization: `Bearer ${Cookies.get("token")}`, Authorization: `Bearer ${Cookies.get("token")}`,
}, },
}, }
); );
const data = await response.json(); const data = await response.json();
return data; return data;
@@ -84,7 +83,7 @@ const LoanTable: React.FC = () => {
}, [reload]); }, [reload]);
return ( return (
<Box h="full" display="flex" flexDirection="column" minH={0}> <>
{/* Action toolbar */} {/* Action toolbar */}
<HStack <HStack
mb={4} mb={4}
@@ -132,51 +131,7 @@ const LoanTable: React.FC = () => {
</VStack> </VStack>
)} )}
{!isLoading && ( {!isLoading && (
<Table.ScrollArea flex="1" minH={0} rounded="md" mt={4}> <Table.Root size="sm" striped>
<Table.Root
size="sm"
striped
stickyHeader
css={{
"& [data-sticky]": {
position: "sticky",
zIndex: 1,
bg: "bg",
_after: {
content: '""',
position: "absolute",
pointerEvents: "none",
top: "0",
bottom: "-1px",
width: "32px",
},
},
"& [data-sticky=end]": {
_after: {
insetInlineEnd: "0",
translate: "100% 0",
shadow: "inset 8px 0px 8px -8px rgba(0, 0, 0, 0.16)",
},
},
"& [data-sticky=start]": {
_after: {
insetInlineStart: "0",
translate: "-100% 0",
shadow: "inset -8px 0px 8px -8px rgba(0, 0, 0, 0.16)",
},
},
"& thead tr": {
shadow: "0 1px 0 0 {colors.border}",
"&:has(th[data-sticky])": {
zIndex: 2,
},
},
}}
>
<Table.Header> <Table.Header>
<Table.Row> <Table.Row>
<Table.ColumnHeader> <Table.ColumnHeader>
@@ -238,7 +193,7 @@ const LoanTable: React.FC = () => {
setError( setError(
"success", "success",
"Loan deleted", "Loan deleted",
"The loan has been successfully deleted.", "The loan has been successfully deleted."
); );
} }
}) })
@@ -254,9 +209,8 @@ const LoanTable: React.FC = () => {
))} ))}
</Table.Body> </Table.Body>
</Table.Root> </Table.Root>
</Table.ScrollArea>
)} )}
</Box> </>
); );
}; };
-175
View File
@@ -1,175 +0,0 @@
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;
+7 -3
View File
@@ -8,9 +8,13 @@ export default defineConfig({
plugins: [react(), svgr(), tailwindcss(), tsconfigPaths()], plugins: [react(), svgr(), tailwindcss(), tsconfigPaths()],
server: { server: {
host: "0.0.0.0", host: "0.0.0.0",
port: 8003, allowedHosts: ["admin.insta.the1s.de"],
watch: { port: 8103,
usePolling: true, watch: { usePolling: true },
hmr: {
host: "admin.insta.the1s.de",
port: 8103,
protocol: "wss",
}, },
}, },
}); });
+3 -3
View File
@@ -1,11 +1,11 @@
{ {
"backend-info": { "backend-info": {
"version": "v2.2 (dev)" "version": "v2.1.1 (demo)"
}, },
"frontend-info": { "frontend-info": {
"version": "v2.2 (dev)" "version": "v2.1.2 (demo)"
}, },
"admin-panel-info": { "admin-panel-info": {
"version": "v1.4(dev)" "version": "v1.3.2 (demo)"
} }
} }
+38 -74
View File
@@ -1,22 +1,21 @@
{ {
"name": "backendv2", "name": "backendv2",
"version": "v2.1.1 (dev)", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "backendv2", "name": "backendv2",
"version": "v2.1.1 (dev)", "version": "1.0.0",
"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": "^8.0.6" "nodemailer": "^7.0.6"
} }
}, },
"node_modules/accepts": { "node_modules/accepts": {
@@ -54,49 +53,29 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "2.2.2", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
"integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bytes": "^3.1.2", "bytes": "^3.1.2",
"content-type": "^1.0.5", "content-type": "^1.0.5",
"debug": "^4.4.3", "debug": "^4.4.0",
"http-errors": "^2.0.0", "http-errors": "^2.0.0",
"iconv-lite": "^0.7.0", "iconv-lite": "^0.6.3",
"on-finished": "^2.4.1", "on-finished": "^2.4.1",
"qs": "^6.14.1", "qs": "^6.14.0",
"raw-body": "^3.0.1", "raw-body": "^3.0.0",
"type-is": "^2.0.1" "type-is": "^2.0.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/body-parser/node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
} }
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "2.1.0", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
@@ -370,24 +349,6 @@
"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",
@@ -548,21 +509,24 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": { "node_modules/inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"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",
@@ -692,9 +656,9 @@
} }
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "5.1.9", "version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^2.0.1"
@@ -767,9 +731,9 @@
} }
}, },
"node_modules/nodemailer": { "node_modules/nodemailer": {
"version": "8.0.6", "version": "7.0.10",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.6.tgz", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.10.tgz",
"integrity": "sha512-Nm2XeuDwwy2wi5A+8jPWwQwNzcjNjhWdE3pVLoXEusxJqCnAPAgnBGkSmiLknbnWuOF9qraRpYZjfxqtKZ4tPw==", "integrity": "sha512-Us/Se1WtT0ylXgNFfyFSx4LElllVLJXQjWi2Xz17xWw7amDKO2MLtFnVp1WACy7GkVGs+oBlRopVNUzlrGSw1w==",
"license": "MIT-0", "license": "MIT-0",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
@@ -827,9 +791,9 @@
} }
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "8.4.2", "version": "8.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
@@ -856,9 +820,9 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.15.1", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"side-channel": "^1.1.0" "side-channel": "^1.1.0"
+1 -2
View File
@@ -15,9 +15,8 @@
"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": "^8.0.6" "nodemailer": "^7.0.6"
} }
} }
@@ -17,10 +17,7 @@ export const getAllLoans = async () => {
}; };
export const deleteLoanById = async (loanId) => { export const deleteLoanById = async (loanId) => {
const [result] = await pool.query( const [result] = await pool.query("DELETE FROM loans WHERE id = ?", [loanId]);
"UPDATE loans SET deleted = true, deleted_admin = true WHERE id = ?",
[loanId],
);
if (result.affectedRows > 0) return { success: true }; if (result.affectedRows > 0) return { success: true };
return { success: false }; return { success: false };
}; };
@@ -1,26 +0,0 @@
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 };
};
@@ -29,14 +29,14 @@ export const createUser = async (
}; };
export const deleteUserById = async (userId) => { export const deleteUserById = async (userId) => {
const [result] = await pool.query("DELETE FROM users WHERE id = ?", [userId]); const [result] = await pool.query("DELETE FROM users WHERE id = ? AND secret_user = false", [userId]);
if (result.affectedRows > 0) return { success: true }; if (result.affectedRows > 0) return { success: true };
return { success: false }; return { success: false };
}; };
export const changePassword = async (username, newPassword) => { export const changePassword = async (username, newPassword) => {
const [result] = await pool.query( const [result] = await pool.query(
"UPDATE users SET password = ?, entry_updated_at = NOW() WHERE username = ?", "UPDATE users SET password = ?, entry_updated_at = NOW() WHERE username = ? AND secret_user = false",
[newPassword, username], [newPassword, username],
); );
if (result.affectedRows > 0) return { success: true }; if (result.affectedRows > 0) return { success: true };
@@ -52,7 +52,7 @@ export const editUserById = async (
is_admin, is_admin,
) => { ) => {
const [result] = await pool.query( const [result] = await pool.query(
"UPDATE users SET first_name = ?, last_name = ?, role = ?, email = ?, is_admin = ?, entry_updated_at = NOW() WHERE id = ?", "UPDATE users SET first_name = ?, last_name = ?, role = ?, email = ?, is_admin = ?, entry_updated_at = NOW() WHERE id = ? AND secret_user = false",
[first_name, last_name, role, email, is_admin, userId], [first_name, last_name, role, email, is_admin, userId],
); );
if (result.affectedRows > 0) return { success: true }; if (result.affectedRows > 0) return { success: true };
@@ -61,7 +61,7 @@ export const editUserById = async (
export const getAllUsers = async () => { export const getAllUsers = async () => {
const [result] = await pool.query( const [result] = await pool.query(
"SELECT id, username, first_name, last_name, role, email, is_admin, entry_created_at, entry_updated_at FROM users", "SELECT id, username, first_name, last_name, role, email, is_admin, entry_created_at, entry_updated_at FROM users WHERE secret_user = false",
); );
if (result.length > 0) return { success: true, data: result }; if (result.length > 0) return { success: true, data: result };
return { success: false }; return { success: false };
@@ -69,7 +69,7 @@ export const getAllUsers = async () => {
export const getUserById = async (userId) => { export const getUserById = async (userId) => {
const [rows] = await pool.query( const [rows] = await pool.query(
"SELECT id, username, first_name, last_name, role, email, is_admin FROM users WHERE id = ?", "SELECT id, username, first_name, last_name, role, email, is_admin FROM users WHERE id = ? AND secret_user = false",
[userId], [userId],
); );
if (rows.length === 0) { if (rows.length === 0) {
@@ -1,50 +0,0 @@
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,12 +1,9 @@
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,
@@ -42,7 +39,6 @@ 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);
@@ -58,7 +54,6 @@ 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);
@@ -74,7 +69,6 @@ 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);
@@ -234,23 +234,6 @@ export const getBorrowableItemsFromDatabase = async (
}; };
export const SETdeleteLoanFromDatabase = async (loanId) => { 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( const [result] = await pool.query(
"UPDATE loans SET deleted = 1 WHERE id = ?;", "UPDATE loans SET deleted = 1 WHERE id = ?;",
[loanId], [loanId],
@@ -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,16 +48,8 @@ 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 };
};
+19 -83
View File
@@ -1,20 +1,8 @@
import express from "express"; import express from "express";
import { authenticate, generateToken } from "../../services/authentication.js"; import { authenticate, generateToken } from "../../services/authentication.js";
import { const router = express.Router();
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"; import dotenv from "dotenv";
dotenv.config(); dotenv.config();
const router = express.Router();
const loan_service = "Loan Service";
const loan_mailer_service = "Loan Mailer";
// database funcs import // database funcs import
import { import {
@@ -28,12 +16,9 @@ import {
setReturnDate, setReturnDate,
setTakeDate, setTakeDate,
} from "./database/loansMgmt.database.js"; } from "./database/loansMgmt.database.js";
import { sendMailLoan } from "./services/mailer.js";
router.post( router.post("/createLoan", authenticate, async (req, res) => {
"/createLoan",
checkIfServiceIsActive(loan_service),
authenticate,
async (req, res) => {
try { try {
const { items, startDate, endDate, note } = req.body || {}; const { items, startDate, endDate, note } = req.body || {};
@@ -67,26 +52,18 @@ router.post(
note, note,
itemIds, itemIds,
); );
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);
const { html, text } = loanMail( sendMailLoan(
req.user.first_name + " " + req.user.last_name, mailInfo.data.username,
mailInfo.data.loaned_items_name, mailInfo.data.loaned_items_name,
mailInfo.data.start_date, mailInfo.data.start_date,
mailInfo.data.end_date, mailInfo.data.end_date,
mailInfo.data.created_at, mailInfo.data.created_at,
mailInfo.data.note, mailInfo.data.note,
); );
await sendMail({
to: process.env.MAIL_SENDEES,
subject: "Neue Ausleihe erstellt!",
html,
text,
});
}
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,
@@ -109,14 +86,9 @@ router.post(
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( router.get("/loans", authenticate, async (req, res) => {
"/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);
@@ -125,14 +97,9 @@ router.get(
} else { } else {
res.status(500).json({ message: "Failed to fetch loans" }); res.status(500).json({ message: "Failed to fetch loans" });
} }
}, });
);
router.post( router.post("/set-return-date/:loan_code", authenticate, async (req, res) => {
"/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) {
@@ -140,14 +107,9 @@ router.post(
} else { } else {
res.status(500).json({ message: "Failed to set return date" }); res.status(500).json({ message: "Failed to set return date" });
} }
}, });
);
router.post( router.post("/set-take-date/:loan_code", authenticate, async (req, res) => {
"/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) {
@@ -155,8 +117,7 @@ router.post(
} 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();
@@ -167,50 +128,26 @@ router.get("/all-items", authenticate, async (req, res) => {
} }
}); });
router.delete( router.delete("/delete-loan/:id", authenticate, async (req, res) => {
"/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) {
res.status(200).json({ message: "Loan deleted successfully" }); res.status(200).json({ message: "Loan deleted successfully" });
} else { } 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" }); res.status(500).json({ message: "Failed to delete loan" });
} }
}, });
);
router.get( router.get("/all-loans", authenticate, async (req, res) => {
"/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( router.post("/borrowable-items", authenticate, async (req, res) => {
"/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
@@ -231,7 +168,6 @@ router.post(
.status(500) .status(500)
.json({ message: "Failed to fetch borrowable items" }); .json({ message: "Failed to fetch borrowable items" });
} }
}, });
);
export default router; export default router;
+215
View File
@@ -0,0 +1,215 @@
import nodemailer from "nodemailer";
import dotenv from "dotenv";
dotenv.config();
const formatDateTime = (value) => {
if (value == null) return "N/A";
const toOut = (d) => {
if (!(d instanceof Date) || isNaN(d.getTime())) return "N/A";
const dd = String(d.getDate()).padStart(2, "0");
const mm = String(d.getMonth() + 1).padStart(2, "0");
const yyyy = d.getFullYear();
const hh = String(d.getHours()).padStart(2, "0");
const mi = String(d.getMinutes()).padStart(2, "0");
return `${dd}.${mm}.${yyyy} ${hh}:${mi} Uhr`;
};
if (value instanceof Date) return toOut(value);
if (typeof value === "number") return toOut(new Date(value));
const s = String(value).trim();
// Direct pattern: "YYYY-MM-DD[ T]HH:mm[:ss]"
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::\d{2})?/);
if (m) {
const [, y, M, d, h, min] = m;
return `${d}.${M}.${y} ${h}:${min} Uhr`;
}
// ISO or other parseable formats
const dObj = new Date(s);
if (!isNaN(dObj.getTime())) return toOut(dObj);
return "N/A";
};
function buildLoanEmail({
user,
items,
startDate,
endDate,
createdDate,
note,
}) {
const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9";
const itemsList =
Array.isArray(items) && items.length
? `<ul style="margin:4px 0 0 18px; padding:0;">${items
.map(
(i) =>
`<li style="margin:2px 0; color:#111827; line-height:1.3;">${i}</li>`,
)
.join("")}</ul>`
: "<span style='color:#111827;'>N/A</span>";
return `<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="color-scheme" content="light">
<meta name="supported-color-schemes" content="light">
<meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
:root { color-scheme: light; supported-color-schemes: light; }
body { margin:0; padding:0; }
/* Mobile stacking */
@media (max-width:480px) {
.outer { width:100% !important; }
.pad-sm { padding:16px !important; }
.w-label { width:120px !important; }
}
/* Dark-mode override safety */
@media (prefers-color-scheme: dark) {
body, table, td, p, a, h1, h2, h3 { background:#ffffff !important; color:#111827 !important; }
.brand-header { background:${brand} !important; color:#ffffff !important; }
a { color:${brand} !important; }
}
</style>
</head>
<body bgcolor="#ffffff" style="background:#ffffff; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif; color:#111827; -webkit-text-size-adjust:100%;">
<!-- Preheader (hidden) -->
<div style="display:none; max-height:0; overflow:hidden; opacity:0; mso-hide:all;">
Neue Ausleihe erstellt Übersicht der Buchung.
</div>
<div role="article" aria-roledescription="email" lang="de" style="padding:24px; background:#f2f4f7;">
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" class="outer" style="max-width:600px; margin:0 auto; background:#ffffff; border:1px solid #e5e7eb; border-radius:14px; overflow:hidden;">
<tr>
<td class="brand-header" style="padding:22px 26px; background:${brand}; color:#ffffff;">
<h1 style="margin:0; font-size:18px; line-height:1.35; font-weight:600;">Neue Ausleihe erstellt</h1>
</td>
</tr>
<tr>
<td class="pad-sm" style="padding:24px 26px; color:#111827;">
<p style="margin:0 0 14px 0; line-height:1.4;">Es wurde eine neue Ausleihe angelegt. Hier sind die Details:</p>
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="border-collapse:collapse; font-size:14px; line-height:1.3; background:#fcfcfd; border:1px solid #e5e7eb; border-radius:10px; overflow:hidden;">
<tbody>
<tr>
<td class="w-label" style="padding:10px 14px; color:#6b7280; width:170px; border-bottom:1px solid #ececec;">Benutzer</td>
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${
user || "N/A"
}</td>
</tr>
<tr>
<td style="padding:10px 14px; color:#6b7280; vertical-align:top; border-bottom:1px solid #ececec;">Ausgeliehene Gegenstände</td>
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${itemsList}</td>
</tr>
<tr>
<td style="padding:10px 14px; color:#6b7280; border-bottom:1px solid #ececec;">Startdatum</td>
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${formatDateTime(
startDate,
)}</td>
</tr>
<tr>
<td style="padding:10px 14px; color:#6b7280; border-bottom:1px solid #ececec;">Enddatum</td>
<td style="padding:10px 14px; font-weight:600; border-bottom:1px solid #ececec; color:#111827;">${formatDateTime(
endDate,
)}</td>
</tr>
<tr>
<td style="padding:10px 14px; color:#6b7280;">Erstellt am</td>
<td style="padding:10px 14px; font-weight:600; color:#111827;">${formatDateTime(
createdDate,
)}</td>
</tr>
<tr>
<td style="padding:10px 14px; color:#6b7280; vertical-align:top;">Notiz</td>
<td style="padding:10px 14px; font-weight:600; color:#111827;">${
note || "Keine Notiz"
}</td>
</tr>
</tbody>
</table>
<p style="margin:22px 0 0 0; font-size:14px;">
<a href="https://admin.insta.the1s.de/api" style="display:inline-block; background:${brand}; color:#ffffff; text-decoration:none; padding:10px 16px; border-radius:6px; font-weight:600; font-size:14px;" target="_blank" rel="noopener noreferrer">
Übersicht öffnen
</a>
</p>
<p style="margin:18px 0 0 0; font-size:12px; color:#6b7280; line-height:1.4;">
Diese E-Mail wurde automatisch vom Ausleihsystem gesendet. Bitte nicht antworten.
</p>
</td>
</tr>
</table>
</div>
</body>
</html>`;
}
function buildLoanEmailText({
user,
items,
startDate,
endDate,
createdDate,
note,
}) {
const itemsText =
Array.isArray(items) && items.length ? items.join(", ") : "N/A";
return [
"Neue Ausleihe erstellt",
"",
`Benutzer: ${user || "N/A"}`,
`Gegenstände: ${itemsText}`,
`Start: ${formatDateTime(startDate)}`,
`Ende: ${formatDateTime(endDate)}`,
`Erstellt am: ${formatDateTime(createdDate)}`,
`Notiz: ${note || "Keine Notiz"}`,
].join("\n");
}
export function sendMailLoan(
user,
items,
startDate,
endDate,
createdDate,
note,
) {
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,
},
});
(async () => {
const info = await transporter.sendMail({
from: '"Ausleihsystem" <noreply@mcs-medien.de>',
to: process.env.MAIL_SENDEES,
subject: "Eine neue Ausleihe wurde erstellt!",
text: buildLoanEmailText({
user,
items,
startDate,
endDate,
createdDate,
note,
}),
html: buildLoanEmail({
user,
items,
startDate,
endDate,
createdDate,
note,
}),
});
console.log("Loan message sent:", info.messageId);
})();
}
@@ -0,0 +1,43 @@
import nodemailer from "nodemailer";
import dotenv from "dotenv";
dotenv.config();
export function sendMail(username, message) {
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,
},
});
(async () => {
const mailText = `Neue Kontaktanfrage im Ausleihsystem.\n\nBenutzername: ${username}\n\nNachricht:\n${message}`;
const mailHtml = `<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<title>Neue Nachricht im Ausleihsystem</title>
</head>
<body style="font-family: Arial, sans-serif; line-height: 1.5; color: #222;">
<h2>Neue Nachricht im Ausleihsystem</h2>
<p><strong>Benutzername:</strong> ${username}</p>
<p><strong>Nachricht:</strong></p>
<p style="white-space: pre-line;">${message}</p>
</body>
</html>`;
const info = await transporter.sendMail({
from: '"Ausleihsystem" <noreply@mcs-medien.de>',
to: process.env.MAIL_SENDEES_CONTACT,
subject: "Sie haben eine neue Nachricht!",
text: mailText,
html: mailHtml,
});
console.log("Contact message sent: %s", info.messageId);
})();
}
+11 -62
View File
@@ -1,28 +1,14 @@
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 { import { loginFunc, changePassword } from "./database/userMgmt.database.js";
loginFunc, import { sendMail } from "./services/mailer_v2.js";
changePassword,
getDeactivatedServices,
} from "./database/userMgmt.database.js";
// mailer imports router.post("/login", async (req, res) => {
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); const result = await loginFunc(req.body.username, req.body.password);
if (result.success) { if (result.success) {
const token = await generateToken({ const token = await generateToken({
@@ -36,14 +22,9 @@ router.post(
} else { } else {
res.status(401).json({ message: "Invalid credentials" }); res.status(401).json({ message: "Invalid credentials" });
} }
}, });
);
router.post( router.post("/change-password", authenticate, async (req, res) => {
"/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;
@@ -53,47 +34,15 @@ router.post(
} else { } else {
res.status(500).json({ message: "Failed to change password" }); 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" }); router.post("/contact", authenticate, async (req, res) => {
} catch (error) { const message = req.body.message;
console.error("Failed to send contact mail:", error); const username = req.user.username;
res.status(500).json({ message: "Failed to send contact message" });
}
},
);
router.get("/deactivated-services", authenticate, async (req, res) => { sendMail(username, message);
const result = await getDeactivatedServices();
if (result.success) { res.status(200).json({ message: "Contact message sent successfully" });
res.status(200).json(result.data);
} else {
res.status(500).json({ message: "Failed to fetch deactivated services" });
}
}); });
export default router; export default router;
+100
View File
@@ -0,0 +1,100 @@
USE borrow_system_new;
-- USERS
INSERT INTO users (username, password, email, first_name, last_name, role, is_admin)
VALUES
('user1', 'passwordhash1', 'user1@example.com', 'First1', 'Last1', 1, false),
('user2', 'passwordhash2', 'user2@example.com', 'First2', 'Last2', 1, false),
('user3', 'passwordhash3', 'user3@example.com', 'First3', 'Last3', 2, false),
('admin1', 'passwordhash4', 'admin1@example.com', 'Admin', 'One', 9, true),
('admin2', 'passwordhash5', 'admin2@example.com', 'Admin', 'Two', 9, true);
-- ITEMS
INSERT INTO items (item_name, can_borrow_role, in_safe, safe_nr, door_key, last_borrowed_person, currently_borrowing)
VALUES
('Item1', 1, true, 1, 101, NULL, NULL),
('Item2', 1, true, 2, 102, 'user1', 'user1'),
('Item3', 2, true, 3, 103, 'user2', NULL),
('Item4', 1, false, NULL, NULL, NULL, NULL),
('Item5', 2, false, NULL, NULL, 'user3', 'user3');
-- LOANS
INSERT INTO loans (
username,
lockers,
loan_code,
start_date,
end_date,
take_date,
returned_date,
created_at,
loaned_items_id,
loaned_items_name,
deleted,
note
)
VALUES
(
'user1',
JSON_ARRAY('Locker1', 'Locker2'),
'123456',
'2026-02-01 09:00:00',
'2026-02-10 17:00:00',
'2026-02-01 09:15:00',
NULL,
'2026-02-01 09:00:00',
JSON_ARRAY(1, 2),
JSON_ARRAY('Item1', 'Item2'),
false,
'Erste allgemeine Ausleihe'
),
(
'user2',
JSON_ARRAY('Locker3'),
'234567',
'2026-02-02 10:00:00',
'2026-02-05 16:00:00',
'2026-02-02 10:05:00',
'2026-02-05 15:30:00',
'2026-02-02 10:00:00',
JSON_ARRAY(3),
JSON_ARRAY('Item3'),
false,
'Zurückgegeben vor Enddatum'
),
(
'user3',
JSON_ARRAY(),
'345678',
'2026-02-03 08:30:00',
'2026-02-15 18:00:00',
NULL,
NULL,
'2026-02-03 08:30:00',
JSON_ARRAY(5),
JSON_ARRAY('Item5'),
false,
'Noch ausgeliehen'
),
(
'user1',
JSON_ARRAY('Locker4'),
'456789',
'2025-12-01 09:00:00',
'2025-12-03 17:00:00',
'2025-12-01 09:10:00',
'2025-12-03 16:45:00',
'2025-12-01 09:00:00',
JSON_ARRAY(1),
JSON_ARRAY('Item1'),
true,
'Alte, gelöschte Ausleihe'
);
-- API KEYS
INSERT INTO apiKeys (api_key, entry_name)
VALUES
('10000001', 'Entry1'),
('10000002', 'Entry2'),
('10000003', 'Entry3'),
('10000004', 'Entry4');
+1 -12
View File
@@ -11,6 +11,7 @@ CREATE TABLE users (
is_admin bool NOT NULL DEFAULT false, is_admin bool NOT NULL DEFAULT false,
entry_created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP, entry_created_at timestamp NULL DEFAULT CURRENT_TIMESTAMP,
entry_updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, entry_updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
secret_user bool NOT NULL DEFAULT false,
PRIMARY KEY (id) PRIMARY KEY (id)
) ENGINE=InnoDB; ) ENGINE=InnoDB;
@@ -27,7 +28,6 @@ CREATE TABLE loans (
loaned_items_id json NOT NULL DEFAULT ('[]'), loaned_items_id json NOT NULL DEFAULT ('[]'),
loaned_items_name json NOT NULL DEFAULT ('[]'), loaned_items_name json NOT NULL DEFAULT ('[]'),
deleted bool NOT NULL DEFAULT false, deleted bool NOT NULL DEFAULT false,
deleted_admin bool NOT NULL DEFAULT false,
note varchar(500) DEFAULT NULL, note varchar(500) DEFAULT NULL,
PRIMARY KEY (id), PRIMARY KEY (id),
CHECK (loan_code REGEXP '^[0-9]{6}$') CHECK (loan_code REGEXP '^[0-9]{6}$')
@@ -56,14 +56,3 @@ 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")
+5 -34
View File
@@ -1,25 +1,8 @@
import express from "express"; import express from "express";
import cors from "cors"; import cors from "cors";
import dotenv from "dotenv"; import env 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";
@@ -31,11 +14,14 @@ 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";
env.config();
const app = express();
const port = 8102;
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" }));
@@ -51,7 +37,6 @@ 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);
@@ -62,20 +47,6 @@ app.listen(port, () => {
console.log(`Server is running on port: ${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) => { app.get("/verify", authenticate, async (req, res) => {
res.status(200).json({ message: "Token is valid", user: req.user }); res.status(200).json({ message: "Token is valid", user: req.user });
}); });
-18
View File
@@ -1,12 +1,8 @@
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");
@@ -49,13 +45,6 @@ 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];
@@ -72,13 +61,6 @@ 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
@@ -1,42 +0,0 @@
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;
}
-13
View File
@@ -1,13 +0,0 @@
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;
}
@@ -1,76 +0,0 @@
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};">&nbsp;</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 };
}
@@ -1,124 +0,0 @@
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&auml;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};">&nbsp;</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&auml;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 &rarr;
</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 };
}
-13
View File
@@ -1,13 +0,0 @@
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,
},
});
-38
View File
@@ -1,38 +0,0 @@
# Changelog for upcoming version: v2.2
This update provides some new features for the design. It also contains some improvements and I have also fixed some bugs.
## New features
- **Deactivatable services:** I have added the ability to deactivate services, which can be useful for maintenance or other purposes. The admin can activate and deactivate services in the admin panel. If a service is deactivated, it will not be available for users and they will get an error message if they try to use it. They will also get an warning banner on the homepage.
- **New Animations:** I have added some new animations to the frontend, which make the user experience more enjoyable.
- **New Icon:** I have added a new icon for the frontend, which is now also used in the header and the favicon. It is a dark version of the old icon, which fits better to the overall design. I have made it with Icon Composer. The old icon is still used for the admin panel, which has a light design. (Maybe I will change the admin panel design in the future...)
- **New Button:** When you go to your user card (over the user icon in the header) you have a new button "Click me". If you click it, you will get an message... _I am just saying: I have implemented the no-as-a-service code in to my Backend._
## Improvements
- The overview page now shows the note column and is overall better organised.
- Improved error logging
- If you try to delete a loan that has not been returned yet, you will get an 507 error code.
- When the admin deletes a loan, the loan will be still visible in the database, but it will be marked as deleted. This is to prevent data loss and to keep track of deleted loans.
- Mailer improvements: The mailer is now more clearly organised. Two large code files are now split into five smaller code files which are easier to maintain. Also the design of the mails has improved.
## Fixed bugs
- Fixed bug: #13
- Fixed bug for messaging when server has an error
- Fixed footer height
---
## New version numbers
**Backend:** v2.2
**Frontend:** v2.2
**Admin panel:** v1.4
---
-[Theis](https://portfolio-theis.de)
+30 -35
View File
@@ -1,56 +1,51 @@
services: services:
# usr-frontend_v2: demo_usr_frontend:
# container_name: borrow_system-usr-frontend container_name: demo_borrow_system-usr-frontend
# build: ./FrontendV2 networks:
# ports: - proxynet
# - "8001:80" build: ./FrontendV2
# restart: always restart: unless-stopped
# admin-frontend: demo_admin_frontend:
# container_name: borrow_system-admin-frontend container_name: demo_borrow_system-admin-frontend
# build: ./admin networks:
# ports: - proxynet
# - "8003:80" build: ./admin
# restart: always restart: unless-stopped
backend_v2: demo_backend_v2:
container_name: borrow_system-backend_v2 container_name: demo_borrow_system-backend_v2
networks:
- proxynet
build: ./backendV2 build: ./backendV2
ports:
- "8004:8004"
environment: environment:
NODE_ENV: production NODE_ENV: production
DB_HOST: mysql_v2 DB_HOST: demo_mysql_v2
DB_USER: root DB_USER: root
DB_PASSWORD: ${DB_PASSWORD_V2} DB_PASSWORD: ${DB_PASSWORD_V2}
DB_NAME: borrow_system_new DB_NAME: borrow_system_new
depends_on: depends_on:
- mysql_v2 - demo_mysql_v2
restart: always restart: unless-stopped
mysql_v2: demo_mysql_v2:
container_name: borrow_system-mysql-v2 container_name: demo_borrow_system-mysql-v2
networks:
- proxynet
image: mysql:8.0 image: mysql:8.0
restart: always restart: unless-stopped
environment: environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD_V2} MYSQL_ROOT_PASSWORD: ${DB_PASSWORD_V2}
MYSQL_DATABASE: borrow_system_new MYSQL_DATABASE: borrow_system_new
TZ: Europe/Berlin TZ: Europe/Berlin
volumes: volumes:
- mysql-v2-data:/var/lib/mysql - demo_mysql-v2-data:/var/lib/mysql
- ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro - ./mysql-timezone.cnf:/etc/mysql/conf.d/timezone.cnf:ro
ports:
- "3310:3306"
no-as-a-service:
container_name: borrow_system-naas
ports:
- "3000:3000"
build:
context: ./no-as-a-service
dockerfile: Dockerfile
restart: always
volumes: volumes:
mysql-data: mysql-data:
mysql-v2-data: demo_mysql-v2-data:
networks:
proxynet:
external: true
Submodule no-as-a-service deleted from 764062a307