diff --git a/src/gui/src/UI/UIWindow2FASetup.js b/src/gui/src/UI/UIWindow2FASetup.js index d444d4d37..0cde4f64a 100644 --- a/src/gui/src/UI/UIWindow2FASetup.js +++ b/src/gui/src/UI/UIWindow2FASetup.js @@ -1,4 +1,4 @@ -/* +/** * Copyright (C) 2024-present Puter Technologies Inc. * * This file is part of Puter. @@ -16,283 +16,940 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -/* - Plan: - Components: OneAtATimeView < ... > - Screen 1: QR code and entry box for testing - Components: Flexer < UIQRCode, CodeEntryView, ActionsView > - Logic: - - when CodeEntryView has a value, check it against the QR code value... - ... then go to the next screen - - CodeEntryView will have callbacks: `verify`, `on_verified` - - cancel action - - Screen 2: Recovery codes - Components: Flexer < ActionsView > - Logic: - - done action - - cancel action - - when done action is clicked, call /auth/configure-2fa/enable - -*/ - -import TeePromise from '../util/TeePromise.js'; -import ValueHolder from '../util/ValueHolder.js'; -import Button from './Components/Button.js'; -import CodeEntryView from './Components/CodeEntryView.js'; -import Flexer from './Components/Flexer.js'; +import UIWindow from './UIWindow.js'; import UIQRCode from './UIQRCode.js'; -import StepView from './Components/StepView.js'; -import JustHTML from './Components/JustHTML.js'; -import UIComponentWindow from './UIComponentWindow.js'; + +// ── CSS (injected once) ───────────────────────────────────────────────────── +const CSS = ` +.tfa-setup { + display: flex; + flex-direction: column; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + color: #1a2233; + user-select: none; + -webkit-user-select: none; +} + +/* ── Progress bar ──────────────────────────────────────────────────────── */ +.tfa-progress { + display: flex; + align-items: center; + margin-bottom: 28px; +} +.tfa-progress-step { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + flex: 0 0 auto; + min-width: 0; +} +.tfa-progress-dot { + width: 28px; + height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + font-weight: 600; + border: 2px solid #d0d5dd; + color: #98a2b3; + background: #fff; + transition: all 0.25s ease; + flex-shrink: 0; +} +.tfa-progress-dot.active { + border-color: #3b82f6; + background: #3b82f6; + color: #fff; +} +.tfa-progress-dot.done { + border-color: #22c55e; + background: #22c55e; + color: #fff; +} +.tfa-progress-label { + font-size: 11px; + font-weight: 500; + color: #98a2b3; + text-align: center; + transition: color 0.25s ease; + max-width: 80px; + line-height: 1.3; +} +.tfa-progress-label.active { + color: #1a2233; +} +.tfa-progress-line { + flex: 1; + height: 2px; + background: #e5e7eb; + margin: 0 8px; + margin-bottom: 22px; + position: relative; + min-width: 20px; +} +.tfa-progress-line-fill { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 0%; + background: #22c55e; + transition: width 0.4s ease; +} + +/* ── Screens ───────────────────────────────────────────────────────────── */ +.tfa-screen { + display: none; + flex-direction: column; + animation: tfa-fade-in 0.3s ease; +} +.tfa-screen.active { + display: flex; +} +@keyframes tfa-fade-in { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +/* ── Screen 1: QR + Code ───────────────────────────────────────────── */ +.tfa-instruction { + font-size: 14px; + line-height: 1.6; + color: #475569; + margin-bottom: 20px; +} +.tfa-qr-area { + display: flex; + justify-content: center; + padding: 16px 0 20px; +} +.tfa-qr-area .qr-code { +} +.tfa-qr-area .qr-code img { + margin-bottom: 0 !important; +} +.tfa-secret-row { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + margin-bottom: 24px; +} +.tfa-secret-key { + font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; + font-size: 13px; + letter-spacing: 1.5px; + color: #475569; + background: #f1f5f9; + padding: 6px 12px; + border-radius: 6px; + border: 1px solid #e2e8f0; +} +.tfa-copy-secret { + background: none; + border: 1px solid #e2e8f0; + border-radius: 6px; + padding: 5px 8px; + cursor: pointer; + color: #64748b; + display: flex; + align-items: center; + transition: all 0.15s ease; +} +.tfa-copy-secret:hover { + background: #f1f5f9; + color: #334155; +} +.tfa-divider { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 20px; +} +.tfa-divider::before, +.tfa-divider::after { + content: ''; + flex: 1; + height: 1px; + background: #e5e7eb; +} +.tfa-divider span { + font-size: 12px; + font-weight: 500; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* ── Code input ────────────────────────────────────────────────────── */ +.tfa-code-section label { + display: block; + font-size: 13px; + font-weight: 600; + color: #334155; + margin-bottom: 8px; +} +.tfa-code-inputs { + display: flex; + gap: 8px; + justify-content: center; + margin-bottom: 8px; +} +.tfa-code-inputs input { + width: 44px; + height: 52px; + text-align: center; + font-size: 22px; + font-weight: 600; + font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; + border: 2px solid #e2e8f0; + border-radius: 10px; + background: #fff; + color: #1a2233; + outline: none; + transition: border-color 0.15s ease, box-shadow 0.15s ease; + caret-color: #3b82f6; + -moz-appearance: textfield; +} +.tfa-code-inputs input::-webkit-outer-spin-button, +.tfa-code-inputs input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} +.tfa-code-inputs input:focus { + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); +} +.tfa-code-inputs input.error { + border-color: #ef4444; + box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1); + animation: tfa-shake 0.4s ease; +} +@keyframes tfa-shake { + 0%, 100% { transform: translateX(0); } + 20% { transform: translateX(-4px); } + 40% { transform: translateX(4px); } + 60% { transform: translateX(-3px); } + 80% { transform: translateX(2px); } +} +.tfa-code-error { + font-size: 13px; + color: #ef4444; + text-align: center; + min-height: 20px; + margin-bottom: 4px; +} +.tfa-code-spinner { + display: none; + justify-content: center; + align-items: center; + gap: 8px; + padding: 8px 0; + font-size: 13px; + color: #64748b; +} +.tfa-code-spinner.visible { + display: flex; +} +.tfa-spinner-icon { + width: 16px; + height: 16px; + border: 2px solid #e2e8f0; + border-top-color: #3b82f6; + border-radius: 50%; + animation: tfa-spin 0.6s linear infinite; +} +@keyframes tfa-spin { + to { transform: rotate(360deg); } +} + +/* ── Screen 2: Recovery codes ──────────────────────────────────────── */ +.tfa-recovery-intro { + font-size: 14px; + line-height: 1.6; + color: #475569; + margin-bottom: 16px; +} +.tfa-recovery-warning { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 14px; + background: #fffbeb; + border: 1px solid #fde68a; + border-radius: 10px; + margin-bottom: 20px; + font-size: 13px; + line-height: 1.5; + color: #92400e; +} +.tfa-recovery-warning svg { + flex-shrink: 0; + margin-top: 1px; +} +.tfa-codes-card { + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 12px; + overflow: hidden; + margin-bottom: 20px; + box-shadow: 0 1px 3px rgba(0,0,0,0.04); +} +.tfa-codes-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0; + padding: 4px 0; +} +.tfa-codes-grid .tfa-code-item { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; + font-size: 14px; + letter-spacing: 1px; + color: #1e293b; +} +.tfa-code-item-idx { + font-size: 11px; + color: #94a3b8; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + min-width: 16px; +} +.tfa-codes-actions { + display: flex; + border-top: 1px solid #e2e8f0; +} +.tfa-codes-actions button { + flex: 1; + padding: 10px; + background: none; + border: none; + font-size: 13px; + font-weight: 500; + color: #3b82f6; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + transition: background 0.15s ease; +} +.tfa-codes-actions button:hover { + background: #f8fafc; +} +.tfa-codes-actions button + button { + border-left: 1px solid #e2e8f0; +} +.tfa-codes-actions button.copied { + color: #22c55e; +} + +/* ── Checkboxes ────────────────────────────────────────────────────── */ +.tfa-confirmations { + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 24px; +} +.tfa-confirm-item { + display: flex; + align-items: flex-start; + gap: 10px; + cursor: pointer; +} +.tfa-confirm-item input[type="checkbox"] { + appearance: none; + -webkit-appearance: none; + width: 20px; + height: 20px; + border: 2px solid #d0d5dd; + border-radius: 6px; + flex-shrink: 0; + margin-top: 1px; + cursor: pointer; + position: relative; + transition: all 0.15s ease; + background: #fff; +} +.tfa-confirm-item input[type="checkbox"]:checked { + background: #3b82f6; + border-color: #3b82f6; +} +.tfa-confirm-item input[type="checkbox"]:checked::after { + content: ''; + position: absolute; + left: 5px; + top: 2px; + width: 6px; + height: 10px; + border: solid #fff; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} +.tfa-confirm-item label { + font-size: 14px; + color: #475569; + line-height: 1.5; + cursor: pointer; +} + +/* ── Buttons ───────────────────────────────────────────────────────── */ +.tfa-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 12px 24px; + border-radius: 10px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s ease; + border: none; + outline: none; + width: 100%; +} +.tfa-btn-primary { + background: #3b82f6; + color: #fff; + box-shadow: 0 1px 2px rgba(59, 130, 246, 0.3); +} +.tfa-btn-primary:hover:not(:disabled) { + background: #2563eb; + box-shadow: 0 2px 8px rgba(59, 130, 246, 0.35); +} +.tfa-btn-primary:disabled { + opacity: 0.45; + cursor: not-allowed; +} +.tfa-btn-success { + background: #22c55e; + color: #fff; + box-shadow: 0 1px 2px rgba(34, 197, 94, 0.3); +} +.tfa-btn-success:hover { + background: #16a34a; +} + +/* ── Screen 3: Success ─────────────────────────────────────────────── */ +.tfa-success-icon { + width: 64px; + height: 64px; + border-radius: 50%; + background: #ecfdf5; + display: flex; + align-items: center; + justify-content: center; + margin: 8px auto 20px; + animation: tfa-pop 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} +@keyframes tfa-pop { + 0% { transform: scale(0); opacity: 0; } + 100% { transform: scale(1); opacity: 1; } +} +.tfa-success-title { + font-size: 20px; + font-weight: 700; + color: #1a2233; + text-align: center; + margin-bottom: 8px; +} +.tfa-success-text { + font-size: 14px; + color: #64748b; + text-align: center; + line-height: 1.6; + margin-bottom: 28px; +} + +/* ── Responsive ────────────────────────────────────────────────────── */ + +/* Small window width (container query via the window class) */ +@media (max-width: 540px) { + .window-tfa-setup { + width: calc(100vw - 24px) !important; + left: 12px !important; + max-height: calc(100vh - 24px); + } + .window-tfa-setup .window-body { + padding: 20px 16px 18px !important; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + } + .tfa-progress { + margin-bottom: 20px; + } + .tfa-qr-area { + padding: 10px 0 14px; + } + .tfa-qr-area .qr-code img, + .tfa-qr-area .qr-code canvas { + max-width: 140px !important; + max-height: 140px !important; + width: 140px !important; + height: 140px !important; + } + .tfa-secret-row { + margin-bottom: 16px; + } + .tfa-secret-key { + font-size: 11px; + letter-spacing: 1px; + padding: 5px 8px; + overflow: hidden; + text-overflow: ellipsis; + max-width: 200px; + } + .tfa-code-inputs { + gap: 6px; + } + .tfa-code-inputs input { + width: 38px; + height: 46px; + font-size: 19px; + border-radius: 8px; + } + .tfa-instruction { + font-size: 13px; + margin-bottom: 14px; + } + .tfa-divider { + margin-bottom: 14px; + } + .tfa-codes-grid .tfa-code-item { + padding: 8px 12px; + font-size: 12px; + letter-spacing: 0.5px; + } + .tfa-recovery-warning { + font-size: 12px; + padding: 10px 12px; + } + .tfa-confirmations { + margin-bottom: 18px; + } + .tfa-confirm-item label { + font-size: 13px; + } + .tfa-btn { + padding: 11px 20px; + font-size: 13px; + } + .tfa-success-icon { + width: 52px; + height: 52px; + } + .tfa-success-title { + font-size: 18px; + } + .tfa-success-text { + font-size: 13px; + margin-bottom: 20px; + } +} + +/* Very small phones */ +@media (max-width: 380px) { + .window-tfa-setup { + width: 100vw !important; + left: 0 !important; + border-radius: 0 !important; + } + .tfa-code-inputs input { + width: 34px; + height: 42px; + font-size: 17px; + gap: 4px; + } + .tfa-codes-grid { + grid-template-columns: 1fr; + } + .tfa-progress-label { + font-size: 10px; + } +} +`; + +let css_injected = false; +const inject_css_ = () => { + if ( css_injected ) return; + css_injected = true; + $(''; + print_frame.contentWindow.document.body.innerHTML = style + '

