Parcourir la source

Add checkboxes and cancel action for 2FA setup

KernelDeimos il y a 1 an
Parent
commit
455d3946d6

+ 12 - 2
packages/backend/src/routers/auth/configure-2fa.js

@@ -24,7 +24,7 @@ module.exports = eggspress('/auth/configure-2fa/:action', {
 
     const db = await x.get('services').get('database').get(DB_WRITE, '2fa');
 
-    actions.enable = async () => {
+    actions.setup = async () => {
         const svc_otp = x.get('services').get('otp');
         const result = svc_otp.create_secret();
         await db.write(
@@ -36,9 +36,19 @@ module.exports = eggspress('/auth/configure-2fa/:action', {
         return result;
     };
 
+    actions.enable = async () => {
+        await db.write(
+            `UPDATE user SET otp_enabled = 1 WHERE uuid = ?`,
+            [user.uuid]
+        );
+        // update cached user
+        req.user.otp_enabled = 1;
+        return {};
+    };
+
     actions.disable = async () => {
         await db.write(
-            `UPDATE user SET otp_secret = NULL WHERE uuid = ?`,
+            `UPDATE user SET otp_enabled = 0 WHERE uuid = ?`,
             [user.uuid]
         );
         return { success: true };

+ 1 - 1
packages/backend/src/routers/login.js

@@ -118,7 +118,7 @@ router.post('/login', express.json(), body_parser_error_handler, async (req, res
         if(await bcrypt.compare(req.body.password, user.password)){
             // We create a JWT that can ONLY be used on the endpoint that
             // accepts the OTP code.
-            if ( user.otp_secret ) {
+            if ( user.otp_enabled ) {
                 const svc_token = req.services.get('token');
                 const otp_jwt_token = svc_token.sign('otp', {
                     user_uid: user.uuid,

+ 1 - 1
packages/backend/src/routers/whoami.js

@@ -56,7 +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,
+        otp: !! req.user.otp_enabled,
         ...(req.new_token ? { token: req.token } : {})
     };
 

+ 0 - 4
packages/backend/src/services/auth/TokenService.js

@@ -104,7 +104,6 @@ class TokenService extends BaseService {
         const secret = this.secret;
 
         const context = this.compression[scope];
-        console.log('original payload', payload)
         const compressed_payload = this._compress_payload(context, payload);
 
         return jwt.sign(compressed_payload, secret, options);
@@ -119,10 +118,7 @@ class TokenService extends BaseService {
         const context = this.compression[scope];
         const payload = jwt.verify(token, secret);
 
-        console.log('payload', payload)
-
         const decoded = this._decompress_payload(context, payload);
-        console.log('decoded', decoded);
         return decoded;
     }
 

+ 2 - 0
packages/backend/src/services/database/sqlite_setup/0008_otp.sql

@@ -1 +1,3 @@
 ALTER TABLE user ADD COLUMN "otp_secret" TEXT DEFAULT NULL;
+ALTER TABLE user ADD COLUMN "otp_enabled" TINYINT(1) DEFAULT '0';
+ALTER TABLE user ADD COLUMN "otp_recovery_codes" TEXT DEFAULT NULL;

+ 19 - 2
src/UI/Settings/UITabSecurity.js

@@ -46,7 +46,7 @@ export default {
     },
     init: ($el_window) => {
         $el_window.find('.enable-2fa').on('click', async function (e) {
-            const resp = await fetch(`${api_origin}/auth/configure-2fa/enable`, {
+            const resp = await fetch(`${api_origin}/auth/configure-2fa/setup`, {
                 method: 'POST',
                 headers: {
                     Authorization: `Bearer ${puter.authToken}`,
@@ -56,10 +56,27 @@ export default {
             });
             const data = await resp.json();
 
-            UIWindowQR({
+            const confirmation = await UIWindowQR({
                 message_i18n_key: 'scan_qr_2fa',
                 text: data.url,
                 text_below: data.secret,
+                confirmations: [
+                    i18n('confirm_2fa_setup'),
+                    i18n('confirm_2fa_recovery'),
+                ],
+            });
+
+            console.log('confirmation?', confirmation);
+
+            if ( ! confirmation ) return;
+
+            await fetch(`${api_origin}/auth/configure-2fa/enable`, {
+                method: 'POST',
+                headers: {
+                    Authorization: `Bearer ${puter.authToken}`,
+                    'Content-Type': 'application/json',
+                },
+                body: JSON.stringify({}),
             });
 
             $el_window.find('.enable-2fa').hide();

+ 94 - 50
src/UI/UIWindowQR.js

@@ -17,64 +17,108 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+import TeePromise from '../util/TeePromise.js';
 import UIWindow from './UIWindow.js'
 
 async function UIWindowQR(options){
-    return new Promise(async (resolve) => {
-        options = options ?? {};
+    const confirmations = options.confirmations || [];
 
-        let h = '';
-        // close button containing the multiplication sign
-        h += `<div class="qr-code-window-close-btn generic-close-window-button"> &times; </div>`;
-        h += `<div class="otp-qr-code">`;
-            h += `<h1 style="text-align: center; font-size: 16px; padding: 10px; font-weight: 400; margin: -10px 10px 20px 10px; -webkit-font-smoothing: antialiased; color: #5f626d;">${
-                i18n(options.message_i18n_key || 'scan_qr_generic')
-            }</h1>`;
+    const promise = new TeePromise();
+
+    options = options ?? {};
+
+    let h = '';
+    // close button containing the multiplication sign
+    // h += `<div class="qr-code-window-close-btn generic-close-window-button"> &times; </div>`;
+    h += `<div class="otp-qr-code">`;
+        h += `<h1 style="text-align: center; font-size: 16px; padding: 10px; font-weight: 400; margin: -10px 10px 20px 10px; -webkit-font-smoothing: antialiased; color: #5f626d;">${
+            i18n(options.message_i18n_key || 'scan_qr_generic')
+        }</h1>`;
+    h += `</div>`;
+
+    for ( let i=0 ; i < confirmations.length ; i++ ) {
+        const confirmation = confirmations[i];
+        // checkbox
+        h += `<div class="qr-code-checkbox">`;
+            h += `<input type="checkbox" name="confirmation_${i}">`;
+            h += `<label for="confirmation_${i}">${confirmation}</label>`;
         h += `</div>`;
+    }
 
-        const el_window = await UIWindow({
-            title: 'Instant Login!',
-            app: 'instant-login',
-            single_instance: true,
-            icon: null,
-            uid: null,
-            is_dir: false,
-            body_content: h,
-            has_head: false,
-            selectable_body: false,
-            allow_context_menu: false,
-            is_resizable: false,
-            is_droppable: false,
-            init_center: true,
-            allow_native_ctxmenu: false,
-            allow_user_select: false,
-            backdrop: true,
-            width: 550,
-            height: 'auto',
-            dominant: true,
-            show_in_taskbar: false,
-            draggable_body: true,
-            onAppend: function(this_window){
-            },
-            window_class: 'window-qr',
-            body_css: {
-                width: 'initial',
-                height: '100%',
-                'background-color': 'rgb(245 247 249)',
-                'backdrop-filter': 'blur(3px)',
-            }    
-        })
+    // h += `<button class="code-confirm-btn" style="margin: 20px auto; display: block; width: 100%; padding: 10px; font-size: 16px; font-weight: 400; background-color: #007bff; color: #fff; border: none; border-radius: 5px; cursor: pointer;">${
+    //     i18n('confirm')
+    // }</button>`;
+    h += `<button type="submit" class="button button-block button-primary code-confirm-btn" style="margin-top:10px;" disabled>${
+        i18n('confirm')
+    }</button>`;
+    h += `<button type="submit" class="button button-block button-secondary code-cancel-btn" style="margin-top:10px;">${
+        i18n('cancel')
+    }</button>`;
 
-        // generate auth token QR code
-        new QRCode($(el_window).find('.otp-qr-code').get(0), {
-            text: options.text,
-            width: 455,
-            height: 455,
-            colorDark : "#000000",
-            colorLight : "#ffffff",
-            correctLevel : QRCode.CorrectLevel.H
-        });        
+    const el_window = await UIWindow({
+        title: 'Instant Login!',
+        app: 'instant-login',
+        single_instance: true,
+        icon: null,
+        uid: null,
+        is_dir: false,
+        body_content: h,
+        has_head: false,
+        selectable_body: false,
+        allow_context_menu: false,
+        is_resizable: false,
+        is_droppable: false,
+        init_center: true,
+        allow_native_ctxmenu: false,
+        allow_user_select: false,
+        backdrop: true,
+        width: 550,
+        height: 'auto',
+        dominant: true,
+        show_in_taskbar: false,
+        draggable_body: true,
+        onAppend: function(this_window){
+        },
+        window_class: 'window-qr',
+        body_css: {
+            width: 'initial',
+            height: '100%',
+            'background-color': 'rgb(245 247 249)',
+            'backdrop-filter': 'blur(3px)',
+            padding: '20px',
+        },
     })
+
+    // generate auth token QR code
+    new QRCode($(el_window).find('.otp-qr-code').get(0), {
+        text: options.text,
+        width: 455,
+        height: 455,
+        colorDark : "#000000",
+        colorLight : "#ffffff",
+        correctLevel : QRCode.CorrectLevel.H
+    });
+
+    if ( confirmations.length > 0 ) {
+        $(el_window).find('.code-confirm-btn').prop('disabled', true);
+    }
+
+    $(el_window).find('.qr-code-checkbox input').on('change', () => {
+        const all_checked = $(el_window).find('.qr-code-checkbox input').toArray().every(el => el.checked);
+        $(el_window).find('.code-confirm-btn').prop('disabled', !all_checked);
+    });
+
+    $(el_window).find('.code-confirm-btn').on('click', () => {
+        $(el_window).close();
+        promise.resolve(true);
+    });
+
+    $(el_window).find('.code-cancel-btn').on('click', () => {
+        $(el_window).close();
+        promise.resolve(false);
+    });
+
+    return await promise;
 }
 
 export default UIWindowQR

+ 2 - 0
src/i18n/translations/en.js

@@ -46,6 +46,8 @@ const en = {
         change_always_open_with: "Do you want to always open this type of file with",
         color: 'Color',
         hue: 'Hue',
+        confirm_2fa_setup: 'I have added the code to my authenticator app',
+        confirm_2fa_recovery: 'I have saved my recovery codes',
         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",