feat: add 2fa

This commit is contained in:
Alexander Medvedev 2026-05-14 21:03:18 +02:00
parent f0700bd0db
commit 1de42e60bf
9 changed files with 584 additions and 290 deletions

458
package-lock.json generated
View File

@ -8,37 +8,38 @@
"name": "client", "name": "client",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@marsidev/react-turnstile": "^1.5.1", "@marsidev/react-turnstile": "^1.5.2",
"@vnedyalk0v/react19-simple-maps": "^2.0.7", "@vnedyalk0v/react19-simple-maps": "^2.0.7",
"axios": "^1.15.2", "axios": "^1.16.1",
"d3-geo": "^3.1.1", "d3-geo": "^3.1.1",
"d3-scale": "^4.0.2", "d3-scale": "^4.0.2",
"i18next": "^26.0.8", "i18next": "^26.1.0",
"i18next-browser-languagedetector": "^8.2.1", "i18next-browser-languagedetector": "^8.2.1",
"lucide-react": "^1.14.0", "lucide-react": "^1.16.0",
"or": "^0.2.0", "or": "^0.2.0",
"react": "^19.2.5", "qrcode.react": "^4.2.0",
"react-dom": "^19.2.5", "react": "^19.2.6",
"react-i18next": "^17.0.6", "react-dom": "^19.2.6",
"react-is": "^19.2.5", "react-i18next": "^17.0.7",
"react-is": "^19.2.6",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-router-dom": "^7.14.2", "react-router-dom": "^7.15.1",
"recharts": "^3.8.1", "recharts": "^3.8.1",
"remark-gfm": "^4.0.1" "remark-gfm": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@types/node": "^25.6.0", "@types/node": "^25.8.0",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-react": "^6.0.1",
"eslint": "^10.2.1", "eslint": "^10.3.0",
"eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2", "eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.5.0", "globals": "^17.6.0",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"typescript-eslint": "^8.59.1", "typescript-eslint": "^8.59.3",
"vite": "^8.0.10" "vite": "^8.0.13"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
@ -569,9 +570,9 @@
} }
}, },
"node_modules/@marsidev/react-turnstile": { "node_modules/@marsidev/react-turnstile": {
"version": "1.5.1", "version": "1.5.2",
"resolved": "https://registry.npmjs.org/@marsidev/react-turnstile/-/react-turnstile-1.5.1.tgz", "resolved": "https://registry.npmjs.org/@marsidev/react-turnstile/-/react-turnstile-1.5.2.tgz",
"integrity": "sha512-dGZeOWEh1t9AfkR0G94k48+gciIzTNz8GhEatIGTk43ZBynWFfHxmgv1ZiSPkJBYBhSqeqfgFMp+nqFbZAViyQ==", "integrity": "sha512-+3aBPxp86JzSC0ZmgyonoGoUEENcUkH3LGahXSpkV87ArvD2DzRCmPgh0FyQk6PQRmJwQJDAfwNavFsxUxMQWA==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"react": "^17.0.2 || ^18.0.0 || ^19.0", "react": "^17.0.2 || ^18.0.0 || ^19.0",
@ -598,9 +599,9 @@
} }
}, },
"node_modules/@oxc-project/types": { "node_modules/@oxc-project/types": {
"version": "0.127.0", "version": "0.130.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz",
"integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@ -644,9 +645,9 @@
} }
}, },
"node_modules/@rolldown/binding-android-arm64": { "node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz",
"integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -661,9 +662,9 @@
} }
}, },
"node_modules/@rolldown/binding-darwin-arm64": { "node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz",
"integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -678,9 +679,9 @@
} }
}, },
"node_modules/@rolldown/binding-darwin-x64": { "node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz",
"integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -695,9 +696,9 @@
} }
}, },
"node_modules/@rolldown/binding-freebsd-x64": { "node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz",
"integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -712,9 +713,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm-gnueabihf": { "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz",
"integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -729,9 +730,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm64-gnu": { "node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz",
"integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -746,9 +747,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm64-musl": { "node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz",
"integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -763,9 +764,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-ppc64-gnu": { "node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz",
"integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -780,9 +781,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-s390x-gnu": { "node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz",
"integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -797,9 +798,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-x64-gnu": { "node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz",
"integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -814,9 +815,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-x64-musl": { "node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz",
"integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -831,9 +832,9 @@
} }
}, },
"node_modules/@rolldown/binding-openharmony-arm64": { "node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz",
"integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -848,9 +849,9 @@
} }
}, },
"node_modules/@rolldown/binding-wasm32-wasi": { "node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz",
"integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==",
"cpu": [ "cpu": [
"wasm32" "wasm32"
], ],
@ -867,9 +868,9 @@
} }
}, },
"node_modules/@rolldown/binding-win32-arm64-msvc": { "node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz",
"integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -884,9 +885,9 @@
} }
}, },
"node_modules/@rolldown/binding-win32-x64-msvc": { "node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
"integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -920,9 +921,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@tybys/wasm-util": { "node_modules/@tybys/wasm-util": {
"version": "0.10.1", "version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@ -1056,13 +1057,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "25.6.0", "version": "25.8.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz",
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.19.0" "undici-types": ">=7.24.0 <7.24.7"
} }
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
@ -1098,17 +1099,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.59.1", "version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz",
"integrity": "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==", "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.12.2", "@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.59.1", "@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/type-utils": "8.59.1", "@typescript-eslint/type-utils": "8.59.3",
"@typescript-eslint/utils": "8.59.1", "@typescript-eslint/utils": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.1", "@typescript-eslint/visitor-keys": "8.59.3",
"ignore": "^7.0.5", "ignore": "^7.0.5",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0" "ts-api-utils": "^2.5.0"
@ -1121,7 +1122,7 @@
"url": "https://opencollective.com/typescript-eslint" "url": "https://opencollective.com/typescript-eslint"
}, },
"peerDependencies": { "peerDependencies": {
"@typescript-eslint/parser": "^8.59.1", "@typescript-eslint/parser": "^8.59.3",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0" "typescript": ">=4.8.4 <6.1.0"
} }
@ -1137,16 +1138,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "8.59.1", "version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz",
"integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==", "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.59.1", "@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/types": "8.59.1", "@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.1", "@typescript-eslint/typescript-estree": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.1", "@typescript-eslint/visitor-keys": "8.59.3",
"debug": "^4.4.3" "debug": "^4.4.3"
}, },
"engines": { "engines": {
@ -1162,14 +1163,14 @@
} }
}, },
"node_modules/@typescript-eslint/project-service": { "node_modules/@typescript-eslint/project-service": {
"version": "8.59.1", "version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz",
"integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==", "integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.59.1", "@typescript-eslint/tsconfig-utils": "^8.59.3",
"@typescript-eslint/types": "^8.59.1", "@typescript-eslint/types": "^8.59.3",
"debug": "^4.4.3" "debug": "^4.4.3"
}, },
"engines": { "engines": {
@ -1184,14 +1185,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "8.59.1", "version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
"integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==", "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.59.1", "@typescript-eslint/types": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.1" "@typescript-eslint/visitor-keys": "8.59.3"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1202,9 +1203,9 @@
} }
}, },
"node_modules/@typescript-eslint/tsconfig-utils": { "node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.59.1", "version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
"integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==", "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -1219,15 +1220,15 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "8.59.1", "version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz",
"integrity": "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==", "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.59.1", "@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.1", "@typescript-eslint/typescript-estree": "8.59.3",
"@typescript-eslint/utils": "8.59.1", "@typescript-eslint/utils": "8.59.3",
"debug": "^4.4.3", "debug": "^4.4.3",
"ts-api-utils": "^2.5.0" "ts-api-utils": "^2.5.0"
}, },
@ -1244,9 +1245,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.59.1", "version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz",
"integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==", "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -1258,16 +1259,16 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.59.1", "version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
"integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==", "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/project-service": "8.59.1", "@typescript-eslint/project-service": "8.59.3",
"@typescript-eslint/tsconfig-utils": "8.59.1", "@typescript-eslint/tsconfig-utils": "8.59.3",
"@typescript-eslint/types": "8.59.1", "@typescript-eslint/types": "8.59.3",
"@typescript-eslint/visitor-keys": "8.59.1", "@typescript-eslint/visitor-keys": "8.59.3",
"debug": "^4.4.3", "debug": "^4.4.3",
"minimatch": "^10.2.2", "minimatch": "^10.2.2",
"semver": "^7.7.3", "semver": "^7.7.3",
@ -1286,9 +1287,9 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
"version": "7.7.4", "version": "7.8.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
@ -1299,16 +1300,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.59.1", "version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz",
"integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==", "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.9.1", "@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.59.1", "@typescript-eslint/scope-manager": "8.59.3",
"@typescript-eslint/types": "8.59.1", "@typescript-eslint/types": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.1" "@typescript-eslint/typescript-estree": "8.59.3"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -1323,13 +1324,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.59.1", "version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz",
"integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==", "integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.59.1", "@typescript-eslint/types": "8.59.3",
"eslint-visitor-keys": "^5.0.0" "eslint-visitor-keys": "^5.0.0"
}, },
"engines": { "engines": {
@ -1417,6 +1418,18 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
} }
}, },
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/ajv": { "node_modules/ajv": {
"version": "6.14.0", "version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
@ -1441,13 +1454,14 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.15.2", "version": "1.16.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz",
"integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.11", "follow-redirects": "^1.16.0",
"form-data": "^4.0.5", "form-data": "^4.0.5",
"https-proxy-agent": "^5.0.1",
"proxy-from-env": "^2.1.0" "proxy-from-env": "^2.1.0"
} }
}, },
@ -2077,9 +2091,9 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "10.2.1", "version": "10.3.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz",
"integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==", "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -2491,9 +2505,9 @@
} }
}, },
"node_modules/globals": { "node_modules/globals": {
"version": "17.5.0", "version": "17.6.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz",
"integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -2630,10 +2644,23 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/i18next": { "node_modules/i18next": {
"version": "26.0.8", "version": "26.1.0",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.8.tgz", "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.1.0.tgz",
"integrity": "sha512-BRzLom0mhDhV9v0QhgUUHWQJuwFmnr1194xEcNLYD6ym8y8s542n4jXUvRLnhNTbh9PmpU6kGZamyuGHQMsGjw==", "integrity": "sha512-dIU6td04DvQuIqVst5S9g0GviTmhZ0DYD4b9ociVGJmuCa5vZ2de/t+Enf4olvj87mF8Y2lwjNQBwC9QZsvzKQ==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@ -3174,9 +3201,9 @@
} }
}, },
"node_modules/lucide-react": { "node_modules/lucide-react": {
"version": "1.14.0", "version": "1.16.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.14.0.tgz", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz",
"integrity": "sha512-+1mdWcfSJVUsaTIjN9zoezmUhfXo5l0vP7ekBMPo3jcS/aIkxHnXqAPsByszMZx/Y8oQBRJxJx5xg+RH3urzxA==", "integrity": "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==",
"license": "ISC", "license": "ISC",
"peerDependencies": { "peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@ -4090,9 +4117,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -4246,9 +4273,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.10", "version": "8.5.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -4313,31 +4340,40 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/qrcode.react": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
"integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react": { "node_modules/react": {
"version": "19.2.5", "version": "19.2.6",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "19.2.5", "version": "19.2.6",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
"integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^19.2.5" "react": "^19.2.6"
} }
}, },
"node_modules/react-i18next": { "node_modules/react-i18next": {
"version": "17.0.6", "version": "17.0.7",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.6.tgz", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.7.tgz",
"integrity": "sha512-WzJ6SMKF+GTD7JZZqxSR1AKKmXjaSu39sClUrNlwxS4Tl7a99O+ltFy6yhPMO+wgZuxpQjJ2PZkfrQKmAqrLhw==", "integrity": "sha512-rwtPXsb/zwzDafN+gytcjF5YnqGQQIRmCQ6DctBC1VSipRB8GD/MWEVrFP42vjMyuYydxWxM8CZRt+yiNuuoHg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.29.2", "@babel/runtime": "^7.29.2",
@ -4345,7 +4381,7 @@
"use-sync-external-store": "^1.6.0" "use-sync-external-store": "^1.6.0"
}, },
"peerDependencies": { "peerDependencies": {
"i18next": ">= 26.0.1", "i18next": ">= 26.0.10",
"react": ">= 16.8.0", "react": ">= 16.8.0",
"typescript": "^5 || ^6" "typescript": "^5 || ^6"
}, },
@ -4362,9 +4398,9 @@
} }
}, },
"node_modules/react-is": { "node_modules/react-is": {
"version": "19.2.5", "version": "19.2.6",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz",
"integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==", "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-markdown": { "node_modules/react-markdown": {
@ -4418,9 +4454,9 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "7.14.2", "version": "7.15.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.2.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.1.tgz",
"integrity": "sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==", "integrity": "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cookie": "^1.0.1", "cookie": "^1.0.1",
@ -4440,12 +4476,12 @@
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "7.14.2", "version": "7.15.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.2.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.15.1.tgz",
"integrity": "sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==", "integrity": "sha512-AzF62gjY6U9rkMq4RfP/r2EVtQ7DMfNMjyOp/flLTCrtRylLiK4wT4pSq6O8rOXZ2eXdZYJPEYe+ifomiv+Igg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"react-router": "7.14.2" "react-router": "7.15.1"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"
@ -4573,14 +4609,14 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/rolldown": { "node_modules/rolldown": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
"integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@oxc-project/types": "=0.127.0", "@oxc-project/types": "=0.130.0",
"@rolldown/pluginutils": "1.0.0-rc.17" "@rolldown/pluginutils": "^1.0.0"
}, },
"bin": { "bin": {
"rolldown": "bin/cli.mjs" "rolldown": "bin/cli.mjs"
@ -4589,27 +4625,27 @@
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-android-arm64": "1.0.1",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.1",
"@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.1",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.1",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.1",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.1",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.1",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.1",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.1",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.1",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.1",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.1",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.1",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" "@rolldown/binding-win32-x64-msvc": "1.0.1"
} }
}, },
"node_modules/rolldown/node_modules/@rolldown/pluginutils": { "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.17", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -4816,16 +4852,16 @@
} }
}, },
"node_modules/typescript-eslint": { "node_modules/typescript-eslint": {
"version": "8.59.1", "version": "8.59.3",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.1.tgz", "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz",
"integrity": "sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ==", "integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/eslint-plugin": "8.59.1", "@typescript-eslint/eslint-plugin": "8.59.3",
"@typescript-eslint/parser": "8.59.1", "@typescript-eslint/parser": "8.59.3",
"@typescript-eslint/typescript-estree": "8.59.1", "@typescript-eslint/typescript-estree": "8.59.3",
"@typescript-eslint/utils": "8.59.1" "@typescript-eslint/utils": "8.59.3"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -4840,9 +4876,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.19.2", "version": "7.24.6",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -5034,16 +5070,16 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "8.0.10", "version": "8.0.13",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
"integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lightningcss": "^1.32.0", "lightningcss": "^1.32.0",
"picomatch": "^4.0.4", "picomatch": "^4.0.4",
"postcss": "^8.5.10", "postcss": "^8.5.14",
"rolldown": "1.0.0-rc.17", "rolldown": "1.0.1",
"tinyglobby": "^0.2.16" "tinyglobby": "^0.2.16"
}, },
"bin": { "bin": {
@ -5060,7 +5096,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0", "@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.1.0", "@vitejs/devtools": "^0.1.18",
"esbuild": "^0.27.0 || ^0.28.0", "esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0", "jiti": ">=1.21.0",
"less": "^4.0.0", "less": "^4.0.0",

View File

@ -11,39 +11,40 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@marsidev/react-turnstile": "^1.5.1", "@marsidev/react-turnstile": "^1.5.2",
"@vnedyalk0v/react19-simple-maps": "^2.0.7", "@vnedyalk0v/react19-simple-maps": "^2.0.7",
"axios": "^1.15.2", "axios": "^1.16.1",
"d3-geo": "^3.1.1", "d3-geo": "^3.1.1",
"d3-scale": "^4.0.2", "d3-scale": "^4.0.2",
"i18next": "^26.0.8", "i18next": "^26.1.0",
"i18next-browser-languagedetector": "^8.2.1", "i18next-browser-languagedetector": "^8.2.1",
"lucide-react": "^1.14.0", "lucide-react": "^1.16.0",
"or": "^0.2.0", "or": "^0.2.0",
"react": "^19.2.5", "qrcode.react": "^4.2.0",
"react-dom": "^19.2.5", "react": "^19.2.6",
"react-i18next": "^17.0.6", "react-dom": "^19.2.6",
"react-is": "^19.2.5", "react-i18next": "^17.0.7",
"react-is": "^19.2.6",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-router-dom": "^7.14.2", "react-router-dom": "^7.15.1",
"recharts": "^3.8.1", "recharts": "^3.8.1",
"remark-gfm": "^4.0.1" "remark-gfm": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@types/node": "^25.6.0", "@types/node": "^25.8.0",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-react": "^6.0.1",
"eslint": "^10.2.1", "eslint": "^10.3.0",
"eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2", "eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.5.0", "globals": "^17.6.0",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"typescript-eslint": "^8.59.1", "typescript-eslint": "^8.59.3",
"vite": "^8.0.10" "vite": "^8.0.13"
}, },
"overrides": { "overrides": {
"vite": "^8.0.10" "vite": "^8.0.13"
} }
} }

