diff --git a/backend/app.js b/backend/app.js index a0c6a52..2b23b46 100644 --- a/backend/app.js +++ b/backend/app.js @@ -16,6 +16,7 @@ import healthRoutes from './routes/healthRoutes.js'; import assessmentRoutes from './routes/assessmentRoutes.js'; import complianceRoutes from './routes/complianceRoutes.js'; import questionnaireRoutes from './routes/questionnaireRoutes.js'; +import questionnairePublicRoutes from './routes/questionnairePublicRoutes.js'; import organizationRoutes from './routes/organizationRoutes.js'; import billingRoutes from './routes/billingRoutes.js'; import { handleStripeWebhook } from './controllers/billingController.js'; @@ -239,9 +240,14 @@ app.use('/api/v1/auth', authRoutes); app.use('/api/v1/organizations', organizationRoutes); app.use('/api/v1/billing', billingRoutes); +// Unguarded: public vendor questionnaire form (token-gated, not workspace auth). +// Mounted ahead of requireActivePlan so a visitor's own session cookie (e.g. the +// workspace owner previewing their own link from a paused-plan org) can't 402 +// a route a vendor with no Retrieva account must be able to reach. +app.use('/api/v1/questionnaires', questionnairePublicRoutes); + // Paid routes — optionalAuth sets req.user when a token is present so the -// plan guard can check it; public sub-routes (e.g. questionnaire respond) -// have no token and pass through to the router's own authenticate. +// plan guard can check it. // authenticate is idempotent so the router's router.use(authenticate) is a // no-op when req.user is already set by optionalAuth. // B2: setTenantContext (after auth) makes the active workspace available to the diff --git a/backend/controllers/ragController.js b/backend/controllers/ragController.js index 81d3503..5983e70 100644 --- a/backend/controllers/ragController.js +++ b/backend/controllers/ragController.js @@ -84,6 +84,13 @@ export const askQuestionStream = catchAsync(async (req, res) => { closed = true; }); + // Keep the SSE connection alive through nginx's proxy_read_timeout (default 60s). + // The embedding phase can take minutes on CPU-only ollama — without this, nginx + // kills the idle connection before the first event is ever flushed. + const heartbeat = setInterval(() => { + if (!closed && !res.writableEnded) res.write(': keepalive\n\n'); + }, 20000); + try { await executeRAG({ question, @@ -104,6 +111,7 @@ export const askQuestionStream = catchAsync(async (req, res) => { send('error', { message: 'Stream failed', code: 'STREAM_ERROR' }); } } finally { + clearInterval(heartbeat); if (!res.writableEnded) res.end(); } }); diff --git a/backend/routes/questionnairePublicRoutes.js b/backend/routes/questionnairePublicRoutes.js new file mode 100644 index 0000000..8a30004 --- /dev/null +++ b/backend/routes/questionnairePublicRoutes.js @@ -0,0 +1,34 @@ +import { Router } from 'express'; +import { getPublicForm, submitResponse } from '../controllers/questionnaireController.js'; +import { validateBody, validateParams } from '../middleware/validate.js'; +import { submitQuestionnaireResponseSchema, tokenParamsSchema } from '../validators/schemas.js'; + +const router = Router(); + +// --------------------------------------------------------------------------- +// Public routes — no authentication, no plan gate (token-based access only). +// Mounted unguarded in app.js so a visitor's own session cookie (e.g. the +// workspace owner previewing their own link) can't trigger requireActivePlan +// for a route a vendor with no Retrieva account must be able to reach. +// --------------------------------------------------------------------------- + +/** + * @route GET /api/v1/questionnaires/respond/:token + * @desc Load the public vendor questionnaire form + * @access Public (token-gated) + */ +router.get('/respond/:token', validateParams(tokenParamsSchema), getPublicForm); + +/** + * @route POST /api/v1/questionnaires/respond/:token + * @desc Save partial or final vendor response + * @access Public (token-gated) + */ +router.post( + '/respond/:token', + validateParams(tokenParamsSchema), + validateBody(submitQuestionnaireResponseSchema), + submitResponse +); + +export default router; diff --git a/backend/routes/questionnaireRoutes.js b/backend/routes/questionnaireRoutes.js index b9c7ef8..0e0226a 100644 --- a/backend/routes/questionnaireRoutes.js +++ b/backend/routes/questionnaireRoutes.js @@ -5,8 +5,6 @@ import { getQuestionnaire, deleteQuestionnaire, sendQuestionnaire, - getPublicForm, - submitResponse, } from '../controllers/questionnaireController.js'; import { authenticate } from '../middleware/auth.js'; import { requireWorkspaceAccess } from '../middleware/workspaceAuth.js'; @@ -14,39 +12,16 @@ import { validateBody, validateParams, validateQuery } from '../middleware/valid import { createQuestionnaireSchema, sendQuestionnaireSchema, - submitQuestionnaireResponseSchema, idParamsSchema, - tokenParamsSchema, listQuestionnairesQuerySchema, } from '../validators/schemas.js'; const router = Router(); -// --------------------------------------------------------------------------- -// Public routes — no authentication required (token-based access only) -// --------------------------------------------------------------------------- - -/** - * @route GET /api/v1/questionnaires/respond/:token - * @desc Load the public vendor questionnaire form - * @access Public (token-gated) - */ -router.get('/respond/:token', validateParams(tokenParamsSchema), getPublicForm); - -/** - * @route POST /api/v1/questionnaires/respond/:token - * @desc Save partial or final vendor response - * @access Public (token-gated) - */ -router.post( - '/respond/:token', - validateParams(tokenParamsSchema), - validateBody(submitQuestionnaireResponseSchema), - submitResponse -); - // --------------------------------------------------------------------------- // Authenticated routes — require JWT + workspace membership +// Public /respond/:token routes live in questionnairePublicRoutes.js, mounted +// unguarded in app.js (ahead of requireActivePlan). // --------------------------------------------------------------------------- /** diff --git a/frontend/package.json b/frontend/package.json index bf1de05..b7008a8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -45,6 +45,7 @@ "lucide-react": "^0.563.0", "next": "16.2.7", "next-themes": "^0.4.6", + "qrcode.react": "^4.2.0", "react": "19.2.3", "react-dom": "19.2.3", "react-dropzone": "^15.0.0", diff --git a/frontend/src/features/questionnaires/api/questionnaires.ts b/frontend/src/features/questionnaires/api/questionnaires.ts index ee4ecf0..5cae5b7 100644 --- a/frontend/src/features/questionnaires/api/questionnaires.ts +++ b/frontend/src/features/questionnaires/api/questionnaires.ts @@ -68,9 +68,9 @@ export interface CreateQuestionnaireDto { // --------------------------------------------------------------------------- const publicApiBaseURL = - (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_API_URL + typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_API_URL ? process.env.NEXT_PUBLIC_API_URL - : '') + '/api/v1'; + : '/api/v1'; const publicClient = axios.create({ baseURL: publicApiBaseURL, diff --git a/frontend/src/features/settings/components/mfa-section.tsx b/frontend/src/features/settings/components/mfa-section.tsx index 19354b2..60ca973 100644 --- a/frontend/src/features/settings/components/mfa-section.tsx +++ b/frontend/src/features/settings/components/mfa-section.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import { useMutation } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { Loader2, ShieldCheck, ShieldOff, Copy } from 'lucide-react'; +import { QRCodeSVG } from 'qrcode.react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; @@ -123,17 +124,18 @@ export function MfaSection() { {!enabled && setup && (
-
+

