Complete the 2FA setup flow

This commit is contained in:
KernelDeimos 2024-05-03 17:57:09 -04:00
parent a95fcc96be
commit c91c0afa71
5 changed files with 188 additions and 5 deletions

View File

@ -0,0 +1,50 @@
import { Component } from "../../util/Component.js";
export default class Button extends Component {
static PROPERTIES = {
label: { value: 'Test Label' },
on_click: { value: null },
enabled: { value: true },
}
static RENDER_MODE = Component.NO_SHADOW;
static CSS = /*css*/`
button {
margin: 0;
color: hsl(220, 25%, 31%);
}
`;
create_template ({ template }) {
$(template).html(/*html*/`
<button type="submit" class="button button-block button-primary code-confirm-btn" style="margin-top:10px;" disabled>${
html_encode(this.get('label'))
}</button>
`);
}
on_ready ({ listen }) {
if ( this.get('on_click') ) {
const $button = $(this.dom_).find('button');
$button.on('click', async () => {
$button.html(`<svg style="width:20px; margin-top: 5px;" xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24"><title>circle anim</title><g fill="#fff" class="nc-icon-wrapper"><g class="nc-loop-circle-24-icon-f"><path d="M12 24a12 12 0 1 1 12-12 12.013 12.013 0 0 1-12 12zm0-22a10 10 0 1 0 10 10A10.011 10.011 0 0 0 12 2z" fill="#eee" opacity=".4"></path><path d="M24 12h-2A10.011 10.011 0 0 0 12 2V0a12.013 12.013 0 0 1 12 12z" data-color="color-2"></path></g><style>.nc-loop-circle-24-icon-f{--animation-duration:0.5s;transform-origin:12px 12px;animation:nc-loop-circle-anim var(--animation-duration) infinite linear}@keyframes nc-loop-circle-anim{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}</style></g></svg>`);
const on_click = this.get('on_click');
await on_click();
$button.html(this.get('label'));
});
}
listen('enabled', enabled => {
$(this.dom_).find('button').prop('disabled', ! enabled);
});
}
}
// TODO: This is necessary because files can be loaded from
// both `/src/UI` and `/UI` in the URL; we need to fix that
if ( ! window.__component_button ) {
window.__component_button = true;
customElements.define('c-button', Button);
}

View File

@ -0,0 +1,67 @@
import { Component } from "../../util/Component.js";
/**
* Display a list of checkboxes for the user to confirm.
*/
export default class ConfirmationsView extends Component {
static PROPERTIES = {
confirmations: {
description: 'The list of confirmations to display',
},
confirmed: {
description: 'True iff all confirmations are checked',
},
}
static CSS = /*css*/`
.confirmations {
display: flex;
flex-direction: column;
}
.looks-good {
margin-top: 20px;
color: hsl(220, 25%, 31%);
font-size: 20px;
font-weight: 700;
display: none;
}
`
create_template ({ template }) {
$(template).html(/*html*/`
<div class="confirmations">
${
this.get('confirmations').map((confirmation, index) => {
return /*html*/`
<div>
<input type="checkbox" id="confirmation-${index}" name="confirmation-${index}">
<label for="confirmation-${index}">${confirmation}</label>
</div>
`;
}).join('')
}
<span class="looks-good">Looks good!</span>
</div>
`);
}
on_ready ({ listen }) {
// update `confirmed` property when checkboxes are checked
$(this.dom_).find('input').on('change', () => {
this.set('confirmed', $(this.dom_).find('input').toArray().every(input => input.checked));
if ( this.get('confirmed') ) {
$(this.dom_).find('.looks-good').show();
} else {
$(this.dom_).find('.looks-good').hide();
}
});
}
}
// TODO: This is necessary because files can be loaded from
// both `/src/UI` and `/UI` in the URL; we need to fix that
if ( ! window.__component_confirmationsView ) {
window.__component_confirmationsView = true;
customElements.define('c-confirmations-view', ConfirmationsView);
}

View File

