Use local QR rendering for MFA setup (#458)

This commit is contained in:
shenlan 2025-10-07 18:56:26 +08:00 committed by GitHub
parent 3a6e03b60c
commit f6d93323f7
4 changed files with 229 additions and 21 deletions

View File

@ -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

View File

@ -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() {
<h4 className="text-sm font-semibold text-gray-900">{copy.guide.step2Title}</h4>
<p className="mt-2 text-sm text-gray-600">{copy.guide.step2Description}</p>
<div className="mt-4 flex flex-col gap-6 lg:flex-row lg:items-start">
{uri ? (
{qrCodeDataUrl ? (
<div className="flex justify-center lg:w-60 lg:justify-start">
<div className="rounded-xl border border-purple-100 bg-purple-50 p-3">
<div className="flex items-center justify-center rounded-lg border border-purple-200 bg-white p-2 shadow-sm">
<QRCode
value={uri}
size={200}
<Image
src={qrCodeDataUrl}
alt={copy.qrLabel}
width={176}
height={176}
className="h-44 w-44"
aria-label={copy.qrLabel}
unoptimized
/>
</div>
</div>

View File

@ -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"

View File

@ -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"