diff --git a/package.json b/package.json index 96aa7e033..5549f03e1 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,16 @@ "next-intl": "4.13.0", "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", + "otplib": "^13.4.0", + "papaparse": "^5.5.3", + "pg": "^8.20.0", + "prisma": "^7.7.0", + "pure-rand": "^8.4.0", + "qrcode": "^1.5.4", + "react": "^19.2.5", + "react-dom": "^19.2.5", + "react-error-boundary": "^6.1.1", + "react-resizable-panels": "^4.10.0", "papaparse": "^5.5.4", "pg": "^8.22.0", "prisma": "^7.8.0", @@ -134,6 +144,10 @@ "@rollup/plugin-replace": "^6.0.3", "@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-typescript": "^12.3.0", + "@types/jest": "^30.0.0", + "@types/node": "^25.6.0", + "@types/qrcode": "^1.5.6", + "@types/react": "^19.2.14", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da88a5ce2..e71239a53 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,6 +137,9 @@ importers: npm-run-all: specifier: ^4.1.5 version: 4.1.5 + otplib: + specifier: ^13.4.0 + version: 13.4.0 papaparse: specifier: ^5.5.4 version: 5.5.4 @@ -247,8 +250,11 @@ importers: specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.1) '@types/node': - specifier: ^26.0.1 - version: 26.0.1 + specifier: ^25.6.0 + version: 25.6.0 + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 '@types/react': specifier: ^19.2.17 version: 19.2.17 @@ -470,6 +476,97 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.28.6': + resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/runtime@7.29.2': resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} @@ -1804,6 +1901,10 @@ packages: cpu: [x64] os: [win32] + '@noble/hashes@2.2.0': + resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==} + engines: {node: '>= 20.19.0'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1816,20 +1917,23 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@open-draft/deferred-promise@2.2.0': - resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + '@otplib/core@13.4.0': + resolution: {integrity: sha512-JqOGcvZQi2wIkEQo8f3/iAjstavpXy6gouIDMHygjNuH6Q0FjbHOiXMdcE94RwfgDNMABhzwUmvaPsxvgm9NYw==} - '@open-draft/deferred-promise@3.0.0': - resolution: {integrity: sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==} + '@otplib/hotp@13.4.0': + resolution: {integrity: sha512-MJjE0x06mn2ptymz5qZmQveb+vWFuaIftqE0b5/TZZqUOK7l97cV8lRTmid5BpAQMwJDNLW6RnYxGeCRiNdekw==} - '@open-draft/logger@0.3.0': - resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + '@otplib/plugin-base32-scure@13.4.0': + resolution: {integrity: sha512-/t9YWJmMbB8bF5z8mXrBZc2FXBe8B/3hG5FhWr9K8cFwFhyxScbPysmZe8s1UTzSA6N+s8Uv8aIfCtVXPNjJWw==} - '@open-draft/until@2.1.0': - resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@otplib/plugin-crypto-noble@13.4.0': + resolution: {integrity: sha512-KrvE4m7Zv+TT1944HzgqFJWJpKb6AyoxDbvhPStmBqdMlv5Gekb80d66cuFRL08kkPgJ5gXUSb5SFpYeB+bACg==} - '@oxc-project/types@0.128.0': - resolution: {integrity: sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==} + '@otplib/totp@13.4.0': + resolution: {integrity: sha512-dK+vl0f0ekzf6mCENRI9AKS2NJUC7OjI3+X8e7QSnhQ2WM7I+i4PGpb3QxKi5hxjTtwVuoZwXR2CFtXdcRtNdQ==} + + '@otplib/uri@13.4.0': + resolution: {integrity: sha512-x1ozBa5bPbdZCrrTL/HK21qchiK7jYElTu+0ft22abeEhiLYgH1+SIULvOcVk3CK8YwF4kdcidvkq4ciejucJA==} '@parcel/watcher-android-arm64@2.5.6': resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} @@ -2506,6 +2610,12 @@ packages: '@schummar/icu-type-parser@1.21.5': resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==} + '@scure/base@2.2.0': + resolution: {integrity: sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==} + + '@sinclair/typebox@0.34.40': + resolution: {integrity: sha512-gwBNIP8ZAYev/ORDWW0QvxdwPXwxBtLsdsJgSc7eDIRt8ubP+rxUBzPsrwnu16fgEF8Bx4lh/+mvQvJzcTM6Kw==} + '@sindresorhus/merge-streams@2.3.0': resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} @@ -2796,6 +2906,9 @@ packages: '@types/pg@8.20.0': resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + '@types/qrcode@1.5.6': + resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -3110,6 +3223,9 @@ packages: client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -3386,6 +3502,17 @@ packages: supports-color: optional: true + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} @@ -3437,6 +3564,9 @@ packages: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -4542,6 +4672,9 @@ packages: outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + otplib@13.4.0: + resolution: {integrity: sha512-RUcYcRMCgRWhUE/XabRppXpUwCwaWBNHe5iPXhdvP8wwDGpGpsIf/kxX/ec3zFsOaM1Oq8lEhUqDwk6W7DHkwg==} + own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -4715,6 +4848,10 @@ packages: engines: {node: '>=18'} hasBin: true + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + po-parser@2.1.1: resolution: {integrity: sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==} @@ -5199,6 +5336,21 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + pure-rand@7.0.1: + resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + + pure-rand@8.4.0: + resolution: {integrity: sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==} + + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + + qs@6.14.2: + resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} + engines: {node: '>=0.6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -5342,6 +5494,13 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -5494,6 +5653,8 @@ packages: resolution: {integrity: sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==} engines: {node: '>=20.0.0'} + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} set-cookie-parser@3.1.0: resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} @@ -5769,6 +5930,12 @@ packages: engines: {node: '>=10'} hasBin: true + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + thenby@1.4.0: + resolution: {integrity: sha512-D+BkEl5MpeTbQ1Rz0io/TLZmwmYUAgvUpS1r2eKUvPicYOnpqzjZE6bVa7rSEYv189zkSM+CVc+hG5X7nVe9LQ==} thenby@1.4.1: resolution: {integrity: sha512-D5a/bO0KdalOE3q8MlrRmSxjbKZHT3MQmXkJP+r97Vw8MMwOZKOwUSEyTtK7eSMj2y0kyAjpYMRMZmmLw1FtNQ==} @@ -5962,6 +6129,12 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@13.0.0: + resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} uuid@14.0.1: resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} hasBin: true @@ -6100,6 +6273,9 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which-typed-array@1.1.19: resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} engines: {node: '>= 0.4'} @@ -6140,6 +6316,9 @@ packages: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -6155,10 +6334,23 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -6392,6 +6584,73 @@ snapshots: '@babel/parser@7.29.7': dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 '@babel/types': 7.29.7 optional: true @@ -7475,6 +7734,8 @@ snapshots: '@next/swc-win32-x64-msvc@16.2.9': optional: true + '@noble/hashes@2.2.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -7487,6 +7748,32 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@otplib/core@13.4.0': {} + + '@otplib/hotp@13.4.0': + dependencies: + '@otplib/core': 13.4.0 + '@otplib/uri': 13.4.0 + + '@otplib/plugin-base32-scure@13.4.0': + dependencies: + '@otplib/core': 13.4.0 + '@scure/base': 2.2.0 + + '@otplib/plugin-crypto-noble@13.4.0': + dependencies: + '@noble/hashes': 2.2.0 + '@otplib/core': 13.4.0 + + '@otplib/totp@13.4.0': + dependencies: + '@otplib/core': 13.4.0 + '@otplib/hotp': 13.4.0 + '@otplib/uri': 13.4.0 + + '@otplib/uri@13.4.0': + dependencies: + '@otplib/core': 13.4.0 '@open-draft/deferred-promise@2.2.0': {} '@open-draft/deferred-promise@3.0.0': {} @@ -8035,6 +8322,9 @@ snapshots: '@rrweb/types': 2.0.1 rrweb: 2.0.1 + '@scure/base@2.2.0': {} + + '@sinclair/typebox@0.34.40': {} '@rrweb/types@2.0.1': {} '@rrweb/utils@2.0.1': {} @@ -8319,6 +8609,11 @@ snapshots: pg-protocol: 1.15.0 pg-types: 2.2.0 + '@types/qrcode@1.5.6': + dependencies: + '@types/node': 25.6.0 + + '@types/react-dom@19.2.3(@types/react@19.2.14)': '@types/react-dom@19.2.3(@types/react@19.2.17)': dependencies: '@types/react': 19.2.17 @@ -8633,6 +8928,12 @@ snapshots: client-only@0.0.1: {} + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -8929,6 +9230,9 @@ snapshots: dependencies: ms: 2.1.3 + decamelize@1.2.0: {} + + dedent@1.7.2: {} decimal.js@10.6.0: {} deepmerge-ts@7.1.5: {} @@ -8973,6 +9277,8 @@ snapshots: diff@4.0.2: {} + dijkstrajs@1.0.3: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -9883,6 +10189,7 @@ snapshots: lru.min@1.1.4: {} + lucide-react@0.555.0(react@19.2.5): lucide-react@0.555.0(react@19.2.7): dependencies: react: 19.2.7 @@ -10147,6 +10454,15 @@ snapshots: outvariant@1.4.3: {} + otplib@13.4.0: + dependencies: + '@otplib/core': 13.4.0 + '@otplib/hotp': 13.4.0 + '@otplib/plugin-base32-scure': 13.4.0 + '@otplib/plugin-crypto-noble': 13.4.0 + '@otplib/totp': 13.4.0 + '@otplib/uri': 13.4.0 + own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -10286,6 +10602,7 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 + pngjs@5.0.0: {} playwright-core@1.61.1: {} playwright@1.61.1: @@ -10820,6 +11137,20 @@ snapshots: pure-rand@6.1.0: {} + pure-rand@7.0.1: {} + + pure-rand@8.4.0: {} + + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + + qs@6.14.2: + dependencies: + side-channel: 1.1.0 + queue-microtask@1.2.3: {} raf-schd@4.0.3: {} @@ -10880,6 +11211,12 @@ snapshots: '@types/react': 19.2.17 redux: 5.0.1 + react-resizable-panels@4.10.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + + react-simple-maps@3.0.0(prop-types@15.8.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): react-resizable-panels@4.11.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: react: 19.2.7 @@ -10909,6 +11246,9 @@ snapshots: dependencies: react: 19.2.7 optionalDependencies: + react-dom: 19.2.5(react@19.2.5) + + react-window@2.2.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5): react-dom: 19.2.7(react@19.2.7) react-window@2.2.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7): @@ -10988,6 +11328,12 @@ snapshots: require-from-string@2.0.2: {} + require-main-filename@2.0.0: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -11196,6 +11542,7 @@ snapshots: serialize-javascript@7.0.5: {} + set-blocking@2.0.0: {} set-cookie-parser@3.1.0: {} set-function-length@1.2.2: @@ -11509,6 +11856,13 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.6 + glob: 7.2.3 + minimatch: 3.1.5 + + thenby@1.4.0: {} thenby@1.4.1: {} thenify-all@1.6.0: @@ -11720,6 +12074,9 @@ snapshots: util-deprecate@1.0.2: {} + uuid@13.0.0: {} + + uuid@8.3.2: {} uuid@14.0.1: {} v8-compile-cache-lib@3.0.1: {} @@ -11827,6 +12184,8 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 + which-module@2.0.1: {} + which-typed-array@1.1.19: dependencies: available-typed-arrays: 1.0.7 @@ -11870,6 +12229,8 @@ snapshots: xtend@4.0.2: {} + y18n@4.0.3: {} + y18n@5.0.8: {} yallist@3.1.1: {} @@ -11878,8 +12239,30 @@ snapshots: yaml@1.10.2: {} + yaml@2.8.1: + optional: true + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + yargs-parser@21.1.1: {} + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + yargs@17.7.2: dependencies: cliui: 8.0.1 diff --git a/prisma/migrations/20_add_2fa/migration.sql b/prisma/migrations/20_add_2fa/migration.sql new file mode 100644 index 000000000..b3accc2df --- /dev/null +++ b/prisma/migrations/20_add_2fa/migration.sql @@ -0,0 +1,72 @@ +-- AlterTable +ALTER TABLE "team" ADD COLUMN "two_factor_required" BOOLEAN NOT NULL DEFAULT false; + +-- AlterTable +ALTER TABLE "user" ADD COLUMN "two_factor_required" BOOLEAN NOT NULL DEFAULT false; + +-- CreateTable +CREATE TABLE "two_factor_auth" ( + "id" TEXT NOT NULL, + "user_id" UUID NOT NULL, + "secret" TEXT NOT NULL, + "is_enabled" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6) NOT NULL, + + CONSTRAINT "two_factor_auth_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "two_factor_backup_code" ( + "id" TEXT NOT NULL, + "user_id" UUID NOT NULL, + "code_hash" TEXT NOT NULL, + "used" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "two_factor_backup_code_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "two_factor_otp_used" ( + "id" TEXT NOT NULL, + "user_id" UUID NOT NULL, + "otp" TEXT NOT NULL, + "expires_at" TIMESTAMPTZ(6) NOT NULL, + + CONSTRAINT "two_factor_otp_used_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "two_factor_rate_limit" ( + "id" TEXT NOT NULL, + "user_id" UUID NOT NULL, + "attempts" INTEGER NOT NULL DEFAULT 0, + "locked_until" TIMESTAMPTZ(6), + "updated_at" TIMESTAMPTZ(6) NOT NULL, + + CONSTRAINT "two_factor_rate_limit_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "app_setting" ( + "key" TEXT NOT NULL, + "value" TEXT NOT NULL, + + CONSTRAINT "app_setting_pkey" PRIMARY KEY ("key") +); + +-- CreateIndex +CREATE UNIQUE INDEX "two_factor_auth_user_id_key" ON "two_factor_auth"("user_id"); + +-- CreateIndex +CREATE INDEX "two_factor_backup_code_user_id_idx" ON "two_factor_backup_code"("user_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "two_factor_otp_used_user_id_otp_key" ON "two_factor_otp_used"("user_id", "otp"); + +-- CreateIndex +CREATE UNIQUE INDEX "two_factor_rate_limit_user_id_key" ON "two_factor_rate_limit"("user_id"); + +-- CreateIndex +CREATE INDEX "session_replay_visit_id_idx" ON "session_replay"("visit_id"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2a7c49e99..2dcbd89d2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,41 +10,44 @@ datasource db { } model User { - id String @id() @map("user_id") @db.Uuid - username String @unique @db.VarChar(255) - password String @db.VarChar(60) - role String @map("role") @db.VarChar(50) - logoUrl String? @map("logo_url") @db.VarChar(2183) - displayName String? @map("display_name") @db.VarChar(255) - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) - deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) - - websites Website[] @relation("user") - createdBy Website[] @relation("createUser") - links Link[] @relation("user") - pixels Pixel[] @relation("user") - teams TeamUser[] - reports Report[] - boards Board[] @relation("user") + id String @id @map("user_id") @db.Uuid + username String @unique @db.VarChar(255) + password String @db.VarChar(60) + role String @map("role") @db.VarChar(50) + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) + deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) + displayName String? @map("display_name") @db.VarChar(255) + logoUrl String? @map("logo_url") @db.VarChar(2183) + twoFactorRequired Boolean @default(false) @map("two_factor_required") + websites Website[] @relation("user") + createdBy Website[] @relation("createUser") + links Link[] @relation("user") + pixels Pixel[] @relation("user") + teams TeamUser[] + reports Report[] + boards Board[] @relation("user") + twoFactorAuth TwoFactorAuth? + twoFactorBackupCodes TwoFactorBackupCode[] + twoFactorOtpUsed TwoFactorOtpUsed[] + twoFactorRateLimit TwoFactorRateLimit? @@map("user") } model Session { - id String @id() @map("session_id") @db.Uuid - websiteId String @map("website_id") @db.Uuid - browser String? @db.VarChar(20) - os String? @db.VarChar(20) - device String? @db.VarChar(20) - screen String? @db.VarChar(11) - language String? @db.VarChar(35) - country String? @db.Char(2) - region String? @db.VarChar(20) - city String? @db.VarChar(50) - distinctId String? @map("distinct_id") @db.VarChar(50) - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - + id String @id @map("session_id") @db.Uuid + websiteId String @map("website_id") @db.Uuid + browser String? @db.VarChar(20) + os String? @db.VarChar(20) + device String? @db.VarChar(20) + screen String? @db.VarChar(11) + language String? @db.VarChar(35) + country String? @db.Char(2) + region String? @db.VarChar(20) + city String? @db.VarChar(50) + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + distinctId String? @map("distinct_id") @db.VarChar(50) websiteEvents WebsiteEvent[] sessionData SessionData[] revenue Revenue[] @@ -109,40 +112,39 @@ model Website { } model WebsiteEvent { - id String @id() @map("event_id") @db.Uuid - websiteId String @map("website_id") @db.Uuid - sessionId String @map("session_id") @db.Uuid - visitId String @map("visit_id") @db.Uuid - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - urlPath String @map("url_path") @db.VarChar(500) - urlQuery String? @map("url_query") @db.VarChar(500) - utmSource String? @map("utm_source") @db.VarChar(255) - utmMedium String? @map("utm_medium") @db.VarChar(255) - utmCampaign String? @map("utm_campaign") @db.VarChar(255) - utmContent String? @map("utm_content") @db.VarChar(255) - utmTerm String? @map("utm_term") @db.VarChar(255) - referrerPath String? @map("referrer_path") @db.VarChar(500) - referrerQuery String? @map("referrer_query") @db.VarChar(500) - referrerDomain String? @map("referrer_domain") @db.VarChar(500) - pageTitle String? @map("page_title") @db.VarChar(500) - gclid String? @db.VarChar(255) - fbclid String? @db.VarChar(255) - msclkid String? @db.VarChar(255) - ttclid String? @db.VarChar(255) - lifatid String? @map("li_fat_id") @db.VarChar(255) - twclid String? @db.VarChar(255) - eventType Int @default(1) @map("event_type") @db.Integer - eventName String? @map("event_name") @db.VarChar(50) - tag String? @db.VarChar(50) - hostname String? @db.VarChar(100) - lcp Decimal? @db.Decimal(10, 1) - inp Decimal? @db.Decimal(10, 1) - cls Decimal? @db.Decimal(10, 4) - fcp Decimal? @db.Decimal(10, 1) - ttfb Decimal? @db.Decimal(10, 1) - - eventData EventData[] - session Session @relation(fields: [sessionId], references: [id]) + id String @id @map("event_id") @db.Uuid + websiteId String @map("website_id") @db.Uuid + sessionId String @map("session_id") @db.Uuid + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + urlPath String @map("url_path") @db.VarChar(500) + urlQuery String? @map("url_query") @db.VarChar(500) + referrerPath String? @map("referrer_path") @db.VarChar(500) + referrerQuery String? @map("referrer_query") @db.VarChar(500) + referrerDomain String? @map("referrer_domain") @db.VarChar(500) + pageTitle String? @map("page_title") @db.VarChar(500) + eventType Int @default(1) @map("event_type") + eventName String? @map("event_name") @db.VarChar(50) + visitId String @map("visit_id") @db.Uuid + tag String? @db.VarChar(50) + fbclid String? @db.VarChar(255) + gclid String? @db.VarChar(255) + lifatid String? @map("li_fat_id") @db.VarChar(255) + msclkid String? @db.VarChar(255) + ttclid String? @db.VarChar(255) + twclid String? @db.VarChar(255) + utmCampaign String? @map("utm_campaign") @db.VarChar(255) + utmContent String? @map("utm_content") @db.VarChar(255) + utmMedium String? @map("utm_medium") @db.VarChar(255) + utmSource String? @map("utm_source") @db.VarChar(255) + utmTerm String? @map("utm_term") @db.VarChar(255) + hostname String? @db.VarChar(100) + cls Decimal? @db.Decimal(10, 4) + fcp Decimal? @db.Decimal(10, 1) + inp Decimal? @db.Decimal(10, 1) + lcp Decimal? @db.Decimal(10, 1) + ttfb Decimal? @db.Decimal(10, 1) + eventData EventData[] + session Session @relation(fields: [sessionId], references: [id]) @@index([createdAt]) @@index([sessionId]) @@ -162,18 +164,17 @@ model WebsiteEvent { } model EventData { - id String @id() @map("event_data_id") @db.Uuid - websiteId String @map("website_id") @db.Uuid - websiteEventId String @map("website_event_id") @db.Uuid - dataKey String @map("data_key") @db.VarChar(500) - stringValue String? @map("string_value") @db.VarChar(500) - numberValue Decimal? @map("number_value") @db.Decimal(19, 4) - dateValue DateTime? @map("date_value") @db.Timestamptz(6) - dataType Int @map("data_type") @db.Integer - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - - website Website @relation(fields: [websiteId], references: [id]) - websiteEvent WebsiteEvent @relation(fields: [websiteEventId], references: [id]) + id String @id @map("event_data_id") @db.Uuid + websiteId String @map("website_id") @db.Uuid + websiteEventId String @map("website_event_id") @db.Uuid + dataKey String @map("data_key") @db.VarChar(500) + stringValue String? @map("string_value") @db.VarChar(500) + numberValue Decimal? @map("number_value") @db.Decimal(19, 4) + dateValue DateTime? @map("date_value") @db.Timestamptz(6) + dataType Int @map("data_type") + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + website Website @relation(fields: [websiteId], references: [id]) + websiteEvent WebsiteEvent @relation(fields: [websiteEventId], references: [id]) @@index([createdAt]) @@index([websiteId]) @@ -184,19 +185,18 @@ model EventData { } model SessionData { - id String @id() @map("session_data_id") @db.Uuid + id String @id @map("session_data_id") @db.Uuid websiteId String @map("website_id") @db.Uuid sessionId String @map("session_id") @db.Uuid dataKey String @map("data_key") @db.VarChar(500) stringValue String? @map("string_value") @db.VarChar(500) numberValue Decimal? @map("number_value") @db.Decimal(19, 4) dateValue DateTime? @map("date_value") @db.Timestamptz(6) - dataType Int @map("data_type") @db.Integer - distinctId String? @map("distinct_id") @db.VarChar(50) + dataType Int @map("data_type") createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - - website Website @relation(fields: [websiteId], references: [id]) - session Session @relation(fields: [sessionId], references: [id]) + distinctId String? @map("distinct_id") @db.VarChar(50) + website Website @relation(fields: [websiteId], references: [id]) + session Session @relation(fields: [sessionId], references: [id]) @@index([createdAt]) @@index([websiteId]) @@ -207,34 +207,33 @@ model SessionData { } model Team { - id String @id() @map("team_id") @db.Uuid - name String @db.VarChar(50) - accessCode String? @unique @map("access_code") @db.VarChar(50) - logoUrl String? @map("logo_url") @db.VarChar(2183) - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) - deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) - - websites Website[] - members TeamUser[] - links Link[] - pixels Pixel[] - boards Board[] + id String @id @map("team_id") @db.Uuid + name String @db.VarChar(50) + accessCode String? @unique @map("access_code") @db.VarChar(50) + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) + deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) + logoUrl String? @map("logo_url") @db.VarChar(2183) + twoFactorRequired Boolean @default(false) @map("two_factor_required") + websites Website[] + members TeamUser[] + links Link[] + pixels Pixel[] + boards Board[] @@index([accessCode]) @@map("team") } model TeamUser { - id String @id() @map("team_user_id") @db.Uuid + id String @id @map("team_user_id") @db.Uuid teamId String @map("team_id") @db.Uuid userId String @map("user_id") @db.Uuid role String @db.VarChar(50) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) - - team Team @relation(fields: [teamId], references: [id]) - user User @relation(fields: [userId], references: [id]) + team Team @relation(fields: [teamId], references: [id]) + user User @relation(fields: [userId], references: [id]) @@index([teamId]) @@index([userId]) @@ -242,7 +241,7 @@ model TeamUser { } model Report { - id String @id() @map("report_id") @db.Uuid + id String @id @map("report_id") @db.Uuid userId String @map("user_id") @db.Uuid websiteId String @map("website_id") @db.Uuid type String @db.VarChar(50) @@ -251,9 +250,8 @@ model Report { parameters Json createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) - - user User @relation(fields: [userId], references: [id]) - website Website @relation(fields: [websiteId], references: [id]) + user User @relation(fields: [userId], references: [id]) + website Website @relation(fields: [websiteId], references: [id]) @@index([userId]) @@index([websiteId]) @@ -263,22 +261,21 @@ model Report { } model Segment { - id String @id() @map("segment_id") @db.Uuid + id String @id @map("segment_id") @db.Uuid websiteId String @map("website_id") @db.Uuid type String @db.VarChar(50) name String @db.VarChar(200) parameters Json createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) - - website Website @relation(fields: [websiteId], references: [id]) + website Website @relation(fields: [websiteId], references: [id]) @@index([websiteId]) @@map("segment") } model Revenue { - id String @id() @map("revenue_id") @db.Uuid + id String @id @map("revenue_id") @db.Uuid websiteId String @map("website_id") @db.Uuid sessionId String @map("session_id") @db.Uuid eventId String @map("event_id") @db.Uuid @@ -286,9 +283,8 @@ model Revenue { currency String @db.VarChar(10) revenue Decimal? @db.Decimal(19, 4) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - - website Website @relation(fields: [websiteId], references: [id]) - session Session @relation(fields: [sessionId], references: [id]) + website Website @relation(fields: [websiteId], references: [id]) + session Session @relation(fields: [sessionId], references: [id]) @@index([websiteId]) @@index([sessionId]) @@ -298,18 +294,17 @@ model Revenue { } model Link { - id String @id() @map("link_id") @db.Uuid + id String @id @map("link_id") @db.Uuid name String @db.VarChar(100) url String @db.VarChar(500) - slug String @unique() @db.VarChar(100) + slug String @unique @db.VarChar(100) userId String? @map("user_id") @db.Uuid teamId String? @map("team_id") @db.Uuid createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) - - user User? @relation("user", fields: [userId], references: [id]) - team Team? @relation(fields: [teamId], references: [id]) + user User? @relation("user", fields: [userId], references: [id]) + team Team? @relation(fields: [teamId], references: [id]) @@index([slug]) @@index([userId]) @@ -319,17 +314,16 @@ model Link { } model Pixel { - id String @id() @map("pixel_id") @db.Uuid + id String @id @map("pixel_id") @db.Uuid name String @db.VarChar(100) - slug String @unique() @db.VarChar(100) + slug String @unique @db.VarChar(100) userId String? @map("user_id") @db.Uuid teamId String? @map("team_id") @db.Uuid createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6) - - user User? @relation("user", fields: [userId], references: [id]) - team Team? @relation(fields: [teamId], references: [id]) + user User? @relation("user", fields: [userId], references: [id]) + team Team? @relation(fields: [teamId], references: [id]) @@index([slug]) @@index([userId]) @@ -339,7 +333,7 @@ model Pixel { } model Board { - id String @id() @map("board_id") @db.Uuid + id String @id @map("board_id") @db.Uuid type String @db.VarChar(50) name String @db.VarChar(200) description String @db.VarChar(500) @@ -348,21 +342,21 @@ model Board { teamId String? @map("team_id") @db.Uuid createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) - - user User? @relation("user", fields: [userId], references: [id]) - team Team? @relation(fields: [teamId], references: [id]) + user User? @relation("user", fields: [userId], references: [id]) + team Team? @relation(fields: [teamId], references: [id]) @@index([userId]) @@index([teamId]) @@index([createdAt]) @@map("board") } + model Share { - id String @id() @map("share_id") @db.Uuid + id String @id @map("share_id") @db.Uuid entityId String @map("entity_id") @db.Uuid name String @db.VarChar(200) - shareType Int @map("share_type") @db.Integer - slug String @unique() @db.VarChar(100) + shareType Int @map("share_type") + slug String @unique @db.VarChar(100) parameters Json createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) @@ -372,18 +366,17 @@ model Share { } model SessionReplay { - id String @id() @map("replay_id") @db.Uuid + id String @id @map("replay_id") @db.Uuid websiteId String @map("website_id") @db.Uuid sessionId String @map("session_id") @db.Uuid visitId String @map("visit_id") @db.Uuid - chunkIndex Int @map("chunk_index") @db.Integer + chunkIndex Int @map("chunk_index") events Bytes @map("events") - eventCount Int @map("event_count") @db.Integer + eventCount Int @map("event_count") startedAt DateTime @map("started_at") @db.Timestamptz(6) endedAt DateTime @map("ended_at") @db.Timestamptz(6) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - - website Website @relation(fields: [websiteId], references: [id]) + website Website @relation(fields: [websiteId], references: [id]) @@index([websiteId]) @@index([sessionId]) @@ -396,14 +389,13 @@ model SessionReplay { } model SessionReplaySaved { - id String @id() @map("saved_replay_id") @db.Uuid + id String @id @map("saved_replay_id") @db.Uuid name String @db.VarChar(100) websiteId String @map("website_id") @db.Uuid visitId String @map("visit_id") @db.Uuid createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) - - website Website @relation(fields: [websiteId], references: [id]) + website Website @relation(fields: [websiteId], references: [id]) @@unique([websiteId, visitId]) @@index([websiteId]) @@ -412,29 +404,55 @@ model SessionReplaySaved { @@map("session_replay_saved") } -model HeatmapEvent { - id String @id() @map("heatmap_event_id") @db.Uuid - websiteId String @map("website_id") @db.Uuid - sessionId String @map("session_id") @db.Uuid - visitId String @map("visit_id") @db.Uuid - urlPath String @map("url_path") @db.VarChar(500) - eventType Int @map("event_type") @db.Integer - x Int? @db.Integer - y Int? @db.Integer - pageX Int? @map("page_x") @db.Integer - pageY Int? @map("page_y") @db.Integer - pageW Int? @map("page_w") @db.Integer - viewportW Int? @map("viewport_w") @db.Integer - viewportH Int? @map("viewport_h") @db.Integer - pageH Int? @map("page_h") @db.Integer - scrollPct Int? @map("scroll_pct") @db.Integer - createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) +model TwoFactorAuth { + id String @id @default(cuid()) + userId String @unique @map("user_id") @db.Uuid + secret String + isEnabled Boolean @default(false) @map("is_enabled") + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) - website Website @relation(fields: [websiteId], references: [id]) - - @@index([websiteId]) - @@index([visitId]) - @@index([websiteId, createdAt]) - @@index([websiteId, urlPath, eventType, createdAt]) - @@map("heatmap_event") + @@map("two_factor_auth") +} + +model TwoFactorBackupCode { + id String @id @default(cuid()) + userId String @map("user_id") @db.Uuid + codeHash String @map("code_hash") + used Boolean @default(false) + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@map("two_factor_backup_code") +} + +model TwoFactorOtpUsed { + id String @id @default(cuid()) + userId String @map("user_id") @db.Uuid + otp String + expiresAt DateTime @map("expires_at") @db.Timestamptz(6) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, otp]) + @@map("two_factor_otp_used") +} + +model TwoFactorRateLimit { + id String @id @default(cuid()) + userId String @unique @map("user_id") @db.Uuid + attempts Int @default(0) + lockedUntil DateTime? @map("locked_until") @db.Timestamptz(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("two_factor_rate_limit") +} + +model AppSetting { + key String @id + value String + + @@map("app_setting") } diff --git a/public/intl/messages/en-US.json b/public/intl/messages/en-US.json index 272823c86..2091ce2be 100644 --- a/public/intl/messages/en-US.json +++ b/public/intl/messages/en-US.json @@ -391,7 +391,37 @@ "website-id": "Website ID", "websites": "Websites", "window": "Window", - "yesterday": "Yesterday" + "yesterday": "Yesterday", + "security": "Security", + "two-factor-auth": "Two-factor authentication", + "two-factor-setup-title": "Setup two-factor authentication", + "two-factor-step1": "Step 1", + "two-factor-step2": "Step 2", + "two-factor-scan-qr": "Scan QR code", + "two-factor-cant-scan": "Can't scan QR code?", + "two-factor-manual-entry": "Enter this secret instead:", + "two-factor-copy-code": "Copy code", + "two-factor-code-copied": "Copied!", + "two-factor-get-code": "Get verification code", + "two-factor-enter-code": "Enter verification code", + "two-factor-use-backup": "Use a backup code instead", + "two-factor-save-backup-codes": "Save your backup codes", + "two-factor-download-codes": "Download codes", + "two-factor-saved-codes": "I have saved my backup codes", + "two-factor-backup-file-title": "Umami - Two-factor authentication backup codes", + "two-factor-success-title": "Two-factor authentication enabled", + "two-factor-enable": "Enable 2FA", + "two-factor-disable": "Disable 2FA", + "two-factor-disable-title": "Disable two-factor authentication", + "two-factor-status-not-configured": "Not configured", + "two-factor-active": "Active", + "two-factor-require-global": "Require 2FA for all users", + "two-factor-require-user": "Require 2FA for this user", + "two-factor-require-team": "Require 2FA for all members of this team", + "two-factor-global-active-tooltip": "Global 2FA is enabled", + "two-factor-verify": "Verify", + "two-factor-login": "Two-factor authentication", + "close": "Close" }, "message": { "action-confirmation": "Type {confirmation} in the box below to confirm.", @@ -445,6 +475,34 @@ "replay-min-duration-description": "Only show replays at or above this duration.", "viewed-page": "Viewed page", "upgrade-required": "This feature requires a {plan} plan subscription.", - "visitor-log": "Visitor from {country} using {browser} on {os} {device}" + "visitor-log": "Visitor from {country} using {browser} on {os} {device}", + "two-factor-setup-description": "Each time you log in, in addition to your password, you'll use an authenticator app to generate and use a one-time code.", + "two-factor-setup-required-description": "Two-factor authentication (2FA) is required to access Umami.", + "two-factor-step1-description": "Scan the QR code below or manually enter the secret key into your authenticator app.", + "two-factor-step2-description": "Enter the 6-digit code you see in your authenticator app.", + "two-factor-enabled-description": "Your account is now protected with two-factor authentication. From now on, you will need to enter a code from your authenticator app each time you log in.", + "two-factor-backup-description": "If you lose access to your authenticator app, you can use one of these one-time backup codes to sign in. Save them somewhere safe; they will not be shown again.", + "two-factor-backup-codes-once": "Keep these codes safe. Each code can only be used once.", + "two-factor-login-description": "Enter the 6-digit code from your authenticator app.", + "two-factor-disable-description": "To confirm, enter your password and a verification code from your authenticator app.", + "two-factor-required-message": "Two-factor authentication is required for your account and cannot be disabled. Contact your administrator for help.", + "two-factor-override-note": "Per-user and per-team 2FA settings are overridden while global 2FA is enabled.", + "two-factor-admin-global-description": "Require all users to set up two-factor authentication when they next log in. Users who have not yet configured 2FA will be prompted to do so before they can access Umami.", + "two-factor-require-user-description": "When enabled, this user will be prompted to set up 2FA on their next login if they haven't already.", + "two-factor-require-team-description": "When enabled, all members of this team will be prompted to set up 2FA on their next login if they haven't already.", + "two-factor-user-description": "Add an extra layer of security to your account. Each time you log in, you will need your password and a code from your authenticator app.", + "two-factor-error-invalid-code": "Invalid verification code. Please try again.", + "two-factor-error-missing-token": "Authentication token is missing.", + "two-factor-error-invalid-partial-token": "Invalid or expired authentication token.", + "two-factor-error-invalid-backup-code": "Invalid backup code.", + "two-factor-error-code-used": "This code has already been used. Wait for the next code.", + "two-factor-error-too-many-attempts": "Too many failed attempts. Your account is temporarily locked.", + "two-factor-error-disable-not-allowed": "Two-factor authentication is required and cannot be disabled.", + "two-factor-error-incorrect-password": "Incorrect password.", + "two-factor-error-already-enabled": "Two-factor authentication is already enabled.", + "two-factor-error-not-enabled": "Two-factor authentication is not enabled.", + "two-factor-error-no-pending-setup": "No pending two-factor authentication setup found.", + "two-factor-locked-message": "Too many failed attempts. Try again at {time}.", + "two-factor-active-description": "Your account is protected with two-factor authentication. Each login requires a code from your authenticator app." } } diff --git a/public/intl/messages/fr-FR.json b/public/intl/messages/fr-FR.json index 8a2173ba3..657a91c4a 100644 --- a/public/intl/messages/fr-FR.json +++ b/public/intl/messages/fr-FR.json @@ -382,7 +382,37 @@ "website-id": "ID de site", "websites": "Sites", "window": "Fenêtre", - "yesterday": "Hier" + "yesterday": "Hier", + "security": "Sécurité", + "two-factor-auth": "Authentification à deux facteurs", + "two-factor-setup-title": "Configurer l’authentification à deux facteurs", + "two-factor-step1": "Étape 1", + "two-factor-step2": "Étape 2", + "two-factor-scan-qr": "Scanner le QR code", + "two-factor-cant-scan": "Impossible de scanner le QR code ?", + "two-factor-manual-entry": "Saisissez plutôt cette clé secrète :", + "two-factor-copy-code": "Copier le code", + "two-factor-code-copied": "Copié !", + "two-factor-get-code": "Obtenir le code de vérification", + "two-factor-enter-code": "Saisir le code de vérifiation", + "two-factor-use-backup": "Utiliser plutôt un code de récupération", + "two-factor-save-backup-codes": "Enregistrer vos codes de récupération", + "two-factor-download-codes": "Télécharger les codes", + "two-factor-saved-codes": "J’ai enregistré mes codes de récupération", + "two-factor-backup-file-title": "Umami - Codes de récupération pour l’authentification à deux facteurs", + "two-factor-success-title": "Authentification à deux facteurs activée", + "two-factor-enable": "Activer l’authentification à deux facteurs", + "two-factor-disable": "Désactiver l’authentification à deux facteurs", + "two-factor-disable-title": "Désactiver l’authentification à deux facteurs", + "two-factor-status-not-configured": "Non configurée", + "two-factor-active": "Activée", + "two-factor-require-global": "Rendre l’authentification à deux facteurs obligatoire pour tous les utilisateurs", + "two-factor-require-user": "Rendre l’authentification à deux facteurs obligatoire pour cet utilisateur", + "two-factor-require-team": "Rendre l’authentification à deux facteurs oblitatoire pour tous les membres de cette équipe", + "two-factor-global-active-tooltip": "L’authentification à deux facteurs est obligatoire pour tous les utilisateurs", + "two-factor-verify": "Vérifier", + "two-factor-login": "Authentification à deux facteurs", + "close": "Fermer" }, "message": { "action-confirmation": "Taper {confirmation} ci-dessous pour confirmer.", @@ -436,6 +466,34 @@ "user-deleted": "Utilisateur supprimé.", "replay-min-duration-description": "Afficher uniquement les lectures de cette durée ou plus.", "viewed-page": "Page vue", - "visitor-log": "Visiteur de {country} utilisant {browser} sur {os} {device}" + "visitor-log": "Visiteur de {country} utilisant {browser} sur {os} {device}", + "two-factor-setup-description": "À chaque connexion, en plus de votre mot de passe, vous devrez utiliser une application d’authentification pour générer et utiliser un code unique.", + "two-factor-setup-required-description": "L’authentification à deux facteurs est obligatoire pour accéder à Umami.", + "two-factor-step1-description": "Scannez le QR code ou saisissez manuellement la clé secrète dans votre application d’authentification.", + "two-factor-step2-description": "Saisissez le code à 6 chiffres qui s’affiche dans votre application d’authentification.", + "two-factor-enabled-description": "Votre compte est désormais protégé par l’authentification à deux facteurs. À partir de maintenant, vous devrez saisir un code provenant de votre application d’authentification à chaque fois que vous vous connecterez.", + "two-factor-backup-description": "Si vous perdez l’accès à votre application d’authentification, vous pouvez utiliser l’un de ces codes de récupération pour vous connecter. Enregistrez-les à un endroit sûr ; ils ne seront pas affichés à nouveau.", + "two-factor-backup-codes-once": "Gardez ces codes en lieu sûr. Chaque code ne peut être utilisé qu’une seule fois.", + "two-factor-login-description": "Saisissez le code à 6 chiffres de votre application d’authentification.", + "two-factor-disable-description": "Pour confirmer, saisissez votre mot de passe et un code de vérification provenant de votre application d’authentification.", + "two-factor-required-message": "L’authentification à deux facteurs est obligatoire pour votre compte et ne peut pas être désactivée. Contactez votre administrateur pour obtenir de l’aide.", + "two-factor-override-note": "Les paramètres d’authentification à deux facteurs au niveau de l’utilisateur et de l’équipe sont ignorés lorsque l’authentification à deux facteurs est activée pour tous les utilisateurs.", + "two-factor-admin-global-description": "Obliger tous les utilisateurs à configurer l’authentification à deux facteurs lors de leur prochaine connexion. Les utilisateurs qui n’ont pas encore configuré l’authentification à deux facteurs seront invités à le faire avant d’accéder à Umami.", + "two-factor-require-user-description": "Lorsque cette option est activée, cet utilisateur sera invité à configurer l’authentification à deux facteurs lors de sa prochaine connexion s’il ne l'a pas déjà fait.", + "two-factor-require-team-description": "Lorsque cette option est activée, tous les membres de cette équipe seront invités à configurer l’authentification à deux facteurs lors de leur prochaine connexion s’ils ne l’ont pas déjà fait.", + "two-factor-user-description": "Ajoutez une couche de sécurité supplémentaire à votre compte. À chaque connexion, vous aurez besoin de votre mot de passe et d'un code provenant de votre application d’authentification.", + "two-factor-error-invalid-code": "Code de vérification invalide. Veuillez réessayer.", + "two-factor-error-missing-token": "Le jeton d'authentification est manquant.", + "two-factor-error-invalid-partial-token": "Jeton d'authentification invalide ou expiré.", + "two-factor-error-invalid-backup-code": "Code de récupération invalide.", + "two-factor-error-code-used": "Ce code a déjà été utilisé. Attendez le prochain code.", + "two-factor-error-too-many-attempts": "Trop de tentatives infructueuses. Votre compte est temporairement verrouillé.", + "two-factor-error-disable-not-allowed": "L’authentification à deux facteurs est obligatoire et ne peut pas être désactivée.", + "two-factor-error-incorrect-password": "Mot de passe incorrect.", + "two-factor-error-already-enabled": "L’authentification à deux facteurs est déjà activée.", + "two-factor-error-not-enabled": "L’authentification à deux facteurs n’est pas activée.", + "two-factor-error-no-pending-setup": "Aucune configuration d’authentification à deux facteurs en attente n’a été trouvée.", + "two-factor-locked-message": "Trop de tentatives infructueuses. Réessayez à {time}.", + "two-factor-active-description": "Votre compte est protégé par l’authentification à deux facteurs. Chaque connexion nécessite un code provenant de votre application d’authentification." } } diff --git a/src/app/(main)/App.tsx b/src/app/(main)/App.tsx index 2f85a86e4..0323ec052 100644 --- a/src/app/(main)/App.tsx +++ b/src/app/(main)/App.tsx @@ -5,7 +5,14 @@ import { useEffect } from 'react'; import { MobileNav } from '@/app/(main)/MobileNav'; import { SideNav } from '@/app/(main)/SideNav'; import { TopNav } from '@/app/(main)/TopNav'; -import { useConfig, useLoginQuery, useNavigation, useTeamQuery } from '@/components/hooks'; +import { + useConfig, + useLoginQuery, + useNavigation, + useTeamQuery, + useTwoFactorStatusQuery, +} from '@/components/hooks'; +import { TwoFactorSetupModal } from '@/components/modals/TwoFactorSetupModal'; import { LAST_TEAM_CONFIG } from '@/lib/constants'; import { removeItem, setItem } from '@/lib/storage'; import { UpdateNotice } from './UpdateNotice'; @@ -15,6 +22,8 @@ export function App({ children }) { const config = useConfig(); const { pathname, router, teamId } = useNavigation(); const { isLoading: isTeamLoading, error: teamError } = useTeamQuery(teamId); + const { data: twoFactorStatus } = useTwoFactorStatusQuery(!!user); + const needsTwoFactorSetup = !!(twoFactorStatus?.isRequired && !twoFactorStatus?.isEnabled); useEffect(() => { if (teamId) { @@ -64,8 +73,15 @@ export function App({ children }) { - {children} + + {children} + + {needsTwoFactorSetup && } {process.env.NODE_ENV === 'production' && !pathname.includes('/share/') && (