mirror of
https://github.com/HeyPuter/puter
synced 2024-11-15 06:15:47 +00:00
Complete the 2FA setup flow
This commit is contained in:
parent
a95fcc96be
commit
c91c0afa71
50
src/UI/Components/Button.js
Normal file
50
src/UI/Components/Button.js
Normal 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);
|
||||
}
|
67
src/UI/Components/ConfirmationsView.js
Normal file
67
src/UI/Components/ConfirmationsView.js
Normal 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);
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user