/** @odoo-module **/ import { jsonrpc } from "@web/core/network/rpc_service"; import { registry } from "@web/core/registry"; import { stepUtils } from "@web_tour/tour_service/tour_utils"; function openRoot() { return [{ content: "return to client root to avoid race condition", trigger: 'body', run() { $('body').addClass('wait'); window.location = '/web'; } }, { content: "wait for client reload", trigger: 'body:not(.wait)', run() {} }]; } function openUserProfileAtSecurityTab() { return [{ content: 'Open user account menu', trigger: '.o_user_menu .dropdown-toggle', run: 'click', }, { content: "Open preferences / profile screen", trigger: '[data-menu=settings]', run: 'click', }, { content: "Switch to security tab", trigger: 'a[role=tab]:contains("Account Security")', run: 'click', }]; } /** * Checks that the TOTP button is in the specified state (true = enabled = * can disable, false = disabled = can enable), then closes the profile dialog * if it's one (= hr not installed). * * If no totp state is provided, just checks that the toggle exists. */ function closeProfileDialog({content, totp_state}) { let trigger; switch (totp_state) { case true: trigger = 'button[name=action_totp_disable]'; break; case false: trigger = 'button[name=action_totp_enable_wizard]'; break; case undefined: trigger = 'button.o_auth_2fa_btn'; break; default: throw new Error(`Invalid totp state ${totp_state}`) } return [{ content, trigger, run() { const $modal = this.$anchor.parents('.o_dialog'); if ($modal.length) { $modal.find('button[name=preference_cancel]').click() } } }, { trigger: 'body', async run() { while (document.querySelector('.o_dialog')) { await Promise.resolve(); } this.$anchor.addClass('dialog-closed'); }, }, { trigger: 'body.dialog-closed', run() {}, }]; } registry.category("web_tour.tours").add('totp_tour_setup', { test: true, url: '/web', steps: () => [...openUserProfileAtSecurityTab(), { content: "Open totp wizard", trigger: 'button[name=action_totp_enable_wizard]', }, { content: "Check that we have to enter enhanced security mode and input password", extra_trigger: 'div:contains("enter your password")', trigger: '[name=password] input', run: 'text demo', }, { content: "Confirm", trigger: "button:contains(Confirm Password)", }, { content: "Check the wizard has opened", trigger: 'li:contains("When requested to do so")', run() {} }, { content: "Get secret from collapsed div", trigger: 'a:contains("Cannot scan it?")', async run(helpers) { const $secret = this.$anchor.closest('div').find('[name=secret] span:first-child'); const $copyBtn = $secret.find('button'); $copyBtn.remove(); const token = await jsonrpc('/totphook', { secret: $secret.text() }); helpers.text(token, '[name=code] input'); helpers.click('button.btn-primary:contains(Activate)'); $('body').addClass('got-token') } }, { content: 'wait for rpc', trigger: 'body.got-token', run() {} }, ...openRoot(), ...openUserProfileAtSecurityTab(), ...closeProfileDialog({ content: "Check that the button has changed", totp_state: true, }), ]}); registry.category("web_tour.tours").add('totp_login_enabled', { test: true, url: '/', steps: () => [{ content: "check that we're on the login page or go to it", trigger: 'input#login, a:contains(Sign in)' }, { content: "input login", trigger: 'input#login', run: 'text demo', }, { content: 'input password', trigger: 'input#password', run: 'text demo', }, { content: "click da button", trigger: 'button:contains("Log in")', }, { content: "expect totp screen", trigger: 'label:contains(Authentication Code)', }, { content: "input code", trigger: 'input[name=totp_token]', async run(helpers) { // TODO: if tours are ever async-aware the click should get moved out, // but currently there's no great way to make the tour wait until // we've retrieved and set the token: `:empty()` is aboutthe text // content of the HTML element, not the JS value property. We // could set a class but that's really no better than // procedurally clicking the button after we've set the input. const token = await jsonrpc('/totphook'); helpers.text(token); helpers.click('button:contains("Log in")'); } }, { content: "check we're logged in", trigger: ".o_user_menu .dropdown-toggle", run() {} }]}); registry.category("web_tour.tours").add('totp_login_device', { test: true, url: '/', steps: () => [{ content: "check that we're on the login page or go to it", trigger: 'input#login, a:contains(Sign in)' }, { content: "input login", trigger: 'input#login', run: 'text demo', }, { content: 'input password', trigger: 'input#password', run: 'text demo', }, { content: "click da button", trigger: 'button:contains("Log in")', }, { content: "expect totp screen", trigger: 'label:contains(Authentication Code)', }, { content: "check remember device box", trigger: 'label[for=switch-remember]', }, { content: "input code", trigger: 'input[name=totp_token]', async run(helpers) { const token = await jsonrpc('/totphook') helpers.text(token); helpers.click('button:contains("Log in")'); } }, { content: "check we're logged in", trigger: ".o_user_menu .dropdown-toggle", run: 'click', }, { content: "click the Log out button", trigger: '.dropdown-item[data-menu=logout]', }, { content: "check that we're back on the login page or go to it", trigger: 'input#login, a:contains(Log in)' }, { content: "input login again", trigger: 'input#login', run: 'text demo', }, { content: 'input password again', trigger: 'input#password', run: 'text demo', }, { content: "click da button again", trigger: 'button:contains("Log in")', }, { content: "check we're logged in without 2FA", trigger: ".o_user_menu .dropdown-toggle", run() {} }, // now go and disable two-factor authentication would be annoying to do in a separate tour // because we'd need to login & totp again as HttpCase.authenticate can't // succeed w/ totp enabled ...openUserProfileAtSecurityTab(), { content: "Open totp wizard", trigger: 'button[name=action_totp_disable]', }, { content: "Check that we have to enter enhanced security mode and input password", extra_trigger: 'div:contains("enter your password")', trigger: '[name=password] input', run: 'text demo', }, { content: "Confirm", trigger: "button:contains(Confirm Password)", }, ...openRoot(), ...openUserProfileAtSecurityTab(), ...closeProfileDialog({ content: "Check that the button has changed", totp_state: false }), ]}); registry.category("web_tour.tours").add('totp_login_disabled', { test: true, url: '/', steps: () => [{ content: "check that we're on the login page or go to it", trigger: 'input#login, a:contains(Sign in)' }, { content: "input login", trigger: 'input#login', run: 'text demo', }, { content: 'input password', trigger: 'input#password', run: 'text demo', }, { content: "click da button", trigger: 'button:contains("Log in")', }, // normally we'd end the tour here as it's all we care about but there are a // bunch of ongoing queries from the loading of the web client which cause // issues, so go and open the preferences / profile screen to make sure // everything settles down ...openUserProfileAtSecurityTab(), // close the dialog if that makes sense ...closeProfileDialog({}) ]}); const columns = {}; registry.category("web_tour.tours").add('totp_admin_disables', { test: true, url: '/web', steps: () => [stepUtils.showAppsMenuItem(), { content: 'Go to settings', trigger: '[data-menu-xmlid="base.menu_administration"]' }, { content: 'Wait for page', trigger: '.o_menu_brand:contains("Settings")', run() {} }, { content: "Open Users menu", trigger: '[data-menu-xmlid="base.menu_users"]' }, { content: "Open Users view", trigger: '[data-menu-xmlid="base.menu_action_res_users"]', run(helpers) { // funny story: the users view we're trying to reach, sometimes we're // already there, but if we re-click the next step executes before the // action has the time to re-load, the one after that doesn't, and our // selection get discarded by the action reloading, so here try to // see if we're already on the users action through the breadcrumb and // just close the menu if so const $crumb = $('.breadcrumb'); if ($crumb.text().indexOf('Users') === -1) { // on general settings page, click menu helpers.click(); } else { // else close menu helpers.click($('[data-menu-xmlid="base.menu_users"]')); } } }, { content: "Find Demo User", trigger: 'td.o_data_cell:contains("demo")', run(helpers) { const $titles = this.$anchor.closest('table').find('tr:first th'); for (let i=0; i<$titles.length; ++i) { columns[$titles[i].getAttribute('data-name')] = i; } const $row = this.$anchor.closest('tr'); const sel = $row.find('.o_list_record_selector input[type=checkbox]'); helpers.click(sel); } }, { content: "Open Actions menu", trigger: 'button.dropdown-toggle:contains("Action")' }, { content: "Select totp remover", trigger: 'span.dropdown-item:contains(Disable two-factor authentication)' }, { // enhanced security yo content: "Check that we have to enter enhanced security mode & input password", extra_trigger: 'div:contains("enter your password")', trigger: '[name=password] input', run: 'text admin', }, { content: "Confirm", trigger: "button:contains(Confirm Password)", }, { content: "open the user's form", trigger: "td.o_data_cell:contains(demo)", }, { content: "go to Account security Tab", trigger: "a.nav-link:contains(Account Security)", }, ...closeProfileDialog({ content: "check that demo user has been de-totp'd", totp_state: false, }), ]})