mirror of
https://github.com/HeyPuter/puter
synced 2024-11-15 06:15:47 +00:00
Add otp test endpoint and next wizard step
This commit is contained in:
parent
22234ad1c1
commit
3e380ba844
@ -47,6 +47,17 @@ module.exports = eggspress('/auth/configure-2fa/:action', {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// IMPORTANT: only use to verify the user's 2FA setup;
|
||||||
|
// this should never be used to verify the user's 2FA code
|
||||||
|
// for authentication purposes.
|
||||||
|
actions.test = async () => {
|
||||||
|
const user = req.user;
|
||||||
|
const svc_otp = x.get('services').get('otp');
|
||||||
|
const code = req.body.code;
|
||||||
|
const delta = svc_otp.verify(user.username, user.otp_secret, code);
|
||||||
|
return { ok: delta !== null, delta };
|
||||||
|
};
|
||||||
|
|
||||||
actions.enable = async () => {
|
actions.enable = async () => {
|
||||||
await db.write(
|
await db.write(
|
||||||
`UPDATE user SET otp_enabled = 1 WHERE uuid = ?`,
|
`UPDATE user SET otp_enabled = 1 WHERE uuid = ?`,
|
||||||
|
@ -3,6 +3,7 @@ import { Component } from "../../util/Component.js";
|
|||||||
export default class CodeEntryView extends Component {
|
export default class CodeEntryView extends Component {
|
||||||
static PROPERTIES = {
|
static PROPERTIES = {
|
||||||
value: {},
|
value: {},
|
||||||
|
error: {},
|
||||||
is_checking_code: {},
|
is_checking_code: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,6 +71,11 @@ export default class CodeEntryView extends Component {
|
|||||||
on_ready ({ listen }) {
|
on_ready ({ listen }) {
|
||||||
let is_checking_code = false;
|
let is_checking_code = false;
|
||||||
|
|
||||||
|
listen('error', (error) => {
|
||||||
|
if ( ! error ) return $(this.dom_).find('.error').hide();
|
||||||
|
$(this.dom_).find('.error').text(error).show();
|
||||||
|
});
|
||||||
|
|
||||||
$(this.dom_).find('.digit-input').first().focus();
|
$(this.dom_).find('.digit-input').first().focus();
|
||||||
|
|
||||||
$(this.dom_).find('.code-confirm-btn').on('click submit', function(e){
|
$(this.dom_).find('.code-confirm-btn').on('click submit', function(e){
|
||||||
|
73
src/UI/Components/RecoveryCodesView.js
Normal file
73
src/UI/Components/RecoveryCodesView.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { Component } from "../../util/Component.js";
|
||||||
|
|
||||||
|
export default class RecoveryCodesView extends Component {
|
||||||
|
static PROPERTIES = {
|
||||||
|
values: {
|
||||||
|
description: 'The recovery codes to display',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static CSS = /*css*/`
|
||||||
|
.recovery-codes {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px auto;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 600px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-codes h2 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-codes-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||||
|
gap: 10px; /* Adds space between grid items */
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-code {
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: center;
|
||||||
|
font-family: 'Courier New', Courier, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
|
||||||
|
create_template ({ template }) {
|
||||||
|
$(template).html(`
|
||||||
|
<div class="recovery-codes">
|
||||||
|
<div class="recovery-codes-list">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
on_ready ({ listen }) {
|
||||||
|
listen('values', values => {
|
||||||
|
for ( const value of values ) {
|
||||||
|
$(this.dom_).find('.recovery-codes-list').append(`
|
||||||
|
<div class="recovery-code">${html_encode(value)}</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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_recoveryCodesView ) {
|
||||||
|
window.__component_recoveryCodesView = true;
|
||||||
|
|
||||||
|
customElements.define('c-recovery-codes-view', RecoveryCodesView);
|
||||||
|
}
|
@ -22,6 +22,7 @@
|
|||||||
import CodeEntryView from "./Components/CodeEntryView.js";
|
import CodeEntryView from "./Components/CodeEntryView.js";
|
||||||
import Flexer from "./Components/Flexer.js";
|
import Flexer from "./Components/Flexer.js";
|
||||||
import QRCodeView from "./Components/QRCode.js";
|
import QRCodeView from "./Components/QRCode.js";
|
||||||
|
import RecoveryCodesView from "./Components/RecoveryCodesView.js";
|
||||||
import StepView from "./Components/StepView.js";
|
import StepView from "./Components/StepView.js";
|
||||||
import TestView from "./Components/TestView.js";
|
import TestView from "./Components/TestView.js";
|
||||||
import UIComponentWindow from "./UIComponentWindow.js";
|
import UIComponentWindow from "./UIComponentWindow.js";
|
||||||
@ -37,7 +38,25 @@ const UIWindow2FASetup = async function UIWindow2FASetup () {
|
|||||||
});
|
});
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
|
|
||||||
|
const check_code_ = async function check_code_ (value) {
|
||||||
|
const resp = await fetch(`${api_origin}/auth/configure-2fa/test`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${puter.authToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
code: value,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await resp.json();
|
||||||
|
|
||||||
|
return data.ok;
|
||||||
|
};
|
||||||
|
|
||||||
let stepper;
|
let stepper;
|
||||||
|
let code_entry;
|
||||||
const component =
|
const component =
|
||||||
new StepView({
|
new StepView({
|
||||||
_ref: me => stepper = me,
|
_ref: me => stepper = me,
|
||||||
@ -48,15 +67,26 @@ const UIWindow2FASetup = async function UIWindow2FASetup () {
|
|||||||
value: data.url,
|
value: data.url,
|
||||||
}),
|
}),
|
||||||
new CodeEntryView({
|
new CodeEntryView({
|
||||||
[`property.value`] (value) {
|
async [`property.value`] (value, { component }) {
|
||||||
console.log('value? ', value)
|
console.log('value? ', value)
|
||||||
|
|
||||||
|
if ( ! await check_code_(value) ) {
|
||||||
|
component.set('error', 'Invalid code');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
stepper.next();
|
stepper.next();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
new TestView(),
|
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
new TestView(),
|
new Flexer({
|
||||||
|
children: [
|
||||||
|
new RecoveryCodesView({
|
||||||
|
values: data.codes,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
;
|
;
|
||||||
|
@ -31,7 +31,10 @@ export class Component extends HTMLElement {
|
|||||||
|
|
||||||
const listener_key = `property.${key}`;
|
const listener_key = `property.${key}`;
|
||||||
if ( property_values[listener_key] ) {
|
if ( property_values[listener_key] ) {
|
||||||
this.values_[key].sub(property_values[listener_key]);
|
this.values_[key].sub((value, more) => {
|
||||||
|
more = { ...more, component: this };
|
||||||
|
property_values[listener_key](value, more);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user