From f6d93323f757d420f0bf4b41c936da8a9a358c05 Mon Sep 17 00:00:00 2001 From: shenlan Date: Tue, 7 Oct 2025 18:56:26 +0800 Subject: [PATCH] Use local QR rendering for MFA setup (#458) --- ui/homepage/app/api/auth/mfa/setup/route.ts | 5 + .../app/panel/account/MfaSetupPanel.tsx | 49 ++++- ui/homepage/package.json | 2 +- ui/homepage/yarn.lock | 194 ++++++++++++++++-- 4 files changed, 229 insertions(+), 21 deletions(-) diff --git a/ui/homepage/app/api/auth/mfa/setup/route.ts b/ui/homepage/app/api/auth/mfa/setup/route.ts index b8500b0..6a16e11 100644 --- a/ui/homepage/app/api/auth/mfa/setup/route.ts +++ b/ui/homepage/app/api/auth/mfa/setup/route.ts @@ -7,6 +7,11 @@ import { getAccountServiceBaseUrl } from '@lib/serviceConfig' const ACCOUNT_SERVICE_URL = getAccountServiceBaseUrl() const ACCOUNT_API_BASE = `${ACCOUNT_SERVICE_URL}/api/auth` +// This Next.js route proxies MFA provisioning requests to the account service. +// The UI calls /api/auth/mfa/setup, which in turn forwards to the Go backend +// at /api/auth/mfa/totp/provision, keeping browser credentials opaque to the +// external service and letting us manage cookies centrally. + type SetupPayload = { token?: string issuer?: string diff --git a/ui/homepage/app/panel/account/MfaSetupPanel.tsx b/ui/homepage/app/panel/account/MfaSetupPanel.tsx index 1d2d151..00a09a7 100644 --- a/ui/homepage/app/panel/account/MfaSetupPanel.tsx +++ b/ui/homepage/app/panel/account/MfaSetupPanel.tsx @@ -1,8 +1,9 @@ 'use client' +import Image from 'next/image' import { FormEvent, useCallback, useEffect, useMemo, useState } from 'react' import { useRouter, useSearchParams } from 'next/navigation' -import QRCode from 'react-qr-code' +import { toDataURL as generateQrCode } from 'qrcode' import Card from '../components/Card' import { useLanguage } from '@i18n/LanguageProvider' @@ -94,6 +95,7 @@ export default function MfaSetupPanel() { const [issuer, setIssuer] = useState('') const [accountLabel, setAccountLabel] = useState('') const [code, setCode] = useState('') + const [qrCodeDataUrl, setQrCodeDataUrl] = useState('') const [isProvisioning, setIsProvisioning] = useState(false) const [isVerifying, setIsVerifying] = useState(false) const [isDisabling, setIsDisabling] = useState(false) @@ -166,6 +168,39 @@ export default function MfaSetupPanel() { } }, [setupRequested]) + useEffect(() => { + let active = true + + const renderQr = async () => { + if (!uri) { + setQrCodeDataUrl('') + return + } + + try { + const dataUrl = await generateQrCode(uri, { + errorCorrectionLevel: 'M', + margin: 1, + scale: 6, + }) + if (active) { + setQrCodeDataUrl(dataUrl) + } + } catch (error) { + console.warn('Failed to generate MFA QR code', error) + if (active) { + setQrCodeDataUrl('') + } + } + } + + void renderQr() + + return () => { + active = false + } + }, [uri]) + const handleProvision = useCallback(async () => { setIsProvisioning(true) setError(null) @@ -515,15 +550,17 @@ export default function MfaSetupPanel() {

{copy.guide.step2Title}

{copy.guide.step2Description}

- {uri ? ( + {qrCodeDataUrl ? (
-
diff --git a/ui/homepage/package.json b/ui/homepage/package.json index bec889a..054de37 100644 --- a/ui/homepage/package.json +++ b/ui/homepage/package.json @@ -23,11 +23,11 @@ "pdfjs-dist": "^4.2.67", "prop-types": "^15.8.1", "qr.js": "0.0.0", + "qrcode": "^1.5.4", "react": "18.2.0", "react-dom": "18.2.0", "react-grid-layout": "^1.4.4", "react-pdf": "^9.1.0", - "react-qr-code": "^2.0.18", "react-resizable": "^3.0.4", "swr": "^2.3.0", "zustand": "^4.5.4" diff --git a/ui/homepage/yarn.lock b/ui/homepage/yarn.lock index 54b838b..b34493c 100644 --- a/ui/homepage/yarn.lock +++ b/ui/homepage/yarn.lock @@ -2025,6 +2025,13 @@ __metadata: languageName: node linkType: hard +"camelcase@npm:^5.0.0": + version: 5.3.1 + resolution: "camelcase@npm:5.3.1" + checksum: 10/e6effce26b9404e3c0f301498184f243811c30dfe6d0b9051863bd8e4034d09c8c2923794f280d6827e5aa055f6c434115ff97864a16a963366fb35fd673024b + languageName: node + linkType: hard + "caniuse-lite@npm:^1.0.30001579, caniuse-lite@npm:^1.0.30001702, caniuse-lite@npm:^1.0.30001746": version: 1.0.30001748 resolution: "caniuse-lite@npm:1.0.30001748" @@ -2117,6 +2124,17 @@ __metadata: languageName: node linkType: hard +"cliui@npm:^6.0.0": + version: 6.0.0 + resolution: "cliui@npm:6.0.0" + dependencies: + string-width: "npm:^4.2.0" + strip-ansi: "npm:^6.0.0" + wrap-ansi: "npm:^6.2.0" + checksum: 10/44afbcc29df0899e87595590792a871cd8c4bc7d6ce92832d9ae268d141a77022adafca1aeaeccff618b62a613b8354e57fe22a275c199ec04baf00d381ef6ab + languageName: node + linkType: hard + "cloudnative-homepage@workspace:.": version: 0.0.0-use.local resolution: "cloudnative-homepage@workspace:." @@ -2140,11 +2158,11 @@ __metadata: postcss: "npm:^8.4.32" prop-types: "npm:^15.8.1" qr.js: "npm:0.0.0" + qrcode: "npm:^1.5.4" react: "npm:18.2.0" react-dom: "npm:18.2.0" react-grid-layout: "npm:^1.4.4" react-pdf: "npm:^9.1.0" - react-qr-code: "npm:^2.0.18" react-resizable: "npm:^3.0.4" swr: "npm:^2.3.0" tailwindcss: "npm:^3.4.3" @@ -2323,6 +2341,13 @@ __metadata: languageName: node linkType: hard +"decamelize@npm:^1.2.0": + version: 1.2.0 + resolution: "decamelize@npm:1.2.0" + checksum: 10/ad8c51a7e7e0720c70ec2eeb1163b66da03e7616d7b98c9ef43cce2416395e84c1e9548dd94f5f6ffecfee9f8b94251fc57121a8b021f2ff2469b2bae247b8aa + languageName: node + linkType: hard + "decimal.js@npm:^10.4.3": version: 10.6.0 resolution: "decimal.js@npm:10.6.0" @@ -2445,6 +2470,13 @@ __metadata: languageName: node linkType: hard +"dijkstrajs@npm:^1.0.1": + version: 1.0.3 + resolution: "dijkstrajs@npm:1.0.3" + checksum: 10/0d8429699a6d5897ed371de494ef3c7072e8052b42abbd978e686a9b8689e70af005fa3e93e93263ee3653673ff5f89c36db830a57ae7c2e088cb9c496307507 + languageName: node + linkType: hard + "dlv@npm:^1.1.3": version: 1.1.3 resolution: "dlv@npm:1.1.3" @@ -3325,6 +3357,16 @@ __metadata: languageName: node linkType: hard +"find-up@npm:^4.1.0": + version: 4.1.0 + resolution: "find-up@npm:4.1.0" + dependencies: + locate-path: "npm:^5.0.0" + path-exists: "npm:^4.0.0" + checksum: 10/4c172680e8f8c1f78839486e14a43ef82e9decd0e74145f40707cc42e7420506d5ec92d9a11c22bd2c48fb0c384ea05dd30e10dd152fefeec6f2f75282a8b844 + languageName: node + linkType: hard + "find-up@npm:^5.0.0": version: 5.0.0 resolution: "find-up@npm:5.0.0" @@ -3469,6 +3511,13 @@ __metadata: languageName: node linkType: hard +"get-caller-file@npm:^2.0.1": + version: 2.0.5 + resolution: "get-caller-file@npm:2.0.5" + checksum: 10/b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9 + languageName: node + linkType: hard + "get-func-name@npm:^2.0.1, get-func-name@npm:^2.0.2": version: 2.0.2 resolution: "get-func-name@npm:2.0.2" @@ -4333,6 +4382,15 @@ __metadata: languageName: node linkType: hard +"locate-path@npm:^5.0.0": + version: 5.0.0 + resolution: "locate-path@npm:5.0.0" + dependencies: + p-locate: "npm:^4.1.0" + checksum: 10/83e51725e67517287d73e1ded92b28602e3ae5580b301fe54bfb76c0c723e3f285b19252e375712316774cf52006cb236aed5704692c32db0d5d089b69696e30 + languageName: node + linkType: hard + "locate-path@npm:^6.0.0": version: 6.0.0 resolution: "locate-path@npm:6.0.0" @@ -4988,6 +5046,15 @@ __metadata: languageName: node linkType: hard +"p-limit@npm:^2.2.0": + version: 2.3.0 + resolution: "p-limit@npm:2.3.0" + dependencies: + p-try: "npm:^2.0.0" + checksum: 10/84ff17f1a38126c3314e91ecfe56aecbf36430940e2873dadaa773ffe072dc23b7af8e46d4b6485d302a11673fe94c6b67ca2cfbb60c989848b02100d0594ac1 + languageName: node + linkType: hard + "p-limit@npm:^3.0.2": version: 3.1.0 resolution: "p-limit@npm:3.1.0" @@ -5006,6 +5073,15 @@ __metadata: languageName: node linkType: hard +"p-locate@npm:^4.1.0": + version: 4.1.0 + resolution: "p-locate@npm:4.1.0" + dependencies: + p-limit: "npm:^2.2.0" + checksum: 10/513bd14a455f5da4ebfcb819ef706c54adb09097703de6aeaa5d26fe5ea16df92b48d1ac45e01e3944ce1e6aa2a66f7f8894742b8c9d6e276e16cd2049a2b870 + languageName: node + linkType: hard + "p-locate@npm:^5.0.0": version: 5.0.0 resolution: "p-locate@npm:5.0.0" @@ -5022,6 +5098,13 @@ __metadata: languageName: node linkType: hard +"p-try@npm:^2.0.0": + version: 2.2.0 + resolution: "p-try@npm:2.2.0" + checksum: 10/f8a8e9a7693659383f06aec604ad5ead237c7a261c18048a6e1b5b85a5f8a067e469aa24f5bc009b991ea3b058a87f5065ef4176793a200d4917349881216cae + languageName: node + linkType: hard + "package-json-from-dist@npm:^1.0.0": version: 1.0.1 resolution: "package-json-from-dist@npm:1.0.1" @@ -5193,6 +5276,13 @@ __metadata: languageName: node linkType: hard +"pngjs@npm:^5.0.0": + version: 5.0.0 + resolution: "pngjs@npm:5.0.0" + checksum: 10/345781644740779752505af2fea3e9043f6c7cc349b18e1fb8842796360d1624791f0c24d33c0f27b05658373f90ffaa177a849e932e5fea1f540cef3975f3c9 + languageName: node + linkType: hard + "possible-typed-array-names@npm:^1.0.0": version: 1.1.0 resolution: "possible-typed-array-names@npm:1.1.0" @@ -5409,6 +5499,19 @@ __metadata: languageName: node linkType: hard +"qrcode@npm:^1.5.4": + version: 1.5.4 + resolution: "qrcode@npm:1.5.4" + dependencies: + dijkstrajs: "npm:^1.0.1" + pngjs: "npm:^5.0.0" + yargs: "npm:^15.3.1" + bin: + qrcode: bin/qrcode + checksum: 10/9a1b61760e4ea334545a0f54bbc11c537aba0a17cf52cab9fa1b07f8a1337eed0bc6f7fde41b197f2c82c249bc48728983bfaf861bb7ecb29dc597b2ae33c424 + languageName: node + linkType: hard + "querystringify@npm:^2.1.1": version: 2.2.0 resolution: "querystringify@npm:2.2.0" @@ -5523,18 +5626,6 @@ __metadata: languageName: node linkType: hard -"react-qr-code@npm:^2.0.18": - version: 2.0.18 - resolution: "react-qr-code@npm:2.0.18" - dependencies: - prop-types: "npm:^15.8.1" - qr.js: "npm:0.0.0" - peerDependencies: - react: "*" - checksum: 10/c90164aac6c124d6a7aba2d9426859072d2233bb8d06102d23ef0c1831048e94cebc7608b821ff2b79f3a1cadf454305c727fd3e1d7742fd71f816cdeac04053 - languageName: node - linkType: hard - "react-resizable@npm:^3.0.4, react-resizable@npm:^3.0.5": version: 3.0.5 resolution: "react-resizable@npm:3.0.5" @@ -5625,6 +5716,20 @@ __metadata: languageName: node linkType: hard +"require-directory@npm:^2.1.1": + version: 2.1.1 + resolution: "require-directory@npm:2.1.1" + checksum: 10/a72468e2589270d91f06c7d36ec97a88db53ae5d6fe3787fadc943f0b0276b10347f89b363b2a82285f650bdcc135ad4a257c61bdd4d00d6df1fa24875b0ddaf + languageName: node + linkType: hard + +"require-main-filename@npm:^2.0.0": + version: 2.0.0 + resolution: "require-main-filename@npm:2.0.0" + checksum: 10/8604a570c06a69c9d939275becc33a65676529e1c3e5a9f42d58471674df79357872b96d70bb93a0380a62d60dc9031c98b1a9dad98c946ffdd61b7ac0c8cedd + languageName: node + linkType: hard + "requires-port@npm:^1.0.0": version: 1.0.0 resolution: "requires-port@npm:1.0.0" @@ -5918,6 +6023,13 @@ __metadata: languageName: node linkType: hard +"set-blocking@npm:^2.0.0": + version: 2.0.0 + resolution: "set-blocking@npm:2.0.0" + checksum: 10/8980ebf7ae9eb945bb036b6e283c547ee783a1ad557a82babf758a065e2fb6ea337fd82cac30dd565c1e606e423f30024a19fff7afbf4977d784720c4026a8ef + languageName: node + linkType: hard + "set-function-length@npm:^1.2.2": version: 1.2.2 resolution: "set-function-length@npm:1.2.2" @@ -6133,7 +6245,7 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -7069,6 +7181,13 @@ __metadata: languageName: node linkType: hard +"which-module@npm:^2.0.0": + version: 2.0.1 + resolution: "which-module@npm:2.0.1" + checksum: 10/1967b7ce17a2485544a4fdd9063599f0f773959cca24176dbe8f405e55472d748b7c549cd7920ff6abb8f1ab7db0b0f1b36de1a21c57a8ff741f4f1e792c52be + languageName: node + linkType: hard + "which-typed-array@npm:^1.1.13, which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.19": version: 1.1.19 resolution: "which-typed-array@npm:1.1.19" @@ -7136,6 +7255,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^6.2.0": + version: 6.2.0 + resolution: "wrap-ansi@npm:6.2.0" + dependencies: + ansi-styles: "npm:^4.0.0" + string-width: "npm:^4.1.0" + strip-ansi: "npm:^6.0.0" + checksum: 10/0d64f2d438e0b555e693b95aee7b2689a12c3be5ac458192a1ce28f542a6e9e59ddfecc37520910c2c88eb1f82a5411260566dba5064e8f9895e76e169e76187 + languageName: node + linkType: hard + "wrap-ansi@npm:^8.1.0": version: 8.1.0 resolution: "wrap-ansi@npm:8.1.0" @@ -7183,6 +7313,13 @@ __metadata: languageName: node linkType: hard +"y18n@npm:^4.0.0": + version: 4.0.3 + resolution: "y18n@npm:4.0.3" + checksum: 10/392870b2a100bbc643bc035fe3a89cef5591b719c7bdc8721bcdb3d27ab39fa4870acdca67b0ee096e146d769f311d68eda6b8195a6d970f227795061923013f + languageName: node + linkType: hard + "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0" @@ -7197,6 +7334,35 @@ __metadata: languageName: node linkType: hard +"yargs-parser@npm:^18.1.2": + version: 18.1.3 + resolution: "yargs-parser@npm:18.1.3" + dependencies: + camelcase: "npm:^5.0.0" + decamelize: "npm:^1.2.0" + checksum: 10/235bcbad5b7ca13e5abc54df61d42f230857c6f83223a38e4ed7b824681875b7f8b6ed52139d88a3ad007050f28dc0324b3c805deac7db22ae3b4815dae0e1bf + languageName: node + linkType: hard + +"yargs@npm:^15.3.1": + version: 15.4.1 + resolution: "yargs@npm:15.4.1" + dependencies: + cliui: "npm:^6.0.0" + decamelize: "npm:^1.2.0" + find-up: "npm:^4.1.0" + get-caller-file: "npm:^2.0.1" + require-directory: "npm:^2.1.1" + require-main-filename: "npm:^2.0.0" + set-blocking: "npm:^2.0.0" + string-width: "npm:^4.2.0" + which-module: "npm:^2.0.0" + y18n: "npm:^4.0.0" + yargs-parser: "npm:^18.1.2" + checksum: 10/bbcc82222996c0982905b668644ca363eebe6ffd6a572fbb52f0c0e8146661d8ce5af2a7df546968779bb03d1e4186f3ad3d55dfaadd1c4f0d5187c0e3a5ba16 + languageName: node + linkType: hard + "yocto-queue@npm:^0.1.0": version: 0.1.0 resolution: "yocto-queue@npm:0.1.0"