diff --git a/backend/atlas_portal/routes/access_requests.py b/backend/atlas_portal/routes/access_requests.py index 24318ae..858d9b8 100644 --- a/backend/atlas_portal/routes/access_requests.py +++ b/backend/atlas_portal/routes/access_requests.py @@ -58,6 +58,7 @@ def _verify_url(request_code: str, token: str) -> str: ONBOARDING_STEPS: tuple[str, ...] = ( "keycloak_password_changed", + "keycloak_mfa_optional", "vaultwarden_master_password", "element_recovery_key", "element_recovery_key_stored", @@ -68,12 +69,20 @@ ONBOARDING_STEPS: tuple[str, ...] = ( "mail_client_setup", ) +ONBOARDING_OPTIONAL_STEPS: set[str] = {"keycloak_mfa_optional"} +ONBOARDING_REQUIRED_STEPS: tuple[str, ...] = tuple( + step for step in ONBOARDING_STEPS if step not in ONBOARDING_OPTIONAL_STEPS +) + KEYCLOAK_MANAGED_STEPS: set[str] = {"keycloak_password_changed"} +_KEYCLOAK_MFA_OPTIONAL_STATE_ARTIFACT = "keycloak_mfa_optional_state" +_KEYCLOAK_MFA_OPTIONAL_VALID_STATES = {"done", "skipped"} def _sequential_prerequisites( steps: tuple[str, ...], keycloak_managed_steps: set[str], + optional_steps: set[str], ) -> dict[str, set[str]]: completed: list[str] = [] prerequisites: dict[str, set[str]] = {} @@ -82,13 +91,15 @@ def _sequential_prerequisites( completed.append(step) continue prerequisites[step] = set(completed) - completed.append(step) + if step not in optional_steps: + completed.append(step) return prerequisites ONBOARDING_STEP_PREREQUISITES: dict[str, set[str]] = _sequential_prerequisites( ONBOARDING_STEPS, KEYCLOAK_MANAGED_STEPS, + ONBOARDING_OPTIONAL_STEPS, ) _ELEMENT_RECOVERY_ARTIFACT = "element_recovery_key_sha256" @@ -212,7 +223,7 @@ def _advance_status(conn, request_code: str, username: str, status: str) -> str: if status == "awaiting_onboarding": completed = _completed_onboarding_steps(conn, request_code, username) - if set(ONBOARDING_STEPS).issubset(completed): + if set(ONBOARDING_REQUIRED_STEPS).issubset(completed): conn.execute( "UPDATE access_requests SET status = 'ready' WHERE request_code = %s AND status = 'awaiting_onboarding'", (request_code,), @@ -222,6 +233,40 @@ def _advance_status(conn, request_code: str, username: str, status: str) -> str: return status +def _fetch_optional_mfa_state(conn, request_code: str) -> str: + row = conn.execute( + """ + SELECT value_hash + FROM access_request_onboarding_artifacts + WHERE request_code = %s AND artifact = %s + """, + (request_code, _KEYCLOAK_MFA_OPTIONAL_STATE_ARTIFACT), + ).fetchone() + if not row: + return "pending" + value = row.get("value_hash") if isinstance(row, dict) else None + if not isinstance(value, str): + return "pending" + cleaned = value.strip().lower() + if cleaned in _KEYCLOAK_MFA_OPTIONAL_VALID_STATES: + return cleaned + return "pending" + + +def _onboarding_payload(conn, request_code: str, username: str) -> dict[str, Any]: + completed_steps = sorted(_completed_onboarding_steps(conn, request_code, username)) + return { + "required_steps": list(ONBOARDING_REQUIRED_STEPS), + "optional_steps": sorted(ONBOARDING_OPTIONAL_STEPS), + "completed_steps": completed_steps, + "optional": { + "keycloak_mfa_optional": { + "state": _fetch_optional_mfa_state(conn, request_code), + } + }, + } + + def register(app) -> None: @app.route("/api/access/request", methods=["POST"]) def request_access() -> Any: @@ -540,11 +585,7 @@ def register(app) -> None: if status in {"awaiting_onboarding", "ready"}: response["onboarding_url"] = f"/onboarding?code={code}" if status in {"awaiting_onboarding", "ready"}: - completed = sorted(_completed_onboarding_steps(conn, code, row.get("username") or "")) - response["onboarding"] = { - "required_steps": list(ONBOARDING_STEPS), - "completed_steps": completed, - } + response["onboarding"] = _onboarding_payload(conn, code, row.get("username") or "") return jsonify(response) except Exception: return jsonify({"error": "failed to load status"}), 502 @@ -623,7 +664,7 @@ def register(app) -> None: # Re-evaluate completion to update request status to ready if applicable. status = _advance_status(conn, code, username, status) - completed_steps = sorted(_completed_onboarding_steps(conn, code, username)) + onboarding_payload = _onboarding_payload(conn, code, username) except Exception: return jsonify({"error": "failed to update onboarding"}), 502 @@ -631,7 +672,7 @@ def register(app) -> None: { "ok": True, "status": status, - "onboarding": {"required_steps": list(ONBOARDING_STEPS), "completed_steps": completed_steps}, + "onboarding": onboarding_payload, } ) @@ -698,7 +739,7 @@ def register(app) -> None: ) status = _advance_status(conn, code, username, status) - completed_steps = sorted(_completed_onboarding_steps(conn, code, username)) + onboarding_payload = _onboarding_payload(conn, code, username) except Exception: return jsonify({"error": "failed to verify element recovery key"}), 502 @@ -706,6 +747,73 @@ def register(app) -> None: { "ok": True, "status": status, - "onboarding": {"required_steps": list(ONBOARDING_STEPS), "completed_steps": completed_steps}, + "onboarding": onboarding_payload, } ) + + @app.route("/api/access/request/onboarding/mfa", methods=["POST"]) + @require_auth + def request_access_onboarding_mfa_optional() -> Any: + if not configured(): + return jsonify({"error": "server not configured"}), 503 + + payload = request.get_json(silent=True) or {} + code = (payload.get("request_code") or payload.get("code") or "").strip() + state = (payload.get("state") or "").strip().lower() + + if not code: + return jsonify({"error": "request_code is required"}), 400 + if state not in _KEYCLOAK_MFA_OPTIONAL_VALID_STATES: + return jsonify({"error": "invalid state"}), 400 + + username = getattr(g, "keycloak_username", "") or "" + if not username: + return jsonify({"error": "invalid token"}), 401 + + try: + with connect() as conn: + row = conn.execute( + "SELECT username, status FROM access_requests WHERE request_code = %s", + (code,), + ).fetchone() + if not row: + return jsonify({"error": "not found"}), 404 + if (row.get("username") or "") != username: + return jsonify({"error": "forbidden"}), 403 + + status = _normalize_status(row.get("status") or "") + if status not in {"awaiting_onboarding", "ready"}: + return jsonify({"error": "onboarding not available"}), 409 + + prerequisites = ONBOARDING_STEP_PREREQUISITES.get("keycloak_mfa_optional", set()) + if prerequisites: + current_completed = _completed_onboarding_steps(conn, code, username) + missing = sorted(prerequisites - current_completed) + if missing: + return jsonify({"error": "step is blocked", "blocked_by": missing}), 409 + + conn.execute( + """ + INSERT INTO access_request_onboarding_artifacts (request_code, artifact, value_hash) + VALUES (%s, %s, %s) + ON CONFLICT (request_code, artifact) DO UPDATE + SET value_hash = EXCLUDED.value_hash, + created_at = NOW() + """, + (code, _KEYCLOAK_MFA_OPTIONAL_STATE_ARTIFACT, state), + ) + conn.execute( + """ + INSERT INTO access_request_onboarding_steps (request_code, step) + VALUES (%s, %s) + ON CONFLICT (request_code, step) DO NOTHING + """, + (code, "keycloak_mfa_optional"), + ) + + status = _advance_status(conn, code, username, status) + onboarding_payload = _onboarding_payload(conn, code, username) + except Exception: + return jsonify({"error": "failed to update onboarding"}), 502 + + return jsonify({"ok": True, "status": status, "onboarding": onboarding_payload}) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7435c86..629ebc8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "axios": "^1.6.7", "keycloak-js": "^26.2.2", "mermaid": "^10.9.1", + "qrcode": "^1.5.4", "vue": "^3.4.21", "vue-router": "^4.3.2" }, @@ -961,6 +962,30 @@ "integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==", "license": "MIT" }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -991,6 +1016,15 @@ "node": ">= 0.4" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/character-entities": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", @@ -1001,6 +1035,35 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1532,6 +1595,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decode-named-character-reference": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", @@ -1581,6 +1653,12 @@ "node": ">=0.3.1" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/dompurify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", @@ -1610,6 +1688,12 @@ "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==", "license": "EPL-2.0" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -1712,6 +1796,19 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "license": "MIT" }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -1772,6 +1869,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1881,6 +1987,15 @@ "node": ">=12" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/katex": { "version": "0.16.27", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.27.tgz", @@ -1935,6 +2050,18 @@ "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", "license": "MIT" }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", @@ -2526,12 +2653,66 @@ "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==", "license": "MIT" }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -2566,6 +2747,38 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/robust-predicates": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", @@ -2638,6 +2851,12 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2647,6 +2866,32 @@ "node": ">=0.10.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/stylis": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", @@ -2807,6 +3052,67 @@ "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", "license": "Apache-2.0" + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 522f56e..c00f337 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "axios": "^1.6.7", "keycloak-js": "^26.2.2", "mermaid": "^10.9.1", + "qrcode": "^1.5.4", "vue": "^3.4.21", "vue-router": "^4.3.2" }, diff --git a/frontend/src/views/OnboardingView.vue b/frontend/src/views/OnboardingView.vue index d553e12..7ce32aa 100644 --- a/frontend/src/views/OnboardingView.vue +++ b/frontend/src/views/OnboardingView.vue @@ -135,6 +135,54 @@
++ Add a second factor with a mobile authenticator app. This is optional and won't block the rest of onboarding. +
+{{ mfaQrError }}
+