From 2dfecb52874a726fa49efd6f6580f8857492ba69 Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Wed, 1 May 2024 17:54:23 -0400 Subject: [PATCH] Add 2fa setting and complete login flow --- .../backend/src/routers/auth/configure-2fa.js | 2 + packages/backend/src/routers/whoami.js | 3 + src/UI/Settings/UITabAccount.js | 8 -- src/UI/Settings/UITabSecurity.js | 85 +++++++++++++++++++ src/UI/Settings/UIWindowSettings.js | 2 + src/UI/UIWindowLogin.js | 40 ++++++++- src/UI/UIWindowVerificationCode.js | 28 ++++-- src/i18n/translations/en.js | 10 +++ 8 files changed, 160 insertions(+), 18 deletions(-) create mode 100644 src/UI/Settings/UITabSecurity.js diff --git a/packages/backend/src/routers/auth/configure-2fa.js b/packages/backend/src/routers/auth/configure-2fa.js index a9aef1d06..0d0422a7f 100644 --- a/packages/backend/src/routers/auth/configure-2fa.js +++ b/packages/backend/src/routers/auth/configure-2fa.js @@ -31,6 +31,8 @@ module.exports = eggspress('/auth/configure-2fa/:action', { `UPDATE user SET otp_secret = ? WHERE uuid = ?`, [result.secret, user.uuid] ); + // update cached user + req.user.otp_secret = result.secret; return result; }; diff --git a/packages/backend/src/routers/whoami.js b/packages/backend/src/routers/whoami.js index 61ce36de3..924b589d0 100644 --- a/packages/backend/src/routers/whoami.js +++ b/packages/backend/src/routers/whoami.js @@ -41,6 +41,8 @@ const WHOAMI_GET = eggspress('/whoami', { const is_user = actor.type instanceof UserActorType; + console.log('user?', req.user); + // send user object const details = { username: req.user.username, @@ -54,6 +56,7 @@ const WHOAMI_GET = eggspress('/whoami', { is_temp: (req.user.password === null && req.user.email === null), taskbar_items: await get_taskbar_items(req.user), referral_code: req.user.referral_code, + otp: !! req.user.otp_secret, ...(req.new_token ? { token: req.token } : {}) }; diff --git a/src/UI/Settings/UITabAccount.js b/src/UI/Settings/UITabAccount.js index d05442065..2d933ef8e 100644 --- a/src/UI/Settings/UITabAccount.js +++ b/src/UI/Settings/UITabAccount.js @@ -64,14 +64,6 @@ export default { h += ``; } - // session manager - h += `
`; - h += `${i18n('sessions')}`; - h += `
`; - h += ``; - h += `
`; - h += `
`; - // 'Delete Account' button h += `
`; h += `${i18n("delete_account")}`; diff --git a/src/UI/Settings/UITabSecurity.js b/src/UI/Settings/UITabSecurity.js new file mode 100644 index 000000000..305692b14 --- /dev/null +++ b/src/UI/Settings/UITabSecurity.js @@ -0,0 +1,85 @@ +import UIWindowQR from "../UIWindowQR.js"; + +export default { + id: 'security', + title_i18n_key: 'security', + icon: 'shield.svg', + html: () => { + let h = `

${i18n('security')}

`; + + // change password button + if(!user.is_temp){ + h += `
`; + h += `${i18n('password')}`; + h += `
`; + h += ``; + h += `
`; + h += `
`; + } + + // session manager + h += `
`; + h += `${i18n('sessions')}`; + h += `
`; + h += ``; + h += `
`; + h += `
`; + + // configure 2FA + if(!user.is_temp){ + h += `
`; + h += `
`; + h += `${i18n('two_factor')}`; + h += `${ + i18n(user.otp ? 'two_factor_enabled' : 'two_factor_disabled') + }`; + h += `
`; + h += `
`; + h += ``; + h += ``; + h += `
`; + h += `
`; + } + + + return h; + }, + init: ($el_window) => { + $el_window.find('.enable-2fa').on('click', async function (e) { + const resp = await fetch(`${api_origin}/auth/configure-2fa/enable`, { + method: 'POST', + headers: { + Authorization: `Bearer ${puter.authToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({}), + }); + const data = await resp.json(); + + UIWindowQR({ + message_i18n_key: 'scan_qr_2fa', + text: data.url, + text_below: data.secret, + }); + + $el_window.find('.enable-2fa').hide(); + $el_window.find('.disable-2fa').show(); + $el_window.find('.user-otp-state').text(i18n('two_factor_enabled')); + }); + + $el_window.find('.disable-2fa').on('click', async function (e) { + const resp = await fetch(`${api_origin}/auth/configure-2fa/disable`, { + method: 'POST', + headers: { + Authorization: `Bearer ${puter.authToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({}), + }); + + $el_window.find('.enable-2fa').show(); + $el_window.find('.disable-2fa').hide(); + $el_window.find('.user-otp-state').text(i18n('two_factor_disabled')); + }); + } +} \ No newline at end of file diff --git a/src/UI/Settings/UIWindowSettings.js b/src/UI/Settings/UIWindowSettings.js index 280a4903d..133cca307 100644 --- a/src/UI/Settings/UIWindowSettings.js +++ b/src/UI/Settings/UIWindowSettings.js @@ -21,6 +21,7 @@ import UIWindow from '../UIWindow.js' import AboutTab from './UITabAbout.js'; import UsageTab from './UITabUsage.js'; import AccountTab from './UITabAccount.js'; +import SecurityTab from './UITabSecurity.js'; import PersonalizationTab from './UITabPersonalization.js'; import LanguageTab from './UITabLanguage.js'; import ClockTab from './UITabClock.js'; @@ -33,6 +34,7 @@ async function UIWindowSettings(options){ AboutTab, UsageTab, AccountTab, + SecurityTab, PersonalizationTab, LanguageTab, ClockTab, diff --git a/src/UI/UIWindowLogin.js b/src/UI/UIWindowLogin.js index e3e78ada8..355cbd6a4 100644 --- a/src/UI/UIWindowLogin.js +++ b/src/UI/UIWindowLogin.js @@ -21,6 +21,8 @@ import UIWindow from './UIWindow.js' import UIWindowSignup from './UIWindowSignup.js' import UIWindowRecoverPassword from './UIWindowRecoverPassword.js' import UIWindowVerificationCode from './UIWindowVerificationCode.js'; +import TeePromise from '../util/TeePromise.js'; +import UIAlert from './UIAlert.js'; async function UIWindowLogin(options){ options = options ?? {}; @@ -165,11 +167,45 @@ async function UIWindowLogin(options){ contentType: "application/json", data: data, success: async function (data){ + let p = Promise.resolve(); if ( data.next_step === 'otp' ) { - const value = await UIWindowVerificationCode(); - console.log('got value', value); + p = new TeePromise(); + UIWindowVerificationCode({ + title_key: 'confirm_code_2fa_title', + instruction_key: 'confirm_code_2fa_instruction', + submit_btn_key: 'confirm_code_2fa_submit_btn', + on_value: async ({ actions, win, value }) => { + try { + const resp = await fetch(`${api_origin}/login/otp`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + token: data.otp_jwt_token, + code: value, + }), + }); + + data = await resp.json(); + + if ( ! data.proceed ) { + actions.clear(); + actions.show_error(i18n('confirm_code_generic_incorrect')); + return; + } + + $(win).close(); + p.resolve(); + } catch (e) { + actions.show_error(e.message ?? i18n('error_unknown_cause')); + } + }, + }); } + await p; + window.update_auth_data(data.token, data.user); if(options.reload_on_success){ diff --git a/src/UI/UIWindowVerificationCode.js b/src/UI/UIWindowVerificationCode.js index 2d82bd193..356e9afcc 100644 --- a/src/UI/UIWindowVerificationCode.js +++ b/src/UI/UIWindowVerificationCode.js @@ -7,7 +7,6 @@ const UIWindowVerificationCode = async function UIWindowVerificationCode ( optio let is_checking_code = false; const html_title = i18n(options.title_key || 'confirm_code_generic_title'); - const html_confirm = i18n(options.confirm_key || 'confirm_code_generic_confirm'); const html_instruction = i18n(options.instruction_key || 'confirm_code_generic_instruction'); const submit_btn_txt = i18n(options.submit_btn_key || 'confirm_code_generic_submit'); @@ -29,9 +28,6 @@ const UIWindowVerificationCode = async function UIWindowVerificationCode ( optio `; h += ``; h += ``; - h += `
`; - h += `what is this text`; - h += `
`; h += `
`; const el_window = await UIWindow({ @@ -69,11 +65,23 @@ const UIWindowVerificationCode = async function UIWindowVerificationCode ( optio } }); - - const p = new TeePromise(); - $(el_window).find('.digit-input').first().focus(); + const actions = { + clear: () => { + final_code = ''; + $(el_window).find('.code-confirm-btn').prop('disabled', false); + $(el_window).find('.code-confirm-btn').html(submit_btn_txt); + $(el_window).find('.digit-input').val(''); + $(el_window).find('.digit-input').first().focus(); + + }, + show_error: (msg) => { + $(el_window).find('.error').html(html_encode(msg)); + $(el_window).find('.error').fadeIn(); + } + }; + $(el_window).find('.code-confirm-btn').on('click submit', function(e){ e.preventDefault(); e.stopPropagation(); @@ -92,7 +100,11 @@ const UIWindowVerificationCode = async function UIWindowVerificationCode ( optio setTimeout(() => { console.log('final code', final_code); - p.resolve(final_code); + options.on_value({ + actions, + value: final_code, + win: el_window + }); }, 1000); }) diff --git a/src/i18n/translations/en.js b/src/i18n/translations/en.js index 37601523f..617a5b031 100644 --- a/src/i18n/translations/en.js +++ b/src/i18n/translations/en.js @@ -47,7 +47,10 @@ const en = { color: 'Color', hue: 'Hue', confirm_account_for_free_referral_storage_c2a: 'Create an account and confirm your email address to receive 1 GB of free storage. Your friend will get 1 GB of free storage too.', + confirm_code_generic_incorrect: "Incorrect Code.", confirm_code_generic_title: "Enter Confirmation Code", + confirm_code_2fa_instruction: "Enter the 6-digit code from your authenticator app.", + confirm_code_2fa_submit_btn: "Submit", confirm_code_2fa_title: "Enter 2FA Code", confirm_delete_multiple_items: 'Are you sure you want to permanently delete these items?', confirm_delete_single_item: 'Do you want to permanently delete this item?', @@ -83,6 +86,7 @@ const en = { desktop_background_fit: "Fit", developers: "Developers", dir_published_as_website: `%strong% has been published to:`, + disable_2fa: 'Disable 2FA', disassociate_dir: "Disassociate Directory", download: 'Download', download_file: 'Download File', @@ -95,10 +99,12 @@ const en = { empty_trash: 'Empty Trash', empty_trash_confirmation: `Are you sure you want to permanently delete the items in Trash?`, emptying_trash: 'Emptying Trash…', + enable_2fa: 'Enable 2FA', end_hard: "End Hard", end_process_force_confirm: "Are you sure you want to force-quit this process?", end_soft: "End Soft", enter_password_to_confirm_delete_user: "Enter your password to confirm account deletion", + error_unknown_cause: "An unknown error occurred.", feedback: "Feedback", feedback_c2a: "Please use the form below to send us your feedback, comments, and bug reports.", feedback_sent_confirmation: "Thank you for contacting us. If you have an email associated with your account, you will hear back from us as soon as possible.", @@ -206,6 +212,7 @@ const en = { scan_qr_c2a: 'Scan the code below to log into this session from other devices', scan_qr_generic: 'Scan this QR code using your phone or another device', seconds: 'seconds', + security: "Security", select: "Select", selected: 'selected', select_color: 'Select color…', @@ -238,6 +245,9 @@ const en = { tos_fineprint: `By clicking 'Create Free Account' you agree to Puter's {{link=terms}}Terms of Service{{/link}} and {{link=privacy}}Privacy Policy{{/link}}.`, transparency: "Transparency", trash: 'Trash', + two_factor: 'Two Factor Authentication', + two_factor_disabled: '2FA Disabled', + two_factor_enabled: '2FA Enabled', type: 'Type', type_confirm_to_delete_account: "Type 'confirm' to delete your account.", ui_colors: "UI Colors",