Sfoglia il codice sorgente

Add 2fa setting and complete login flow

KernelDeimos 1 anno fa
parent
commit
2dfecb5287

+ 2 - 0
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;
     };
 

+ 3 - 0
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 } : {})
     };
 

+ 0 - 8
src/UI/Settings/UITabAccount.js

@@ -64,14 +64,6 @@ export default {
             h += `</div>`;
         }
 
-        // session manager
-        h += `<div class="settings-card">`;
-            h += `<strong>${i18n('sessions')}</strong>`;
-            h += `<div style="flex-grow:1;">`;
-                h += `<button class="button manage-sessions" style="float:right;">${i18n('manage_sessions')}</button>`;
-            h += `</div>`;
-        h += `</div>`;
-
         // 'Delete Account' button
         h += `<div class="settings-card settings-card-danger">`;
             h += `<strong style="display: inline-block;">${i18n("delete_account")}</strong>`;

+ 85 - 0
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 = `<h1>${i18n('security')}</h1>`;
+
+        // change password button
+        if(!user.is_temp){
+            h += `<div class="settings-card">`;
+                h += `<strong>${i18n('password')}</strong>`;
+                h += `<div style="flex-grow:1;">`;
+                    h += `<button class="button change-password" style="float:right;">${i18n('change_password')}</button>`;
+                h += `</div>`;
+            h += `</div>`;
+        }
+
+        // session manager
+        h += `<div class="settings-card">`;
+            h += `<strong>${i18n('sessions')}</strong>`;
+            h += `<div style="flex-grow:1;">`;
+                h += `<button class="button manage-sessions" style="float:right;">${i18n('manage_sessions')}</button>`;
+            h += `</div>`;
+        h += `</div>`;
+
+        // configure 2FA
+        if(!user.is_temp){
+            h += `<div class="settings-card">`;
+                h += `<div>`;
+                    h += `<strong style="display:block;">${i18n('two_factor')}</strong>`;
+                    h += `<span class="user-otp-state" style="display:block; margin-top:5px;">${
+                        i18n(user.otp ? 'two_factor_enabled' : 'two_factor_disabled')
+                    }</span>`;
+                h += `</div>`;
+                h += `<div style="flex-grow:1;">`;
+                    h += `<button class="button enable-2fa" style="float:right;${user.otp ? 'display:none;' : ''}">${i18n('enable_2fa')}</button>`;
+                    h += `<button class="button disable-2fa" style="float:right;${user.otp ? '' : 'display:none;'}">${i18n('disable_2fa')}</button>`;
+                h += `</div>`;
+            h += `</div>`;
+        }
+
+
+        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'));
+        });
+    }
+}

+ 2 - 0
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,

+ 38 - 2
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){

+ 20 - 8
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
             </fieldset>`;
             h += `<button type="submit" class="button button-block button-primary code-confirm-btn" style="margin-top:10px;" disabled>${submit_btn_txt}</button>`;
         h += `</form>`;
-        h += `<div style="text-align:center; padding:10px; font-size:14px; margin-top:10px;">`;
-            h += `<span class="send-conf-code">what is this text</span>`;
-        h += `</div>`;
     h += `</div>`;
 
     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);
     })
 

+ 10 - 0
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",