Compare commits
18 Commits
host
...
dev_feat_mailer
| Author | SHA1 | Date | |
|---|---|---|---|
| 4291552b6d | |||
| 5191871681 | |||
| c61a283127 | |||
| 4a3c948386 | |||
| 6fb03530df | |||
| d2c36e71be | |||
| 40d784ab36 | |||
| 60c85efd37 | |||
| 747932cf03 | |||
| 0964109c4b | |||
| 5442f2f1f3 | |||
| 56e073244f | |||
| d2dc74971f | |||
| 25709ea0d9 | |||
| f8ab2490fe | |||
| 3de877dd2b | |||
| 5d0134017a | |||
| 07503ec079 |
@@ -0,0 +1,3 @@
|
||||
[submodule "no-as-a-service"]
|
||||
path = no-as-a-service
|
||||
url = https://github.com/hotheadhacker/no-as-a-service.git
|
||||
Generated
+328
-260
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^3.28.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@lottiefiles/dotlottie-react": "^0.19.0",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.90.5",
|
||||
"i18next": "^25.6.0",
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Alert, Stack, VStack, Spinner, Text, Heading } from "@chakra-ui/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { API_BASE } from "@/config/api.config";
|
||||
import Cookies from "js-cookie";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export const DeactivatedServices = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [deactivatedServices, setDeactivatedServices] = useState<
|
||||
{ function_name: string }[]
|
||||
>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDeactivatedServices = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/users/deactivated-services`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token") || ""}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setDeactivatedServices(data);
|
||||
} else {
|
||||
console.error("Failed to fetch deactivated services");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching deactivated services:", error);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
fetchDeactivatedServices();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{deactivatedServices.length >= 1 && (
|
||||
<Stack gap="2">
|
||||
<Heading size={"xl"}>{t("deactivated-services")}</Heading>
|
||||
{isLoading && (
|
||||
<VStack colorPalette="teal">
|
||||
<Spinner color="colorPalette.600" />
|
||||
<Text color="colorPalette.600">{t("loading")}</Text>
|
||||
</VStack>
|
||||
)}
|
||||
{deactivatedServices.length >= 1 &&
|
||||
deactivatedServices.map((item) => (
|
||||
<Alert.Root key={item.function_name} status="warning">
|
||||
<Alert.Indicator />
|
||||
<Alert.Title>
|
||||
{item.function_name} {t("is-deactivated")}
|
||||
</Alert.Title>
|
||||
</Alert.Root>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
Image,
|
||||
Heading,
|
||||
Stack,
|
||||
Text,
|
||||
@@ -68,6 +69,7 @@ export const Header = () => {
|
||||
className="mb-6"
|
||||
position="relative"
|
||||
pr={{ base: 10, md: 0 }} // Platz für den Mobile-Button rechts
|
||||
marginBottom={1}
|
||||
>
|
||||
{/* Mobile: Drei-Punkte-Button, vertikal zentriert im Header */}
|
||||
<Box
|
||||
@@ -190,6 +192,13 @@ export const Header = () => {
|
||||
<Stack gap={1}>
|
||||
{/* Titelzeile ohne Mobile-Menu (wurde nach oben verlegt) */}
|
||||
<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
|
||||
size="2xl"
|
||||
className="tracking-tight text-slate-900 dark:text-slate-100"
|
||||
|
||||
@@ -36,12 +36,43 @@ export const UserDialogue = (props: UserDialogueProps) => {
|
||||
const [msgTitle, setMsgTitle] = 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 [newPassword, setNewPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
|
||||
// Dialog control
|
||||
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 () => {
|
||||
if (newPassword !== confirmPassword) {
|
||||
@@ -147,14 +178,31 @@ export const UserDialogue = (props: UserDialogueProps) => {
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card.Body>
|
||||
<Card.Footer justifyContent="flex-end">
|
||||
<Button variant="outline" onClick={() => props.setUserDialog(false)}>
|
||||
<Card.Footer>
|
||||
<Stack w="100%" gap={3}>
|
||||
{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")}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => openNAAS()}>
|
||||
{t("try-naas")}
|
||||
</Button>
|
||||
</HStack>
|
||||
</Stack>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
|
||||
{/* Passwort-Dialog (kontrolliert) */}
|
||||
{/* Passwort-Dialog */}
|
||||
<Dialog.Root open={isPwOpen} onOpenChange={(e: any) => setPwOpen(e.open)}>
|
||||
<Portal>
|
||||
<Dialog.Backdrop />
|
||||
@@ -215,6 +263,31 @@ export const UserDialogue = (props: UserDialogueProps) => {
|
||||
</Dialog.Positioner>
|
||||
</Portal>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
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
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
Alert,
|
||||
Container,
|
||||
Text,
|
||||
VStack,
|
||||
Spinner,
|
||||
} from "@chakra-ui/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
@@ -21,9 +23,21 @@ interface Alert {
|
||||
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",
|
||||
@@ -42,6 +56,12 @@ export const ContactPage = () => {
|
||||
text: t("contactPage_successText"),
|
||||
});
|
||||
setMessage("");
|
||||
} else if (result.status === 503) {
|
||||
setAlert({
|
||||
type: "error",
|
||||
headline: t("serviceDeactivatedHeadline"),
|
||||
text: t("contactPage_serviceDeactivatedText"),
|
||||
});
|
||||
} else {
|
||||
setAlert({
|
||||
type: "error",
|
||||
@@ -49,6 +69,7 @@ export const ContactPage = () => {
|
||||
text: t("contactPage_errorText"),
|
||||
});
|
||||
}
|
||||
setIsSending(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -78,6 +99,12 @@ export const ContactPage = () => {
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -18,6 +18,8 @@ import { borrowAbleItemsAtom } from "@/states/Atoms";
|
||||
import { createLoan } from "@/utils/Fetcher";
|
||||
import { Header } from "@/components/Header";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { approvalAnimation } from "@/components/dotLottie";
|
||||
import { DeactivatedServices } from "@/components/DeactivatedServices";
|
||||
|
||||
export interface User {
|
||||
username: string;
|
||||
@@ -27,6 +29,8 @@ export interface User {
|
||||
export const HomePage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [showAnimation, setShowAnimation] = useState(false);
|
||||
|
||||
const [borrowableItems, setBorrowableItems] = useAtom(borrowAbleItemsAtom);
|
||||
const [startDate, setStartDate] = useState("");
|
||||
const [endDate, setEndDate] = useState("");
|
||||
@@ -46,13 +50,29 @@ export const HomePage = () => {
|
||||
setSelectedItems((prevSelected) =>
|
||||
prevSelected.includes(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 (
|
||||
<>
|
||||
{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">
|
||||
<Header />
|
||||
<DeactivatedServices />
|
||||
{isMsg && (
|
||||
<MyAlert
|
||||
status={msgStatus}
|
||||
@@ -113,12 +133,6 @@ export const HomePage = () => {
|
||||
>
|
||||
{t("get-borrowable-items")}
|
||||
</Button>
|
||||
{isLoadingA && (
|
||||
<VStack colorPalette="teal">
|
||||
<Spinner color="colorPalette.600" />
|
||||
<Text color="colorPalette.600">{t("loading")}</Text>
|
||||
</VStack>
|
||||
)}
|
||||
{borrowableItems.length > 0 && (
|
||||
<Table.ScrollArea borderWidth="1px" rounded="md">
|
||||
<Table.Root size="sm" stickyHeader>
|
||||
@@ -158,7 +172,7 @@ export const HomePage = () => {
|
||||
maxLength={MAX_CHARACTERS}
|
||||
onChange={(e) => {
|
||||
setNote(
|
||||
e.currentTarget.value.slice(0, MAX_CHARACTERS)
|
||||
e.currentTarget.value.slice(0, MAX_CHARACTERS),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -171,30 +185,40 @@ export const HomePage = () => {
|
||||
)}
|
||||
{selectedItems.length >= 1 && (
|
||||
<Button
|
||||
onClick={() =>
|
||||
onClick={() => {
|
||||
setIsLoadingA(true);
|
||||
createLoan(selectedItems, startDate, endDate, note).then(
|
||||
(response) => {
|
||||
setIsLoadingA(false);
|
||||
if (response.status === "error") {
|
||||
setMsgStatus("error");
|
||||
setMsgTitle(response.title || t("error"));
|
||||
setMsgDescription(
|
||||
response.description || t("unknown-error")
|
||||
response.description || t("unknown-error"),
|
||||
);
|
||||
setIsMsg(true);
|
||||
return;
|
||||
}
|
||||
showApprovalAnimation(3);
|
||||
setMsgStatus("success");
|
||||
setMsgTitle(t("success"));
|
||||
setMsgDescription(t("loan-success"));
|
||||
setIsMsg(true);
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t("create-loan")}
|
||||
</Button>
|
||||
)}
|
||||
{isLoadingA && (
|
||||
<VStack colorPalette="teal">
|
||||
<Spinner color="colorPalette.600" />
|
||||
<Text color="colorPalette.600">{t("loading")}</Text>
|
||||
</VStack>
|
||||
)}
|
||||
</Stack>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -79,6 +79,16 @@ const Landingpage: React.FC = () => {
|
||||
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();
|
||||
if (Array.isArray(loanData)) {
|
||||
setLoans(loanData);
|
||||
|
||||
@@ -4,26 +4,47 @@ import { Button, Card, Field, Input, Stack } from "@chakra-ui/react";
|
||||
import { setIsLoggedInAtom, triggerLogoutAtom } from "@/states/Atoms";
|
||||
import { useAtom } from "jotai";
|
||||
import Cookies from "js-cookie";
|
||||
import { Navigate, useNavigate, useLocation } from "react-router-dom";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { API_BASE } from "@/config/api.config";
|
||||
import { unlockAnimation } from "@/components/dotLottie";
|
||||
import { logoutAnimation } from "@/components/dotLottie";
|
||||
|
||||
export const LoginPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isLoggedIn, setIsLoggedIn] = useAtom(setIsLoggedInAtom);
|
||||
const [triggerLogout, setTriggerLogout] = useAtom(triggerLogoutAtom);
|
||||
const [showAnimation, setShowAnimation] = useState(false);
|
||||
const [showLogout, setShowLogout] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const from = location.state?.from?.pathname || "/";
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn) {
|
||||
navigate(from, { replace: true });
|
||||
window.location.reload(); // if deleted, the user context is not updated in time
|
||||
if (triggerLogout) {
|
||||
setShowLogout(true);
|
||||
window.setTimeout(() => {
|
||||
setShowLogout(false);
|
||||
}, 4500);
|
||||
}
|
||||
}, [isLoggedIn, navigate, from]);
|
||||
|
||||
if (!isLoggedIn) return;
|
||||
|
||||
// Existing sessions should redirect immediately, fresh logins wait for animation.
|
||||
if (!showAnimation) {
|
||||
navigate(from, { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
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 response = await fetch(`${API_BASE}/api/users/login`, {
|
||||
@@ -42,6 +63,8 @@ export const LoginPage = () => {
|
||||
};
|
||||
}
|
||||
|
||||
setShowAnimation(true);
|
||||
|
||||
Cookies.set("token", data.token);
|
||||
setIsLoggedIn(true);
|
||||
return { success: true };
|
||||
@@ -62,14 +85,22 @@ export const LoginPage = () => {
|
||||
return;
|
||||
}
|
||||
setTriggerLogout(false);
|
||||
navigate(from, { replace: true });
|
||||
};
|
||||
|
||||
if (isLoggedIn) {
|
||||
return <Navigate to={from} replace />;
|
||||
}
|
||||
|
||||
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">
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<Card.Root maxW="sm">
|
||||
@@ -97,9 +128,17 @@ export const LoginPage = () => {
|
||||
</Card.Body>
|
||||
<Card.Footer justifyContent="flex-end">
|
||||
{isError && (
|
||||
<MyAlert status="error" title={errorMsg} description={errorDsc} />
|
||||
<MyAlert
|
||||
status="error"
|
||||
title={errorMsg}
|
||||
description={errorDsc}
|
||||
/>
|
||||
)}
|
||||
<Button type="submit" onClick={() => handleLogin()} variant="solid">
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={() => handleLogin()}
|
||||
variant="solid"
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
@@ -115,5 +154,6 @@ export const LoginPage = () => {
|
||||
</Card.Root>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -52,6 +52,13 @@ export const MyLoansPage = () => {
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.status === 503) {
|
||||
setMsgStatus("error");
|
||||
setMsgTitle(t("serviceDeactivatedHeadline"));
|
||||
setMsgDescription(t("loan_page_serviceDeactivatedText"));
|
||||
setIsMsg(true);
|
||||
return;
|
||||
}
|
||||
setMsgStatus("error");
|
||||
setMsgTitle(t("error"));
|
||||
setMsgDescription(t("error-fetching-loans"));
|
||||
@@ -84,6 +91,14 @@ export const MyLoansPage = () => {
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
if (res.status === 507) {
|
||||
setMsgStatus("error");
|
||||
setMsgTitle(t("error"));
|
||||
setMsgDescription(t("error-deleting-loan-507"));
|
||||
setIsMsg(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setMsgStatus("error");
|
||||
setMsgTitle(t("error"));
|
||||
setMsgDescription(t("error-deleting-loan"));
|
||||
|
||||
@@ -17,6 +17,16 @@ export const getBorrowableItems = async (
|
||||
});
|
||||
|
||||
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 {
|
||||
data: null,
|
||||
status: "error",
|
||||
@@ -60,6 +70,16 @@ export const createLoan = async (
|
||||
});
|
||||
|
||||
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 {
|
||||
data: null,
|
||||
status: "error",
|
||||
|
||||
@@ -88,5 +88,18 @@
|
||||
"take-loan-success": "Ausleihe erfolgreich abgeholt",
|
||||
"return-loan-success": "Ausleihe erfolgreich zurückgegeben",
|
||||
"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."
|
||||
}
|
||||
@@ -88,5 +88,18 @@
|
||||
"take-loan-success": "Loan taken successfully",
|
||||
"return-loan-success": "Loan returned successfully",
|
||||
"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."
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import UserTable from "../components/UserTable";
|
||||
import ItemTable from "../components/ItemTable";
|
||||
import LoanTable from "../components/LoanTable";
|
||||
import APIKeyTable from "@/components/APIKeyTable";
|
||||
import ServerConfig from "@/components/ServerConfig";
|
||||
import { MoveLeft } from "lucide-react";
|
||||
|
||||
type DashboardProps = {
|
||||
@@ -44,6 +45,7 @@ const Dashboard: React.FC<DashboardProps> = ({ onLogout }) => {
|
||||
viewSchliessfaecher={() => setActiveView("Schließfächer")}
|
||||
viewUser={() => setActiveView("User")}
|
||||
viewAPI={() => setActiveView("API")}
|
||||
viewConfig={() => setActiveView("Server Konfiguration")}
|
||||
/>
|
||||
<Box flex="1" display="flex" flexDirection="column">
|
||||
<Flex
|
||||
@@ -88,6 +90,7 @@ const Dashboard: React.FC<DashboardProps> = ({ onLogout }) => {
|
||||
{activeView === "Ausleihen" && <LoanTable />}
|
||||
{activeView === "Gegenstände" && <ItemTable />}
|
||||
{activeView === "API" && <APIKeyTable />}
|
||||
{activeView === "Server Konfiguration" && <ServerConfig />}
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
@@ -9,6 +9,7 @@ type SidebarProps = {
|
||||
viewSchliessfaecher: () => void;
|
||||
viewUser: () => void;
|
||||
viewAPI: () => void;
|
||||
viewConfig: () => void;
|
||||
};
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({
|
||||
@@ -16,6 +17,7 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||
viewGegenstaende,
|
||||
viewUser,
|
||||
viewAPI,
|
||||
viewConfig
|
||||
}) => {
|
||||
const [info, setInfo] = useState<any>(null);
|
||||
|
||||
@@ -83,6 +85,15 @@ const Sidebar: React.FC<SidebarProps> = ({
|
||||
>
|
||||
API Keys
|
||||
</Link>
|
||||
<Link
|
||||
px={3}
|
||||
py={2}
|
||||
rounded="md"
|
||||
_hover={{ bg: "gray.700", textDecoration: "none" }}
|
||||
onClick={viewConfig}
|
||||
>
|
||||
Server Konfiguration
|
||||
</Link>
|
||||
</VStack>
|
||||
|
||||
<Box mt="auto" pt={8} fontSize="xs" color="gray.500">
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Table,
|
||||
Spinner,
|
||||
Text,
|
||||
VStack,
|
||||
Heading,
|
||||
Switch,
|
||||
} from "@chakra-ui/react";
|
||||
import MyAlert from "./myChakra/MyAlert";
|
||||
import Cookies from "js-cookie";
|
||||
import { useState, useEffect } from "react";
|
||||
import { formatDateTime } from "@/utils/userFuncs";
|
||||
import { API_BASE } from "@/config/api.config";
|
||||
|
||||
type Items = {
|
||||
id: number;
|
||||
function_name: string;
|
||||
active: boolean;
|
||||
entry_created_at: string;
|
||||
updated_at: string | null;
|
||||
};
|
||||
|
||||
const ServerConfig: React.FC = () => {
|
||||
const [items, setItems] = useState<Items[]>([]);
|
||||
const [errorStatus, setErrorStatus] = useState<"error" | "success">("error");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [errorDsc, setErrorDsc] = useState("");
|
||||
const [isError, setIsError] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [reload, setReload] = useState(false);
|
||||
|
||||
const handleSwitchChange = async (id: number, newState: boolean) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/admin/server-config/update?functionName=${encodeURIComponent(
|
||||
items.find((item) => item.id === id)?.function_name || "",
|
||||
)}&active=${newState}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
if (response.ok) {
|
||||
setReload((prev) => !prev);
|
||||
setError(
|
||||
"success",
|
||||
"Status updated",
|
||||
"The function status was updated successfully.",
|
||||
);
|
||||
} else {
|
||||
setError(
|
||||
"error",
|
||||
"Failed to update status",
|
||||
"There is an error updating the function status.",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
setError(
|
||||
"error",
|
||||
"Failed to update status",
|
||||
"There is an error updating the function status.",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const setError = (
|
||||
status: "error" | "success",
|
||||
message: string,
|
||||
description: string,
|
||||
) => {
|
||||
setIsError(false);
|
||||
setErrorStatus(status);
|
||||
setErrorMessage(message);
|
||||
setErrorDsc(description);
|
||||
setIsError(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/admin/server-config/all`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${Cookies.get("token")}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
const data = await response.json();
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
setError("error", "Failed to fetch items", "There is an error");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchData().then((data) => {
|
||||
if (Array.isArray(data)) {
|
||||
setItems(data);
|
||||
}
|
||||
});
|
||||
}, [reload]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Heading marginBottom={4} size="2xl">
|
||||
Server Konfiguration
|
||||
</Heading>
|
||||
{isError && (
|
||||
<MyAlert
|
||||
status={errorStatus}
|
||||
description={errorDsc}
|
||||
title={errorMessage}
|
||||
/>
|
||||
)}
|
||||
{isLoading && (
|
||||
<VStack colorPalette="teal">
|
||||
<Spinner color="colorPalette.600" />
|
||||
<Text color="colorPalette.600">Loading...</Text>
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
<Table.Root size="sm" striped w="100%" style={{ tableLayout: "auto" }}>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.ColumnHeader width="1%" whiteSpace="nowrap">
|
||||
<strong>#</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Service Name</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Toggle</strong>
|
||||
</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>
|
||||
<strong>Eintrag erstellt am</strong>
|
||||
</Table.ColumnHeader>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{items.map((item) => (
|
||||
<Table.Row key={item.id}>
|
||||
<Table.Cell whiteSpace="nowrap">{item.id}</Table.Cell>
|
||||
<Table.Cell fontFamily="mono">{item.function_name}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Switch.Root
|
||||
checked={item.active}
|
||||
onCheckedChange={() =>
|
||||
handleSwitchChange(item.id, !item.active)
|
||||
}
|
||||
>
|
||||
<Switch.HiddenInput />
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
<Switch.Label />
|
||||
</Switch.Root>
|
||||
</Table.Cell>
|
||||
<Table.Cell whiteSpace="nowrap">
|
||||
{formatDateTime(item.entry_created_at)}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerConfig;
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"backend-info": {
|
||||
"version": "v2.1.1 (dev)"
|
||||
"version": "v2.2 (dev)"
|
||||
},
|
||||
"frontend-info": {
|
||||
"version": "v2.1.2 (dev)"
|
||||
"version": "v2.2 (dev)"
|
||||
},
|
||||
"admin-panel-info": {
|
||||
"version": "v1.3.2 (dev)"
|
||||
"version": "v1.4(dev)"
|
||||
}
|
||||
}
|
||||
Generated
+74
-38
@@ -1,21 +1,22 @@
|
||||
{
|
||||
"name": "backendv2",
|
||||
"version": "1.0.0",
|
||||
"version": "v2.1.1 (dev)",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "backendv2",
|
||||
"version": "1.0.0",
|
||||
"version": "v2.1.1 (dev)",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.1",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^8.4.1",
|
||||
"jose": "^6.0.12",
|
||||
"mysql2": "^3.14.3",
|
||||
"nodemailer": "^7.0.6"
|
||||
"nodemailer": "^8.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
@@ -53,29 +54,49 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
||||
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
|
||||
"integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "^3.1.2",
|
||||
"content-type": "^1.0.5",
|
||||
"debug": "^4.4.0",
|
||||
"debug": "^4.4.3",
|
||||
"http-errors": "^2.0.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"iconv-lite": "^0.7.0",
|
||||
"on-finished": "^2.4.1",
|
||||
"qs": "^6.14.0",
|
||||
"raw-body": "^3.0.0",
|
||||
"type-is": "^2.0.0"
|
||||
"qs": "^6.14.1",
|
||||
"raw-body": "^3.0.1",
|
||||
"type-is": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
||||
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -349,6 +370,24 @@
|
||||
"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": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
|
||||
@@ -509,24 +548,21 @@
|
||||
"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": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"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": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -656,9 +692,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"version": "5.1.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
|
||||
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
@@ -731,9 +767,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "7.0.10",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.10.tgz",
|
||||
"integrity": "sha512-Us/Se1WtT0ylXgNFfyFSx4LElllVLJXQjWi2Xz17xWw7amDKO2MLtFnVp1WACy7GkVGs+oBlRopVNUzlrGSw1w==",
|
||||
"version": "8.0.6",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.6.tgz",
|
||||
"integrity": "sha512-Nm2XeuDwwy2wi5A+8jPWwQwNzcjNjhWdE3pVLoXEusxJqCnAPAgnBGkSmiLknbnWuOF9qraRpYZjfxqtKZ4tPw==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -791,9 +827,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
|
||||
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
||||
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -820,9 +856,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||
"version": "6.15.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
|
||||
"integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
"dotenv": "^17.2.1",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^5.1.0",
|
||||
"express-rate-limit": "^8.4.1",
|
||||
"jose": "^6.0.12",
|
||||
"mysql2": "^3.14.3",
|
||||
"nodemailer": "^7.0.6"
|
||||
"nodemailer": "^8.0.6"
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,10 @@ export const getAllLoans = async () => {
|
||||
};
|
||||
|
||||
export const deleteLoanById = async (loanId) => {
|
||||
const [result] = await pool.query("DELETE FROM loans WHERE id = ?", [loanId]);
|
||||
const [result] = await pool.query(
|
||||
"UPDATE loans SET deleted = true, deleted_admin = true WHERE id = ?",
|
||||
[loanId],
|
||||
);
|
||||
if (result.affectedRows > 0) return { success: true };
|
||||
return { success: false };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import mysql from "mysql2";
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
const pool = mysql
|
||||
.createPool({
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
})
|
||||
.promise();
|
||||
|
||||
export const getAllFunctions = async () => {
|
||||
const [rows] = await pool.query("SELECT * FROM functions");
|
||||
return { success: true, data: rows };
|
||||
};
|
||||
|
||||
export const updateFunctionStatus = async (functionName, active) => {
|
||||
const [result] = await pool.query(
|
||||
"UPDATE functions SET active = ? WHERE function_name = ?",
|
||||
[active, functionName],
|
||||
);
|
||||
if (result.affectedRows > 0) return { success: true };
|
||||
return { success: false };
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import express from "express";
|
||||
import { authenticateAdmin } from "../../services/authentication.js";
|
||||
const router = express.Router();
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
// database funcs import
|
||||
import {
|
||||
getAllFunctions,
|
||||
updateFunctionStatus,
|
||||
} from "./database/serverConfMgmt.database.js";
|
||||
|
||||
// Route to get all functions and their statuses
|
||||
router.get("/all", async (req, res) => {
|
||||
try {
|
||||
const result = await getAllFunctions();
|
||||
if (result.success) {
|
||||
res.status(200).json({ data: result.data });
|
||||
} else {
|
||||
res.status(500).json({ message: "Failed to fetch functions" });
|
||||
}
|
||||
} catch (error) {
|
||||
res
|
||||
.status(500)
|
||||
.json({ message: "An error occurred", error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Route to update the status of a function
|
||||
router.post("/update", async (req, res) => {
|
||||
const functionName = req.query.functionName;
|
||||
let active = req.query.active;
|
||||
|
||||
if (active === "false") {
|
||||
active = 0;
|
||||
} else if (active === "true") {
|
||||
active = 1;
|
||||
} else {
|
||||
res.status(406).json({ message: "Got unexpected format" });
|
||||
}
|
||||
|
||||
const result = await updateFunctionStatus(functionName, active);
|
||||
if (result.success) {
|
||||
res.status(200).json({ message: "Function status updated successfully" });
|
||||
} else {
|
||||
res.status(500).json({ message: "Failed to update function status" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,9 +1,12 @@
|
||||
import express from "express";
|
||||
import { authenticate } from "../../services/authentication.js";
|
||||
import { checkIfServiceIsActive } from "../../services/functions.js";
|
||||
const router = express.Router();
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
const loan_service = "Loan Service";
|
||||
|
||||
import {
|
||||
getItemsFromDatabaseV2,
|
||||
changeInSafeStateV2,
|
||||
@@ -39,6 +42,7 @@ router.post("/change-state/:key/:itemId", authenticate, async (req, res) => {
|
||||
router.get(
|
||||
"/get-loan-by-code/:key/:loan_code",
|
||||
authenticate,
|
||||
checkIfServiceIsActive(loan_service),
|
||||
async (req, res) => {
|
||||
const loan_code = req.params.loan_code;
|
||||
const result = await getLoanByCodeV2(loan_code);
|
||||
@@ -54,6 +58,7 @@ router.get(
|
||||
router.post(
|
||||
"/set-return-date/:key/:loan_code",
|
||||
authenticate,
|
||||
checkIfServiceIsActive(loan_service),
|
||||
async (req, res) => {
|
||||
const loanCode = req.params.loan_code;
|
||||
const result = await setReturnDateV2(loanCode);
|
||||
@@ -69,6 +74,7 @@ router.post(
|
||||
router.post(
|
||||
"/set-take-date/:key/:loan_code",
|
||||
authenticate,
|
||||
checkIfServiceIsActive(loan_service),
|
||||
async (req, res) => {
|
||||
const loanCode = req.params.loan_code;
|
||||
const result = await setTakeDateV2(loanCode);
|
||||
|
||||
@@ -234,6 +234,23 @@ export const getBorrowableItemsFromDatabase = async (
|
||||
};
|
||||
|
||||
export const SETdeleteLoanFromDatabase = async (loanId) => {
|
||||
const [checkIfdatesReturned] = await pool.query(
|
||||
"SELECT take_date, returned_date FROM loans WHERE id = ? AND deleted = 0",
|
||||
[loanId],
|
||||
);
|
||||
|
||||
if (checkIfdatesReturned.length === 0) {
|
||||
return { success: false, code: "LOAN_NOT_FOUND" };
|
||||
}
|
||||
|
||||
const { take_date, returned_date } = checkIfdatesReturned[0];
|
||||
const bothNull = take_date === null && returned_date === null;
|
||||
const bothSet = take_date !== null && returned_date !== null;
|
||||
|
||||
if (!(bothNull || bothSet)) {
|
||||
return { success: false, code: "LOAN_NOT_RETURNED" };
|
||||
}
|
||||
|
||||
const [result] = await pool.query(
|
||||
"UPDATE loans SET deleted = 1 WHERE id = ?;",
|
||||
[loanId],
|
||||
|
||||
@@ -14,7 +14,7 @@ const pool = mysql
|
||||
export const loginFunc = async (username, password) => {
|
||||
const [result] = await pool.query(
|
||||
"SELECT * FROM users WHERE username = ? AND password = ?",
|
||||
[username, password]
|
||||
[username, password],
|
||||
);
|
||||
if (result.length > 0) return { success: true, data: result[0] };
|
||||
return { success: false };
|
||||
@@ -40,7 +40,7 @@ export const changePassword = async (username, oldPassword, newPassword) => {
|
||||
// get user current password
|
||||
const [user] = await pool.query(
|
||||
"SELECT * FROM users WHERE username = ? AND password = ?",
|
||||
[username, oldPassword]
|
||||
[username, oldPassword],
|
||||
);
|
||||
if (user.length === 0) return { success: false };
|
||||
|
||||
@@ -48,8 +48,16 @@ export const changePassword = async (username, oldPassword, newPassword) => {
|
||||
|
||||
const [result] = await pool.query(
|
||||
"UPDATE users SET password = ? WHERE username = ?",
|
||||
[newPassword, username]
|
||||
[newPassword, username],
|
||||
);
|
||||
if (result.affectedRows > 0) return { success: true };
|
||||
return { success: false };
|
||||
};
|
||||
|
||||
export const getDeactivatedServices = async () => {
|
||||
const [rows] = await pool.query("SELECT function_name FROM functions WHERE active = 0;");
|
||||
if (rows.length > 0) {
|
||||
return { success: true, data: rows };
|
||||
}
|
||||
return { success: false };
|
||||
};
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
import express from "express";
|
||||
import { authenticate, generateToken } from "../../services/authentication.js";
|
||||
const router = express.Router();
|
||||
import {
|
||||
checkIfServiceIsActive,
|
||||
checkIfServiceIsActive2,
|
||||
} from "../../services/functions.js";
|
||||
|
||||
// mailer imports
|
||||
import { sendMail } from "../../services/mailer/send.js";
|
||||
import { loanMail } from "../../services/mailer/templates/loan_created.js";
|
||||
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
const router = express.Router();
|
||||
|
||||
const loan_service = "Loan Service";
|
||||
const loan_mailer_service = "Loan Mailer";
|
||||
|
||||
// database funcs import
|
||||
import {
|
||||
@@ -16,9 +28,12 @@ import {
|
||||
setReturnDate,
|
||||
setTakeDate,
|
||||
} from "./database/loansMgmt.database.js";
|
||||
import { sendMailLoan } from "./services/mailer.js";
|
||||
|
||||
router.post("/createLoan", authenticate, async (req, res) => {
|
||||
router.post(
|
||||
"/createLoan",
|
||||
checkIfServiceIsActive(loan_service),
|
||||
authenticate,
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { items, startDate, endDate, note } = req.body || {};
|
||||
|
||||
@@ -52,18 +67,26 @@ router.post("/createLoan", authenticate, async (req, res) => {
|
||||
note,
|
||||
itemIds,
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
if (await checkIfServiceIsActive2(loan_mailer_service)) {
|
||||
const mailInfo = await getLoanInfoWithID(result.data.id);
|
||||
console.log(mailInfo);
|
||||
sendMailLoan(
|
||||
mailInfo.data.username,
|
||||
const { html, text } = loanMail(
|
||||
req.user.first_name + " " + req.user.last_name,
|
||||
mailInfo.data.loaned_items_name,
|
||||
mailInfo.data.start_date,
|
||||
mailInfo.data.end_date,
|
||||
mailInfo.data.created_at,
|
||||
mailInfo.data.note,
|
||||
);
|
||||
await sendMail({
|
||||
to: process.env.MAIL_SENDEES,
|
||||
subject: "Neue Ausleihe erstellt!",
|
||||
html,
|
||||
text,
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(201).json({
|
||||
message: "Loan created successfully",
|
||||
loanId: result.data.id,
|
||||
@@ -86,9 +109,14 @@ router.post("/createLoan", authenticate, async (req, res) => {
|
||||
console.error("createLoan error:", err);
|
||||
return res.status(500).json({ message: "Failed to create loan" });
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
router.get("/loans", authenticate, async (req, res) => {
|
||||
router.get(
|
||||
"/loans",
|
||||
checkIfServiceIsActive(loan_service),
|
||||
authenticate,
|
||||
async (req, res) => {
|
||||
const result = await getLoansFromDatabase(req.user.username);
|
||||
if (result.success) {
|
||||
res.status(200).json(result.data);
|
||||
@@ -97,9 +125,14 @@ router.get("/loans", authenticate, async (req, res) => {
|
||||
} else {
|
||||
res.status(500).json({ message: "Failed to fetch loans" });
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
router.post("/set-return-date/:loan_code", authenticate, async (req, res) => {
|
||||
router.post(
|
||||
"/set-return-date/:loan_code",
|
||||
checkIfServiceIsActive(loan_service),
|
||||
authenticate,
|
||||
async (req, res) => {
|
||||
const loanCode = req.params.loan_code;
|
||||
const result = await setReturnDate(loanCode);
|
||||
if (result.success) {
|
||||
@@ -107,9 +140,14 @@ router.post("/set-return-date/:loan_code", authenticate, async (req, res) => {
|
||||
} else {
|
||||
res.status(500).json({ message: "Failed to set return date" });
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
router.post("/set-take-date/:loan_code", authenticate, async (req, res) => {
|
||||
router.post(
|
||||
"/set-take-date/:loan_code",
|
||||
checkIfServiceIsActive(loan_service),
|
||||
authenticate,
|
||||
async (req, res) => {
|
||||
const loanCode = req.params.loan_code;
|
||||
const result = await setTakeDate(loanCode);
|
||||
if (result.success) {
|
||||
@@ -117,7 +155,8 @@ router.post("/set-take-date/:loan_code", authenticate, async (req, res) => {
|
||||
} else {
|
||||
res.status(500).json({ message: "Failed to set take date" });
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
router.get("/all-items", authenticate, async (req, res) => {
|
||||
const result = await getItems();
|
||||
@@ -128,26 +167,50 @@ router.get("/all-items", authenticate, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/delete-loan/:id", authenticate, async (req, res) => {
|
||||
router.delete(
|
||||
"/delete-loan/:id",
|
||||
checkIfServiceIsActive(loan_service),
|
||||
authenticate,
|
||||
async (req, res) => {
|
||||
const loanId = req.params.id;
|
||||
const result = await SETdeleteLoanFromDatabase(loanId);
|
||||
if (result.success) {
|
||||
res.status(200).json({ message: "Loan deleted successfully" });
|
||||
} else {
|
||||
if (result.code === "LOAN_NOT_FOUND") {
|
||||
res.status(404).json({ message: "Loan not found" });
|
||||
}
|
||||
|
||||
if (result.code === "LOAN_NOT_RETURNED") {
|
||||
res.status(507).json({
|
||||
message: "Cannot delete loan that has not been returned",
|
||||
});
|
||||
}
|
||||
|
||||
res.status(500).json({ message: "Failed to delete loan" });
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
router.get("/all-loans", authenticate, async (req, res) => {
|
||||
router.get(
|
||||
"/all-loans",
|
||||
checkIfServiceIsActive(loan_service),
|
||||
authenticate,
|
||||
async (req, res) => {
|
||||
const result = await getALLLoans();
|
||||
if (result.success) {
|
||||
res.status(200).json(result.data);
|
||||
} else {
|
||||
res.status(500).json({ message: "Failed to fetch loans" });
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
router.post("/borrowable-items", authenticate, async (req, res) => {
|
||||
router.post(
|
||||
"/borrowable-items",
|
||||
checkIfServiceIsActive(loan_service),
|
||||
authenticate,
|
||||
async (req, res) => {
|
||||
const { startDate, endDate } = req.body || {};
|
||||
if (!startDate || !endDate) {
|
||||
return res
|
||||
@@ -168,6 +231,7 @@ router.post("/borrowable-items", authenticate, async (req, res) => {
|
||||
.status(500)
|
||||
.json({ message: "Failed to fetch borrowable items" });
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
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);
|
||||
})();
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
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);
|
||||
})();
|
||||
}
|
||||
@@ -1,14 +1,28 @@
|
||||
import express from "express";
|
||||
import { authenticate, generateToken } from "../../services/authentication.js";
|
||||
import { checkIfServiceIsActive } from "../../services/functions.js";
|
||||
const router = express.Router();
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
// database funcs import
|
||||
import { loginFunc, changePassword } from "./database/userMgmt.database.js";
|
||||
import { sendMail } from "./services/mailer_v2.js";
|
||||
const user_frontend_service = "User Frontend";
|
||||
const contact_form_service = "Contact Form Service";
|
||||
|
||||
router.post("/login", async (req, res) => {
|
||||
// database funcs import
|
||||
import {
|
||||
loginFunc,
|
||||
changePassword,
|
||||
getDeactivatedServices,
|
||||
} from "./database/userMgmt.database.js";
|
||||
|
||||
// mailer imports
|
||||
import { sendMail } from "../../services/mailer/send.js";
|
||||
import { contactMail } from "../../services/mailer/templates/contact.js";
|
||||
|
||||
router.post(
|
||||
"/login",
|
||||
checkIfServiceIsActive(user_frontend_service),
|
||||
async (req, res) => {
|
||||
const result = await loginFunc(req.body.username, req.body.password);
|
||||
if (result.success) {
|
||||
const token = await generateToken({
|
||||
@@ -22,9 +36,14 @@ router.post("/login", async (req, res) => {
|
||||
} else {
|
||||
res.status(401).json({ message: "Invalid credentials" });
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
router.post("/change-password", authenticate, async (req, res) => {
|
||||
router.post(
|
||||
"/change-password",
|
||||
checkIfServiceIsActive(user_frontend_service),
|
||||
authenticate,
|
||||
async (req, res) => {
|
||||
const oldPassword = req.body.oldPassword;
|
||||
const newPassword = req.body.newPassword;
|
||||
const username = req.user.username;
|
||||
@@ -34,15 +53,47 @@ router.post("/change-password", authenticate, async (req, res) => {
|
||||
} else {
|
||||
res.status(500).json({ message: "Failed to change password" });
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
router.post("/contact", authenticate, async (req, res) => {
|
||||
const message = req.body.message;
|
||||
const username = req.user.username;
|
||||
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;
|
||||
|
||||
sendMail(username, message);
|
||||
if (!username || !message) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ message: "Username and message are required" });
|
||||
}
|
||||
|
||||
const { html, text } = contactMail({ username, message });
|
||||
await sendMail({
|
||||
to: process.env.MAIL_SENDEES_CONTACT,
|
||||
subject: "Neue Nachricht!",
|
||||
html,
|
||||
text,
|
||||
});
|
||||
|
||||
res.status(200).json({ message: "Contact message sent successfully" });
|
||||
} catch (error) {
|
||||
console.error("Failed to send contact mail:", error);
|
||||
res.status(500).json({ message: "Failed to send contact message" });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
router.get("/deactivated-services", authenticate, async (req, res) => {
|
||||
const result = await getDeactivatedServices();
|
||||
if (result.success) {
|
||||
res.status(200).json(result.data);
|
||||
} else {
|
||||
res.status(500).json({ message: "Failed to fetch deactivated services" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -27,6 +27,7 @@ CREATE TABLE loans (
|
||||
loaned_items_id json NOT NULL DEFAULT ('[]'),
|
||||
loaned_items_name json NOT NULL DEFAULT ('[]'),
|
||||
deleted bool NOT NULL DEFAULT false,
|
||||
deleted_admin bool NOT NULL DEFAULT false,
|
||||
note varchar(500) DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CHECK (loan_code REGEXP '^[0-9]{6}$')
|
||||
@@ -55,3 +56,14 @@ CREATE TABLE apiKeys (
|
||||
PRIMARY KEY (id),
|
||||
CHECK (api_key REGEXP '^[0-9]{8}$')
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE functions (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
function_name VARCHAR(500) NOT NULL UNIQUE,
|
||||
active BOOLEAN NOT NULL DEFAULT true,
|
||||
entry_updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
entry_created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
INSERT INTO functions (function_name) VALUES ("Loan Mailer"), ("Loan Service"), ("Contact Form Service"), ("User Frontend"), ("API")
|
||||
+34
-5
@@ -1,8 +1,25 @@
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import env from "dotenv";
|
||||
import dotenv from "dotenv";
|
||||
import info from "./info.json" assert { type: "json" };
|
||||
import { authenticate } from "./services/authentication.js";
|
||||
import { rateLimit } from "express-rate-limit";
|
||||
|
||||
dotenv.config();
|
||||
const app = express();
|
||||
const port = 8004;
|
||||
const naasURL = process.env.NAAS_URL;
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: 1 * 60 * 1000, // 1 minute
|
||||
limit: 50, // Limit each IP to 50 requests per `window` (here, per 1 minute).
|
||||
standardHeaders: "draft-8", // draft-6: `RateLimit-*` headers; draft-7 & draft-8: combined `RateLimit` header
|
||||
legacyHeaders: false, // Disable the `X-RateLimit-*` headers.
|
||||
ipv6Subnet: 56, // Set to 60 or 64 to be less aggressive, or 52 or 48 to be more aggressive
|
||||
// store: ... , // Redis, Memcached, etc. See below.
|
||||
});
|
||||
|
||||
app.use(limiter);
|
||||
|
||||
// frontend routes
|
||||
import loansMgmtRouter from "./routes/app/loanMgmt.route.js";
|
||||
@@ -14,14 +31,11 @@ import loanDataMgmtRouter from "./routes/admin/loanDataMgmt.route.js";
|
||||
import itemDataMgmtRouter from "./routes/admin/itemDataMgmt.route.js";
|
||||
import apiDataMgmtRouter from "./routes/admin/apiDataMgmt.route.js";
|
||||
import userMgmtRouterADMIN from "./routes/admin/userMgmt.route.js";
|
||||
import serverConfMgmtRouter from "./routes/admin/serverConfMgmt.route.js";
|
||||
|
||||
// API routes
|
||||
import apiRouter from "./routes/api/api.route.js";
|
||||
|
||||
env.config();
|
||||
const app = express();
|
||||
const port = 8004;
|
||||
|
||||
app.use(cors());
|
||||
// Body-Parser VOR den Routen registrieren
|
||||
app.use(express.json({ limit: "10mb" }));
|
||||
@@ -37,6 +51,7 @@ app.use("/api/admin/user-data", userDataMgmtRouter);
|
||||
app.use("/api/admin/item-data", itemDataMgmtRouter);
|
||||
app.use("/api/admin/api-data", apiDataMgmtRouter);
|
||||
app.use("/api/admin/user-mgmt", userMgmtRouterADMIN);
|
||||
app.use("/api/admin/server-config", serverConfMgmtRouter);
|
||||
|
||||
// API routes
|
||||
app.use("/api", apiRouter);
|
||||
@@ -47,6 +62,20 @@ app.listen(port, () => {
|
||||
console.log(`Server is running on port: ${port}`);
|
||||
});
|
||||
|
||||
app.get("/no", async (req, res) => {
|
||||
try {
|
||||
const response = await fetch(naasURL);
|
||||
if (!response.ok) {
|
||||
res.status(500).send("Request to no-as-a-service went wrong.");
|
||||
}
|
||||
const data = await response.json();
|
||||
res.json(data);
|
||||
} catch (error) {
|
||||
console.error("Error communicating with no-as-a-service:", error);
|
||||
res.status(500).send("Error communicating with no-as-a-service.");
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/verify", authenticate, async (req, res) => {
|
||||
res.status(200).json({ message: "Token is valid", user: req.user });
|
||||
});
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { SignJWT, jwtVerify } from "jose";
|
||||
import env from "dotenv";
|
||||
import { verifyAPIKeyDB } from "./database.js";
|
||||
import { checkIfServiceIsActive2 } from "./functions.js";
|
||||
env.config();
|
||||
|
||||
const api_service = "API";
|
||||
const user_frontend_service = "User Frontend";
|
||||
|
||||
const secretKey = process.env.SECRET_KEY;
|
||||
if (!secretKey) {
|
||||
throw new Error("Missing SECRET_KEY environment variable");
|
||||
@@ -45,6 +49,13 @@ export async function authenticate(req, res, next) {
|
||||
const apiKey = req.params.key;
|
||||
|
||||
if (authHeader) {
|
||||
const serviceActive = await checkIfServiceIsActive2(user_frontend_service);
|
||||
if (!serviceActive) {
|
||||
return res
|
||||
.status(503)
|
||||
.json({ message: "User Frontend is currently unavailable." });
|
||||
}
|
||||
|
||||
const parts = authHeader.split(" ");
|
||||
const scheme = parts[0];
|
||||
const token = parts[1];
|
||||
@@ -61,6 +72,13 @@ export async function authenticate(req, res, next) {
|
||||
return res.status(403).json({ message: "Present token invalid" }); // present token invalid
|
||||
}
|
||||
} else if (apiKey) {
|
||||
const serviceActive = await checkIfServiceIsActive2(api_service);
|
||||
if (!serviceActive) {
|
||||
return res
|
||||
.status(503)
|
||||
.json({ message: "API Service is currently unavailable." });
|
||||
}
|
||||
|
||||
try {
|
||||
await verifyAPIKey(apiKey);
|
||||
return next();
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import mysql from "mysql2";
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
const pool = mysql
|
||||
.createPool({
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
})
|
||||
.promise();
|
||||
|
||||
export function checkIfServiceIsActive(service) {
|
||||
return async (req, res, next) => {
|
||||
const [result] = await pool.query(
|
||||
"SELECT * FROM functions WHERE function_name = ? AND active = 1;",
|
||||
[service],
|
||||
);
|
||||
|
||||
if (result.length > 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return res
|
||||
.status(503)
|
||||
.json({ message: `-${service}- is currently unavailable.` });
|
||||
};
|
||||
}
|
||||
|
||||
export async function checkIfServiceIsActive2(service) {
|
||||
const [result] = await pool.query(
|
||||
"SELECT * FROM functions WHERE function_name = ? AND active = 1;",
|
||||
[service],
|
||||
);
|
||||
|
||||
if (result.length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { transporter } from "./transporter.js";
|
||||
|
||||
export async function sendMail({ to, subject, text, html }) {
|
||||
const info = await transporter.sendMail({
|
||||
from: '"Ausleihsystem" <noreply@mcs-medien.de>',
|
||||
to,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
});
|
||||
console.log("Mail sent:", info.messageId);
|
||||
return info;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
export function contactMail({ username, message }) {
|
||||
const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9";
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title>Neue Nachricht</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f2f4f7;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;color:#111827;-webkit-text-size-adjust:100%;">
|
||||
<div style="display:none;max-height:0;overflow:hidden;opacity:0;">Neue Kontaktanfrage im Ausleihsystem.</div>
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#f2f4f7;">
|
||||
<tr>
|
||||
<td align="center" style="padding:32px 16px;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" width="600" style="max-width:600px;width:100%;border-radius:14px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.06);">
|
||||
|
||||
<!-- Header -->
|
||||
<tr>
|
||||
<td style="background:${brand};padding:32px 30px 28px;">
|
||||
<h1 style="margin:0;font-size:24px;color:#ffffff;font-weight:700;">Neue Nachricht</h1>
|
||||
<p style="margin:8px 0 0;font-size:14px;color:rgba(255,255,255,0.85);">Eine neue Kontaktanfrage ist eingegangen.</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Accent line -->
|
||||
<tr>
|
||||
<td style="height:3px;line-height:3px;font-size:1px;background:${brand};"> </td>
|
||||
</tr>
|
||||
|
||||
<!-- Content -->
|
||||
<tr>
|
||||
<td style="background:#ffffff;padding:28px 30px;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#fafbfc;border:1px solid #f3f4f6;border-radius:10px;border-collapse:collapse;">
|
||||
<tr>
|
||||
<td style="padding:14px 16px;color:#6b7280;font-size:13px;white-space:nowrap;vertical-align:top;border-bottom:1px solid #f3f4f6;">
|
||||
Benutzername
|
||||
</td>
|
||||
<td style="padding:14px 16px;font-weight:600;color:#111827;font-size:14px;vertical-align:top;border-bottom:1px solid #f3f4f6;">
|
||||
${username || "N/A"}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:14px 16px;color:#6b7280;font-size:13px;white-space:nowrap;vertical-align:top;">
|
||||
Nachricht
|
||||
</td>
|
||||
<td style="padding:14px 16px;font-weight:600;color:#111827;font-size:14px;vertical-align:top;white-space:pre-line;">
|
||||
${message || "N/A"}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td style="background:#f9fafb;padding:20px 30px;border-top:1px solid #e5e7eb;">
|
||||
<p style="margin:0;font-size:12px;color:#9ca3af;text-align:center;line-height:1.6;">
|
||||
Diese E-Mail wurde automatisch vom Ausleihsystem gesendet.<br>
|
||||
Bitte antworten Sie nicht auf diese Nachricht.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const text = `Neue Kontaktanfrage\n\nBenutzername: ${username}\nNachricht:\n${message}`;
|
||||
|
||||
return { html, text };
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
const formatDateTime = (value) => {
|
||||
if (value == null) return "N/A";
|
||||
const d = value instanceof Date ? value : new Date(value);
|
||||
if (isNaN(d.getTime())) return "N/A";
|
||||
return (
|
||||
d.toLocaleString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}) + " Uhr"
|
||||
);
|
||||
};
|
||||
|
||||
export function loanMail(user, items, startDate, endDate, createdDate, note) {
|
||||
const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9";
|
||||
|
||||
const itemsHtml =
|
||||
Array.isArray(items) && items.length
|
||||
? items
|
||||
.map(
|
||||
(i) =>
|
||||
`<span style="display:inline-block;background:#f0f9ff;color:#0369a1;padding:4px 12px;margin:2px 4px 2px 0;border-radius:20px;font-size:13px;font-weight:500;">${i}</span>`,
|
||||
)
|
||||
.join(" ")
|
||||
: '<span style="color:#9ca3af;">Keine Gegenstände</span>';
|
||||
|
||||
const row = (label, value, isLast = false) => `
|
||||
<tr>
|
||||
<td style="padding:14px 16px;color:#6b7280;font-size:13px;white-space:nowrap;vertical-align:top;${isLast ? "" : "border-bottom:1px solid #f3f4f6;"}">
|
||||
${label}
|
||||
</td>
|
||||
<td style="padding:14px 16px;font-weight:600;color:#111827;font-size:14px;vertical-align:top;${isLast ? "" : "border-bottom:1px solid #f3f4f6;"}">
|
||||
${value}
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
const html = `<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title>Neue Ausleihe</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#f2f4f7;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;color:#111827;-webkit-text-size-adjust:100%;">
|
||||
<div style="display:none;max-height:0;overflow:hidden;opacity:0;">Neue Ausleihe erstellt – Details zur Buchung.</div>
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#f2f4f7;">
|
||||
<tr>
|
||||
<td align="center" style="padding:32px 16px;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" width="600" style="max-width:600px;width:100%;border-radius:14px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.06);">
|
||||
|
||||
<!-- Header -->
|
||||
<tr>
|
||||
<td style="background:${brand};padding:32px 30px 28px;">
|
||||
<h1 style="margin:0;font-size:24px;color:#ffffff;font-weight:700;">Neue Ausleihe</h1>
|
||||
<p style="margin:8px 0 0;font-size:14px;color:rgba(255,255,255,0.85);">Es wurde soeben eine neue Ausleihe im System angelegt.</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Accent line -->
|
||||
<tr>
|
||||
<td style="background:#ffffff;padding:0;height:3px;line-height:3px;font-size:1px;background:${brand};"> </td>
|
||||
</tr>
|
||||
|
||||
<!-- Details -->
|
||||
<tr>
|
||||
<td style="background:#ffffff;padding:28px 30px 10px;">
|
||||
<p style="margin:0 0 14px;font-size:11px;font-weight:700;color:#9ca3af;letter-spacing:1.5px;text-transform:uppercase;">Details zur Ausleihe</p>
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#fafbfc;border:1px solid #f3f4f6;border-radius:10px;border-collapse:collapse;">
|
||||
${row("Benutzer", user || "N/A")}
|
||||
${row("Gegenstände", itemsHtml)}
|
||||
${row("Startdatum", formatDateTime(startDate))}
|
||||
${row("Enddatum", formatDateTime(endDate))}
|
||||
${row("Erstellt am", formatDateTime(createdDate))}
|
||||
${row("Notiz", note || '<span style="color:#9ca3af;">Keine Notiz</span>', true)}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Button -->
|
||||
<tr>
|
||||
<td style="background:#ffffff;padding:20px 30px 32px;" align="center">
|
||||
<a href="https://admin.insta.the1s.de/api" target="_blank" rel="noopener noreferrer"
|
||||
style="display:inline-block;background:${brand};color:#ffffff;text-decoration:none;padding:13px 28px;border-radius:8px;font-weight:600;font-size:15px;">
|
||||
Ausleihe ansehen →
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td style="background:#f9fafb;padding:20px 30px;border-top:1px solid #e5e7eb;">
|
||||
<p style="margin:0;font-size:12px;color:#9ca3af;text-align:center;line-height:1.6;">
|
||||
Diese E-Mail wurde automatisch vom Ausleihsystem gesendet.<br>
|
||||
Bitte antworten Sie nicht auf diese Nachricht.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const itemsText = Array.isArray(items) ? items.join(", ") : "N/A";
|
||||
const text = [
|
||||
"Neue Ausleihe erstellt",
|
||||
"-".repeat(30),
|
||||
`Benutzer: ${user || "N/A"}`,
|
||||
`Gegenstaende: ${itemsText}`,
|
||||
`Start: ${formatDateTime(startDate)}`,
|
||||
`Ende: ${formatDateTime(endDate)}`,
|
||||
`Erstellt: ${formatDateTime(createdDate)}`,
|
||||
`Notiz: ${note || "Keine Notiz"}`,
|
||||
"",
|
||||
"-> https://admin.insta.the1s.de/api",
|
||||
].join("\n");
|
||||
|
||||
return { html, text };
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import nodemailer from "nodemailer";
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
export const transporter = nodemailer.createTransport({
|
||||
host: process.env.MAIL_HOST,
|
||||
port: process.env.MAIL_PORT,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: process.env.MAIL_USER,
|
||||
pass: process.env.MAIL_PASSWORD,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
# 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.
|
||||
|
||||
## 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)
|
||||
+13
-4
@@ -4,14 +4,14 @@ services:
|
||||
# build: ./FrontendV2
|
||||
# ports:
|
||||
# - "8001:80"
|
||||
# restart: unless-stopped
|
||||
# restart: always
|
||||
|
||||
# admin-frontend:
|
||||
# container_name: borrow_system-admin-frontend
|
||||
# build: ./admin
|
||||
# ports:
|
||||
# - "8003:80"
|
||||
# restart: unless-stopped
|
||||
# restart: always
|
||||
|
||||
backend_v2:
|
||||
container_name: borrow_system-backend_v2
|
||||
@@ -26,12 +26,12 @@ services:
|
||||
DB_NAME: borrow_system_new
|
||||
depends_on:
|
||||
- mysql_v2
|
||||
restart: unless-stopped
|
||||
restart: always
|
||||
|
||||
mysql_v2:
|
||||
container_name: borrow_system-mysql-v2
|
||||
image: mysql:8.0
|
||||
restart: unless-stopped
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD_V2}
|
||||
MYSQL_DATABASE: borrow_system_new
|
||||
@@ -42,6 +42,15 @@ services:
|
||||
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:
|
||||
mysql-data:
|
||||
mysql-v2-data:
|
||||
|
||||
Submodule
+1
Submodule no-as-a-service added at 764062a307
Reference in New Issue
Block a user