Merge pull request #433 from mauriceboe/fix/mfa-qr-svg
fix(mfa): generate SVG QR code
This commit is contained in:
@@ -253,8 +253,8 @@ export default function AccountTab(): React.ReactElement {
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setMfaLoading(true)
|
setMfaLoading(true)
|
||||||
try {
|
try {
|
||||||
const data = await authApi.mfaSetup() as { qr_data_url: string; secret: string }
|
const data = await authApi.mfaSetup() as { qr_svg: string; secret: string }
|
||||||
setMfaQr(data.qr_data_url)
|
setMfaQr(data.qr_svg)
|
||||||
setMfaSecret(data.secret)
|
setMfaSecret(data.secret)
|
||||||
setMfaSetupCode('')
|
setMfaSetupCode('')
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
@@ -274,7 +274,7 @@ export default function AccountTab(): React.ReactElement {
|
|||||||
{!user?.mfa_enabled && mfaQr && (
|
{!user?.mfa_enabled && mfaQr && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<p className="text-sm" style={{ color: 'var(--text-muted)' }}>{t('settings.mfa.scanQr')}</p>
|
<p className="text-sm" style={{ color: 'var(--text-muted)' }}>{t('settings.mfa.scanQr')}</p>
|
||||||
<img src={mfaQr} alt="" className="rounded-lg border mx-auto block" style={{ maxWidth: 200, borderColor: 'var(--border-primary)' }} />
|
<div className="rounded-lg border mx-auto block overflow-hidden" style={{ width: 'fit-content', borderColor: 'var(--border-primary)' }} dangerouslySetInnerHTML={{ __html: mfaQr! }} />
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium mb-1" style={{ color: 'var(--text-secondary)' }}>{t('settings.mfa.secretLabel')}</label>
|
<label className="block text-xs font-medium mb-1" style={{ color: 'var(--text-secondary)' }}>{t('settings.mfa.secretLabel')}</label>
|
||||||
<code className="block text-xs p-2 rounded break-all" style={{ background: 'var(--bg-hover)', color: 'var(--text-primary)' }}>{mfaSecret}</code>
|
<code className="block text-xs p-2 rounded break-all" style={{ background: 'var(--bg-hover)', color: 'var(--text-primary)' }}>{mfaSecret}</code>
|
||||||
|
|||||||
@@ -260,8 +260,8 @@ router.post('/mfa/setup', authenticate, (req: Request, res: Response) => {
|
|||||||
const result = setupMfa(authReq.user.id, authReq.user.email);
|
const result = setupMfa(authReq.user.id, authReq.user.email);
|
||||||
if (result.error) return res.status(result.status!).json({ error: result.error });
|
if (result.error) return res.status(result.status!).json({ error: result.error });
|
||||||
result.qrPromise!
|
result.qrPromise!
|
||||||
.then((qr_data_url: string) => {
|
.then((qr_svg: string) => {
|
||||||
res.json({ secret: result.secret, otpauth_url: result.otpauth_url, qr_data_url });
|
res.json({ secret: result.secret, otpauth_url: result.otpauth_url, qr_svg });
|
||||||
})
|
})
|
||||||
.catch((err: unknown) => {
|
.catch((err: unknown) => {
|
||||||
console.error('[MFA] QR code generation error:', err);
|
console.error('[MFA] QR code generation error:', err);
|
||||||
|
|||||||
@@ -816,7 +816,7 @@ export function setupMfa(userId: number, userEmail: string): { error?: string; s
|
|||||||
console.error('[MFA] Setup error:', err);
|
console.error('[MFA] Setup error:', err);
|
||||||
return { error: 'MFA setup failed', status: 500 };
|
return { error: 'MFA setup failed', status: 500 };
|
||||||
}
|
}
|
||||||
return { secret, otpauth_url, qrPromise: QRCode.toDataURL(otpauth_url) };
|
return { secret, otpauth_url, qrPromise: QRCode.toString(otpauth_url, { type: 'svg', width: 250 }) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enableMfa(userId: number, code?: string): { error?: string; status?: number; success?: boolean; mfa_enabled?: boolean; backup_codes?: string[] } {
|
export function enableMfa(userId: number, code?: string): { error?: string; status?: number; success?: boolean; mfa_enabled?: boolean; backup_codes?: string[] } {
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ describe('MFA', () => {
|
|||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.body.secret).toBeDefined();
|
expect(res.body.secret).toBeDefined();
|
||||||
expect(res.body.otpauth_url).toContain('otpauth://');
|
expect(res.body.otpauth_url).toContain('otpauth://');
|
||||||
expect(res.body.qr_data_url).toMatch(/^data:image/);
|
expect(res.body.qr_svg).toMatch(/^<svg/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('AUTH-015 — POST /api/auth/mfa/enable with valid TOTP code enables MFA', async () => {
|
it('AUTH-015 — POST /api/auth/mfa/enable with valid TOTP code enables MFA', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user