From 07503ec079471cc946c96fe942bf6a827799b9e0 Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Sun, 19 Apr 2026 21:32:56 +0200 Subject: [PATCH 01/20] added animations to borrow-system --- FrontendV2/package-lock.json | 582 +++++++++++++----------- FrontendV2/package.json | 1 + FrontendV2/src/components/dotLottie.tsx | 28 ++ FrontendV2/src/pages/HomePage.tsx | 294 ++++++------ FrontendV2/src/pages/LoginPage.tsx | 146 +++--- 5 files changed, 604 insertions(+), 447 deletions(-) create mode 100644 FrontendV2/src/components/dotLottie.tsx diff --git a/FrontendV2/package-lock.json b/FrontendV2/package-lock.json index 90f17b0..8e2a7ef 100644 --- a/FrontendV2/package-lock.json +++ b/FrontendV2/package-lock.json @@ -1,15 +1,16 @@ { "name": "admin", - "version": "0.0.0", + "version": "v2.1.2 (dev)", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "admin", - "version": "0.0.0", + "version": "v2.1.2 (dev)", "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", @@ -547,9 +548,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", - "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "cpu": [ "ppc64" ], @@ -563,9 +564,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", - "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "cpu": [ "arm" ], @@ -579,9 +580,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", - "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "cpu": [ "arm64" ], @@ -595,9 +596,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", - "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "cpu": [ "x64" ], @@ -611,9 +612,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", - "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "cpu": [ "arm64" ], @@ -627,9 +628,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", - "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "cpu": [ "x64" ], @@ -643,9 +644,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", - "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "cpu": [ "arm64" ], @@ -659,9 +660,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", - "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "cpu": [ "x64" ], @@ -675,9 +676,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", - "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "cpu": [ "arm" ], @@ -691,9 +692,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", - "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "cpu": [ "arm64" ], @@ -707,9 +708,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", - "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "cpu": [ "ia32" ], @@ -723,9 +724,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", - "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "cpu": [ "loong64" ], @@ -739,9 +740,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", - "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "cpu": [ "mips64el" ], @@ -755,9 +756,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", - "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "cpu": [ "ppc64" ], @@ -771,9 +772,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", - "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "cpu": [ "riscv64" ], @@ -787,9 +788,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", - "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "cpu": [ "s390x" ], @@ -803,9 +804,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", - "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "cpu": [ "x64" ], @@ -819,9 +820,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", - "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", "cpu": [ "arm64" ], @@ -835,9 +836,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", - "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "cpu": [ "x64" ], @@ -851,9 +852,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", - "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "cpu": [ "arm64" ], @@ -867,9 +868,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", - "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "cpu": [ "x64" ], @@ -883,9 +884,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", - "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", "cpu": [ "arm64" ], @@ -899,9 +900,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", - "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "cpu": [ "x64" ], @@ -915,9 +916,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", - "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "cpu": [ "arm64" ], @@ -931,9 +932,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", - "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "cpu": [ "ia32" ], @@ -947,9 +948,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", - "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "cpu": [ "x64" ], @@ -1259,6 +1260,24 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lottiefiles/dotlottie-react": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.19.0.tgz", + "integrity": "sha512-SMhHwE68WiHdV7vVWHuCU3YD2tDAaNQmyGMmhSP7uXOaILoLRbdRRWpk9NsUxFPrwYuqKQ+3FNQw6bAJ19HCAg==", + "license": "MIT", + "dependencies": { + "@lottiefiles/dotlottie-web": "0.71.0" + }, + "peerDependencies": { + "react": "^17 || ^18 || ^19" + } + }, + "node_modules/@lottiefiles/dotlottie-web": { + "version": "0.71.0", + "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-web/-/dotlottie-web-0.71.0.tgz", + "integrity": "sha512-CliQttL0Rk0or/aDQkqUJXnC9Cm5TxzNdOqy/YlzKlU6mu+XgTMwyt5/HpPiltlMeaqBmM9mOzfevpyP4QmudQ==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1333,9 +1352,9 @@ } }, "node_modules/@rollup/pluginutils/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -1345,9 +1364,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", - "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", "cpu": [ "arm" ], @@ -1358,9 +1377,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", - "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", "cpu": [ "arm64" ], @@ -1371,9 +1390,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", - "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", "cpu": [ "arm64" ], @@ -1384,9 +1403,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", - "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", "cpu": [ "x64" ], @@ -1397,9 +1416,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", - "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", "cpu": [ "arm64" ], @@ -1410,9 +1429,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", - "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", "cpu": [ "x64" ], @@ -1423,9 +1442,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", - "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", "cpu": [ "arm" ], @@ -1436,9 +1455,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", - "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", "cpu": [ "arm" ], @@ -1449,9 +1468,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", - "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", "cpu": [ "arm64" ], @@ -1462,9 +1481,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", - "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", "cpu": [ "arm64" ], @@ -1475,9 +1494,22 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", - "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", "cpu": [ "loong64" ], @@ -1488,9 +1520,22 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", - "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", "cpu": [ "ppc64" ], @@ -1501,9 +1546,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", - "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", "cpu": [ "riscv64" ], @@ -1514,9 +1559,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", - "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", "cpu": [ "riscv64" ], @@ -1527,9 +1572,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", - "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", "cpu": [ "s390x" ], @@ -1540,9 +1585,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", - "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", "cpu": [ "x64" ], @@ -1553,9 +1598,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", - "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", "cpu": [ "x64" ], @@ -1565,10 +1610,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", - "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", "cpu": [ "arm64" ], @@ -1579,9 +1637,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", - "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", "cpu": [ "arm64" ], @@ -1592,9 +1650,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", - "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", "cpu": [ "ia32" ], @@ -1605,9 +1663,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", - "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", "cpu": [ "x64" ], @@ -1618,9 +1676,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", - "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", "cpu": [ "x64" ], @@ -2462,9 +2520,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/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==", "dev": true, "license": "MIT", "dependencies": { @@ -2472,13 +2530,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -3450,9 +3508,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -3520,9 +3578,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -3677,12 +3735,16 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "license": "MIT", "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/cosmiconfig": { @@ -3702,9 +3764,9 @@ } }, "node_modules/cosmiconfig/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "license": "ISC", "engines": { "node": ">= 6" @@ -3825,9 +3887,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", - "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -3837,32 +3899,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.11", - "@esbuild/android-arm": "0.25.11", - "@esbuild/android-arm64": "0.25.11", - "@esbuild/android-x64": "0.25.11", - "@esbuild/darwin-arm64": "0.25.11", - "@esbuild/darwin-x64": "0.25.11", - "@esbuild/freebsd-arm64": "0.25.11", - "@esbuild/freebsd-x64": "0.25.11", - "@esbuild/linux-arm": "0.25.11", - "@esbuild/linux-arm64": "0.25.11", - "@esbuild/linux-ia32": "0.25.11", - "@esbuild/linux-loong64": "0.25.11", - "@esbuild/linux-mips64el": "0.25.11", - "@esbuild/linux-ppc64": "0.25.11", - "@esbuild/linux-riscv64": "0.25.11", - "@esbuild/linux-s390x": "0.25.11", - "@esbuild/linux-x64": "0.25.11", - "@esbuild/netbsd-arm64": "0.25.11", - "@esbuild/netbsd-x64": "0.25.11", - "@esbuild/openbsd-arm64": "0.25.11", - "@esbuild/openbsd-x64": "0.25.11", - "@esbuild/openharmony-arm64": "0.25.11", - "@esbuild/sunos-x64": "0.25.11", - "@esbuild/win32-arm64": "0.25.11", - "@esbuild/win32-ia32": "0.25.11", - "@esbuild/win32-x64": "0.25.11" + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" } }, "node_modules/escalade": { @@ -4200,9 +4262,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -4506,9 +4568,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4943,9 +5005,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -5149,9 +5211,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -5359,9 +5421,9 @@ } }, "node_modules/react-router": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz", - "integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.1.tgz", + "integrity": "sha512-5BCvFskyAAVumqhEKh/iPhLOIkfxcEUz8WqFIARCkMg8hZZzDYX9CtwxXA0e+qT8zAxmMC0x3Ckb9iMONwc5jg==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -5381,12 +5443,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.9.4", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz", - "integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.1.tgz", + "integrity": "sha512-ZkrQuwwhGibjQLqH1eCdyiZyLWglPxzxdl5tgwgKEyCSGC76vmAjleGocRe3J/MLfzMUIKwaFJWpFVJhK3d2xA==", "license": "MIT", "dependencies": { - "react-router": "7.9.4" + "react-router": "7.14.1" }, "engines": { "node": ">=20.0.0" @@ -5466,9 +5528,9 @@ } }, "node_modules/rollup": { - "version": "4.52.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", - "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -5481,28 +5543,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.5", - "@rollup/rollup-android-arm64": "4.52.5", - "@rollup/rollup-darwin-arm64": "4.52.5", - "@rollup/rollup-darwin-x64": "4.52.5", - "@rollup/rollup-freebsd-arm64": "4.52.5", - "@rollup/rollup-freebsd-x64": "4.52.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", - "@rollup/rollup-linux-arm-musleabihf": "4.52.5", - "@rollup/rollup-linux-arm64-gnu": "4.52.5", - "@rollup/rollup-linux-arm64-musl": "4.52.5", - "@rollup/rollup-linux-loong64-gnu": "4.52.5", - "@rollup/rollup-linux-ppc64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-musl": "4.52.5", - "@rollup/rollup-linux-s390x-gnu": "4.52.5", - "@rollup/rollup-linux-x64-gnu": "4.52.5", - "@rollup/rollup-linux-x64-musl": "4.52.5", - "@rollup/rollup-openharmony-arm64": "4.52.5", - "@rollup/rollup-win32-arm64-msvc": "4.52.5", - "@rollup/rollup-win32-ia32-msvc": "4.52.5", - "@rollup/rollup-win32-x64-gnu": "4.52.5", - "@rollup/rollup-win32-x64-msvc": "4.52.5", + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", "fsevents": "~2.3.2" } }, @@ -5546,9 +5611,9 @@ } }, "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, "node_modules/shebang-command": { @@ -5736,9 +5801,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -5924,12 +5989,12 @@ } }, "node_modules/vite": { - "version": "7.1.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", - "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -6049,9 +6114,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" @@ -6102,9 +6167,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "license": "ISC", "optional": true, "peer": true, @@ -6113,6 +6178,9 @@ }, "engines": { "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yocto-queue": { diff --git a/FrontendV2/package.json b/FrontendV2/package.json index a3dd7d3..385b4b3 100644 --- a/FrontendV2/package.json +++ b/FrontendV2/package.json @@ -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", diff --git a/FrontendV2/src/components/dotLottie.tsx b/FrontendV2/src/components/dotLottie.tsx new file mode 100644 index 0000000..73ad5a5 --- /dev/null +++ b/FrontendV2/src/components/dotLottie.tsx @@ -0,0 +1,28 @@ +import { DotLottieReact } from "@lottiefiles/dotlottie-react"; + +export const unlockAnimation = () => { + return ( + + ); +}; + +export const approvalAnimation = () => { + return ( + + ); +}; + +export const logoutAnimation = () => { + return ( + + ); +}; diff --git a/FrontendV2/src/pages/HomePage.tsx b/FrontendV2/src/pages/HomePage.tsx index ba0d1ab..dd68add 100644 --- a/FrontendV2/src/pages/HomePage.tsx +++ b/FrontendV2/src/pages/HomePage.tsx @@ -18,6 +18,7 @@ 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"; export interface User { username: string; @@ -27,6 +28,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,155 +49,172 @@ 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 ( - -
- {isMsg && ( - + <> + {showAnimation && ( +
+
{approvalAnimation()}
+
)} - - {t("timezone-info")} - - setStartDate(e.target.value)} - /> - - setEndDate(e.target.value)} - /> - - {isLoadingA && ( - - - {t("loading")} - - )} - {borrowableItems.length > 0 && ( - - - - - - {t("item")} - - - - - {borrowableItems.map((item) => ( - - - handleCheckboxChange(item.id)} - type="checkbox" - name={item.id} - id={item.id} - /> - - {item.item_name} - - ))} - - - - {note.length} / {MAX_CHARACTERS} - - } - > - { - setNote( - e.currentTarget.value.slice(0, MAX_CHARACTERS) - ); - }} - /> - - - - - - - )} - {selectedItems.length >= 1 && ( - - )} - - + {isLoadingA && ( + + + {t("loading")} + + )} + {borrowableItems.length > 0 && ( + + + + + + {t("item")} + + + + + {borrowableItems.map((item) => ( + + + handleCheckboxChange(item.id)} + type="checkbox" + name={item.id} + id={item.id} + /> + + {item.item_name} + + ))} + + + + {note.length} / {MAX_CHARACTERS} + + } + > + { + setNote( + e.currentTarget.value.slice(0, MAX_CHARACTERS), + ); + }} + /> + + + + + + + )} + {selectedItems.length >= 1 && ( + + )} + + + ); }; diff --git a/FrontendV2/src/pages/LoginPage.tsx b/FrontendV2/src/pages/LoginPage.tsx index 3a386f6..58e2f59 100644 --- a/FrontendV2/src/pages/LoginPage.tsx +++ b/FrontendV2/src/pages/LoginPage.tsx @@ -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,58 +85,75 @@ export const LoginPage = () => { return; } setTriggerLogout(false); - navigate(from, { replace: true }); }; - if (isLoggedIn) { - return ; - } - return ( -
-
e.preventDefault()}> - - - {t("login")} - {t("enter-credentials")} - - - - - {t("username")} - setUsername(e.target.value)} + <> + {showAnimation && ( +
+
{unlockAnimation()}
+
+ )} + + {showLogout && ( +
+
{logoutAnimation()}
+
+ )} + +
+ e.preventDefault()}> + + + {t("login")} + {t("enter-credentials")} + + + + + {t("username")} + setUsername(e.target.value)} + /> + + + {t("password")} + setPassword(e.target.value)} + /> + + + + + {isError && ( + - - - {t("password")} - setPassword(e.target.value)} + )} + + + + {triggerLogout && ( + - - - - - {isError && ( - - )} - - - - {triggerLogout && ( - - )} - - - -
+ )} + +
+ +
+ ); }; From 5d0134017a79116487c99ec11a4c865cd877cf81 Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Sun, 19 Apr 2026 21:47:53 +0200 Subject: [PATCH 02/20] added changelog and edited version numbers --- backendV2/info.json | 4 ++-- changelog.md | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 changelog.md diff --git a/backendV2/info.json b/backendV2/info.json index 47923c7..fadf00a 100644 --- a/backendV2/info.json +++ b/backendV2/info.json @@ -1,9 +1,9 @@ { "backend-info": { - "version": "v2.1.1 (dev)" + "version": "v2.1.2 (dev)" }, "frontend-info": { - "version": "v2.1.2 (dev)" + "version": "v2.2 (dev)" }, "admin-panel-info": { "version": "v1.3.2 (dev)" diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..980bc7b --- /dev/null +++ b/changelog.md @@ -0,0 +1,24 @@ +# 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 +- The overview page now has the note column and is overall better organised +- I also addded the regular header to the page +- I have added three animations to the Borrow System + +## Improvements +- I updated the small web icons of the borrow system. I have made them with Icon Composer. +- I have also improved the error logging for the API route wehre you can take loans + +## Fixed bugs +- Fixed bug: #13 +- Fixed bug for messaging when server has an error +- Fixed footer height + +--- + +## Next version numbers +**Backend:** v2.1.2 +**Frontend:** v2.2 +**Admin panel:** v1.3.2 \ No newline at end of file From 3de877dd2bead5f6daca435ec1ff1f5ebe12315a Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Sun, 19 Apr 2026 22:04:49 +0200 Subject: [PATCH 03/20] added icon to header and updated changelog --- FrontendV2/src/components/Header.tsx | 8 ++++++++ changelog.md | 16 +++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/FrontendV2/src/components/Header.tsx b/FrontendV2/src/components/Header.tsx index dff897d..545a103 100644 --- a/FrontendV2/src/components/Header.tsx +++ b/FrontendV2/src/components/Header.tsx @@ -1,6 +1,7 @@ import { Button, Flex, + Image, Heading, Stack, Text, @@ -190,6 +191,13 @@ export const Header = () => { {/* Titelzeile ohne Mobile-Menu (wurde nach oben verlegt) */} + borrow-system logo Date: Tue, 21 Apr 2026 20:48:57 +0200 Subject: [PATCH 04/20] added no-as-a-service project as a submodule --- .gitmodules | 3 +++ no-as-a-service | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 no-as-a-service diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..11c2a4d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "no-as-a-service"] + path = no-as-a-service + url = https://github.com/hotheadhacker/no-as-a-service.git diff --git a/no-as-a-service b/no-as-a-service new file mode 160000 index 0000000..764062a --- /dev/null +++ b/no-as-a-service @@ -0,0 +1 @@ +Subproject commit 764062a307c725cb55f56308ee842f5e42529dd1 From 25709ea0d9193652dee0dcdc0b27b5ebd6c762b4 Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Tue, 21 Apr 2026 21:49:54 +0200 Subject: [PATCH 05/20] implemented naas in to user frontend --- FrontendV2/src/components/UserDialogue.tsx | 83 ++++++++++++++++++-- FrontendV2/src/utils/i18n/locales/de/de.json | 7 +- FrontendV2/src/utils/i18n/locales/en/en.json | 7 +- backendV2/server.js | 21 ++++- docker-compose.yml | 17 +++- 5 files changed, 122 insertions(+), 13 deletions(-) diff --git a/FrontendV2/src/components/UserDialogue.tsx b/FrontendV2/src/components/UserDialogue.tsx index d4e88b6..90b5aa3 100644 --- a/FrontendV2/src/components/UserDialogue.tsx +++ b/FrontendV2/src/components/UserDialogue.tsx @@ -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) => { - - + + + {isMsgNAAS && ( + + )} + + + + + - {/* Passwort-Dialog (kontrolliert) */} + {/* Passwort-Dialog */} setPwOpen(e.open)}> @@ -215,6 +263,31 @@ export const UserDialogue = (props: UserDialogueProps) => { + + + + + + + + + {t("naas-header")} + + +

{naas}

+
+ + setNaasDialog(false)} size="sm" /> + +
+
+
+
+
); }; diff --git a/FrontendV2/src/utils/i18n/locales/de/de.json b/FrontendV2/src/utils/i18n/locales/de/de.json index f6ed2dc..b02e852 100644 --- a/FrontendV2/src/utils/i18n/locales/de/de.json +++ b/FrontendV2/src/utils/i18n/locales/de/de.json @@ -88,5 +88,10 @@ "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..." } \ No newline at end of file diff --git a/FrontendV2/src/utils/i18n/locales/en/en.json b/FrontendV2/src/utils/i18n/locales/en/en.json index 6a52916..d093b1c 100644 --- a/FrontendV2/src/utils/i18n/locales/en/en.json +++ b/FrontendV2/src/utils/i18n/locales/en/en.json @@ -88,5 +88,10 @@ "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..." } \ No newline at end of file diff --git a/backendV2/server.js b/backendV2/server.js index 3b86597..a8d2006 100644 --- a/backendV2/server.js +++ b/backendV2/server.js @@ -1,6 +1,6 @@ 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"; @@ -18,9 +18,12 @@ import userMgmtRouterADMIN from "./routes/admin/userMgmt.route.js"; // API routes import apiRouter from "./routes/api/api.route.js"; -env.config(); +dotenv.config(); const app = express(); const port = 8004; +const naasURL = process.env.NAAS_URL; + +console.log(naasURL); app.use(cors()); // Body-Parser VOR den Routen registrieren @@ -47,6 +50,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 }); }); diff --git a/docker-compose.yml b/docker-compose.yml index 90ab08c..c0b95ef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: From d2dc74971f12cbd0bcb4951ad3b9af36a4cd6c54 Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Tue, 21 Apr 2026 21:53:16 +0200 Subject: [PATCH 06/20] edited changelog --- changelog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 83ac9a6..e934f26 100644 --- a/changelog.md +++ b/changelog.md @@ -8,6 +8,7 @@ This update provides some new features for the design. It also contains some imp - I also addded the regular header to the page - I have added three animations to the Borrow System - 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...) +- 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 @@ -21,7 +22,7 @@ This update provides some new features for the design. It also contains some imp --- -## Next version numbers +## New version numbers **Backend:** v2.1.2 From 56e073244f66f72b0c3a2e3cfd8b3111e5c7663e Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Tue, 21 Apr 2026 21:54:09 +0200 Subject: [PATCH 07/20] edited version numbers --- backendV2/info.json | 2 +- changelog.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backendV2/info.json b/backendV2/info.json index fadf00a..134da02 100644 --- a/backendV2/info.json +++ b/backendV2/info.json @@ -1,6 +1,6 @@ { "backend-info": { - "version": "v2.1.2 (dev)" + "version": "v2.2 (dev)" }, "frontend-info": { "version": "v2.2 (dev)" diff --git a/changelog.md b/changelog.md index e934f26..54c0245 100644 --- a/changelog.md +++ b/changelog.md @@ -24,7 +24,7 @@ This update provides some new features for the design. It also contains some imp ## New version numbers -**Backend:** v2.1.2 +**Backend:** v2.2 **Frontend:** v2.2 From 5442f2f1f39b33a809ba6bca0aa08adb9445de2c Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Fri, 24 Apr 2026 18:44:07 +0200 Subject: [PATCH 08/20] new feature: Error code 507 will return if you want to delete a loan that has not been returned --- FrontendV2/src/pages/MyLoansPage.tsx | 8 ++++++++ FrontendV2/src/utils/i18n/locales/de/de.json | 3 ++- FrontendV2/src/utils/i18n/locales/en/en.json | 3 ++- .../routes/app/database/loansMgmt.database.js | 17 +++++++++++++++++ backendV2/routes/app/loanMgmt.route.js | 10 ++++++++++ backendV2/server.js | 2 -- changelog.md | 1 + 7 files changed, 40 insertions(+), 4 deletions(-) diff --git a/FrontendV2/src/pages/MyLoansPage.tsx b/FrontendV2/src/pages/MyLoansPage.tsx index 8fa5904..ef040de 100644 --- a/FrontendV2/src/pages/MyLoansPage.tsx +++ b/FrontendV2/src/pages/MyLoansPage.tsx @@ -84,6 +84,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")); diff --git a/FrontendV2/src/utils/i18n/locales/de/de.json b/FrontendV2/src/utils/i18n/locales/de/de.json index b02e852..353b16d 100644 --- a/FrontendV2/src/utils/i18n/locales/de/de.json +++ b/FrontendV2/src/utils/i18n/locales/de/de.json @@ -93,5 +93,6 @@ "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..." + "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." } \ No newline at end of file diff --git a/FrontendV2/src/utils/i18n/locales/en/en.json b/FrontendV2/src/utils/i18n/locales/en/en.json index d093b1c..8dc1059 100644 --- a/FrontendV2/src/utils/i18n/locales/en/en.json +++ b/FrontendV2/src/utils/i18n/locales/en/en.json @@ -93,5 +93,6 @@ "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..." + "naas-header": "A good way to say no...", + "error-deleting-loan-507": "The loan cannot be deleted because it has not been returned yet." } \ No newline at end of file diff --git a/backendV2/routes/app/database/loansMgmt.database.js b/backendV2/routes/app/database/loansMgmt.database.js index 3fdc67e..aa9a261 100644 --- a/backendV2/routes/app/database/loansMgmt.database.js +++ b/backendV2/routes/app/database/loansMgmt.database.js @@ -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], diff --git a/backendV2/routes/app/loanMgmt.route.js b/backendV2/routes/app/loanMgmt.route.js index 7e0c0ce..2a57f60 100644 --- a/backendV2/routes/app/loanMgmt.route.js +++ b/backendV2/routes/app/loanMgmt.route.js @@ -134,6 +134,16 @@ router.delete("/delete-loan/:id", authenticate, async (req, res) => { 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" }); } }); diff --git a/backendV2/server.js b/backendV2/server.js index a8d2006..61855d8 100644 --- a/backendV2/server.js +++ b/backendV2/server.js @@ -23,8 +23,6 @@ const app = express(); const port = 8004; const naasURL = process.env.NAAS_URL; -console.log(naasURL); - app.use(cors()); // Body-Parser VOR den Routen registrieren app.use(express.json({ limit: "10mb" })); diff --git a/changelog.md b/changelog.md index 54c0245..878a0db 100644 --- a/changelog.md +++ b/changelog.md @@ -13,6 +13,7 @@ This update provides some new features for the design. It also contains some imp ## Improvements - I have the error logging for the API route wehre you can take loans improved. +- If you try to delete a loan that has not been returned yet, you will get an 507 error code. ## Fixed bugs From 0964109c4bce40416cc9f4b153a2405ec24e1c9b Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Sun, 26 Apr 2026 14:40:53 +0200 Subject: [PATCH 09/20] added new feature: service config; Currently implemented in: loanMgmt and userMgmt (only Backend) Co-authored-by: Copilot --- backendV2/routes/app/loanMgmt.route.js | 314 ++++++++++++++----------- backendV2/routes/app/userMgmt.route.js | 79 ++++--- backendV2/schemeV2.sql | 13 +- backendV2/services/functions.js | 42 ++++ 4 files changed, 279 insertions(+), 169 deletions(-) create mode 100644 backendV2/services/functions.js diff --git a/backendV2/routes/app/loanMgmt.route.js b/backendV2/routes/app/loanMgmt.route.js index 2a57f60..eb4f203 100644 --- a/backendV2/routes/app/loanMgmt.route.js +++ b/backendV2/routes/app/loanMgmt.route.js @@ -1,5 +1,9 @@ import express from "express"; import { authenticate, generateToken } from "../../services/authentication.js"; +import { + checkIfServiceIsActive, + checkIfServiceIsActive2, +} from "../../services/functions.js"; const router = express.Router(); import dotenv from "dotenv"; dotenv.config(); @@ -18,106 +22,129 @@ import { } from "./database/loansMgmt.database.js"; import { sendMailLoan } from "./services/mailer.js"; -router.post("/createLoan", authenticate, async (req, res) => { - try { - const { items, startDate, endDate, note } = req.body || {}; +router.post( + "/createLoan", + checkIfServiceIsActive("Loan Service"), + authenticate, + async (req, res) => { + try { + const { items, startDate, endDate, note } = req.body || {}; - if (!Array.isArray(items) || items.length === 0) { - return res.status(400).json({ message: "Items array is required" }); - } + if (!Array.isArray(items) || items.length === 0) { + return res.status(400).json({ message: "Items array is required" }); + } - // If dates are not provided, default to now .. +7 days - const start = - startDate ?? new Date().toISOString().slice(0, 19).replace("T", " "); - const end = - endDate ?? - new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) - .toISOString() - .slice(0, 19) - .replace("T", " "); + // If dates are not provided, default to now .. +7 days + const start = + startDate ?? new Date().toISOString().slice(0, 19).replace("T", " "); + const end = + endDate ?? + new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) + .toISOString() + .slice(0, 19) + .replace("T", " "); - // Coerce item IDs to numbers and filter invalids - const itemIds = items - .map((v) => Number(v)) - .filter((n) => Number.isFinite(n)); + // Coerce item IDs to numbers and filter invalids + const itemIds = items + .map((v) => Number(v)) + .filter((n) => Number.isFinite(n)); - if (itemIds.length === 0) { - return res.status(400).json({ message: "No valid item IDs provided" }); - } + if (itemIds.length === 0) { + return res.status(400).json({ message: "No valid item IDs provided" }); + } - const result = await createLoanInDatabase( - req.user.username, - start, - end, - note, - itemIds, - ); - - if (result.success) { - const mailInfo = await getLoanInfoWithID(result.data.id); - console.log(mailInfo); - sendMailLoan( - mailInfo.data.username, - mailInfo.data.loaned_items_name, - mailInfo.data.start_date, - mailInfo.data.end_date, - mailInfo.data.created_at, - mailInfo.data.note, + const result = await createLoanInDatabase( + req.user.username, + start, + end, + note, + itemIds, ); - return res.status(201).json({ - message: "Loan created successfully", - loanId: result.data.id, - loanCode: result.data.loan_code, - }); + + if (result.success) { + if (await checkIfServiceIsActive2("Loan Mailer")) { + const mailInfo = await getLoanInfoWithID(result.data.id); + console.log(mailInfo); + sendMailLoan( + mailInfo.data.username, + mailInfo.data.loaned_items_name, + mailInfo.data.start_date, + mailInfo.data.end_date, + mailInfo.data.created_at, + mailInfo.data.note, + ); + } + + return res.status(201).json({ + message: "Loan created successfully", + loanId: result.data.id, + loanCode: result.data.loan_code, + }); + } + + if (result.code === "CONFLICT") { + return res + .status(409) + .json({ message: "Items not available in the selected period" }); + } + + if (result.code === "BAD_REQUEST") { + return res.status(400).json({ message: result.message }); + } + + return res.status(500).json({ message: "Failed to create loan" }); + } catch (err) { + console.error("createLoan error:", err); + return res.status(500).json({ message: "Failed to create loan" }); } + }, +); - if (result.code === "CONFLICT") { - return res - .status(409) - .json({ message: "Items not available in the selected period" }); +router.get( + "/loans", + checkIfServiceIsActive("Loan Service"), + authenticate, + async (req, res) => { + const result = await getLoansFromDatabase(req.user.username); + if (result.success) { + res.status(200).json(result.data); + } else if (result.status) { + res.status(200).json([]); + } else { + res.status(500).json({ message: "Failed to fetch loans" }); } + }, +); - if (result.code === "BAD_REQUEST") { - return res.status(400).json({ message: result.message }); +router.post( + "/set-return-date/:loan_code", + checkIfServiceIsActive("Loan Service"), + authenticate, + async (req, res) => { + const loanCode = req.params.loan_code; + const result = await setReturnDate(loanCode); + if (result.success) { + res.status(200).json({ data: result.data }); + } else { + res.status(500).json({ message: "Failed to set return date" }); } + }, +); - return res.status(500).json({ message: "Failed to create loan" }); - } catch (err) { - console.error("createLoan error:", err); - return res.status(500).json({ message: "Failed to create loan" }); - } -}); - -router.get("/loans", authenticate, async (req, res) => { - const result = await getLoansFromDatabase(req.user.username); - if (result.success) { - res.status(200).json(result.data); - } else if (result.status) { - res.status(200).json([]); - } else { - res.status(500).json({ message: "Failed to fetch loans" }); - } -}); - -router.post("/set-return-date/:loan_code", authenticate, async (req, res) => { - const loanCode = req.params.loan_code; - const result = await setReturnDate(loanCode); - if (result.success) { - res.status(200).json({ data: result.data }); - } else { - res.status(500).json({ message: "Failed to set return date" }); - } -}); - -router.post("/set-take-date/:loan_code", authenticate, async (req, res) => { - const loanCode = req.params.loan_code; - const result = await setTakeDate(loanCode); - if (result.success) { - res.status(200).json({ data: result.data }); - } else { - res.status(500).json({ message: "Failed to set take date" }); - } -}); +router.post( + "/set-take-date/:loan_code", + checkIfServiceIsActive("Loan Service"), + authenticate, + async (req, res) => { + const loanCode = req.params.loan_code; + const result = await setTakeDate(loanCode); + if (result.success) { + res.status(200).json({ data: result.data }); + } else { + res.status(500).json({ message: "Failed to set take date" }); + } + }, +); router.get("/all-items", authenticate, async (req, res) => { const result = await getItems(); @@ -128,56 +155,71 @@ router.get("/all-items", authenticate, async (req, res) => { } }); -router.delete("/delete-loan/:id", 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" }); +router.delete( + "/delete-loan/:id", + checkIfServiceIsActive("Loan Service"), + authenticate, + async (req, res) => { + const loanId = req.params.id; + const result = await SETdeleteLoanFromDatabase(loanId); + if (result.success) { + res.status(200).json({ message: "Loan deleted successfully" }); + } else { + if (result.code === "LOAN_NOT_FOUND") { + res.status(404).json({ message: "Loan not found" }); + } + + if (result.code === "LOAN_NOT_RETURNED") { + res.status(507).json({ + message: "Cannot delete loan that has not been returned", + }); + } + + res.status(500).json({ message: "Failed to delete loan" }); + } + }, +); + +router.get( + "/all-loans", + checkIfServiceIsActive("Loan Service"), + authenticate, + async (req, res) => { + const result = await getALLLoans(); + if (result.success) { + res.status(200).json(result.data); + } else { + res.status(500).json({ message: "Failed to fetch loans" }); + } + }, +); + +router.post( + "/borrowable-items", + checkIfServiceIsActive("Loan Service"), + authenticate, + async (req, res) => { + const { startDate, endDate } = req.body || {}; + if (!startDate || !endDate) { + return res + .status(400) + .json({ message: "startDate and endDate are required" }); } - if (result.code === "LOAN_NOT_RETURNED") { - res.status(507).json({ - message: "Cannot delete loan that has not been returned", - }); + const result = await getBorrowableItemsFromDatabase( + startDate, + endDate, + req.user.role, + ); + if (result.success) { + // return the array directly for consistency with /items + return res.status(200).json(result.data); + } else { + return res + .status(500) + .json({ message: "Failed to fetch borrowable items" }); } - - res.status(500).json({ message: "Failed to delete loan" }); - } -}); - -router.get("/all-loans", 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) => { - const { startDate, endDate } = req.body || {}; - if (!startDate || !endDate) { - return res - .status(400) - .json({ message: "startDate and endDate are required" }); - } - - const result = await getBorrowableItemsFromDatabase( - startDate, - endDate, - req.user.role, - ); - if (result.success) { - // return the array directly for consistency with /items - return res.status(200).json(result.data); - } else { - return res - .status(500) - .json({ message: "Failed to fetch borrowable items" }); - } -}); + }, +); export default router; diff --git a/backendV2/routes/app/userMgmt.route.js b/backendV2/routes/app/userMgmt.route.js index 0f63862..d91b6b3 100644 --- a/backendV2/routes/app/userMgmt.route.js +++ b/backendV2/routes/app/userMgmt.route.js @@ -1,5 +1,6 @@ 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(); @@ -8,41 +9,55 @@ dotenv.config(); import { loginFunc, changePassword } from "./database/userMgmt.database.js"; import { sendMail } from "./services/mailer_v2.js"; -router.post("/login", async (req, res) => { - const result = await loginFunc(req.body.username, req.body.password); - if (result.success) { - const token = await generateToken({ - username: result.data.username, - is_admin: result.data.is_admin, - first_name: result.data.first_name, - last_name: result.data.last_name, - role: result.data.role, - }); - res.status(200).json({ message: "Login successful", token }); - } else { - res.status(401).json({ message: "Invalid credentials" }); - } -}); +router.post( + "/login", + checkIfServiceIsActive("User Frontend"), + async (req, res) => { + const result = await loginFunc(req.body.username, req.body.password); + if (result.success) { + const token = await generateToken({ + username: result.data.username, + is_admin: result.data.is_admin, + first_name: result.data.first_name, + last_name: result.data.last_name, + role: result.data.role, + }); + res.status(200).json({ message: "Login successful", token }); + } else { + res.status(401).json({ message: "Invalid credentials" }); + } + }, +); -router.post("/change-password", authenticate, async (req, res) => { - const oldPassword = req.body.oldPassword; - const newPassword = req.body.newPassword; - const username = req.user.username; - const result = await changePassword(username, oldPassword, newPassword); - if (result.success) { - res.status(200).json({ message: "Password changed successfully" }); - } else { - res.status(500).json({ message: "Failed to change password" }); - } -}); +router.post( + "/change-password", + checkIfServiceIsActive("User Frontend"), + authenticate, + async (req, res) => { + const oldPassword = req.body.oldPassword; + const newPassword = req.body.newPassword; + const username = req.user.username; + const result = await changePassword(username, oldPassword, newPassword); + if (result.success) { + res.status(200).json({ message: "Password changed successfully" }); + } else { + res.status(500).json({ message: "Failed to change password" }); + } + }, +); -router.post("/contact", 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) => { + const message = req.body.message; + const username = req.user.username; - sendMail(username, message); + sendMail(username, message); - res.status(200).json({ message: "Contact message sent successfully" }); -}); + res.status(200).json({ message: "Contact message sent successfully" }); + }, +); export default router; diff --git a/backendV2/schemeV2.sql b/backendV2/schemeV2.sql index 95ff6ed..699e404 100644 --- a/backendV2/schemeV2.sql +++ b/backendV2/schemeV2.sql @@ -54,4 +54,15 @@ CREATE TABLE apiKeys ( entry_created_at TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), CHECK (api_key REGEXP '^[0-9]{8}$') -) ENGINE=InnoDB; \ No newline at end of file +) 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") \ No newline at end of file diff --git a/backendV2/services/functions.js b/backendV2/services/functions.js new file mode 100644 index 0000000..eea290c --- /dev/null +++ b/backendV2/services/functions.js @@ -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; +} From 747932cf03bd9980e300663de8b45d648ea4438a Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Sun, 26 Apr 2026 14:57:59 +0200 Subject: [PATCH 10/20] implemented service configuration to API service Co-authored-by: Copilot --- backendV2/routes/api/api.route.js | 4 ++++ backendV2/services/authentication.js | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/backendV2/routes/api/api.route.js b/backendV2/routes/api/api.route.js index fc638d8..53bd852 100644 --- a/backendV2/routes/api/api.route.js +++ b/backendV2/routes/api/api.route.js @@ -1,5 +1,6 @@ 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(); @@ -39,6 +40,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 +56,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 +72,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); diff --git a/backendV2/services/authentication.js b/backendV2/services/authentication.js index 4fadd38..feb8ed8 100644 --- a/backendV2/services/authentication.js +++ b/backendV2/services/authentication.js @@ -1,6 +1,7 @@ import { SignJWT, jwtVerify } from "jose"; import env from "dotenv"; import { verifyAPIKeyDB } from "./database.js"; +import { checkIfServiceIsActive2 } from "./functions.js"; env.config(); const secretKey = process.env.SECRET_KEY; @@ -45,6 +46,13 @@ export async function authenticate(req, res, next) { const apiKey = req.params.key; if (authHeader) { + const serviceActive = await checkIfServiceIsActive2("User Frontend"); + 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 +69,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"); + if (!serviceActive) { + return res + .status(503) + .json({ message: "API Service is currently unavailable." }); + } + try { await verifyAPIKey(apiKey); return next(); From 60c85efd37c267fb191f696ecc41b91d1e36d57d Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Sun, 26 Apr 2026 15:03:37 +0200 Subject: [PATCH 11/20] refactored backend Co-authored-by: Copilot --- backendV2/routes/api/api.route.js | 8 +++++--- backendV2/routes/app/loanMgmt.route.js | 19 +++++++++++-------- backendV2/routes/app/userMgmt.route.js | 9 ++++++--- backendV2/services/authentication.js | 7 +++++-- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/backendV2/routes/api/api.route.js b/backendV2/routes/api/api.route.js index 53bd852..a79ee3e 100644 --- a/backendV2/routes/api/api.route.js +++ b/backendV2/routes/api/api.route.js @@ -5,6 +5,8 @@ const router = express.Router(); import dotenv from "dotenv"; dotenv.config(); +const loan_service = loan_service; + import { getItemsFromDatabaseV2, changeInSafeStateV2, @@ -40,7 +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"), + checkIfServiceIsActive(loan_service), async (req, res) => { const loan_code = req.params.loan_code; const result = await getLoanByCodeV2(loan_code); @@ -56,7 +58,7 @@ router.get( router.post( "/set-return-date/:key/:loan_code", authenticate, - checkIfServiceIsActive("Loan Service"), + checkIfServiceIsActive(loan_service), async (req, res) => { const loanCode = req.params.loan_code; const result = await setReturnDateV2(loanCode); @@ -72,7 +74,7 @@ router.post( router.post( "/set-take-date/:key/:loan_code", authenticate, - checkIfServiceIsActive("Loan Service"), + checkIfServiceIsActive(loan_service), async (req, res) => { const loanCode = req.params.loan_code; const result = await setTakeDateV2(loanCode); diff --git a/backendV2/routes/app/loanMgmt.route.js b/backendV2/routes/app/loanMgmt.route.js index eb4f203..ad1126a 100644 --- a/backendV2/routes/app/loanMgmt.route.js +++ b/backendV2/routes/app/loanMgmt.route.js @@ -8,6 +8,9 @@ const router = express.Router(); import dotenv from "dotenv"; dotenv.config(); +const loan_service = "Loan Service"; +const loan_mailer_service = "Loan Mailer"; + // database funcs import import { createLoanInDatabase, @@ -24,7 +27,7 @@ import { sendMailLoan } from "./services/mailer.js"; router.post( "/createLoan", - checkIfServiceIsActive("Loan Service"), + checkIfServiceIsActive(loan_service), authenticate, async (req, res) => { try { @@ -62,7 +65,7 @@ router.post( ); if (result.success) { - if (await checkIfServiceIsActive2("Loan Mailer")) { + if (await checkIfServiceIsActive2(loan_mailer_service)) { const mailInfo = await getLoanInfoWithID(result.data.id); console.log(mailInfo); sendMailLoan( @@ -102,7 +105,7 @@ router.post( router.get( "/loans", - checkIfServiceIsActive("Loan Service"), + checkIfServiceIsActive(loan_service), authenticate, async (req, res) => { const result = await getLoansFromDatabase(req.user.username); @@ -118,7 +121,7 @@ router.get( router.post( "/set-return-date/:loan_code", - checkIfServiceIsActive("Loan Service"), + checkIfServiceIsActive(loan_service), authenticate, async (req, res) => { const loanCode = req.params.loan_code; @@ -133,7 +136,7 @@ router.post( router.post( "/set-take-date/:loan_code", - checkIfServiceIsActive("Loan Service"), + checkIfServiceIsActive(loan_service), authenticate, async (req, res) => { const loanCode = req.params.loan_code; @@ -157,7 +160,7 @@ router.get("/all-items", authenticate, async (req, res) => { router.delete( "/delete-loan/:id", - checkIfServiceIsActive("Loan Service"), + checkIfServiceIsActive(loan_service), authenticate, async (req, res) => { const loanId = req.params.id; @@ -182,7 +185,7 @@ router.delete( router.get( "/all-loans", - checkIfServiceIsActive("Loan Service"), + checkIfServiceIsActive(loan_service), authenticate, async (req, res) => { const result = await getALLLoans(); @@ -196,7 +199,7 @@ router.get( router.post( "/borrowable-items", - checkIfServiceIsActive("Loan Service"), + checkIfServiceIsActive(loan_service), authenticate, async (req, res) => { const { startDate, endDate } = req.body || {}; diff --git a/backendV2/routes/app/userMgmt.route.js b/backendV2/routes/app/userMgmt.route.js index d91b6b3..654030b 100644 --- a/backendV2/routes/app/userMgmt.route.js +++ b/backendV2/routes/app/userMgmt.route.js @@ -5,13 +5,16 @@ const router = express.Router(); import dotenv from "dotenv"; dotenv.config(); +const user_frontend_service = "User Frontend"; +const contact_form_service = "Contact Form Service"; + // database funcs import import { loginFunc, changePassword } from "./database/userMgmt.database.js"; import { sendMail } from "./services/mailer_v2.js"; router.post( "/login", - checkIfServiceIsActive("User Frontend"), + checkIfServiceIsActive(user_frontend_service), async (req, res) => { const result = await loginFunc(req.body.username, req.body.password); if (result.success) { @@ -31,7 +34,7 @@ router.post( router.post( "/change-password", - checkIfServiceIsActive("User Frontend"), + checkIfServiceIsActive(user_frontend_service), authenticate, async (req, res) => { const oldPassword = req.body.oldPassword; @@ -48,7 +51,7 @@ router.post( router.post( "/contact", - checkIfServiceIsActive("Contact Form Service"), + checkIfServiceIsActive(contact_form_service), authenticate, async (req, res) => { const message = req.body.message; diff --git a/backendV2/services/authentication.js b/backendV2/services/authentication.js index feb8ed8..b0de555 100644 --- a/backendV2/services/authentication.js +++ b/backendV2/services/authentication.js @@ -4,6 +4,9 @@ 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"); @@ -46,7 +49,7 @@ export async function authenticate(req, res, next) { const apiKey = req.params.key; if (authHeader) { - const serviceActive = await checkIfServiceIsActive2("User Frontend"); + const serviceActive = await checkIfServiceIsActive2(user_frontend_service); if (!serviceActive) { return res .status(503) @@ -69,7 +72,7 @@ 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"); + const serviceActive = await checkIfServiceIsActive2(api_service); if (!serviceActive) { return res .status(503) From 40d784ab36d6d48aa62bfd243e7cb30d9a4169a3 Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Sun, 26 Apr 2026 15:59:57 +0200 Subject: [PATCH 12/20] implemented service configuration to admin panel Co-authored-by: Copilot --- admin/src/Layout/Dashboard.tsx | 3 + admin/src/Layout/Sidebar.tsx | 11 ++ admin/src/components/ServerConfig.tsx | 175 ++++++++++++++++++ .../admin/database/serverConfMgmt.database.js | 26 +++ .../routes/admin/serverConfMgmt.route.js | 50 +++++ backendV2/routes/api/api.route.js | 2 +- backendV2/server.js | 2 + 7 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 admin/src/components/ServerConfig.tsx create mode 100644 backendV2/routes/admin/database/serverConfMgmt.database.js create mode 100644 backendV2/routes/admin/serverConfMgmt.route.js diff --git a/admin/src/Layout/Dashboard.tsx b/admin/src/Layout/Dashboard.tsx index fd37bb6..b0f830d 100644 --- a/admin/src/Layout/Dashboard.tsx +++ b/admin/src/Layout/Dashboard.tsx @@ -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 = ({ onLogout }) => { viewSchliessfaecher={() => setActiveView("Schließfächer")} viewUser={() => setActiveView("User")} viewAPI={() => setActiveView("API")} + viewConfig={() => setActiveView("Server Konfiguration")} /> = ({ onLogout }) => { {activeView === "Ausleihen" && } {activeView === "Gegenstände" && } {activeView === "API" && } + {activeView === "Server Konfiguration" && } diff --git a/admin/src/Layout/Sidebar.tsx b/admin/src/Layout/Sidebar.tsx index 029ba27..05ee64f 100644 --- a/admin/src/Layout/Sidebar.tsx +++ b/admin/src/Layout/Sidebar.tsx @@ -9,6 +9,7 @@ type SidebarProps = { viewSchliessfaecher: () => void; viewUser: () => void; viewAPI: () => void; + viewConfig: () => void; }; const Sidebar: React.FC = ({ @@ -16,6 +17,7 @@ const Sidebar: React.FC = ({ viewGegenstaende, viewUser, viewAPI, + viewConfig }) => { const [info, setInfo] = useState(null); @@ -83,6 +85,15 @@ const Sidebar: React.FC = ({ > API Keys + + Server Konfiguration + diff --git a/admin/src/components/ServerConfig.tsx b/admin/src/components/ServerConfig.tsx new file mode 100644 index 0000000..d4d8b67 --- /dev/null +++ b/admin/src/components/ServerConfig.tsx @@ -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([]); + 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 ( + <> + + Server Konfiguration + + {isError && ( + + )} + {isLoading && ( + + + Loading... + + )} + + + + + + # + + + Service Name + + + Toggle + + + Eintrag erstellt am + + + + + {items.map((item) => ( + + {item.id} + {item.function_name} + + + handleSwitchChange(item.id, !item.active) + } + > + + + + + + + + + {formatDateTime(item.entry_created_at)} + + + ))} + + + + ); +}; + +export default ServerConfig; diff --git a/backendV2/routes/admin/database/serverConfMgmt.database.js b/backendV2/routes/admin/database/serverConfMgmt.database.js new file mode 100644 index 0000000..33185a5 --- /dev/null +++ b/backendV2/routes/admin/database/serverConfMgmt.database.js @@ -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 }; +}; diff --git a/backendV2/routes/admin/serverConfMgmt.route.js b/backendV2/routes/admin/serverConfMgmt.route.js new file mode 100644 index 0000000..3def349 --- /dev/null +++ b/backendV2/routes/admin/serverConfMgmt.route.js @@ -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; diff --git a/backendV2/routes/api/api.route.js b/backendV2/routes/api/api.route.js index a79ee3e..17b6edc 100644 --- a/backendV2/routes/api/api.route.js +++ b/backendV2/routes/api/api.route.js @@ -5,7 +5,7 @@ const router = express.Router(); import dotenv from "dotenv"; dotenv.config(); -const loan_service = loan_service; +const loan_service = "Loan Service"; import { getItemsFromDatabaseV2, diff --git a/backendV2/server.js b/backendV2/server.js index 61855d8..c268042 100644 --- a/backendV2/server.js +++ b/backendV2/server.js @@ -14,6 +14,7 @@ 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"; @@ -38,6 +39,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); From d2c36e71be414226a053c09fcbb696a31c2069e3 Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Sun, 26 Apr 2026 16:10:34 +0200 Subject: [PATCH 13/20] added request limiter to backend --- backendV2/package-lock.json | 32 ++++++++++++++++++++++++++++++-- backendV2/package.json | 3 ++- backendV2/server.js | 22 +++++++++++++++++----- 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/backendV2/package-lock.json b/backendV2/package-lock.json index 0b39461..aeb4f7d 100644 --- a/backendV2/package-lock.json +++ b/backendV2/package-lock.json @@ -1,18 +1,19 @@ { "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" @@ -349,6 +350,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", @@ -527,6 +546,15 @@ "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", diff --git a/backendV2/package.json b/backendV2/package.json index 9c9a53e..c16054e 100644 --- a/backendV2/package.json +++ b/backendV2/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/backendV2/server.js b/backendV2/server.js index c268042..c255551 100644 --- a/backendV2/server.js +++ b/backendV2/server.js @@ -3,6 +3,23 @@ import cors from "cors"; 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"; @@ -19,11 +36,6 @@ import serverConfMgmtRouter from "./routes/admin/serverConfMgmt.route.js"; // API routes import apiRouter from "./routes/api/api.route.js"; -dotenv.config(); -const app = express(); -const port = 8004; -const naasURL = process.env.NAAS_URL; - app.use(cors()); // Body-Parser VOR den Routen registrieren app.use(express.json({ limit: "10mb" })); From 6fb03530df459dba593e32b956203c9d20e327cf Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Sun, 26 Apr 2026 16:24:11 +0200 Subject: [PATCH 14/20] improved error logging Co-authored-by: Copilot --- FrontendV2/package-lock.json | 6 +++--- FrontendV2/src/pages/ContactPage.tsx | 6 ++++++ FrontendV2/src/pages/Landingpage.tsx | 10 ++++++++++ FrontendV2/src/pages/MyLoansPage.tsx | 7 +++++++ FrontendV2/src/utils/Fetcher.ts | 20 ++++++++++++++++++++ FrontendV2/src/utils/i18n/locales/en/en.json | 5 ++++- 6 files changed, 50 insertions(+), 4 deletions(-) diff --git a/FrontendV2/package-lock.json b/FrontendV2/package-lock.json index 8e2a7ef..4f015b7 100644 --- a/FrontendV2/package-lock.json +++ b/FrontendV2/package-lock.json @@ -5224,9 +5224,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.11", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.11.tgz", + "integrity": "sha512-5dDj8+lmvA8XB78SmzGI8NlQoksv7IfutGWeVZxiixHbO+p4LDPT3wuG/D9sM/wrjZZ9I+Siy/e117vbFPxSZg==", "funding": [ { "type": "opencollective", diff --git a/FrontendV2/src/pages/ContactPage.tsx b/FrontendV2/src/pages/ContactPage.tsx index f02f5cd..6e62c29 100644 --- a/FrontendV2/src/pages/ContactPage.tsx +++ b/FrontendV2/src/pages/ContactPage.tsx @@ -42,6 +42,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", diff --git a/FrontendV2/src/pages/Landingpage.tsx b/FrontendV2/src/pages/Landingpage.tsx index e68c393..3c49a04 100644 --- a/FrontendV2/src/pages/Landingpage.tsx +++ b/FrontendV2/src/pages/Landingpage.tsx @@ -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); diff --git a/FrontendV2/src/pages/MyLoansPage.tsx b/FrontendV2/src/pages/MyLoansPage.tsx index ef040de..5cca95d 100644 --- a/FrontendV2/src/pages/MyLoansPage.tsx +++ b/FrontendV2/src/pages/MyLoansPage.tsx @@ -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")); diff --git a/FrontendV2/src/utils/Fetcher.ts b/FrontendV2/src/utils/Fetcher.ts index a684490..18ed01a 100644 --- a/FrontendV2/src/utils/Fetcher.ts +++ b/FrontendV2/src/utils/Fetcher.ts @@ -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", diff --git a/FrontendV2/src/utils/i18n/locales/en/en.json b/FrontendV2/src/utils/i18n/locales/en/en.json index 8dc1059..9cf19c5 100644 --- a/FrontendV2/src/utils/i18n/locales/en/en.json +++ b/FrontendV2/src/utils/i18n/locales/en/en.json @@ -94,5 +94,8 @@ "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." + "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." } \ No newline at end of file From 4a3c948386c3aeecaed21add982432efee22ee06 Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Sun, 26 Apr 2026 16:49:32 +0200 Subject: [PATCH 15/20] implemented deactivated services banner Co-authored-by: Copilot --- .../src/components/DeactivatedServices.tsx | 67 +++++++++++++++++++ FrontendV2/src/components/Header.tsx | 1 + FrontendV2/src/pages/HomePage.tsx | 2 + FrontendV2/src/utils/i18n/locales/de/de.json | 7 +- FrontendV2/src/utils/i18n/locales/en/en.json | 4 +- .../routes/app/database/userMgmt.database.js | 14 +++- backendV2/routes/app/userMgmt.route.js | 15 ++++- 7 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 FrontendV2/src/components/DeactivatedServices.tsx diff --git a/FrontendV2/src/components/DeactivatedServices.tsx b/FrontendV2/src/components/DeactivatedServices.tsx new file mode 100644 index 0000000..100edfb --- /dev/null +++ b/FrontendV2/src/components/DeactivatedServices.tsx @@ -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 && ( + + {t("deactivated-services")} + {isLoading && ( + + + {t("loading")} + + )} + {deactivatedServices.length >= 1 && + deactivatedServices.map((item) => ( + + + + {item.function_name} {t("is-deactivated")} + + + ))} + + )} + + ); +}; diff --git a/FrontendV2/src/components/Header.tsx b/FrontendV2/src/components/Header.tsx index 545a103..52b48f2 100644 --- a/FrontendV2/src/components/Header.tsx +++ b/FrontendV2/src/components/Header.tsx @@ -69,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 */} { )}
+ {isMsg && ( { 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 }; +}; diff --git a/backendV2/routes/app/userMgmt.route.js b/backendV2/routes/app/userMgmt.route.js index 654030b..e1eec7a 100644 --- a/backendV2/routes/app/userMgmt.route.js +++ b/backendV2/routes/app/userMgmt.route.js @@ -9,7 +9,11 @@ const user_frontend_service = "User Frontend"; const contact_form_service = "Contact Form Service"; // database funcs import -import { loginFunc, changePassword } from "./database/userMgmt.database.js"; +import { + loginFunc, + changePassword, + getDeactivatedServices, +} from "./database/userMgmt.database.js"; import { sendMail } from "./services/mailer_v2.js"; router.post( @@ -63,4 +67,13 @@ router.post( }, ); +router.get("/deactivated-services", authenticate, async (req, res) => { + const result = await getDeactivatedServices(); + if (result.success) { + res.status(200).json(result.data); + } else { + res.status(500).json({ message: "Failed to fetch deactivated services" }); + } +}); + export default router; From c61a2831274853fa7ff70f99fa629cefd09c290e Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Sun, 26 Apr 2026 16:55:03 +0200 Subject: [PATCH 16/20] edited changelog and edited version numbers --- backendV2/info.json | 2 +- changelog.md | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backendV2/info.json b/backendV2/info.json index 134da02..efefbf3 100644 --- a/backendV2/info.json +++ b/backendV2/info.json @@ -6,6 +6,6 @@ "version": "v2.2 (dev)" }, "admin-panel-info": { - "version": "v1.3.2 (dev)" + "version": "v1.4(dev)" } } \ No newline at end of file diff --git a/changelog.md b/changelog.md index 878a0db..1ae6467 100644 --- a/changelog.md +++ b/changelog.md @@ -4,14 +4,14 @@ This update provides some new features for the design. It also contains some imp ## New features -- The overview page now has the note column and is overall better organised -- I also addded the regular header to the page -- I have added three animations to the Borrow System -- 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...) -- 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._ +- **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 has the note column and is overall better organised - I have the error logging for the API route wehre you can take loans improved. - If you try to delete a loan that has not been returned yet, you will get an 507 error code. @@ -29,7 +29,7 @@ This update provides some new features for the design. It also contains some imp **Frontend:** v2.2 -**Admin panel:** v1.3.2 +**Admin panel:** v1.4 --- From 51918716815ecbb20cd0d8cd318b3f896be814e0 Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Sun, 26 Apr 2026 19:35:48 +0200 Subject: [PATCH 17/20] added feature: That no loan can be deleted and edited changelog accordingly --- backendV2/routes/admin/database/loanDataMgmt.database.js | 5 ++++- backendV2/schemeV2.sql | 1 + changelog.md | 5 +++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/backendV2/routes/admin/database/loanDataMgmt.database.js b/backendV2/routes/admin/database/loanDataMgmt.database.js index 5bdee37..776e41f 100644 --- a/backendV2/routes/admin/database/loanDataMgmt.database.js +++ b/backendV2/routes/admin/database/loanDataMgmt.database.js @@ -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 }; }; diff --git a/backendV2/schemeV2.sql b/backendV2/schemeV2.sql index 699e404..24e27cc 100644 --- a/backendV2/schemeV2.sql +++ b/backendV2/schemeV2.sql @@ -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}$') diff --git a/changelog.md b/changelog.md index 1ae6467..de56962 100644 --- a/changelog.md +++ b/changelog.md @@ -11,9 +11,10 @@ This update provides some new features for the design. It also contains some imp ## Improvements -- The overview page now has the note column and is overall better organised -- I have the error logging for the API route wehre you can take loans improved. +- 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 From 4291552b6d37dbd0b43f56c8e4b36704da8c95b4 Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Sun, 26 Apr 2026 21:52:35 +0200 Subject: [PATCH 18/20] edited mailer and changed orchestration Co-authored-by: Copilot --- FrontendV2/src/pages/ContactPage.tsx | 21 ++ FrontendV2/src/pages/HomePage.tsx | 20 +- FrontendV2/src/utils/i18n/locales/de/de.json | 4 +- FrontendV2/src/utils/i18n/locales/en/en.json | 4 +- backendV2/package-lock.json | 80 ++++--- backendV2/package.json | 2 +- backendV2/routes/app/loanMgmt.route.js | 19 +- backendV2/routes/app/services/mailer.js | 215 ------------------ backendV2/routes/app/services/mailer_v2.js | 43 ---- backendV2/routes/app/userMgmt.route.js | 30 ++- backendV2/services/mailer/send.js | 13 ++ .../services/mailer/templates/contact.js | 76 +++++++ .../services/mailer/templates/loan_created.js | 124 ++++++++++ backendV2/services/mailer/transporter.js | 13 ++ 14 files changed, 348 insertions(+), 316 deletions(-) delete mode 100644 backendV2/routes/app/services/mailer.js delete mode 100644 backendV2/routes/app/services/mailer_v2.js create mode 100644 backendV2/services/mailer/send.js create mode 100644 backendV2/services/mailer/templates/contact.js create mode 100644 backendV2/services/mailer/templates/loan_created.js create mode 100644 backendV2/services/mailer/transporter.js diff --git a/FrontendV2/src/pages/ContactPage.tsx b/FrontendV2/src/pages/ContactPage.tsx index 6e62c29..6c27775 100644 --- a/FrontendV2/src/pages/ContactPage.tsx +++ b/FrontendV2/src/pages/ContactPage.tsx @@ -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(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", @@ -55,6 +69,7 @@ export const ContactPage = () => { text: t("contactPage_errorText"), }); } + setIsSending(false); }; return ( @@ -84,6 +99,12 @@ export const ContactPage = () => { )} + {isSending && ( + + + {t("loading")} + + )} ); diff --git a/FrontendV2/src/pages/HomePage.tsx b/FrontendV2/src/pages/HomePage.tsx index 17afd4d..835006d 100644 --- a/FrontendV2/src/pages/HomePage.tsx +++ b/FrontendV2/src/pages/HomePage.tsx @@ -133,12 +133,6 @@ export const HomePage = () => { > {t("get-borrowable-items")} - {isLoadingA && ( - - - {t("loading")} - - )} {borrowableItems.length > 0 && ( @@ -191,9 +185,11 @@ export const HomePage = () => { )} {selectedItems.length >= 1 && ( )} + {isLoadingA && ( + + + {t("loading")} + + )} diff --git a/FrontendV2/src/utils/i18n/locales/de/de.json b/FrontendV2/src/utils/i18n/locales/de/de.json index 0de2e7e..e385379 100644 --- a/FrontendV2/src/utils/i18n/locales/de/de.json +++ b/FrontendV2/src/utils/i18n/locales/de/de.json @@ -99,5 +99,7 @@ "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" + "deactivated-services": "Deaktivierte Services", + "contactPage_messageErrorHeadline": "Fehler bei der Nachrichteneingabe", + "contactPage_messageErrorText2": "Bitte geben Sie eine Nachricht ein, bevor Sie sie senden." } \ No newline at end of file diff --git a/FrontendV2/src/utils/i18n/locales/en/en.json b/FrontendV2/src/utils/i18n/locales/en/en.json index c44d4fa..33a99d9 100644 --- a/FrontendV2/src/utils/i18n/locales/en/en.json +++ b/FrontendV2/src/utils/i18n/locales/en/en.json @@ -99,5 +99,7 @@ "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" + "deactivated-services": "Deactivated services", + "contactPage_messageErrorHeadline": "Error submitting message", + "contactPage_messageErrorText2": "Please enter a message before sending it." } \ No newline at end of file diff --git a/backendV2/package-lock.json b/backendV2/package-lock.json index aeb4f7d..67aff2b 100644 --- a/backendV2/package-lock.json +++ b/backendV2/package-lock.json @@ -16,7 +16,7 @@ "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": { @@ -54,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" @@ -528,18 +548,6 @@ "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", @@ -684,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" @@ -759,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" @@ -819,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", @@ -848,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" diff --git a/backendV2/package.json b/backendV2/package.json index c16054e..de1003c 100644 --- a/backendV2/package.json +++ b/backendV2/package.json @@ -18,6 +18,6 @@ "express-rate-limit": "^8.4.1", "jose": "^6.0.12", "mysql2": "^3.14.3", - "nodemailer": "^7.0.6" + "nodemailer": "^8.0.6" } } diff --git a/backendV2/routes/app/loanMgmt.route.js b/backendV2/routes/app/loanMgmt.route.js index ad1126a..ef1d63c 100644 --- a/backendV2/routes/app/loanMgmt.route.js +++ b/backendV2/routes/app/loanMgmt.route.js @@ -4,9 +4,14 @@ import { checkIfServiceIsActive, checkIfServiceIsActive2, } from "../../services/functions.js"; -const router = express.Router(); + +// 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"; @@ -23,7 +28,6 @@ import { setReturnDate, setTakeDate, } from "./database/loansMgmt.database.js"; -import { sendMailLoan } from "./services/mailer.js"; router.post( "/createLoan", @@ -63,19 +67,24 @@ router.post( 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({ diff --git a/backendV2/routes/app/services/mailer.js b/backendV2/routes/app/services/mailer.js deleted file mode 100644 index 3a081d6..0000000 --- a/backendV2/routes/app/services/mailer.js +++ /dev/null @@ -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 - ? `
    ${items - .map( - (i) => - `
  • ${i}
  • `, - ) - .join("")}
` - : "N/A"; - - return ` - - - - - - - - - - - -
- Neue Ausleihe erstellt – Übersicht der Buchung. -
-
- - - - - - - - -
- -`; -} - -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" ', - 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); - })(); -} diff --git a/backendV2/routes/app/services/mailer_v2.js b/backendV2/routes/app/services/mailer_v2.js deleted file mode 100644 index 4a3ed70..0000000 --- a/backendV2/routes/app/services/mailer_v2.js +++ /dev/null @@ -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 = ` - - - - Neue Nachricht im Ausleihsystem - - -

Neue Nachricht im Ausleihsystem

-

Benutzername: ${username}

-

Nachricht:

-

${message}

- -`; - - const info = await transporter.sendMail({ - from: '"Ausleihsystem" ', - to: process.env.MAIL_SENDEES_CONTACT, - subject: "Sie haben eine neue Nachricht!", - text: mailText, - html: mailHtml, - }); - - console.log("Contact message sent: %s", info.messageId); - })(); -} diff --git a/backendV2/routes/app/userMgmt.route.js b/backendV2/routes/app/userMgmt.route.js index e1eec7a..b05a0ec 100644 --- a/backendV2/routes/app/userMgmt.route.js +++ b/backendV2/routes/app/userMgmt.route.js @@ -14,7 +14,10 @@ import { changePassword, getDeactivatedServices, } from "./database/userMgmt.database.js"; -import { sendMail } from "./services/mailer_v2.js"; + +// mailer imports +import { sendMail } from "../../services/mailer/send.js"; +import { contactMail } from "../../services/mailer/templates/contact.js"; router.post( "/login", @@ -58,12 +61,29 @@ router.post( checkIfServiceIsActive(contact_form_service), authenticate, async (req, res) => { - const message = req.body.message; - const username = req.user.username; + 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" }); + } - res.status(200).json({ message: "Contact message sent successfully" }); + 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" }); + } }, ); diff --git a/backendV2/services/mailer/send.js b/backendV2/services/mailer/send.js new file mode 100644 index 0000000..1de4643 --- /dev/null +++ b/backendV2/services/mailer/send.js @@ -0,0 +1,13 @@ +import { transporter } from "./transporter.js"; + +export async function sendMail({ to, subject, text, html }) { + const info = await transporter.sendMail({ + from: '"Ausleihsystem" ', + to, + subject, + text, + html, + }); + console.log("Mail sent:", info.messageId); + return info; +} diff --git a/backendV2/services/mailer/templates/contact.js b/backendV2/services/mailer/templates/contact.js new file mode 100644 index 0000000..7977a1c --- /dev/null +++ b/backendV2/services/mailer/templates/contact.js @@ -0,0 +1,76 @@ +export function contactMail({ username, message }) { + const brand = process.env.MAIL_BRAND_COLOR || "#0ea5e9"; + + const html = ` + + + + + + Neue Nachricht + + +
Neue Kontaktanfrage im Ausleihsystem.
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
+

Neue Nachricht

+

Eine neue Kontaktanfrage ist eingegangen.

+
 
+ + + + + + + + + +
+ Benutzername + + ${username || "N/A"} +
+ Nachricht + + ${message || "N/A"} +
+
+

+ Diese E-Mail wurde automatisch vom Ausleihsystem gesendet.
+ Bitte antworten Sie nicht auf diese Nachricht. +

+
+
+ +`; + + const text = `Neue Kontaktanfrage\n\nBenutzername: ${username}\nNachricht:\n${message}`; + + return { html, text }; +} diff --git a/backendV2/services/mailer/templates/loan_created.js b/backendV2/services/mailer/templates/loan_created.js new file mode 100644 index 0000000..a75a71e --- /dev/null +++ b/backendV2/services/mailer/templates/loan_created.js @@ -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) => + `${i}`, + ) + .join(" ") + : 'Keine Gegenstände'; + + const row = (label, value, isLast = false) => ` + + + ${label} + + + ${value} + + `; + + const html = ` + + + + + + Neue Ausleihe + + +
Neue Ausleihe erstellt – Details zur Buchung.
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Neue Ausleihe

+

Es wurde soeben eine neue Ausleihe im System angelegt.

+
 
+

Details zur Ausleihe

+ + ${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 || 'Keine Notiz', true)} +
+
+ + Ausleihe ansehen → + +
+

+ Diese E-Mail wurde automatisch vom Ausleihsystem gesendet.
+ Bitte antworten Sie nicht auf diese Nachricht. +

+
+
+ +`; + + 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 }; +} diff --git a/backendV2/services/mailer/transporter.js b/backendV2/services/mailer/transporter.js new file mode 100644 index 0000000..47d9f4f --- /dev/null +++ b/backendV2/services/mailer/transporter.js @@ -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, + }, +}); From e52fc13da42c172e0020115eed104faa32906d86 Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Sun, 26 Apr 2026 22:09:19 +0200 Subject: [PATCH 19/20] updated changelog --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index de56962..24e18f0 100644 --- a/changelog.md +++ b/changelog.md @@ -15,6 +15,7 @@ This update provides some new features for the design. It also contains some imp - 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 From f8e29dca10c6770820ef9a4cfb3d5af3e35d15ed Mon Sep 17 00:00:00 2001 From: Theis Gaedigk Date: Sun, 26 Apr 2026 22:21:00 +0200 Subject: [PATCH 20/20] improved loan tabel on admin panel --- admin/src/Layout/Dashboard.tsx | 4 +- admin/src/components/ItemTable.tsx | 412 ++++++++++++++++------------- admin/src/components/LoanTable.tsx | 208 +++++++++------ 3 files changed, 357 insertions(+), 267 deletions(-) diff --git a/admin/src/Layout/Dashboard.tsx b/admin/src/Layout/Dashboard.tsx index b0f830d..352b645 100644 --- a/admin/src/Layout/Dashboard.tsx +++ b/admin/src/Layout/Dashboard.tsx @@ -47,7 +47,7 @@ const Dashboard: React.FC = ({ onLogout }) => { viewAPI={() => setActiveView("API")} viewConfig={() => setActiveView("Server Konfiguration")} /> - + = ({ onLogout }) => { - + {activeView === "" && ( { const handleItemNameChange = (id: number, value: string) => { 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) => { 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) => { 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) => { 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 = ( status: "error" | "success", message: string, - description: string + description: string, ) => { setIsError(false); setErrorStatus(status); @@ -102,7 +102,7 @@ const ItemTable: React.FC = () => { headers: { Authorization: `Bearer ${Cookies.get("token")}`, }, - } + }, ); const data = await response.json(); return data; @@ -193,185 +193,229 @@ const ItemTable: React.FC = () => { {/* make table fill available width, like UserTable */} {!isLoading && ( - - - - - # - - - Gegenstand - - - Ausleih Berechtigung - - - Im Schließfach - - - Schließfachnummer - - - Schlüssel - - - Eintrag erstellt am - - - Eintrag aktualisiert am - - - LaP * - - - Dav ** - - - Aktionen - - - - - {items.map((item) => ( - - {item.id} - - - handleItemNameChange(item.id, e.target.value) - } - value={item.item_name} - /> - - - - handleCanBorrowRoleChange(item.id, e.target.value) - } - value={item.can_borrow_role} - /> - - - - - - - handleLockerNumberChange(item.id, e.target.value) - } - value={item.safe_nr} - /> - - - - handleDoorKeyChange(item.id, e.target.value) - } - value={item.door_key} - /> - - {formatDateTime(item.entry_created_at)} - {formatDateTime(item.entry_updated_at)} - {item.last_borrowed_person} - {item.currently_borrowing} - - - - + + + + + + # + + + Gegenstand + + + Ausleih Berechtigung + + + Im Schließfach + + + Schließfachnummer + + + Schlüssel + + + Eintrag erstellt am + + + Eintrag aktualisiert am + + + LaP * + + + Dav ** + + + Aktionen + - ))} - - + + + {items.map((item) => ( + + {item.id} + + + handleItemNameChange(item.id, e.target.value) + } + value={item.item_name} + /> + + + + handleCanBorrowRoleChange(item.id, e.target.value) + } + value={item.can_borrow_role} + /> + + + + + + + handleLockerNumberChange(item.id, e.target.value) + } + value={item.safe_nr} + /> + + + + handleDoorKeyChange(item.id, e.target.value) + } + value={item.door_key} + /> + + + {formatDateTime(item.entry_created_at)} + + + {formatDateTime(item.entry_updated_at)} + + {item.last_borrowed_person} + {item.currently_borrowing} + + + + + + ))} + +
+
)} * LaP = Letzte ausleihende Person ** Dav = Derzeit ausgeliehen von diff --git a/admin/src/components/LoanTable.tsx b/admin/src/components/LoanTable.tsx index db7a735..3c6e51a 100644 --- a/admin/src/components/LoanTable.tsx +++ b/admin/src/components/LoanTable.tsx @@ -1,5 +1,6 @@ import React from "react"; import { + Box, Table, Spinner, Text, @@ -31,7 +32,7 @@ const LoanTable: React.FC = () => { const setError = ( status: "error" | "success", message: string, - description: string + description: string, ) => { setIsError(false); setErrorStatus(status); @@ -65,7 +66,7 @@ const LoanTable: React.FC = () => { headers: { Authorization: `Bearer ${Cookies.get("token")}`, }, - } + }, ); const data = await response.json(); return data; @@ -83,7 +84,7 @@ const LoanTable: React.FC = () => { }, [reload]); return ( - <> + {/* Action toolbar */} { )} {!isLoading && ( - - - - - # - - - Besitzer - - - Ausleih code - - - Startdatum - - - Enddatum - - - Ausleihdatum - - - Rückgabedatum - - - Erstellt am - - - Ausgeliehene Artikel - - - Notiz - - - Aktionen - - - - - {items.map((item) => ( - - {item.id} - {item.username} - - {item.loan_code} - - {formatDateTime(item.start_date)} - {formatDateTime(item.end_date)} - {formatDateTime(item.take_date)} - {formatDateTime(item.returned_date)} - {formatDateTime(item.created_at)} - {item.loaned_items_name.join(", ")} - {item.note} - - - + + + + + + # + + + Besitzer + + + Ausleihcode + + + Startdatum + + + Enddatum + + + Ausleihdatum + + + Rückgabedatum + + + Erstellt am + + + Ausgeliehene Artikel + + + Notiz + + + Aktionen + - ))} - - + + + {items.map((item) => ( + + {item.id} + {item.username} + + {item.loan_code} + + {formatDateTime(item.start_date)} + {formatDateTime(item.end_date)} + {formatDateTime(item.take_date)} + {formatDateTime(item.returned_date)} + {formatDateTime(item.created_at)} + {item.loaned_items_name.join(", ")} + {item.note} + + + + + ))} + + + )} - + ); };