From 00e4f522abd987da11ff028be01ff39781bf3556 Mon Sep 17 00:00:00 2001 From: Maieul BOYER Date: Sat, 6 Dec 2025 19:07:13 +0100 Subject: [PATCH] feat(frontend/otp): allow changing otp status and showing qrcode --- frontend/package.json | 2 + frontend/pnpm-lock.yaml | 255 +++++++++++++++++++++++- frontend/src/pages/profile/profile.html | 7 +- frontend/src/pages/profile/profile.ts | 154 +++++++++----- 4 files changed, 359 insertions(+), 59 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index b0fc5a2..a25c3ec 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,9 @@ }, "dependencies": { "@tailwindcss/vite": "^4.1.17", + "@types/qrcode": "^1.5.6", "js-cookie": "^3.0.5", + "qrcode": "^1.5.4", "socket.io-client": "^4.8.1", "tailwindcss": "^4.1.17" } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index ac3aea5..4ca399c 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -10,10 +10,16 @@ importers: dependencies: '@tailwindcss/vite': specifier: ^4.1.17 - version: 4.1.17(vite@7.2.7(jiti@2.6.1)(lightningcss@1.30.2)) + version: 4.1.17(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)) + '@types/qrcode': + specifier: ^1.5.6 + version: 1.5.6 js-cookie: specifier: ^3.0.5 version: 3.0.5 + qrcode: + specifier: ^1.5.4 + version: 1.5.4 socket.io-client: specifier: ^4.8.1 version: 4.8.1 @@ -29,10 +35,10 @@ importers: version: 5.9.3 vite: specifier: ^7.2.7 - version: 7.2.7(jiti@2.6.1)(lightningcss@1.30.2) + version: 7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@7.2.7(jiti@2.6.1)(lightningcss@1.30.2)) + version: 5.1.4(typescript@5.9.3)(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)) packages: @@ -417,6 +423,34 @@ packages: '@types/js-cookie@3.0.6': resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} + '@types/node@24.10.2': + resolution: {integrity: sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==} + + '@types/qrcode@1.5.6': + resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + debug@4.3.7: resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} engines: {node: '>=6.0'} @@ -435,10 +469,20 @@ packages: supports-color: optional: true + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + engine.io-client@6.6.3: resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==} @@ -464,17 +508,29 @@ packages: picomatch: optional: true + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -553,6 +609,10 @@ packages: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -564,6 +624,22 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -571,15 +647,34 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} + hasBin: true + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + rollup@4.53.3: resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + socket.io-client@4.8.1: resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==} engines: {node: '>=10.0.0'} @@ -592,6 +687,14 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + tailwindcss@4.1.17: resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==} @@ -618,6 +721,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + vite-tsconfig-paths@5.1.4: resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} peerDependencies: @@ -666,6 +772,13 @@ packages: yaml: optional: true + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + ws@8.17.1: resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} engines: {node: '>=10.0.0'} @@ -682,6 +795,17 @@ packages: resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} engines: {node: '>=0.4.0'} + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + snapshots: '@esbuild/aix-ppc64@0.25.12': @@ -910,17 +1034,45 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.17 '@tailwindcss/oxide-win32-x64-msvc': 4.1.17 - '@tailwindcss/vite@4.1.17(vite@7.2.7(jiti@2.6.1)(lightningcss@1.30.2))': + '@tailwindcss/vite@4.1.17(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2))': dependencies: '@tailwindcss/node': 4.1.17 '@tailwindcss/oxide': 4.1.17 tailwindcss: 4.1.17 - vite: 7.2.7(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2) '@types/estree@1.0.8': {} '@types/js-cookie@3.0.6': {} + '@types/node@24.10.2': + dependencies: + undici-types: 7.16.0 + + '@types/qrcode@1.5.6': + dependencies: + '@types/node': 24.10.2 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + camelcase@5.3.1: {} + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + debug@4.3.7: dependencies: ms: 2.1.3 @@ -929,8 +1081,14 @@ snapshots: dependencies: ms: 2.1.3 + decamelize@1.2.0: {} + detect-libc@2.1.2: {} + dijkstrajs@1.0.3: {} + + emoji-regex@8.0.0: {} + engine.io-client@6.6.3: dependencies: '@socket.io/component-emitter': 3.1.2 @@ -983,13 +1141,22 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + fsevents@2.3.3: optional: true + get-caller-file@2.0.5: {} + globrex@0.1.2: {} graceful-fs@4.2.11: {} + is-fullwidth-code-point@3.0.0: {} + jiti@2.6.1: {} js-cookie@3.0.5: {} @@ -1043,6 +1210,10 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1051,16 +1222,40 @@ snapshots: nanoid@3.3.11: {} + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-try@2.2.0: {} + + path-exists@4.0.0: {} + picocolors@1.1.1: {} picomatch@4.0.3: {} + pngjs@5.0.0: {} + postcss@8.5.6: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + + require-directory@2.1.1: {} + + require-main-filename@2.0.0: {} + rollup@4.53.3: dependencies: '@types/estree': 1.0.8 @@ -1089,6 +1284,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.53.3 fsevents: 2.3.3 + set-blocking@2.0.0: {} + socket.io-client@4.8.1: dependencies: '@socket.io/component-emitter': 3.1.2 @@ -1109,6 +1306,16 @@ snapshots: source-map-js@1.2.1: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + tailwindcss@4.1.17: {} tapable@2.3.0: {} @@ -1124,18 +1331,20 @@ snapshots: typescript@5.9.3: {} - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.2.7(jiti@2.6.1)(lightningcss@1.30.2)): + undici-types@7.16.0: {} + + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 7.2.7(jiti@2.6.1)(lightningcss@1.30.2) + vite: 7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2) transitivePeerDependencies: - supports-color - typescript - vite@7.2.7(jiti@2.6.1)(lightningcss@1.30.2): + vite@7.2.7(@types/node@24.10.2)(jiti@2.6.1)(lightningcss@1.30.2): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -1144,10 +1353,40 @@ snapshots: rollup: 4.53.3 tinyglobby: 0.2.15 optionalDependencies: + '@types/node': 24.10.2 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 + which-module@2.0.1: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + ws@8.17.1: {} xmlhttprequest-ssl@2.1.2: {} + + y18n@4.0.3: {} + + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + 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 diff --git a/frontend/src/pages/profile/profile.html b/frontend/src/pages/profile/profile.html index 1c56014..1b2d180 100644 --- a/frontend/src/pages/profile/profile.html +++ b/frontend/src/pages/profile/profile.html @@ -59,7 +59,12 @@ - + diff --git a/frontend/src/pages/profile/profile.ts b/frontend/src/pages/profile/profile.ts index b3d6e77..c6a7bb0 100644 --- a/frontend/src/pages/profile/profile.ts +++ b/frontend/src/pages/profile/profile.ts @@ -1,109 +1,164 @@ import { addRoute, navigateTo, setTitle } from "@app/routing"; import { showError } from "@app/toast"; -import page from './profile.html?raw' +import page from "./profile.html?raw"; import { updateUser } from "@app/auth"; import { isNullish } from "@app/utils"; import client from "@app/api"; +import QRCode from "qrcode"; +type OAuthQRCodeOptions = { + label?: string; // e.g. your-app:user@example.com + issuer?: string; // e.g. "YourApp" + algorithm?: "SHA1" | "SHA256" | "SHA512"; + digits?: number; + period?: number; +}; + +/** + * Renders an OAuth2-compatible TOTP QR code into a canvas. + * + * @param canvas HTMLCanvasElement to draw into + * @param secret Base32-encoded shared secret + * @param options Meta data for QR (label, issuer, etc.) + */ +export async function renderOAuth2QRCode( + canvas: HTMLCanvasElement, + secret: string, +): Promise { + // Encode the otpauth:// URL + const otpauthUrl = new URL(`otpauth://totp/ft_boule:totp`); + + otpauthUrl.searchParams.set("secret", secret.replace(/=+$/, "")); + otpauthUrl.searchParams.set("issuer", "ft_boule"); + + // Render QR code into the canvas + await QRCode.toCanvas(canvas, otpauthUrl.toString(), { + margin: 1, + scale: 5, + }); + canvas.style.width = ""; + canvas.style.height = ""; +} async function route(url: string, _args: { [k: string]: string }) { - setTitle('Edit Profile') + setTitle("Edit Profile"); return { - html: page, postInsert: async (app: HTMLElement | undefined) => { + html: page, + postInsert: async (app: HTMLElement | undefined) => { const user = await updateUser(); - if (isNullish(user)) - return showError('No User'); - if (isNullish(app)) - return showError('Failed to render'); + if (isNullish(user)) return showError("No User"); + if (isNullish(app)) return showError("Failed to render"); let totpState = await (async () => { let res = await client.statusOtp(); if (res.kind === "success") return { - enabled: (res.msg as string) === "statusOtp.success.enabled", - secret: ((res.msg as string) === "statusOtp.success.enabled") ? res.payload.secret : null, + enabled: + (res.msg as string) === "statusOtp.success.enabled", + secret: + (res.msg as string) === "statusOtp.success.enabled" + ? res.payload.secret + : null, }; else { - showError('Failed to get OTP status') + showError("Failed to get OTP status"); return { - enabled: false, secret: null, - } + enabled: false, + secret: null, + }; } - - })() + })(); // ---- Simulated State ---- let totpEnabled = totpState.enabled; let totpSecret = totpState.secret; // would come from backend let guestBox = app.querySelector("#isGuestBox")!; - let displayNameWrapper = app.querySelector("#displayNameWrapper")!; - let displayNameBox = app.querySelector("#displayNameBox")!; - let displayNameButton = app.querySelector("#displayNameButton")!; - let loginNameWrapper = app.querySelector("#loginNameWrapper")!; - let loginNameBox = app.querySelector("#loginNameBox")!; - let passwordWrapper = app.querySelector("#passwordWrapper")!; - let passwordBox = app.querySelector("#passwordBox")!; - let passwordButton = app.querySelector("#passwordButton")!; - + let displayNameWrapper = app.querySelector( + "#displayNameWrapper", + )!; + let displayNameBox = + app.querySelector("#displayNameBox")!; + let displayNameButton = + app.querySelector("#displayNameButton")!; + let loginNameWrapper = + app.querySelector("#loginNameWrapper")!; + let loginNameBox = + app.querySelector("#loginNameBox")!; + let passwordWrapper = + app.querySelector("#passwordWrapper")!; + let passwordBox = + app.querySelector("#passwordBox")!; + let passwordButton = + app.querySelector("#passwordButton")!; if (!isNullish(user.selfInfo?.loginName)) loginNameBox.innerText = user.selfInfo?.loginName; else - loginNameBox.innerHTML = 'You don\'t have a login name'; + loginNameBox.innerHTML = + 'You don\'t have a login name'; displayNameBox.value = user.name; guestBox.hidden = !user.guest; // ---- DOM Elements ---- const totpStatusText = app.querySelector("#totpStatusText")!; - const enableBtn = app.querySelector("#enableTotp")!; - const disableBtn = app.querySelector("#disableTotp")!; - const showSecretBtn = app.querySelector("#showSecret")!; + const enableBtn = + app.querySelector("#enableTotp")!; + const disableBtn = + app.querySelector("#disableTotp")!; + const showSecretBtn = + app.querySelector("#showSecret")!; const secretBox = app.querySelector("#totpSecretBox")!; + const secretText = + app.querySelector("#totpSecretText")!; + const secretCanvas = + app.querySelector("#totpSecretCanvas")!; if (user.guest) { for (let c of passwordButton.classList.values()) { - if (c.startsWith('bg-') || c.startsWith('hover:bg-')) + if (c.startsWith("bg-") || c.startsWith("hover:bg-")) passwordButton.classList.remove(c); } passwordButton.disabled = true; - passwordButton.classList.add('bg-gray-700', 'hover:bg-gray-700'); + passwordButton.classList.add( + "bg-gray-700", + "hover:bg-gray-700", + ); passwordBox.disabled = true; - passwordBox.classList.add('color-white'); + passwordBox.classList.add("color-white"); for (let c of displayNameButton.classList.values()) { - if (c.startsWith('bg-') || c.startsWith('hover:bg-')) + if (c.startsWith("bg-") || c.startsWith("hover:bg-")) displayNameButton.classList.remove(c); } displayNameButton.disabled = true; - displayNameButton.classList.add('bg-gray-700'); - displayNameButton.classList.add('color-white'); + displayNameButton.classList.add("bg-gray-700"); + displayNameButton.classList.add("color-white"); displayNameBox.disabled = true; - displayNameBox.classList.add('color-white'); + displayNameBox.classList.add("color-white"); for (let c of enableBtn.classList.values()) { - if (c.startsWith('bg-') || c.startsWith('hover:bg-')) + if (c.startsWith("bg-") || c.startsWith("hover:bg-")) enableBtn.classList.remove(c); } for (let c of disableBtn.classList.values()) { - if (c.startsWith('bg-') || c.startsWith('hover:bg-')) + if (c.startsWith("bg-") || c.startsWith("hover:bg-")) disableBtn.classList.remove(c); } for (let c of showSecretBtn.classList.values()) { - if (c.startsWith('bg-') || c.startsWith('hover:bg-')) + if (c.startsWith("bg-") || c.startsWith("hover:bg-")) showSecretBtn.classList.remove(c); } - enableBtn.classList.add('bg-gray-700', 'hover:bg-gray-700'); - disableBtn.classList.add('bg-gray-700', 'hover:bg-gray-700'); - showSecretBtn.classList.add('bg-gray-700', 'hover:bg-gray-700'); + enableBtn.classList.add("bg-gray-700", "hover:bg-gray-700"); + disableBtn.classList.add("bg-gray-700", "hover:bg-gray-700"); + showSecretBtn.classList.add("bg-gray-700", "hover:bg-gray-700"); enableBtn.disabled = true; disableBtn.disabled = true; showSecretBtn.disabled = true; } - // ---- Update UI ---- function refreshTotpUI() { if (totpEnabled) { @@ -127,8 +182,7 @@ async function route(url: string, _args: { [k: string]: string }) { let res = await client.enableOtp(); if (res.kind === "success") { navigateTo(url); - } - else { + } else { showError(`failed to activate OTP: ${res.msg}`); } }; @@ -137,23 +191,23 @@ async function route(url: string, _args: { [k: string]: string }) { let res = await client.disableOtp(); if (res.kind === "success") { navigateTo(url); - } - else { + } else { showError(`failed to deactivate OTP: ${res.msg}`); } }; showSecretBtn.onclick = () => { - secretBox.textContent = `TOTP Secret: ${totpSecret}`; + if (!isNullish(totpSecret)) { + secretText.textContent = totpSecret; + renderOAuth2QRCode(secretCanvas, totpSecret); + } secretBox.classList.toggle("hidden"); }; // Initialize UI state refreshTotpUI(); - } + }, }; } - - -addRoute('/profile', route) +addRoute("/profile", route);