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/FrontendV2/package-lock.json b/FrontendV2/package-lock.json index 90f17b0..4f015b7 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": { @@ -5162,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", @@ -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/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 dff897d..52b48f2 100644 --- a/FrontendV2/src/components/Header.tsx +++ b/FrontendV2/src/components/Header.tsx @@ -1,6 +1,7 @@ import { Button, Flex, + Image, Heading, Stack, Text, @@ -68,6 +69,7 @@ export const Header = () => { className="mb-6" position="relative" pr={{ base: 10, md: 0 }} // Platz für den Mobile-Button rechts + marginBottom={1} > {/* Mobile: Drei-Punkte-Button, vertikal zentriert im Header */} { {/* Titelzeile ohne Mobile-Menu (wurde nach oben verlegt) */} + borrow-system logo { 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/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/ContactPage.tsx b/FrontendV2/src/pages/ContactPage.tsx index f02f5cd..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", @@ -42,6 +56,12 @@ export const ContactPage = () => { text: t("contactPage_successText"), }); setMessage(""); + } else if (result.status === 503) { + setAlert({ + type: "error", + headline: t("serviceDeactivatedHeadline"), + text: t("contactPage_serviceDeactivatedText"), + }); } else { setAlert({ type: "error", @@ -49,6 +69,7 @@ export const ContactPage = () => { text: t("contactPage_errorText"), }); } + setIsSending(false); }; return ( @@ -78,6 +99,12 @@ export const ContactPage = () => { )} + {isSending && ( + + + {t("loading")} + + )} ); diff --git a/FrontendV2/src/pages/HomePage.tsx b/FrontendV2/src/pages/HomePage.tsx index ba0d1ab..835006d 100644 --- a/FrontendV2/src/pages/HomePage.tsx +++ b/FrontendV2/src/pages/HomePage.tsx @@ -18,6 +18,8 @@ import { borrowAbleItemsAtom } from "@/states/Atoms"; import { createLoan } from "@/utils/Fetcher"; import { Header } from "@/components/Header"; import { useTranslation } from "react-i18next"; +import { approvalAnimation } from "@/components/dotLottie"; +import { DeactivatedServices } from "@/components/DeactivatedServices"; export interface User { username: string; @@ -27,6 +29,8 @@ export interface User { export const HomePage = () => { const { t } = useTranslation(); + const [showAnimation, setShowAnimation] = useState(false); + const [borrowableItems, setBorrowableItems] = useAtom(borrowAbleItemsAtom); const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); @@ -46,155 +50,175 @@ 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 && ( - - )} - - + {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")} + + )} + + + ); }; 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/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 && ( - - )} - - - -
+ )} + +
+ +
+ ); }; diff --git a/FrontendV2/src/pages/MyLoansPage.tsx b/FrontendV2/src/pages/MyLoansPage.tsx index 8fa5904..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")); @@ -84,6 +91,14 @@ export const MyLoansPage = () => { }); if (!res.ok) { + if (res.status === 507) { + setMsgStatus("error"); + setMsgTitle(t("error")); + setMsgDescription(t("error-deleting-loan-507")); + setIsMsg(true); + return; + } + setMsgStatus("error"); setMsgTitle(t("error")); setMsgDescription(t("error-deleting-loan")); 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/de/de.json b/FrontendV2/src/utils/i18n/locales/de/de.json index f6ed2dc..e385379 100644 --- a/FrontendV2/src/utils/i18n/locales/de/de.json +++ b/FrontendV2/src/utils/i18n/locales/de/de.json @@ -88,5 +88,18 @@ "take-loan-success": "Ausleihe erfolgreich abgeholt", "return-loan-success": "Ausleihe erfolgreich zurückgegeben", "network-error": "Netzwerkfehler. Kontaktieren Sie den Administrator.", - "contactPage_messageDescription": "Bitte geben Sie hier Ihre Nachricht ein. Der Systemadministrator (Theis Gaedigk) wird sich so schnell wie möglich bei Ihnen melden." + "contactPage_messageDescription": "Bitte geben Sie hier Ihre Nachricht ein. Der Systemadministrator (Theis Gaedigk) wird sich so schnell wie möglich bei Ihnen melden.", + "naas": "No-as-a-service", + "try-naas": "Klick mich", + "naas-error": "Fehler mit no-as-a-service", + "naas-error-desc": "Ein Fehler ist beim Kommunizieren mit no-as-a-service aufgetreten.", + "naas-header": "Eine gute Möglichkeit, nein zu sagen...", + "error-deleting-loan-507": "Die Ausleihe kann nicht gelöscht werden, da sie noch nicht zurückgegeben wurde.", + "serviceDeactivatedHeadline": "Service deaktiviert", + "contactPage_serviceDeactivatedText": "Der Kontaktservice ist derzeit deaktiviert. Bitte versuchen Sie es später erneut.", + "loan_page_serviceDeactivatedText": "Der Ausleihservice ist derzeit deaktiviert. Bitte versuchen Sie es später erneut.", + "is-deactivated": "ist deaktiviert.", + "deactivated-services": "Deaktivierte Services", + "contactPage_messageErrorHeadline": "Fehler bei der Nachrichteneingabe", + "contactPage_messageErrorText2": "Bitte geben Sie eine Nachricht ein, bevor Sie sie senden." } \ 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..33a99d9 100644 --- a/FrontendV2/src/utils/i18n/locales/en/en.json +++ b/FrontendV2/src/utils/i18n/locales/en/en.json @@ -88,5 +88,18 @@ "take-loan-success": "Loan taken successfully", "return-loan-success": "Loan returned successfully", "network-error": "Network error. Please contact the administrator.", - "contactPage_messageDescription": "Please enter your message here. The system administrator (Theis Gaedigk) will get back to you as soon as possible." + "contactPage_messageDescription": "Please enter your message here. The system administrator (Theis Gaedigk) will get back to you as soon as possible.", + "naas": "No-as-a-service", + "try-naas": "Click me", + "naas-error": "Error with no-as-a-service", + "naas-error-desc": "An error occurred while communicating with no-as-a-service.", + "naas-header": "A good way to say no...", + "error-deleting-loan-507": "The loan cannot be deleted because it has not been returned yet.", + "serviceDeactivatedHeadline": "Service deactivated", + "contactPage_serviceDeactivatedText": "The contact service is currently deactivated. Please try again later.", + "loan_page_serviceDeactivatedText": "The loan service is currently deactivated. Please try again later.", + "is-deactivated": "is deactivated.", + "deactivated-services": "Deactivated services", + "contactPage_messageErrorHeadline": "Error submitting message", + "contactPage_messageErrorText2": "Please enter a message before sending it." } \ No newline at end of file diff --git a/admin/src/Layout/Dashboard.tsx b/admin/src/Layout/Dashboard.tsx index fd37bb6..352b645 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,8 +45,9 @@ const Dashboard: React.FC = ({ onLogout }) => { viewSchliessfaecher={() => setActiveView("Schließfächer")} viewUser={() => setActiveView("User")} viewAPI={() => setActiveView("API")} + viewConfig={() => setActiveView("Server Konfiguration")} /> - + = ({ onLogout }) => { - + {activeView === "" && ( = ({ 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/ItemTable.tsx b/admin/src/components/ItemTable.tsx index fbb3d75..55a3d97 100644 --- a/admin/src/components/ItemTable.tsx +++ b/admin/src/components/ItemTable.tsx @@ -57,32 +57,32 @@ const ItemTable: React.FC = () => { 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} + + + + + ))} + + + )} - + ); }; 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/info.json b/backendV2/info.json index 4b912d8..40dd88f 100644 --- a/backendV2/info.json +++ b/backendV2/info.json @@ -1,11 +1,11 @@ { "backend-info": { - "version": "v2.1.1 (demo)" + "version": "v2.2 (dev)" }, "frontend-info": { - "version": "v2.1.2 (demo)" + "version": "v2.2 (dev)" }, "admin-panel-info": { - "version": "v1.3.2 (demo)" + "version": "v1.4 (dev)" } } \ No newline at end of file diff --git a/backendV2/package-lock.json b/backendV2/package-lock.json index 0b39461..67aff2b 100644 --- a/backendV2/package-lock.json +++ b/backendV2/package-lock.json @@ -1,21 +1,22 @@ { "name": "backendv2", - "version": "1.0.0", + "version": "v2.1.1 (dev)", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "backendv2", - "version": "1.0.0", + "version": "v2.1.1 (dev)", "license": "ISC", "dependencies": { "cors": "^2.8.5", "dotenv": "^17.2.1", "ejs": "^3.1.10", "express": "^5.1.0", + "express-rate-limit": "^8.4.1", "jose": "^6.0.12", "mysql2": "^3.14.3", - "nodemailer": "^7.0.6" + "nodemailer": "^8.0.6" } }, "node_modules/accepts": { @@ -53,29 +54,49 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -349,6 +370,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz", + "integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -509,24 +548,21 @@ "node": ">= 0.8" } }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -656,9 +692,9 @@ } }, "node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -731,9 +767,9 @@ } }, "node_modules/nodemailer": { - "version": "7.0.10", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.10.tgz", - "integrity": "sha512-Us/Se1WtT0ylXgNFfyFSx4LElllVLJXQjWi2Xz17xWw7amDKO2MLtFnVp1WACy7GkVGs+oBlRopVNUzlrGSw1w==", + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.6.tgz", + "integrity": "sha512-Nm2XeuDwwy2wi5A+8jPWwQwNzcjNjhWdE3pVLoXEusxJqCnAPAgnBGkSmiLknbnWuOF9qraRpYZjfxqtKZ4tPw==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -791,9 +827,9 @@ } }, "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", "funding": { "type": "opencollective", @@ -820,9 +856,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" diff --git a/backendV2/package.json b/backendV2/package.json index 9c9a53e..de1003c 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" + "nodemailer": "^8.0.6" } -} \ No newline at end of file +} 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/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 fc638d8..17b6edc 100644 --- a/backendV2/routes/api/api.route.js +++ b/backendV2/routes/api/api.route.js @@ -1,9 +1,12 @@ import express from "express"; import { authenticate } from "../../services/authentication.js"; +import { checkIfServiceIsActive } from "../../services/functions.js"; const router = express.Router(); import dotenv from "dotenv"; dotenv.config(); +const loan_service = "Loan Service"; + import { getItemsFromDatabaseV2, changeInSafeStateV2, @@ -39,6 +42,7 @@ router.post("/change-state/:key/:itemId", authenticate, async (req, res) => { router.get( "/get-loan-by-code/:key/:loan_code", authenticate, + checkIfServiceIsActive(loan_service), async (req, res) => { const loan_code = req.params.loan_code; const result = await getLoanByCodeV2(loan_code); @@ -54,6 +58,7 @@ router.get( router.post( "/set-return-date/:key/:loan_code", authenticate, + checkIfServiceIsActive(loan_service), async (req, res) => { const loanCode = req.params.loan_code; const result = await setReturnDateV2(loanCode); @@ -69,6 +74,7 @@ router.post( router.post( "/set-take-date/:key/:loan_code", authenticate, + checkIfServiceIsActive(loan_service), async (req, res) => { const loanCode = req.params.loan_code; const result = await setTakeDateV2(loanCode); 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/database/userMgmt.database.js b/backendV2/routes/app/database/userMgmt.database.js index 10a345f..4c533a8 100644 --- a/backendV2/routes/app/database/userMgmt.database.js +++ b/backendV2/routes/app/database/userMgmt.database.js @@ -14,7 +14,7 @@ const pool = mysql export const loginFunc = async (username, password) => { const [result] = await pool.query( "SELECT * FROM users WHERE username = ? AND password = ?", - [username, password] + [username, password], ); if (result.length > 0) return { success: true, data: result[0] }; return { success: false }; @@ -40,7 +40,7 @@ export const changePassword = async (username, oldPassword, newPassword) => { // get user current password const [user] = await pool.query( "SELECT * FROM users WHERE username = ? AND password = ?", - [username, oldPassword] + [username, oldPassword], ); if (user.length === 0) return { success: false }; @@ -48,8 +48,16 @@ export const changePassword = async (username, oldPassword, newPassword) => { const [result] = await pool.query( "UPDATE users SET password = ? WHERE username = ?", - [newPassword, username] + [newPassword, username], ); if (result.affectedRows > 0) return { success: true }; return { success: false }; }; + +export const getDeactivatedServices = async () => { + const [rows] = await pool.query("SELECT function_name FROM functions WHERE active = 0;"); + if (rows.length > 0) { + return { success: true, data: rows }; + } + return { success: false }; +}; diff --git a/backendV2/routes/app/loanMgmt.route.js b/backendV2/routes/app/loanMgmt.route.js index 7e0c0ce..ef1d63c 100644 --- a/backendV2/routes/app/loanMgmt.route.js +++ b/backendV2/routes/app/loanMgmt.route.js @@ -1,8 +1,20 @@ import express from "express"; import { authenticate, generateToken } from "../../services/authentication.js"; -const router = express.Router(); +import { + checkIfServiceIsActive, + checkIfServiceIsActive2, +} from "../../services/functions.js"; + +// mailer imports +import { sendMail } from "../../services/mailer/send.js"; +import { loanMail } from "../../services/mailer/templates/loan_created.js"; + import dotenv from "dotenv"; dotenv.config(); +const router = express.Router(); + +const loan_service = "Loan Service"; +const loan_mailer_service = "Loan Mailer"; // database funcs import import { @@ -16,108 +28,135 @@ import { setReturnDate, setTakeDate, } 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_service)) { + const mailInfo = await getLoanInfoWithID(result.data.id); + console.log(mailInfo); + const { html, text } = loanMail( + req.user.first_name + " " + req.user.last_name, + mailInfo.data.loaned_items_name, + mailInfo.data.start_date, + mailInfo.data.end_date, + mailInfo.data.created_at, + mailInfo.data.note, + ); + await sendMail({ + to: process.env.MAIL_SENDEES, + subject: "Neue Ausleihe erstellt!", + html, + text, + }); + } + + return res.status(201).json({ + message: "Loan created successfully", + loanId: result.data.id, + 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,46 +167,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 { - res.status(500).json({ message: "Failed to delete loan" }); - } -}); +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" }); + } -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" }); - } -}); + if (result.code === "LOAN_NOT_RETURNED") { + res.status(507).json({ + message: "Cannot delete loan that has not been returned", + }); + } -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" }); - } + res.status(500).json({ message: "Failed to delete loan" }); + } + }, +); - 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" }); - } -}); +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" }); + } + + 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/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 0f63862..b05a0ec 100644 --- a/backendV2/routes/app/userMgmt.route.js +++ b/backendV2/routes/app/userMgmt.route.js @@ -1,48 +1,99 @@ 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(); +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"; +import { + loginFunc, + changePassword, + getDeactivatedServices, +} from "./database/userMgmt.database.js"; -router.post("/login", async (req, res) => { - const result = await loginFunc(req.body.username, req.body.password); +// mailer imports +import { sendMail } from "../../services/mailer/send.js"; +import { contactMail } from "../../services/mailer/templates/contact.js"; + +router.post( + "/login", + checkIfServiceIsActive(user_frontend_service), + async (req, res) => { + const result = await loginFunc(req.body.username, req.body.password); + if (result.success) { + const token = await generateToken({ + 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", + checkIfServiceIsActive(user_frontend_service), + 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", + checkIfServiceIsActive(contact_form_service), + authenticate, + async (req, res) => { + try { + const message = req.body?.message; + const username = req.user?.first_name + " " + req.user?.last_name; + + if (!username || !message) { + return res + .status(400) + .json({ message: "Username and message are required" }); + } + + const { html, text } = contactMail({ username, message }); + await sendMail({ + to: process.env.MAIL_SENDEES_CONTACT, + subject: "Neue Nachricht!", + html, + text, + }); + + res.status(200).json({ message: "Contact message sent successfully" }); + } catch (error) { + console.error("Failed to send contact mail:", error); + res.status(500).json({ message: "Failed to send contact message" }); + } + }, +); + +router.get("/deactivated-services", authenticate, async (req, res) => { + const result = await getDeactivatedServices(); if (result.success) { - 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 }); + res.status(200).json(result.data); } else { - res.status(401).json({ message: "Invalid credentials" }); + res.status(500).json({ message: "Failed to fetch deactivated services" }); } }); -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("/contact", authenticate, async (req, res) => { - const message = req.body.message; - const username = req.user.username; - - sendMail(username, message); - - res.status(200).json({ message: "Contact message sent successfully" }); -}); - export default router; diff --git a/backendV2/schemeV2.sql b/backendV2/schemeV2.sql index 95ff6ed..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}$') @@ -54,4 +55,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/server.js b/backendV2/server.js index 3b86597..c255551 100644 --- a/backendV2/server.js +++ b/backendV2/server.js @@ -1,8 +1,25 @@ import express from "express"; import cors from "cors"; -import env from "dotenv"; +import dotenv from "dotenv"; import info from "./info.json" assert { type: "json" }; import { authenticate } from "./services/authentication.js"; +import { rateLimit } from "express-rate-limit"; + +dotenv.config(); +const app = express(); +const port = 8004; +const naasURL = process.env.NAAS_URL; + +const limiter = rateLimit({ + windowMs: 1 * 60 * 1000, // 1 minute + limit: 50, // Limit each IP to 50 requests per `window` (here, per 1 minute). + standardHeaders: "draft-8", // draft-6: `RateLimit-*` headers; draft-7 & draft-8: combined `RateLimit` header + legacyHeaders: false, // Disable the `X-RateLimit-*` headers. + ipv6Subnet: 56, // Set to 60 or 64 to be less aggressive, or 52 or 48 to be more aggressive + // store: ... , // Redis, Memcached, etc. See below. +}); + +app.use(limiter); // frontend routes import loansMgmtRouter from "./routes/app/loanMgmt.route.js"; @@ -14,14 +31,11 @@ import loanDataMgmtRouter from "./routes/admin/loanDataMgmt.route.js"; import itemDataMgmtRouter from "./routes/admin/itemDataMgmt.route.js"; import apiDataMgmtRouter from "./routes/admin/apiDataMgmt.route.js"; import userMgmtRouterADMIN from "./routes/admin/userMgmt.route.js"; +import serverConfMgmtRouter from "./routes/admin/serverConfMgmt.route.js"; // API routes import apiRouter from "./routes/api/api.route.js"; -env.config(); -const app = express(); -const port = 8004; - app.use(cors()); // Body-Parser VOR den Routen registrieren app.use(express.json({ limit: "10mb" })); @@ -37,6 +51,7 @@ app.use("/api/admin/user-data", userDataMgmtRouter); app.use("/api/admin/item-data", itemDataMgmtRouter); app.use("/api/admin/api-data", apiDataMgmtRouter); app.use("/api/admin/user-mgmt", userMgmtRouterADMIN); +app.use("/api/admin/server-config", serverConfMgmtRouter); // API routes app.use("/api", apiRouter); @@ -47,6 +62,20 @@ app.listen(port, () => { console.log(`Server is running on port: ${port}`); }); +app.get("/no", async (req, res) => { + try { + const response = await fetch(naasURL); + if (!response.ok) { + res.status(500).send("Request to no-as-a-service went wrong."); + } + const data = await response.json(); + res.json(data); + } catch (error) { + console.error("Error communicating with no-as-a-service:", error); + res.status(500).send("Error communicating with no-as-a-service."); + } +}); + app.get("/verify", authenticate, async (req, res) => { res.status(200).json({ message: "Token is valid", user: req.user }); }); diff --git a/backendV2/services/authentication.js b/backendV2/services/authentication.js index 4fadd38..b0de555 100644 --- a/backendV2/services/authentication.js +++ b/backendV2/services/authentication.js @@ -1,8 +1,12 @@ import { SignJWT, jwtVerify } from "jose"; import env from "dotenv"; import { verifyAPIKeyDB } from "./database.js"; +import { checkIfServiceIsActive2 } from "./functions.js"; env.config(); +const api_service = "API"; +const user_frontend_service = "User Frontend"; + const secretKey = process.env.SECRET_KEY; if (!secretKey) { throw new Error("Missing SECRET_KEY environment variable"); @@ -45,6 +49,13 @@ export async function authenticate(req, res, next) { const apiKey = req.params.key; if (authHeader) { + const serviceActive = await checkIfServiceIsActive2(user_frontend_service); + if (!serviceActive) { + return res + .status(503) + .json({ message: "User Frontend is currently unavailable." }); + } + const parts = authHeader.split(" "); const scheme = parts[0]; const token = parts[1]; @@ -61,6 +72,13 @@ export async function authenticate(req, res, next) { return res.status(403).json({ message: "Present token invalid" }); // present token invalid } } else if (apiKey) { + const serviceActive = await checkIfServiceIsActive2(api_service); + if (!serviceActive) { + return res + .status(503) + .json({ message: "API Service is currently unavailable." }); + } + try { await verifyAPIKey(apiKey); return next(); 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; +} 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, + }, +}); diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..24e18f0 --- /dev/null +++ b/changelog.md @@ -0,0 +1,38 @@ +# Changelog for upcoming version: v2.2 + +This update provides some new features for the design. It also contains some improvements and I have also fixed some bugs. + +## New features + +- **Deactivatable services:** I have added the ability to deactivate services, which can be useful for maintenance or other purposes. The admin can activate and deactivate services in the admin panel. If a service is deactivated, it will not be available for users and they will get an error message if they try to use it. They will also get an warning banner on the homepage. +- **New Animations:** I have added some new animations to the frontend, which make the user experience more enjoyable. +- **New Icon:** I have added a new icon for the frontend, which is now also used in the header and the favicon. It is a dark version of the old icon, which fits better to the overall design. I have made it with Icon Composer. The old icon is still used for the admin panel, which has a light design. (Maybe I will change the admin panel design in the future...) +- **New Button:** When you go to your user card (over the user icon in the header) you have a new button "Click me". If you click it, you will get an message... _I am just saying: I have implemented the no-as-a-service code in to my Backend._ + +## Improvements + +- The overview page now shows the note column and is overall better organised. +- Improved error logging +- If you try to delete a loan that has not been returned yet, you will get an 507 error code. +- When the admin deletes a loan, the loan will be still visible in the database, but it will be marked as deleted. This is to prevent data loss and to keep track of deleted loans. +- Mailer improvements: The mailer is now more clearly organised. Two large code files are now split into five smaller code files which are easier to maintain. Also the design of the mails has improved. + +## Fixed bugs + +- Fixed bug: #13 +- Fixed bug for messaging when server has an error +- Fixed footer height + +--- + +## New version numbers + +**Backend:** v2.2 + +**Frontend:** v2.2 + +**Admin panel:** v1.4 + +--- + +-[Theis](https://portfolio-theis.de) 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: 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