@ -48,7 +48,14 @@ export default {
init: ($el_window) => {
$el_window.find('.enable-2fa').on('click', async function (e) {
UIWindow2FASetup();
const { promise } = await UIWindow2FASetup();
const tfa_was_enabled = await promise;
if ( tfa_was_enabled ) {
$el_window.find('.enable-2fa').hide();
$el_window.find('.disable-2fa').show();
$el_window.find('.user-otp-state').text(i18n('two_factor_enabled'));
}
return;

View File

@ -13,7 +13,7 @@ import Placeholder from "../util/Placeholder.js"
export default async function UIComponentWindow (options) {
const placeholder = Placeholder();
await UIWindow({
const win = await UIWindow({
...options,
body_content: placeholder.html,
@ -22,4 +22,6 @@ export default async function UIComponentWindow (options) {
options.component.attach(placeholder);
options.component.focus();
console.log('UIComponentWindow', options.component);
return win;
}

View File

@ -19,7 +19,11 @@
*/
import TeePromise from "../util/TeePromise.js";
import ValueHolder from "../util/ValueHolder.js";
import Button from "./Components/Button.js";
import CodeEntryView from "./Components/CodeEntryView.js";
import ConfirmationsView from "./Components/ConfirmationsView.js";
import Flexer from "./Components/Flexer.js";
import QRCodeView from "./Components/QRCode.js";
import RecoveryCodesView from "./Components/RecoveryCodesView.js";
@ -31,6 +35,7 @@ import UIAlert from "./UIAlert.js";
import UIComponentWindow from "./UIComponentWindow.js";
const UIWindow2FASetup = async function UIWindow2FASetup () {
// FIRST REQUEST :: Generate the QR code and recovery codes
const resp = await fetch(`${api_origin}/auth/configure-2fa/setup`, {
method: 'POST',
headers: {
@ -41,6 +46,7 @@ const UIWindow2FASetup = async function UIWindow2FASetup () {
});
const data = await resp.json();
// SECOND REQUEST :: Verify the code [first wizard screen]
const check_code_ = async function check_code_ (value) {
const resp = await fetch(`${api_origin}/auth/configure-2fa/test`, {
method: 'POST',
@ -58,8 +64,29 @@ const UIWindow2FASetup = async function UIWindow2FASetup () {
return data.ok;
};
// FINAL REQUEST :: Enable 2FA [second wizard screen]
const enable_2fa_ = async function check_code_ (value) {
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();
return data.ok;
};
let stepper;
let code_entry;
let win;
let done_enabled = new ValueHolder(false);
const promise = new TeePromise();
const component =
new StepView({
_ref: me => stepper = me,
@ -118,17 +145,40 @@ const UIWindow2FASetup = async function UIWindow2FASetup () {
symbol: '5',
text: 'Confirm Recovery Codes',
}),
new ConfirmationsView({
confirmations: [
'I have copied the recovery codes',
],
confirmed: done_enabled,
}),
new Button({
enabled: done_enabled,
on_click: async () => {
await enable_2fa_();
stepper.next();
},
}),
]
}),
]
})
;
UIComponentWindow({
stepper.values_['done'].sub(value => {
if ( ! value ) return;
$(win).close();
console.log('WE GOT HERE')
promise.resolve(true);
})
win = await UIComponentWindow({
component,
on_before_exit: async () => {
console.log('this was called?');
return await UIAlert({
// If stepper was exhausted, we can close the window
if ( stepper.get('done') ) return true;
// Otherwise the user is trying to cancel the setup
const will_close = await UIAlert({
message: i18n('cancel_2fa_setup'),
buttons: [
{
@ -142,6 +192,11 @@ const UIWindow2FASetup = async function UIWindow2FASetup () {
},
]
});
if ( will_close ) {
promise.resolve(false);
return true;
}
},
title: 'Instant Login!',
@ -176,6 +231,8 @@ const UIWindow2FASetup = async function UIWindow2FASetup () {
padding: '20px',
},
});
return { promise };
}
export default UIWindow2FASetup;