View File

@ -381,7 +381,6 @@ h3 {
gap: 1.5rem; gap: 1.5rem;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
overflow: hidden;
} }
.plugin-sidebar { .plugin-sidebar {
@ -406,11 +405,13 @@ h3 {
.plugin-title { .plugin-title {
font-size: 2.25rem; font-size: 2.25rem;
word-break: break-word;
overflow-wrap: break-word;
} }
.plugin-detail-header { .plugin-detail-header {
flex-direction: row; flex-direction: row;
align-items: center; align-items: flex-start;
gap: 1.25rem; gap: 1.25rem;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
@ -1458,6 +1459,11 @@ h3 {
margin-bottom: 3rem; margin-bottom: 3rem;
} }
.plugin-header-info {
flex: 1;
min-width: 0;
}
.plugin-detail-layout { .plugin-detail-layout {
display: grid; display: grid;
grid-template-columns: 1fr 400px; grid-template-columns: 1fr 400px;
@ -1468,6 +1474,8 @@ h3 {
.plugin-title { .plugin-title {
font-size: 3.5rem; font-size: 3.5rem;
text-transform: uppercase; text-transform: uppercase;
overflow-wrap: break-word;
word-break: break-word;
} }
.plugin-description { .plugin-description {
@ -1521,6 +1529,26 @@ h3 {
border: 1px solid var(--border); border: 1px solid var(--border);
} }
.markdown-content table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1.5rem;
display: block;
overflow-x: auto;
}
.markdown-content th,
.markdown-content td {
padding: 0.75rem 1rem;
border: 1px solid var(--border);
}
.markdown-content th {
background: rgba(255, 255, 255, 0.05);
font-weight: 700;
text-align: left;
}
.markdown-content pre code { .markdown-content pre code {
background: none; background: none;
padding: 0; padding: 0;

View File

@ -8,6 +8,7 @@ import RegisterPage from './pages/auth/RegisterPage';
import ForgotPasswordPage from './pages/auth/ForgotPasswordPage'; import ForgotPasswordPage from './pages/auth/ForgotPasswordPage';
import ResetPasswordPage from './pages/auth/ResetPasswordPage'; import ResetPasswordPage from './pages/auth/ResetPasswordPage';
import VerifyEmailPage from './pages/auth/VerifyEmailPage'; import VerifyEmailPage from './pages/auth/VerifyEmailPage';
import ConfirmEmailChangePage from './pages/auth/ConfirmEmailChangePage';
import CheckEmailPage from './pages/auth/CheckEmailPage'; import CheckEmailPage from './pages/auth/CheckEmailPage';
import ProfilePage from './pages/ProfilePage'; import ProfilePage from './pages/ProfilePage';
import AuthorProfilePage from './pages/AuthorProfilePage'; import AuthorProfilePage from './pages/AuthorProfilePage';
@ -125,6 +126,7 @@ const App = () => (
<Route path="forgot-password" element={<ForgotPasswordPage />} /> <Route path="forgot-password" element={<ForgotPasswordPage />} />
<Route path="reset-password" element={<ResetPasswordPage />} /> <Route path="reset-password" element={<ResetPasswordPage />} />
<Route path="verify-email" element={<VerifyEmailPage />} /> <Route path="verify-email" element={<VerifyEmailPage />} />
<Route path="confirm-email" element={<ConfirmEmailChangePage />} />
<Route path="check-email" element={<CheckEmailPage />} /> <Route path="check-email" element={<CheckEmailPage />} />
<Route path="dashboard" element={<ProtectedRoute><DashboardLayout /></ProtectedRoute>}> <Route path="dashboard" element={<ProtectedRoute><DashboardLayout /></ProtectedRoute>}>
<Route index element={<DashboardOverview />} /> <Route index element={<DashboardOverview />} />

View File

@ -494,7 +494,7 @@ const PluginDetail = () => {
> >
{!plugin.preview_path && plugin.name.charAt(0)} {!plugin.preview_path && plugin.name.charAt(0)}
</div> </div>
<div> <div className="plugin-header-info">
<h1 className="plugin-title">{plugin.name}</h1> <h1 className="plugin-title">{plugin.name}</h1>
<p className="dev-name">by <Link to={`/profile/${plugin.dev_name}`} style={{ color: 'inherit', textDecoration: 'none' }} onMouseEnter={e => (e.currentTarget.style.textDecoration = 'underline')} onMouseLeave={e => (e.currentTarget.style.textDecoration = 'none')}>{plugin.dev_name}</Link></p> <p className="dev-name">by <Link to={`/profile/${plugin.dev_name}`} style={{ color: 'inherit', textDecoration: 'none' }} onMouseEnter={e => (e.currentTarget.style.textDecoration = 'underline')} onMouseLeave={e => (e.currentTarget.style.textDecoration = 'none')}>{plugin.dev_name}</Link></p>
</div> </div>

View File

@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useNavigate, useLocation, Link } from 'react-router-dom'; import { useNavigate, useLocation, Link } from 'react-router-dom';
import { QRCodeSVG } from 'qrcode.react';
import api from '../api'; import api from '../api';
import { useAuth } from '../App'; import { useAuth } from '../App';
@ -30,7 +31,7 @@ const ProfilePage = () => {
else setActiveTab('accountDetails'); else setActiveTab('accountDetails');
}, [location]); }, [location]);
const [formData, setFormData] = useState({ username: '', email: '', password: '' }); const [formData, setFormData] = useState({ username: '', email: '', password: '', currentPassword: '', disable2faPassword: '' });
const [message, setMessage] = useState({ text: '', type: '' }); const [message, setMessage] = useState({ text: '', type: '' });
const [isStripeLoading, setIsStripeLoading] = useState(false); const [isStripeLoading, setIsStripeLoading] = useState(false);
@ -39,11 +40,17 @@ const ProfilePage = () => {
const [libraryLoading, setLibraryLoading] = useState(false); const [libraryLoading, setLibraryLoading] = useState(false);
const [libraryError, setLibraryError] = useState<string | null>(null); const [libraryError, setLibraryError] = useState<string | null>(null);
// 2FA state
const [is2faEnabled, setIs2faEnabled] = useState(false);
const [setup2faData, setSetup2faData] = useState<{ uri: string, secret: string } | null>(null);
const [verify2faCode, setVerify2faCode] = useState('');
useEffect(() => { refreshUser?.(); }, []); useEffect(() => { refreshUser?.(); }, []);
useEffect(() => { useEffect(() => {
if (user) { if (user) {
setFormData(prev => ({ ...prev, username: user.username || '', email: user.email || '' })); setFormData(prev => ({ ...prev, username: user.username || '', email: user.email || '' }));
setIs2faEnabled(user.totp_enabled || false);
} }
}, [user]); }, [user]);
@ -109,6 +116,46 @@ const ProfilePage = () => {
} }
}; };
const start2faSetup = async () => {
try {
const res = await api.post('/user/2fa/setup');
setSetup2faData(res.data);
} catch (err: any) {
showMessage(err.response?.data?.error || 'Failed to start 2FA setup.', true);
if (err.response?.status === 400 && err.response?.data?.error === '2FA is already enabled') {
setIs2faEnabled(true);
refreshUser?.();
}
}
};
const confirm2faSetup = async (e: React.FormEvent) => {
e.preventDefault();
try {
await api.post('/user/2fa/verify', { code: verify2faCode });
setIs2faEnabled(true);
setSetup2faData(null);
setVerify2faCode('');
showMessage('2FA enabled successfully!');
refreshUser?.();
} catch (err: any) {
showMessage(err.response?.data?.error || 'Invalid 2FA code.', true);
}
};
const disable2fa = async (e: React.FormEvent) => {
e.preventDefault();
try {
await api.post('/user/2fa/disable', { password: formData.disable2faPassword });
setIs2faEnabled(false);
setFormData({ ...formData, disable2faPassword: '' });
showMessage('2FA has been disabled.');
refreshUser?.();
} catch (err: any) {
showMessage(err.response?.data?.error || 'Failed to disable 2FA.', true);
}
};
return ( return (
<div className="container profile-page"> <div className="container profile-page">
<h1 className="page-title">User <span>Profile</span></h1> <h1 className="page-title">User <span>Profile</span></h1>
@ -328,11 +375,54 @@ const ProfilePage = () => {
{/* ── Security ── */} {/* ── Security ── */}
{activeTab === 'security' && ( {activeTab === 'security' && (
<div className="profile-section"> <div className="profile-section">
<h2 className="section-title"><span>Two-Factor</span> Authentication</h2>
<div style={{ background: 'var(--surface-light)', padding: '1.5rem', borderRadius: '12px', border: '1px solid var(--border)', marginBottom: '3rem' }}>
{is2faEnabled ? (
<div>
<p style={{ color: 'var(--success)', fontWeight: 600, marginBottom: '1rem' }}>✓ Two-Factor Authentication is currently enabled.</p>
<form onSubmit={disable2fa} style={{ marginTop: '1rem' }}>
<p style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginBottom: '1rem' }}>Enter your password to disable 2FA.</p>
<div className="form-group">
<input type="password" placeholder="Current Password" value={formData.disable2faPassword} onChange={e => setFormData({ ...formData, disable2faPassword: e.target.value })} required />
</div>
<button type="submit" className="btn btn-secondary" style={{ color: '#ff4d4d', borderColor: '#ff4d4d' }}>Disable 2FA</button>
</form>
</div>
) : (
<div>
<p style={{ marginBottom: '1rem', color: 'var(--text-muted)' }}>Protect your account with Two-Factor Authentication using an app like Google Authenticator or Authy.</p>
{!setup2faData ? (
<button className="btn" onClick={start2faSetup}>Set Up 2FA</button>
) : (
<div style={{ marginTop: '1rem' }}>
<p style={{ fontSize: '0.9rem', marginBottom: '1rem' }}>1. Scan this QR code with your authenticator app:</p>
<div style={{ background: 'white', padding: '1rem', display: 'inline-block', borderRadius: '8px', marginBottom: '1rem' }}>
<QRCodeSVG value={setup2faData.uri} size={150} />
</div>
<p style={{ fontSize: '0.8rem', color: 'var(--text-muted)', marginBottom: '1.5rem' }}>Or enter this code manually: <strong style={{ fontFamily: 'var(--font-mono)' }}>{setup2faData.secret}</strong></p>
<form onSubmit={confirm2faSetup}>
<p style={{ fontSize: '0.9rem', marginBottom: '0.5rem' }}>2. Enter the 6-digit code from your app:</p>
<div className="form-group" style={{ display: 'flex', gap: '0.5rem', maxWidth: '300px' }}>
<input type="text" placeholder="000000" maxLength={6} value={verify2faCode} onChange={e => setVerify2faCode(e.target.value.replace(/\D/g, ''))} required style={{ fontFamily: 'var(--font-mono)', fontSize: '1.2rem', letterSpacing: '0.2em', textAlign: 'center' }} />
<button type="submit" className="btn">Verify</button>
</div>
</form>
</div>
)}
</div>
)}
</div>
<h2 className="section-title"><span>Change</span> Password</h2> <h2 className="section-title"><span>Change</span> Password</h2>
<form onSubmit={(e) => handleUpdate(e, '/user/settings', { newPassword: formData.password })}> <form onSubmit={(e) => handleUpdate(e, '/user/settings', { currentPassword: formData.currentPassword, newPassword: formData.password })}>
<div className="form-group">
<label>CURRENT PASSWORD</label>
<input type="password" value={formData.currentPassword} onChange={e => setFormData({ ...formData, currentPassword: e.target.value })} required />
</div>
<div className="form-group"> <div className="form-group">
<label>NEW PASSWORD</label> <label>NEW PASSWORD</label>
<input type="password" value={formData.password} onChange={e => setFormData({ ...formData, password: e.target.value })} /> <input type="password" value={formData.password} onChange={e => setFormData({ ...formData, password: e.target.value })} required />
</div> </div>
<button type="submit" className="btn">Update Password</button> <button type="submit" className="btn">Update Password</button>
</form> </form>

View File

@ -0,0 +1,66 @@
import { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import api from '../../api';
import { useAuth } from '../../App';
const ConfirmEmailChangePage = () => {
const [status, setStatus] = useState<'verifying' | 'success' | 'error'>('verifying');
const [error, setError] = useState<string | null>(null);
const location = useLocation();
const navigate = useNavigate();
const { login } = useAuth();
useEffect(() => {
const verify = async () => {
const params = new URLSearchParams(location.search);
const token = params.get('token');
if (!token) {
setStatus('error');
setError('No verification token found.');
return;
}
try {
const res = await api.post('/user/confirm-email-change', { token });
if (res.data.token) {
login(res.data.token);
}
setStatus('success');
setTimeout(() => {
navigate('/profile');
}, 3000);
} catch (err: any) {
setStatus('error');
setError(err.response?.data?.error || 'Verification failed. The link may be invalid or expired.');
}
};
verify();
}, [location, navigate, login]);
return (
<div style={{ textAlign: 'center', padding: '50px' }}>
{status === 'verifying' && <h1>Verifying your email change...</h1>}
{status === 'success' && (
<div>
<h1>Email Updated Successfully!</h1>
<p>Your new email is now active. Redirecting to your profile...</p>
</div>
)}
{status === 'error' && (
<div>
<h1>Update Failed</h1>
<p>{error}</p>
<p>Please try updating your email again from your profile page.</p>
</div>
)}
</div>
);
};
export default ConfirmEmailChangePage;

View File

@ -1,6 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Turnstile } from '@marsidev/react-turnstile';
import api from '../../api'; import api from '../../api';
import { useAuth } from '../../App'; import { useAuth } from '../../App';
@ -11,6 +12,13 @@ const LoginPage = () => {
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [emailTouched, setEmailTouched] = useState(false); const [emailTouched, setEmailTouched] = useState(false);
const [emailValid, setEmailValid] = useState<boolean | null>(null); const [emailValid, setEmailValid] = useState<boolean | null>(null);
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
// 2FA login state
const [requires2fa, setRequires2fa] = useState(false);
const [tempToken, setTempToken] = useState<string | null>(null);
const [totpCode, setTotpCode] = useState('');
const navigate = useNavigate(); const navigate = useNavigate();
const { login } = useAuth(); const { login } = useAuth();
@ -34,6 +42,24 @@ const LoginPage = () => {
e.preventDefault(); e.preventDefault();
if (form.website) return; if (form.website) return;
if (requires2fa) {
setIsSubmitting(true);
setError('');
try {
const res = await api.post('/auth/login/2fa', {
temp_token: tempToken,
code: totpCode,
});
login(res.data.token);
navigate('/dashboard');
} catch (err: any) {
setError(err.response?.data?.error || 'Invalid 2FA code.');
} finally {
setIsSubmitting(false);
}
return;
}
setEmailTouched(true); setEmailTouched(true);
const isEmailValid = validateEmail(form.email); const isEmailValid = validateEmail(form.email);
setEmailValid(isEmailValid); setEmailValid(isEmailValid);
@ -43,15 +69,27 @@ const LoginPage = () => {
return; return;
} }
if (!captchaToken) {
setError('Please complete the security check.');
return;
}
setIsSubmitting(true); setIsSubmitting(true);
setError(''); setError('');
try { try {
const res = await api.post('/auth/login', { const res = await api.post('/auth/login', {
email: form.email, email: form.email,
password: form.password, password: form.password,
captchaToken,
}); });
if (res.data.requires_2fa) {
setRequires2fa(true);
setTempToken(res.data.temp_token);
} else {
login(res.data.token); login(res.data.token);
navigate('/dashboard'); navigate('/dashboard');
}
} catch (err: any) { } catch (err: any) {
setError(err.response?.data?.error || 'Invalid credentials.'); setError(err.response?.data?.error || 'Invalid credentials.');
} finally { } finally {
@ -313,6 +351,28 @@ const LoginPage = () => {
<input type="text" style={{ display: 'none' }} tabIndex={-1} <input type="text" style={{ display: 'none' }} tabIndex={-1}
onChange={e => setForm({ ...form, website: e.target.value })} /> onChange={e => setForm({ ...form, website: e.target.value })} />
{requires2fa ? (
<div className="form-field">
<label className="field-label" htmlFor="totpCode">Authentication Code</label>
<p style={{ fontSize: '13px', color: '#697386', marginBottom: '12px' }}>Enter the 6-digit code from your authenticator app.</p>
<div className="field-input-wrapper">
<input
id="totpCode"
className="login-input no-icon"
type="text"
placeholder="000000"
maxLength={6}
value={totpCode}
onChange={e => setTotpCode(e.target.value.replace(/\D/g, ''))}
required
disabled={isSubmitting}
autoFocus
style={{ letterSpacing: '0.2em', textAlign: 'center', fontSize: '1.2rem', fontFamily: 'var(--font-mono, monospace)' }}
/>
</div>
</div>
) : (
<>
<div className="form-field"> <div className="form-field">
<label className="field-label" htmlFor="email">{t('auth.email')}</label> <label className="field-label" htmlFor="email">{t('auth.email')}</label>
<div className="field-input-wrapper"> <div className="field-input-wrapper">
@ -371,10 +431,21 @@ const LoginPage = () => {
/> />
</div> </div>
</div> </div>
</>
)}
{!requires2fa && (
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: '20px', marginTop: '4px' }}>
<Turnstile siteKey="0x4AAAAAAClcSibyhKfR0H6o"
onSuccess={(token) => setCaptchaToken(token)} options={{ theme: 'light', appearance: 'interaction-only' } as any} />
</div>
)}
<button className="submit-btn" type="submit" disabled={isSubmitting}> <button className="submit-btn" type="submit" disabled={isSubmitting}>
{isSubmitting ? ( {isSubmitting ? (
<><span className="spinner" />{t('loading')}</> <><span className="spinner" />{t('loading')}</>
) : requires2fa ? (
'Verify'
) : ( ) : (
t('auth.sign_in') t('auth.sign_in')
)} )}

View File

@ -415,7 +415,7 @@ const RegisterPage = () => {
<div className="captcha-container"> <div className="captcha-container">
<Turnstile siteKey="0x4AAAAAAClcSibyhKfR0H6o" <Turnstile siteKey="0x4AAAAAAClcSibyhKfR0H6o"
onSuccess={(token) => setCaptchaToken(token)} options={{ theme: 'light' } as any} /> onSuccess={(token) => setCaptchaToken(token)} options={{ theme: 'light', appearance: 'interaction-only' } as any} />
</div> </div>
<button className="submit-btn" type="submit" disabled={isSubmitting}> <button className="submit-btn" type="submit" disabled={isSubmitting}>