mirror of
https://github.com/bringout/oca-ocb-security.git
synced 2026-04-25 22:22:07 +02:00
19.0 vanilla
This commit is contained in:
parent
20ddc1b4a3
commit
c0efcc53f5
1162 changed files with 125577 additions and 105287 deletions
|
|
@ -0,0 +1,25 @@
|
|||
import { Interaction } from "@web/public/interaction";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { handleCheckIdentity } from "@portal/interactions/portal_security";
|
||||
import { user } from "@web/core/user";
|
||||
|
||||
export class RevokeAllTrustedDevices extends Interaction {
|
||||
static selector = "#auth_totp_portal_revoke_all_devices";
|
||||
dynamicContent = {
|
||||
_root: { "t-on-click.prevent": this.onClick },
|
||||
};
|
||||
|
||||
async onClick() {
|
||||
await this.waitFor(handleCheckIdentity(
|
||||
this.waitFor(this.services.orm.call("res.users", "revoke_all_devices", [user.userId])),
|
||||
this.services.orm,
|
||||
this.services.dialog,
|
||||
));
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
registry
|
||||
.category("public.interactions")
|
||||
.add("auth_totp_portal.revoke_all_trusted_devices", RevokeAllTrustedDevices);
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { Interaction } from "@web/public/interaction";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { handleCheckIdentity } from "@portal/interactions/portal_security";
|
||||
|
||||
export class RevokeTrustedDevice extends Interaction {
|
||||
static selector = "#totp_wizard_view + * .fa.fa-trash.text-danger";
|
||||
dynamicContent = {
|
||||
_root: { "t-on-click.prevent": this.onClick },
|
||||
};
|
||||
|
||||
async onClick() {
|
||||
await this.waitFor(handleCheckIdentity(
|
||||
this.waitFor(this.services.orm.call("auth_totp.device", "remove", [parseInt(this.el.id)])),
|
||||
this.services.orm,
|
||||
this.services.dialog,
|
||||
));
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
registry
|
||||
.category("public.interactions")
|
||||
.add("auth_totp_portal.revoke_trusted_device", RevokeTrustedDevice);
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { Interaction } from "@web/public/interaction";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { handleCheckIdentity } from "@portal/interactions/portal_security";
|
||||
import { user } from "@web/core/user";
|
||||
|
||||
export class TOTPDisable extends Interaction {
|
||||
static selector = "#auth_totp_portal_disable";
|
||||
dynamicContent = {
|
||||
_root: { "t-on-click.prevent": this.onClick }
|
||||
}
|
||||
|
||||
async onClick() {
|
||||
await this.waitFor(handleCheckIdentity(
|
||||
this.waitFor(this.services.orm.call("res.users", "action_totp_disable", [user.userId])),
|
||||
this.services.orm,
|
||||
this.services.dialog,
|
||||
));
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
registry
|
||||
.category("public.interactions")
|
||||
.add("auth_totp_portal.totp_disable", TOTPDisable);
|
||||
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
import { Interaction } from "@web/public/interaction";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { InputConfirmationDialog } from "@portal/js/components/input_confirmation_dialog/input_confirmation_dialog";
|
||||
import { handleCheckIdentity } from "@portal/interactions/portal_security";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { user } from "@web/core/user";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
import { markup } from "@odoo/owl";
|
||||
|
||||
/**
|
||||
* Replaces specific <field> elements by normal HTML, strip out the rest entirely
|
||||
*/
|
||||
function fromField(f, record) {
|
||||
switch (f.getAttribute("name")) {
|
||||
case "qrcode":
|
||||
const qrcode = document.createElement("img");
|
||||
qrcode.setAttribute("class", "img img-fluid");
|
||||
qrcode.setAttribute("src", "data:image/png;base64," + record["qrcode"]);
|
||||
return qrcode;
|
||||
case "url":
|
||||
const url = document.createElement("a");
|
||||
url.setAttribute("href", record["url"]);
|
||||
url.textContent = f.getAttribute("text") || record["url"];
|
||||
return url;
|
||||
case "code":
|
||||
const code = document.createElement("input");
|
||||
code.setAttribute("name", "code");
|
||||
code.setAttribute("class", "form-control col-10 col-md-6");
|
||||
code.setAttribute("placeholder", "6-digit code");
|
||||
code.required = true;
|
||||
code.maxLength = 6;
|
||||
code.minLength = 6;
|
||||
return code;
|
||||
case "secret":
|
||||
// As CopyClipboard wizard is backend only, mimic his behaviour to use it in frontend.
|
||||
// Field
|
||||
const secretSpan = document.createElement("span");
|
||||
secretSpan.setAttribute("name", "secret");
|
||||
secretSpan.setAttribute("class", "o_field_copy_url");
|
||||
secretSpan.textContent = record["secret"];
|
||||
|
||||
// Copy Button
|
||||
const copySpanIcon = document.createElement("span");
|
||||
copySpanIcon.setAttribute("class", "fa fa-clipboard");
|
||||
const copySpanText = document.createElement("span");
|
||||
copySpanText.textContent = _t(" Copy");
|
||||
|
||||
const copyButton = document.createElement("button");
|
||||
copyButton.setAttribute("class", "btn btn-sm btn-primary o_clipboard_button o_btn_char_copy py-0 px-2");
|
||||
copyButton.onclick = async function (event) {
|
||||
event.preventDefault();
|
||||
$(copyButton).tooltip({ title: _t("Copied!"), trigger: "manual", placement: "bottom" });
|
||||
await browser.navigator.clipboard.writeText($(secretSpan)[0].innerText);
|
||||
$(copyButton).tooltip("show");
|
||||
setTimeout(() => $(copyButton).tooltip("hide"), 800);
|
||||
};
|
||||
|
||||
copyButton.appendChild(copySpanIcon);
|
||||
copyButton.appendChild(copySpanText);
|
||||
|
||||
// CopyClipboard Div
|
||||
const secretDiv = document.createElement("div");
|
||||
secretDiv.setAttribute("class", "o_field_copy d-flex justify-content-center align-items-center");
|
||||
secretDiv.appendChild(secretSpan);
|
||||
secretDiv.appendChild(copyButton);
|
||||
|
||||
return secretDiv;
|
||||
default: // just display the field's data
|
||||
return document.createTextNode(record[f.getAttribute("name")] || "");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apparently chrome literally absolutely can't handle parsing XML and using
|
||||
* those nodes in an HTML document (even when parsing as application/xhtml+xml),
|
||||
* this results in broken rendering and a number of things not working (e.g.
|
||||
* classes) without any specific warning in the console or anything, things are
|
||||
* just broken with no indication of why.
|
||||
*
|
||||
* So... rebuild the entire f'ing body using document.createElement to ensure
|
||||
* we have HTML elements.
|
||||
*
|
||||
* This is a recursive implementation so it's not super efficient but the views
|
||||
* to fixup *should* be relatively simple.
|
||||
*/
|
||||
function fixupViewBody(oldNode, record) {
|
||||
let qrcode = null, code = null, node = null;
|
||||
|
||||
switch (oldNode.nodeType) {
|
||||
case 1: // element
|
||||
if (oldNode.tagName === "field") {
|
||||
node = fromField(oldNode, record);
|
||||
switch (oldNode.getAttribute("name")) {
|
||||
case "qrcode":
|
||||
qrcode = node;
|
||||
break;
|
||||
case "code":
|
||||
code = node;
|
||||
break
|
||||
}
|
||||
break; // no need to recurse here
|
||||
}
|
||||
node = document.createElement(oldNode.tagName);
|
||||
for (let i = 0; i < oldNode.attributes.length; ++i) {
|
||||
const attr = oldNode.attributes[i];
|
||||
node.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
for (let j = 0; j < oldNode.childNodes.length; ++j) {
|
||||
const [ch, qr, co] = fixupViewBody(oldNode.childNodes[j], record);
|
||||
if (ch) { node.appendChild(ch); }
|
||||
if (qr) { qrcode = qr; }
|
||||
if (co) { code = co; }
|
||||
}
|
||||
break;
|
||||
case 3: case 4: // text, cdata
|
||||
node = document.createTextNode(oldNode.data);
|
||||
break;
|
||||
default:
|
||||
// don't care about PI & al
|
||||
}
|
||||
|
||||
return [node, qrcode, code]
|
||||
}
|
||||
|
||||
export class TOTPEnable extends Interaction {
|
||||
static selector = "#auth_totp_portal_enable";
|
||||
dynamicContent = {
|
||||
_root: { "t-on-click.prevent": this.onClick },
|
||||
};
|
||||
|
||||
async onClick() {
|
||||
const data = await this.waitFor(handleCheckIdentity(
|
||||
this.waitFor(this.services.orm.call("res.users", "action_totp_enable_wizard", [user.userId])),
|
||||
this.services.orm,
|
||||
this.services.dialog,
|
||||
));
|
||||
|
||||
if (!data) {
|
||||
// TOTP probably already enabled, just reload page
|
||||
location.reload()
|
||||
return;
|
||||
}
|
||||
|
||||
const model = data.res_model;
|
||||
const wizard_id = data.res_id;
|
||||
const record = (await this.services.orm.read(model, [wizard_id], []))[0];
|
||||
|
||||
const doc = new DOMParser().parseFromString(
|
||||
document.getElementById("totp_wizard_view").textContent,
|
||||
"application/xhtml+xml"
|
||||
);
|
||||
|
||||
const xmlBody = doc.querySelector("sheet *");
|
||||
const [body, ,] = fixupViewBody(xmlBody, record);
|
||||
|
||||
this.services.dialog.add(InputConfirmationDialog, {
|
||||
body: markup(body.outerHTML),
|
||||
onInput: ({ inputEl }) => { inputEl.setCustomValidity("") },
|
||||
confirmLabel: _t("Activate"),
|
||||
confirm: async ({ inputEl }) => {
|
||||
if (!inputEl.reportValidity()) {
|
||||
inputEl.classList.add("is-invalid");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await handleCheckIdentity(
|
||||
this.waitFor(this.services.orm.call(model, "enable",
|
||||
[ record.id ],
|
||||
{ 'context': {'code': inputEl.value} },
|
||||
)),
|
||||
this.services.orm,
|
||||
this.services.dialog
|
||||
);
|
||||
} catch (e) {
|
||||
const errorMessage = (
|
||||
!e.message ? e.toString()
|
||||
: !e.message.data ? e.message.message
|
||||
: e.message.data.message || _t("Operation failed for unknown reason.")
|
||||
);
|
||||
inputEl.classList.add("is-invalid");
|
||||
// show custom validity error message
|
||||
inputEl.setCustomValidity(errorMessage);
|
||||
inputEl.reportValidity();
|
||||
return false;
|
||||
}
|
||||
// reloads page, avoid window.location.reload() because it re-posts forms
|
||||
location.reload();
|
||||
},
|
||||
cancel: () => { },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registry
|
||||
.category("public.interactions")
|
||||
.add("auth_totp_portal.totp_enable", TOTPEnable);
|
||||
|
|
@ -1,306 +0,0 @@
|
|||
odoo.define('auth_totp_portal.button', function (require) {
|
||||
'use strict';
|
||||
|
||||
const {_t} = require('web.core');
|
||||
const publicWidget = require('web.public.widget');
|
||||
const Dialog = require('web.Dialog');
|
||||
const {handleCheckIdentity} = require('portal.portal');
|
||||
|
||||
/**
|
||||
* Replaces specific <field> elements by normal HTML, strip out the rest entirely
|
||||
*/
|
||||
function fromField(f, record) {
|
||||
switch (f.getAttribute('name')) {
|
||||
case 'qrcode':
|
||||
const qrcode = document.createElement('img');
|
||||
qrcode.setAttribute('class', 'img img-fluid');
|
||||
qrcode.setAttribute('src', 'data:image/png;base64,' + record['qrcode']);
|
||||
return qrcode;
|
||||
case 'url':
|
||||
const url = document.createElement('a');
|
||||
url.setAttribute('href', record['url']);
|
||||
url.textContent = f.getAttribute('text') || record['url'];
|
||||
return url;
|
||||
case 'code':
|
||||
const code = document.createElement('input');
|
||||
code.setAttribute('name', 'code');
|
||||
code.setAttribute('class', 'form-control col-10 col-md-6');
|
||||
code.setAttribute('placeholder', '6-digit code');
|
||||
code.required = true;
|
||||
code.maxLength = 6;
|
||||
code.minLength = 6;
|
||||
return code;
|
||||
case 'secret':
|
||||
// As CopyClipboard wizard is backend only, mimic his behaviour to use it in frontend.
|
||||
// Field
|
||||
const secretSpan = document.createElement('span');
|
||||
secretSpan.setAttribute('name', 'secret');
|
||||
secretSpan.setAttribute('class', 'o_field_copy_url');
|
||||
secretSpan.textContent = record['secret'];
|
||||
|
||||
// Copy Button
|
||||
const copySpanIcon = document.createElement('span');
|
||||
copySpanIcon.setAttribute('class', 'fa fa-clipboard');
|
||||
const copySpanText = document.createElement('span');
|
||||
copySpanText.textContent = _t(' Copy');
|
||||
|
||||
const copyButton = document.createElement('button');
|
||||
copyButton.setAttribute('class', 'btn btn-sm btn-primary o_clipboard_button o_btn_char_copy py-0 px-2');
|
||||
copyButton.onclick = function(event) {
|
||||
event.preventDefault();
|
||||
$(copyButton).tooltip({title: _t("Copied !"), trigger: "manual", placement: "bottom"});
|
||||
var clipboard = new ClipboardJS('.o_clipboard_button', {
|
||||
target: function () {
|
||||
return $(secretSpan)[0];
|
||||
},
|
||||
container: this.el
|
||||
});
|
||||
clipboard.on('success', function () {
|
||||
clipboard.destroy();
|
||||
$(copyButton).tooltip('show');
|
||||
_.delay(function () {
|
||||
$(copyButton).tooltip("hide");
|
||||
}, 800);
|
||||
});
|
||||
clipboard.on('error', function (e) {
|
||||
clipboard.destroy();
|
||||
});
|
||||
};
|
||||
|
||||
copyButton.appendChild(copySpanIcon);
|
||||
copyButton.appendChild(copySpanText);
|
||||
|
||||
// CopyClipboard Div
|
||||
const secretDiv = document.createElement('div');
|
||||
secretDiv.setAttribute('class', 'o_field_copy d-flex justify-content-center align-items-center');
|
||||
secretDiv.appendChild(secretSpan);
|
||||
secretDiv.appendChild(copyButton);
|
||||
|
||||
return secretDiv;
|
||||
default: // just display the field's data
|
||||
return document.createTextNode(record[f.getAttribute('name')] || '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apparently chrome literally absolutely can't handle parsing XML and using
|
||||
* those nodes in an HTML document (even when parsing as application/xhtml+xml),
|
||||
* this results in broken rendering and a number of things not working (e.g.
|
||||
* classes) without any specific warning in the console or anything, things are
|
||||
* just broken with no indication of why.
|
||||
*
|
||||
* So... rebuild the entire f'ing body using document.createElement to ensure
|
||||
* we have HTML elements.
|
||||
*
|
||||
* This is a recursive implementation so it's not super efficient but the views
|
||||
* to fixup *should* be relatively simple.
|
||||
*/
|
||||
function fixupViewBody(oldNode, record) {
|
||||
let qrcode = null, code = null, node = null;
|
||||
|
||||
switch (oldNode.nodeType) {
|
||||
case 1: // element
|
||||
if (oldNode.tagName === 'field') {
|
||||
node = fromField(oldNode, record);
|
||||
switch (oldNode.getAttribute('name')) {
|
||||
case 'qrcode':
|
||||
qrcode = node;
|
||||
break;
|
||||
case 'code':
|
||||
code = node;
|
||||
break
|
||||
}
|
||||
break; // no need to recurse here
|
||||
}
|
||||
node = document.createElement(oldNode.tagName);
|
||||
for(let i=0; i<oldNode.attributes.length; ++i) {
|
||||
const attr = oldNode.attributes[i];
|
||||
node.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
for(let j=0; j<oldNode.childNodes.length; ++j) {
|
||||
const [ch, qr, co] = fixupViewBody(oldNode.childNodes[j], record);
|
||||
if (ch) { node.appendChild(ch); }
|
||||
if (qr) { qrcode = qr; }
|
||||
if (co) { code = co; }
|
||||
}
|
||||
break;
|
||||
case 3: case 4: // text, cdata
|
||||
node = document.createTextNode(oldNode.data);
|
||||
break;
|
||||
default:
|
||||
// don't care about PI & al
|
||||
}
|
||||
|
||||
return [node, qrcode, code]
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a backend <button> element and a bunch of metadata into a structure
|
||||
* which can kinda be of use to Dialog.
|
||||
*/
|
||||
class Button {
|
||||
constructor(parent, model, record_id, input_node, button_node) {
|
||||
this._parent = parent;
|
||||
this.model = model;
|
||||
this.record_id = record_id;
|
||||
this.input = input_node;
|
||||
this.text = button_node.getAttribute('string');
|
||||
this.classes = button_node.getAttribute('class') || null;
|
||||
this.action = button_node.getAttribute('name');
|
||||
if (button_node.getAttribute('special') === 'cancel') {
|
||||
this.close = true;
|
||||
this.click = null;
|
||||
} else {
|
||||
this.close = false;
|
||||
// because Dialog doesnt' call() click on the descriptor object
|
||||
this.click = this._click.bind(this);
|
||||
}
|
||||
}
|
||||
async _click() {
|
||||
if (!this.input.reportValidity()) {
|
||||
this.input.classList.add('is-invalid');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.callAction(this.record_id, {code: this.input.value});
|
||||
} catch (e) {
|
||||
this.input.classList.add('is-invalid');
|
||||
// show custom validity error message
|
||||
this.input.setCustomValidity(e.message);
|
||||
this.input.reportValidity();
|
||||
return;
|
||||
}
|
||||
this.input.classList.remove('is-invalid');
|
||||
// reloads page, avoid window.location.reload() because it re-posts forms
|
||||
window.location = window.location;
|
||||
}
|
||||
async callAction(id, update) {
|
||||
try {
|
||||
await this._parent._rpc({model: this.model, method: 'write', args: [id, update]});
|
||||
await handleCheckIdentity(
|
||||
this._parent.proxy('_rpc'),
|
||||
this._parent._rpc({model: this.model, method: this.action, args: [id]})
|
||||
);
|
||||
} catch(e) {
|
||||
// avoid error toast (crashmanager)
|
||||
e.event.preventDefault();
|
||||
// try to unwrap mess of an error object to a usable error message
|
||||
throw new Error(
|
||||
!e.message ? e.toString()
|
||||
: !e.message.data ? e.message.message
|
||||
: e.message.data.message || _t("Operation failed for unknown reason.")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publicWidget.registry.TOTPButton = publicWidget.Widget.extend({
|
||||
selector: '#auth_totp_portal_enable',
|
||||
events: {
|
||||
click: '_onClick',
|
||||
},
|
||||
|
||||
async _onClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const w = await handleCheckIdentity(this.proxy('_rpc'), this._rpc({
|
||||
model: 'res.users',
|
||||
method: 'action_totp_enable_wizard',
|
||||
args: [this.getSession().user_id]
|
||||
}));
|
||||
|
||||
if (!w) {
|
||||
// TOTP probably already enabled, just reload page
|
||||
window.location = window.location;
|
||||
return;
|
||||
}
|
||||
|
||||
const {res_model: model, res_id: wizard_id} = w;
|
||||
|
||||
const record = await this._rpc({
|
||||
model, method: 'read', args: [wizard_id, []]
|
||||
}).then(ar => ar[0]);
|
||||
|
||||
const doc = new DOMParser().parseFromString(
|
||||
document.getElementById('totp_wizard_view').textContent,
|
||||
'application/xhtml+xml'
|
||||
);
|
||||
|
||||
const xmlBody = doc.querySelector('sheet *');
|
||||
const [body, , codeInput] = fixupViewBody(xmlBody, record);
|
||||
// remove custom validity error message any time the field changes
|
||||
// otherwise it sticks and browsers suppress submit
|
||||
codeInput.addEventListener('input', () => codeInput.setCustomValidity(''));
|
||||
|
||||
const buttons = [];
|
||||
for(const button of doc.querySelectorAll('footer button')) {
|
||||
buttons.push(new Button(this, model, record.id, codeInput, button));
|
||||
}
|
||||
|
||||
// wrap in a root host of .modal-body otherwise it breaks our neat flex layout
|
||||
const $content = document.createElement('form');
|
||||
$content.appendChild(body);
|
||||
// implicit submission by pressing [return] from within input
|
||||
$content.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
// sadness: footer not available as normal element
|
||||
dialog.$footer.find('.btn-primary').click();
|
||||
});
|
||||
var dialog = new Dialog(this, {$content, buttons}).open();
|
||||
}
|
||||
});
|
||||
publicWidget.registry.DisableTOTPButton = publicWidget.Widget.extend({
|
||||
selector: '#auth_totp_portal_disable',
|
||||
events: {
|
||||
click: '_onClick'
|
||||
},
|
||||
|
||||
async _onClick(e) {
|
||||
e.preventDefault();
|
||||
await handleCheckIdentity(
|
||||
this.proxy('_rpc'),
|
||||
this._rpc({model: 'res.users', method: 'action_totp_disable', args: [this.getSession().user_id]})
|
||||
)
|
||||
window.location = window.location;
|
||||
}
|
||||
});
|
||||
publicWidget.registry.RevokeTrustedDeviceButton = publicWidget.Widget.extend({
|
||||
selector: '#totp_wizard_view + * .fa.fa-trash.text-danger',
|
||||
events: {
|
||||
click: '_onClick'
|
||||
},
|
||||
|
||||
async _onClick(e){
|
||||
e.preventDefault();
|
||||
await handleCheckIdentity(
|
||||
this.proxy('_rpc'),
|
||||
this._rpc({
|
||||
model: 'auth_totp.device',
|
||||
method: 'remove',
|
||||
args: [parseInt(this.target.id)]
|
||||
})
|
||||
);
|
||||
window.location = window.location;
|
||||
}
|
||||
});
|
||||
publicWidget.registry.RevokeAllTrustedDevicesButton = publicWidget.Widget.extend({
|
||||
selector: '#auth_totp_portal_revoke_all_devices',
|
||||
events: {
|
||||
click: '_onClick'
|
||||
},
|
||||
|
||||
async _onClick(e){
|
||||
e.preventDefault();
|
||||
await handleCheckIdentity(
|
||||
this.proxy('_rpc'),
|
||||
this._rpc({
|
||||
model: 'res.users',
|
||||
method: 'revoke_all_devices',
|
||||
args: [this.getSession().user_id]
|
||||
})
|
||||
);
|
||||
window.location = window.location;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
width: 100% !important;
|
||||
border-radius: 5px;
|
||||
border: 1px solid $primary;
|
||||
font-size: $font-size-sm;
|
||||
@include font-size($font-size-sm);
|
||||
text-transform: uppercase;
|
||||
color: $o-brand-primary;
|
||||
font-weight: $badge-font-weight;
|
||||
|
|
|
|||
|
|
@ -1,124 +1,136 @@
|
|||
odoo.define('auth_totp_portal.tours', function(require) {
|
||||
"use strict";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
|
||||
const tour = require('web_tour.tour');
|
||||
const ajax = require('web.ajax');
|
||||
|
||||
tour.register('totportal_tour_setup', {
|
||||
test: true,
|
||||
url: '/my/security'
|
||||
}, [{
|
||||
registry.category("web_tour.tours").add('totportal_tour_setup', {
|
||||
url: '/my/security',
|
||||
steps: () => [{
|
||||
content: "Open totp wizard",
|
||||
trigger: 'button#auth_totp_portal_enable',
|
||||
run: "click",
|
||||
}, {
|
||||
content: "Check that we have to enter enhanced security mode",
|
||||
trigger: 'div:contains("enter your password")',
|
||||
run: () => {},
|
||||
trigger: ".modal div:contains(enter your password)",
|
||||
}, {
|
||||
content: "Input password",
|
||||
trigger: '[name=password]',
|
||||
run: 'text portal', // FIXME: better way to do this?
|
||||
run: "edit portal", // FIXME: better way to do this?
|
||||
}, {
|
||||
content: "Confirm",
|
||||
trigger: "button:contains(Confirm Password)",
|
||||
run: "click",
|
||||
}, {
|
||||
content: "Check the wizard has opened",
|
||||
trigger: 'li:contains("scan the barcode below")',
|
||||
run: () => {}
|
||||
trigger: '.o_auth_totp_enable_2FA',
|
||||
}, {
|
||||
content: "Get secret from collapsed div",
|
||||
trigger: 'a:contains("Cannot scan it?")',
|
||||
run: async function(helpers) {
|
||||
const secret = this.$anchor.closest('div').find('span[name="secret"]').text();
|
||||
const token = await ajax.jsonRpc('/totphook', 'call', {
|
||||
secret
|
||||
},
|
||||
{
|
||||
trigger: `span[name="secret"]:hidden`,
|
||||
async run(helpers) {
|
||||
const secret = this.anchor.textContent;
|
||||
const token = await rpc("/totphook", {
|
||||
secret,
|
||||
offset: 0,
|
||||
});
|
||||
helpers._text(helpers._get_action_values('input[name=code]'), token);
|
||||
helpers._click(helpers._get_action_values('button.btn-primary:contains(Activate)'));
|
||||
await helpers.edit(token, 'input[name="code"]');
|
||||
}
|
||||
}, {
|
||||
trigger: "button.btn-primary:contains(Activate)",
|
||||
run: "click",
|
||||
expectUnloadPage: true,
|
||||
}, {
|
||||
content: "Check that the button has changed",
|
||||
trigger: 'button:contains(Disable two-factor authentication)',
|
||||
run: () => {}
|
||||
}]);
|
||||
}]});
|
||||
|
||||
tour.register('totportal_login_enabled', {
|
||||
test: true,
|
||||
url: '/'
|
||||
}, [{
|
||||
registry.category("web_tour.tours").add('totportal_login_enabled', {
|
||||
url: '/',
|
||||
steps: () => [{
|
||||
content: "check that we're on the login page or go to it",
|
||||
trigger: 'input#login, a:contains(Sign in)'
|
||||
isActive: ["body:not(:has(input#login))"],
|
||||
trigger: "a:contains(Sign in)",
|
||||
run: "click",
|
||||
expectUnloadPage: true,
|
||||
}, {
|
||||
content: "input login",
|
||||
trigger: 'input#login',
|
||||
run: 'text portal',
|
||||
run: "edit portal",
|
||||
}, {
|
||||
content: 'input password',
|
||||
trigger: 'input#password',
|
||||
run: 'text portal',
|
||||
run: "edit portal",
|
||||
}, {
|
||||
content: "click da button",
|
||||
trigger: 'button:contains("Log in")',
|
||||
run: "click",
|
||||
expectUnloadPage: true,
|
||||
}, {
|
||||
content: "expect totp screen",
|
||||
trigger: 'label:contains(Authentication Code)',
|
||||
run: "click",
|
||||
}, {
|
||||
content: "input code",
|
||||
trigger: 'input[name=totp_token]',
|
||||
run: async function (helpers) {
|
||||
const token = await ajax.jsonRpc('/totphook', 'call', {});
|
||||
helpers._text(helpers._get_action_values(), token);
|
||||
// FIXME: is there a way to put the button as its own step trigger without
|
||||
// the tour straight blowing through and not waiting for this?
|
||||
helpers._click(helpers._get_action_values('button:contains("Log in")'));
|
||||
const token = await rpc('/totphook', { offset: 1 });
|
||||
await helpers.edit(token);
|
||||
}
|
||||
}, {
|
||||
trigger: "button:contains(Log in)",
|
||||
run: "click",
|
||||
expectUnloadPage: true,
|
||||
}, {
|
||||
content: "check we're logged in",
|
||||
trigger: "h3:contains(Documents)",
|
||||
run: () => {}
|
||||
trigger: "h3:contains(My account)",
|
||||
}, {
|
||||
content: "go back to security",
|
||||
trigger: "a:contains(Security)",
|
||||
run: "click",
|
||||
expectUnloadPage: true,
|
||||
},{
|
||||
content: "Open totp wizard",
|
||||
trigger: 'button#auth_totp_portal_disable',
|
||||
run: "click",
|
||||
}, {
|
||||
content: "Check that we have to enter enhanced security mode",
|
||||
trigger: 'div:contains("enter your password")',
|
||||
run: () => {},
|
||||
trigger: ".modal div:contains(enter your password)",
|
||||
}, {
|
||||
content: "Input password",
|
||||
trigger: '[name=password]',
|
||||
run: 'text portal', // FIXME: better way to do this?
|
||||
run: "edit portal",
|
||||
}, {
|
||||
content: "Confirm",
|
||||
trigger: "button:contains(Confirm Password)",
|
||||
run: "click",
|
||||
expectUnloadPage: true,
|
||||
}, {
|
||||
content: "Check that the button has changed",
|
||||
trigger: 'button:contains(Enable two-factor authentication)',
|
||||
run: () => {}
|
||||
}]);
|
||||
}]});
|
||||
|
||||
tour.register('totportal_login_disabled', {
|
||||
test: true,
|
||||
url: '/'
|
||||
}, [{
|
||||
registry.category("web_tour.tours").add('totportal_login_disabled', {
|
||||
url: '/',
|
||||
steps: () => [{
|
||||
content: "check that we're on the login page or go to it",
|
||||
trigger: 'input#login, a:contains(Sign in)'
|
||||
isActive: ["body:not(:has(input#login))"],
|
||||
trigger: "a:contains(Sign in)",
|
||||
run: "click",
|
||||
expectUnloadPage: true,
|
||||
}, {
|
||||
content: "input login",
|
||||
trigger: 'input#login',
|
||||
run: 'text portal',
|
||||
run: "edit portal",
|
||||
}, {
|
||||
content: 'input password',
|
||||
trigger: 'input#password',
|
||||
run: 'text portal',
|
||||
run: "edit portal",
|
||||
}, {
|
||||
content: "click da button",
|
||||
trigger: 'button:contains("Log in")',
|
||||
run: "click",
|
||||
expectUnloadPage: true,
|
||||
}, {
|
||||
content: "check we're logged in",
|
||||
trigger: "h3:contains(Documents)",
|
||||
run: () => {}
|
||||
}]);
|
||||
});
|
||||
trigger: "h3:contains(My account)",
|
||||
}]});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue