mirror of
https://github.com/HeyPuter/puter.git
synced 2025-02-03 07:48:46 +08:00
Re-use CodeEntryView for login
This commit is contained in:
parent
5cbe256120
commit
60a561c84c
24
src/UI/Components/JustHTML.js
Normal file
24
src/UI/Components/JustHTML.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Component } from "../../util/Component.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows using an HTML string as a component.
|
||||||
|
*/
|
||||||
|
export default class JustHTML extends Component {
|
||||||
|
static PROPERTIES = { html: { value: '' } };
|
||||||
|
create_template ({ template }) {
|
||||||
|
$(template).html(`<span></span>`);
|
||||||
|
}
|
||||||
|
on_ready ({ listen }) {
|
||||||
|
listen('html', html => {
|
||||||
|
$(this.dom_).find('span').html(html);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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_justHTML ) {
|
||||||
|
window.__component_justHTML = true;
|
||||||
|
|
||||||
|
customElements.define('c-just-html', JustHTML);
|
||||||
|
}
|
@ -20,9 +20,12 @@
|
|||||||
import UIWindow from './UIWindow.js'
|
import UIWindow from './UIWindow.js'
|
||||||
import UIWindowSignup from './UIWindowSignup.js'
|
import UIWindowSignup from './UIWindowSignup.js'
|
||||||
import UIWindowRecoverPassword from './UIWindowRecoverPassword.js'
|
import UIWindowRecoverPassword from './UIWindowRecoverPassword.js'
|
||||||
import UIWindowVerificationCode from './UIWindowVerificationCode.js';
|
|
||||||
import TeePromise from '../util/TeePromise.js';
|
import TeePromise from '../util/TeePromise.js';
|
||||||
import UIAlert from './UIAlert.js';
|
import UIAlert from './UIAlert.js';
|
||||||
|
import UIComponentWindow from './UIComponentWindow.js';
|
||||||
|
import Flexer from './Components/Flexer.js';
|
||||||
|
import CodeEntryView from './Components/CodeEntryView.js';
|
||||||
|
import JustHTML from './Components/JustHTML.js';
|
||||||
|
|
||||||
async function UIWindowLogin(options){
|
async function UIWindowLogin(options){
|
||||||
options = options ?? {};
|
options = options ?? {};
|
||||||
@ -170,38 +173,60 @@ async function UIWindowLogin(options){
|
|||||||
let p = Promise.resolve();
|
let p = Promise.resolve();
|
||||||
if ( data.next_step === 'otp' ) {
|
if ( data.next_step === 'otp' ) {
|
||||||
p = new TeePromise();
|
p = new TeePromise();
|
||||||
UIWindowVerificationCode({
|
let code_entry;
|
||||||
title_key: 'confirm_code_2fa_title',
|
let win;
|
||||||
instruction_key: 'confirm_code_2fa_instruction',
|
const component = new Flexer({
|
||||||
submit_btn_key: 'confirm_code_2fa_submit_btn',
|
children: [
|
||||||
on_value: async ({ actions, win, value }) => {
|
new JustHTML({
|
||||||
try {
|
html: /*html*/`
|
||||||
const resp = await fetch(`${api_origin}/login/otp`, {
|
<h3 style="text-align:center; font-weight: 500; font-size: 20px;">Enter 2FA Code</h3>
|
||||||
method: 'POST',
|
<p style="text-align:center; padding: 0 20px;">Enter the 6-digit code from your authenticator app.</p>
|
||||||
headers: {
|
`
|
||||||
'Content-Type': 'application/json',
|
}),
|
||||||
},
|
new CodeEntryView({
|
||||||
body: JSON.stringify({
|
_ref: me => code_entry = me,
|
||||||
token: data.otp_jwt_token,
|
async [`property.value`] (value, { component }) {
|
||||||
code: value,
|
const resp = await fetch(`${api_origin}/login/otp`, {
|
||||||
}),
|
method: 'POST',
|
||||||
});
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
token: data.otp_jwt_token,
|
||||||
|
code: value,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
data = await resp.json();
|
data = await resp.json();
|
||||||
|
|
||||||
if ( ! data.proceed ) {
|
if ( ! data.proceed ) {
|
||||||
actions.clear();
|
actions.clear();
|
||||||
actions.show_error(i18n('confirm_code_generic_incorrect'));
|
actions.show_error(i18n('confirm_code_generic_incorrect'));
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(win).close();
|
||||||
|
p.resolve();
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
$(win).close();
|
],
|
||||||
p.resolve();
|
['event.focus'] () {
|
||||||
} catch (e) {
|
code_entry.focus();
|
||||||
actions.show_error(e.message ?? i18n('error_unknown_cause'));
|
}
|
||||||
}
|
});
|
||||||
|
win = await UIComponentWindow({
|
||||||
|
component,
|
||||||
|
width: 500,
|
||||||
|
backdrop: true,
|
||||||
|
body_css: {
|
||||||
|
width: 'initial',
|
||||||
|
height: '100%',
|
||||||
|
'background-color': 'rgb(245 247 249)',
|
||||||
|
'backdrop-filter': 'blur(3px)',
|
||||||
|
padding: '20px',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
component.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
await p;
|
await p;
|
||||||
|
@ -1,193 +0,0 @@
|
|||||||
import TeePromise from "../util/TeePromise.js";
|
|
||||||
import UIWindow from "./UIWindow.js";
|
|
||||||
|
|
||||||
const UIWindowVerificationCode = async function UIWindowVerificationCode ( options ) {
|
|
||||||
options = options ?? {};
|
|
||||||
let final_code = '';
|
|
||||||
let is_checking_code = false;
|
|
||||||
|
|
||||||
const html_title = i18n(options.title_key || 'confirm_code_generic_title');
|
|
||||||
const html_instruction = i18n(options.instruction_key || 'confirm_code_generic_instruction');
|
|
||||||
const submit_btn_txt = i18n(options.submit_btn_key || 'confirm_code_generic_submit');
|
|
||||||
|
|
||||||
let h = '';
|
|
||||||
h += `<div class="qr-code-window-close-btn generic-close-window-button"> × </div>`;
|
|
||||||
h += `<div style="-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; color: #3e5362;">`;
|
|
||||||
h += `<h3 style="text-align:center; font-weight: 500; font-size: 20px;">${ html_title }</h3>`;
|
|
||||||
h += `<form>`;
|
|
||||||
h += `<p style="text-align:center; padding: 0 20px;">${ html_instruction }</p>`;
|
|
||||||
h += `<div class="error"></div>`;
|
|
||||||
h += ` <fieldset name="number-code" style="border: none; padding:0;" data-number-code-form>
|
|
||||||
<input class="digit-input" type="number" min='0' max='9' name='number-code-0' data-number-code-input='0' required />
|
|
||||||
<input class="digit-input" type="number" min='0' max='9' name='number-code-1' data-number-code-input='1' required />
|
|
||||||
<input class="digit-input" type="number" min='0' max='9' name='number-code-2' data-number-code-input='2' required />
|
|
||||||
<span class="confirm-code-hyphen">-</span>
|
|
||||||
<input class="digit-input" type="number" min='0' max='9' name='number-code-3' data-number-code-input='3' required />
|
|
||||||
<input class="digit-input" type="number" min='0' max='9' name='number-code-4' data-number-code-input='4' required />
|
|
||||||
<input class="digit-input" type="number" min='0' max='9' name='number-code-5' data-number-code-input='5' required />
|
|
||||||
</fieldset>`;
|
|
||||||
h += `<button type="submit" class="button button-block button-primary code-confirm-btn" style="margin-top:10px;" disabled>${submit_btn_txt}</button>`;
|
|
||||||
h += `</form>`;
|
|
||||||
h += `</div>`;
|
|
||||||
|
|
||||||
const el_window = await UIWindow({
|
|
||||||
title: null,
|
|
||||||
icon: null,
|
|
||||||
uid: null,
|
|
||||||
is_dir: false,
|
|
||||||
body_content: h,
|
|
||||||
has_head: false,
|
|
||||||
selectable_body: false,
|
|
||||||
draggable_body: true,
|
|
||||||
allow_context_menu: false,
|
|
||||||
is_draggable: options.is_draggable ?? true,
|
|
||||||
is_droppable: false,
|
|
||||||
is_resizable: false,
|
|
||||||
stay_on_top: options.stay_on_top ?? false,
|
|
||||||
allow_native_ctxmenu: true,
|
|
||||||
allow_user_select: true,
|
|
||||||
backdrop: true,
|
|
||||||
width: 390,
|
|
||||||
dominant: true,
|
|
||||||
onAppend: function(el_window){
|
|
||||||
$(el_window).find('.digit-input').first().focus();
|
|
||||||
},
|
|
||||||
window_class: 'window-item-properties',
|
|
||||||
window_css:{
|
|
||||||
height: 'initial',
|
|
||||||
},
|
|
||||||
body_css: {
|
|
||||||
padding: '30px',
|
|
||||||
width: 'initial',
|
|
||||||
height: 'initial',
|
|
||||||
'background-color': 'rgb(247 251 255)',
|
|
||||||
'backdrop-filter': 'blur(3px)',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$(el_window).find('.digit-input').first().focus();
|
|
||||||
|
|
||||||
const actions = {
|
|
||||||
clear: () => {
|
|
||||||
final_code = '';
|
|
||||||
$(el_window).find('.code-confirm-btn').prop('disabled', false);
|
|
||||||
$(el_window).find('.code-confirm-btn').html(submit_btn_txt);
|
|
||||||
$(el_window).find('.digit-input').val('');
|
|
||||||
$(el_window).find('.digit-input').first().focus();
|
|
||||||
|
|
||||||
},
|
|
||||||
show_error: (msg) => {
|
|
||||||
$(el_window).find('.error').html(html_encode(msg));
|
|
||||||
$(el_window).find('.error').fadeIn();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$(el_window).find('.code-confirm-btn').on('click submit', function(e){
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
$(el_window).find('.code-confirm-btn').prop('disabled', true);
|
|
||||||
$(el_window).find('.error').hide();
|
|
||||||
|
|
||||||
// Check if already checking code to prevent multiple requests
|
|
||||||
if(is_checking_code)
|
|
||||||
return;
|
|
||||||
// Confirm button
|
|
||||||
is_checking_code = true;
|
|
||||||
|
|
||||||
// set animation
|
|
||||||
$(el_window).find('.code-confirm-btn').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>`);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('final code', final_code);
|
|
||||||
options.on_value({
|
|
||||||
actions,
|
|
||||||
value: final_code,
|
|
||||||
win: el_window
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
})
|
|
||||||
|
|
||||||
// Elements
|
|
||||||
const numberCodeForm = document.querySelector('[data-number-code-form]');
|
|
||||||
const numberCodeInputs = [...numberCodeForm.querySelectorAll('[data-number-code-input]')];
|
|
||||||
|
|
||||||
// Event listeners
|
|
||||||
numberCodeForm.addEventListener('input', ({ target }) => {
|
|
||||||
if(!target.value.length) { return target.value = null; }
|
|
||||||
const inputLength = target.value.length;
|
|
||||||
let currentIndex = Number(target.dataset.numberCodeInput);
|
|
||||||
if(inputLength === 2){
|
|
||||||
const inputValues = target.value.split('');
|
|
||||||
target.value = inputValues[0];
|
|
||||||
}
|
|
||||||
else if (inputLength > 1) {
|
|
||||||
const inputValues = target.value.split('');
|
|
||||||
|
|
||||||
inputValues.forEach((value, valueIndex) => {
|
|
||||||
const nextValueIndex = currentIndex + valueIndex;
|
|
||||||
|
|
||||||
if (nextValueIndex >= numberCodeInputs.length) { return; }
|
|
||||||
|
|
||||||
numberCodeInputs[nextValueIndex].value = value;
|
|
||||||
});
|
|
||||||
currentIndex += inputValues.length - 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextIndex = currentIndex + 1;
|
|
||||||
|
|
||||||
if (nextIndex < numberCodeInputs.length) {
|
|
||||||
numberCodeInputs[nextIndex].focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Concatenate all inputs into one string to create the final code
|
|
||||||
final_code = '';
|
|
||||||
for(let i=0; i< numberCodeInputs.length; i++){
|
|
||||||
final_code += numberCodeInputs[i].value;
|
|
||||||
}
|
|
||||||
// Automatically submit if 6 digits entered
|
|
||||||
if(final_code.length === 6){
|
|
||||||
$(el_window).find('.code-confirm-btn').prop('disabled', false);
|
|
||||||
$(el_window).find('.code-confirm-btn').trigger('click');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
numberCodeForm.addEventListener('keydown', (e) => {
|
|
||||||
const { code, target } = e;
|
|
||||||
|
|
||||||
const currentIndex = Number(target.dataset.numberCodeInput);
|
|
||||||
const previousIndex = currentIndex - 1;
|
|
||||||
const nextIndex = currentIndex + 1;
|
|
||||||
|
|
||||||
const hasPreviousIndex = previousIndex >= 0;
|
|
||||||
const hasNextIndex = nextIndex <= numberCodeInputs.length - 1
|
|
||||||
|
|
||||||
switch (code) {
|
|
||||||
case 'ArrowLeft':
|
|
||||||
case 'ArrowUp':
|
|
||||||
if (hasPreviousIndex) {
|
|
||||||
numberCodeInputs[previousIndex].focus();
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'ArrowRight':
|
|
||||||
case 'ArrowDown':
|
|
||||||
if (hasNextIndex) {
|
|
||||||
numberCodeInputs[nextIndex].focus();
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
|
||||||
break;
|
|
||||||
case 'Backspace':
|
|
||||||
if (!e.target.value.length && hasPreviousIndex) {
|
|
||||||
numberCodeInputs[previousIndex].value = null;
|
|
||||||
numberCodeInputs[previousIndex].focus();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UIWindowVerificationCode;
|
|
Loading…
Reference in New Issue
Block a user