Recovery Codes

' + grid_html; + print_frame.contentWindow.focus(); + print_frame.contentWindow.print(); + }); + + // ── Confirmation checkboxes ────────────────────────────────────────── + $w.find('.tfa-confirmations input[type="checkbox"]').on('change', function () { + const all_checked = $w.find('.tfa-confirmations input[type="checkbox"]').toArray() + .every(el => el.checked); + $w.find('.tfa-enable-btn').prop('disabled', ! all_checked); + }); + + // ── Enable 2FA ─────────────────────────────────────────────────────── + $w.find('.tfa-enable-btn').on('click', async function () { + $(this).prop('disabled', true).text('Enabling...'); + await api('enable'); + setup_succeeded = true; + go_to_screen(3); + }); + + // ── Done button ────────────────────────────────────────────────────── + $w.find('.tfa-done-btn').on('click', function () { + resolve_promise(true); + $w.close(); }); return { promise }; diff --git a/src/gui/src/i18n/translations/en.js b/src/gui/src/i18n/translations/en.js index 5a140ad61..e81d0d1f6 100644 --- a/src/gui/src/i18n/translations/en.js +++ b/src/gui/src/i18n/translations/en.js @@ -397,12 +397,7 @@ const en = { // === 2FA Setup === setup2fa_1_step_heading: 'Open your authenticator app', - setup2fa_1_instructions: ` - You can use any authenticator app that supports the Time-based One-Time Password (TOTP) protocol. - There are many to choose from, but if you're unsure - Authy - is a solid choice for Android and iOS. - `, + setup2fa_1_instructions: 'Scan the QR code with your authenticator app, then enter the 6-digit code it generates.', setup2fa_2_step_heading: 'Scan the QR code', setup2fa_3_step_heading: 'Enter the 6-digit code', setup2fa_4_step_heading: 'Copy your recovery codes',