{t('settings.mfa.step1')}

{t('settings.mfa.step1Desc')}

- +
+ +
+

{t('settings.mfa.manualEntry')}

+ {setup.secret} - - {setup.otpauthUrl} -
diff --git a/frontend/src/shared/i18n/locales/en.json b/frontend/src/shared/i18n/locales/en.json index cc96134..95edebd 100644 --- a/frontend/src/shared/i18n/locales/en.json +++ b/frontend/src/shared/i18n/locales/en.json @@ -982,7 +982,8 @@ "copyCodes": "Copy codes", "setup": "Set up two-factor authentication", "step1": "1. Add this account to your authenticator app", - "step1Desc": "Scan the otpauth URL as a QR code, or enter the secret manually:", + "step1Desc": "Open Microsoft Authenticator (or Google Authenticator) and scan the QR code below:", + "manualEntry": "Can't scan? Enter this secret manually in your app:", "step2": "2. Enter the 6-digit code to confirm", "codePlaceholder": "123456", "enable": "Enable", diff --git a/frontend/src/shared/i18n/locales/fr.json b/frontend/src/shared/i18n/locales/fr.json index 91419a2..62b79ce 100644 --- a/frontend/src/shared/i18n/locales/fr.json +++ b/frontend/src/shared/i18n/locales/fr.json @@ -982,7 +982,8 @@ "copyCodes": "Copier les codes", "setup": "Configurer l'authentification à deux facteurs", "step1": "1. Ajoutez ce compte à votre application d'authentification", - "step1Desc": "Scannez l'URL otpauth sous forme de QR code, ou saisissez le secret manuellement :", + "step1Desc": "Ouvrez Microsoft Authenticator (ou Google Authenticator) et scannez le QR code ci-dessous :", + "manualEntry": "Impossible de scanner ? Saisissez ce secret manuellement dans votre application :", "step2": "2. Saisissez le code à 6 chiffres pour confirmer", "codePlaceholder": "123456", "enable": "Activer", diff --git a/package-lock.json b/package-lock.json index c87b6a3..c1661bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -181,6 +181,7 @@ "lucide-react": "^0.563.0", "next": "16.2.7", "next-themes": "^0.4.6", + "qrcode.react": "^4.2.0", "react": "19.2.3", "react-dom": "19.2.3", "react-dropzone": "^15.0.0", @@ -231,7 +232,7 @@ "version": "0.9.31", "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@adobe/css-tools": { @@ -258,7 +259,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@csstools/css-calc": "^3.0.0", @@ -272,7 +273,7 @@ "version": "11.5.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", - "dev": true, + "devOptional": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -282,7 +283,7 @@ "version": "6.8.1", "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", @@ -296,7 +297,7 @@ "version": "11.5.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", - "dev": true, + "devOptional": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -306,7 +307,7 @@ "version": "2.3.9", "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@asteasolutions/zod-to-openapi": { @@ -1859,7 +1860,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1879,7 +1880,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1903,7 +1904,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1931,7 +1932,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1954,7 +1955,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -1979,7 +1980,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -2250,7 +2251,7 @@ "version": "1.15.1", "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" @@ -4187,7 +4188,7 @@ "version": "1.60.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "playwright": "1.60.0" @@ -6941,7 +6942,7 @@ "version": "19.2.17", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -6951,7 +6952,7 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -8634,7 +8635,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "require-from-string": "^2.0.2" @@ -9657,7 +9658,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "mdn-data": "2.27.1", @@ -9691,7 +9692,7 @@ "version": "5.3.7", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@asamuzakjp/css-color": "^4.1.1", @@ -9707,7 +9708,7 @@ "version": "11.5.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", - "dev": true, + "devOptional": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -9717,7 +9718,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/d3-array": { @@ -9865,7 +9866,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "whatwg-mimetype": "^5.0.0", @@ -9879,7 +9880,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=20" @@ -9971,7 +9972,7 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/decimal.js-light": { @@ -10410,7 +10411,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=20.19.0" @@ -12541,7 +12542,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@exodus/bytes": "^1.6.0" @@ -12590,7 +12591,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -12604,7 +12605,7 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 14" @@ -12614,7 +12615,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -12632,7 +12633,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/https-proxy-agent": { @@ -12749,7 +12750,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 4" @@ -13286,7 +13287,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/is-promise": { @@ -13627,7 +13628,7 @@ "version": "27.4.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz", "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@acemir/cssom": "^0.9.28", @@ -13667,7 +13668,7 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 14" @@ -13677,7 +13678,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -13695,7 +13696,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -13709,7 +13710,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/jsesc": { @@ -14765,7 +14766,7 @@ "version": "2.27.1", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "dev": true, + "devOptional": true, "license": "CC0-1.0" }, "node_modules/media-typer": { @@ -16429,7 +16430,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "entities": "^8.0.0" @@ -16870,7 +16871,7 @@ "version": "1.60.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "playwright-core": "1.60.0" @@ -16889,7 +16890,7 @@ "version": "1.60.0", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -17329,6 +17330,15 @@ "node": ">=6" } }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/qs": { "version": "6.15.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", @@ -18250,7 +18260,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" @@ -19465,7 +19475,7 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/tailwind-merge": { @@ -19736,7 +19746,7 @@ "version": "7.4.2", "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz", "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tldts-core": "^7.4.2" @@ -19749,7 +19759,7 @@ "version": "7.4.2", "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz", "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/to-regex-range": { @@ -19788,7 +19798,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "tldts": "^7.0.5" @@ -19801,7 +19811,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "punycode": "^2.3.1" @@ -20013,7 +20023,6 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -20491,7 +20500,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "xml-name-validator": "^5.0.0" @@ -20504,7 +20513,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", - "dev": true, + "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=20" @@ -20520,7 +20529,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=18" @@ -20530,7 +20539,7 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "tr46": "^6.0.0", @@ -20759,7 +20768,7 @@ "version": "8.21.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -20811,7 +20820,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=18" @@ -20836,7 +20845,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/xmlhttprequest-ssl": {