Add otp test endpoint and next wizard step

This commit is contained in:
KernelDeimos 2024-05-03 01:34:36 -04:00
parent 22234ad1c1
commit 3e380ba844
5 changed files with 127 additions and 4 deletions

View File

@ -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 = ?`,

View File

@ -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){

View 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);
}

View File

@ -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,
}),
]
}),
] ]
}) })
; ;

View File

@ -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);
});
} }
} }