mirror of
https://github.com/HeyPuter/puter.git
synced 2025-02-02 23:28:39 +08:00
Require password entry to disable 2FA
This commit is contained in:
parent
23215bd6f7
commit
c2f1694107
@ -107,22 +107,6 @@ module.exports = eggspress('/auth/configure-2fa/:action', {
|
|||||||
return {};
|
return {};
|
||||||
};
|
};
|
||||||
|
|
||||||
actions.disable = async () => {
|
|
||||||
await db.write(
|
|
||||||
`UPDATE user SET otp_enabled = 0, otp_recovery_codes = '' WHERE uuid = ?`,
|
|
||||||
[user.uuid]
|
|
||||||
);
|
|
||||||
// update cached user
|
|
||||||
req.user.otp_enabled = 0;
|
|
||||||
|
|
||||||
const svc_email = req.services.get('email');
|
|
||||||
await svc_email.send_email({ email: user.email }, 'disabled_2fa', {
|
|
||||||
username: user.username,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
};
|
|
||||||
|
|
||||||
if ( ! actions[action] ) {
|
if ( ! actions[action] ) {
|
||||||
throw APIError.create('invalid_action', null, { action });
|
throw APIError.create('invalid_action', null, { action });
|
||||||
}
|
}
|
||||||
|
22
packages/backend/src/routers/user-protected/disable-2fa.js
Normal file
22
packages/backend/src/routers/user-protected/disable-2fa.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
const { DB_WRITE } = require("../../services/database/consts");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
route: '/disable-2fa',
|
||||||
|
methods: ['POST'],
|
||||||
|
handler: async (req, res, next) => {
|
||||||
|
const db = req.services.get('database').get(DB_WRITE, '2fa.disable');
|
||||||
|
await db.write(
|
||||||
|
`UPDATE user SET otp_enabled = 0, otp_recovery_codes = NULL, otp_secret = NULL WHERE uuid = ?`,
|
||||||
|
[req.user.uuid]
|
||||||
|
);
|
||||||
|
// update cached user
|
||||||
|
req.user.otp_enabled = 0;
|
||||||
|
|
||||||
|
const svc_email = req.services.get('email');
|
||||||
|
await svc_email.send_email({ email: req.user.email }, 'disabled_2fa', {
|
||||||
|
username: req.user.username,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.send({ success: true });
|
||||||
|
}
|
||||||
|
};
|
@ -70,6 +70,10 @@ class EdgeRateLimitService extends BaseService {
|
|||||||
limit: 10,
|
limit: 10,
|
||||||
window: HOUR,
|
window: HOUR,
|
||||||
},
|
},
|
||||||
|
['/user-protected/disable-2fa']: {
|
||||||
|
limit: 10,
|
||||||
|
window: HOUR,
|
||||||
|
},
|
||||||
['login-otp']: {
|
['login-otp']: {
|
||||||
limit: 15,
|
limit: 15,
|
||||||
window: 30 * MINUTE,
|
window: 30 * MINUTE,
|
||||||
|
@ -88,6 +88,10 @@ class UserProtectedEndpointsService extends BaseService {
|
|||||||
Endpoint(
|
Endpoint(
|
||||||
require('../../routers/user-protected/change-email.js'),
|
require('../../routers/user-protected/change-email.js'),
|
||||||
).attach(router);
|
).attach(router);
|
||||||
|
|
||||||
|
Endpoint(
|
||||||
|
require('../../routers/user-protected/disable-2fa.js'),
|
||||||
|
).attach(router);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
136
src/UI/Components/PasswordEntry.js
Normal file
136
src/UI/Components/PasswordEntry.js
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { Component, defineComponent } from "../../util/Component.js";
|
||||||
|
|
||||||
|
export default class PasswordEntry extends Component {
|
||||||
|
static PROPERTIES = {
|
||||||
|
spec: {},
|
||||||
|
value: {},
|
||||||
|
error: {},
|
||||||
|
on_submit: {},
|
||||||
|
show_password: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
static CSS = /*css*/`
|
||||||
|
fieldset {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TODO: I'd rather not duplicate this */
|
||||||
|
.error {
|
||||||
|
display: none;
|
||||||
|
color: red;
|
||||||
|
border: 1px solid red;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 9px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.error-message {
|
||||||
|
display: none;
|
||||||
|
color: rgb(215 2 2);
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid rgb(215 2 2);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.password-and-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.password-and-toggle input {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* TODO: DRY: This is from style.css */
|
||||||
|
input[type=text], input[type=password], input[type=email], select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
outline: none;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
color: #393f46;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* to prevent auto-zoom on input focus in mobile */
|
||||||
|
.device-phone input[type=text], .device-phone input[type=password], .device-phone input[type=email], .device-phone select {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text]:focus, input[type=password]:focus, input[type=email]:focus, select:focus {
|
||||||
|
border: 2px solid #01a0fd;
|
||||||
|
padding: 7px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
create_template ({ template }) {
|
||||||
|
$(template).html(/*html*/`
|
||||||
|
<form>
|
||||||
|
<div class="error"></div>
|
||||||
|
<div class="password-and-toggle">
|
||||||
|
<input type="password" class="value-input" id="password" placeholder="${i18n('password')}" required>
|
||||||
|
<img
|
||||||
|
id="toggle-show-password"
|
||||||
|
src="${this.get('show_password')
|
||||||
|
? window.icons["eye-closed.svg"]
|
||||||
|
: window.icons["eye-open.svg"]}"
|
||||||
|
width="20"
|
||||||
|
height="20">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
on_focus () {
|
||||||
|
$(this.dom_).find('input').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
on_ready ({ listen }) {
|
||||||
|
listen('error', (error) => {
|
||||||
|
if ( ! error ) return $(this.dom_).find('.error').hide();
|
||||||
|
$(this.dom_).find('.error').text(error).show();
|
||||||
|
});
|
||||||
|
|
||||||
|
listen('value', (value) => {
|
||||||
|
// clear input
|
||||||
|
if ( value === undefined ) {
|
||||||
|
$(this.dom_).find('input').val('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = $(this.dom_).find('input');
|
||||||
|
input.on('input', () => {
|
||||||
|
this.set('value', input.val());
|
||||||
|
});
|
||||||
|
|
||||||
|
const on_submit = this.get('on_submit');
|
||||||
|
if ( on_submit ) {
|
||||||
|
$(this.dom_).find('input').on('keyup', (e) => {
|
||||||
|
if ( e.key === 'Enter' ) {
|
||||||
|
on_submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(this.dom_).find("#toggle-show-password").on("click", () => {
|
||||||
|
this.set('show_password', !this.get('show_password'));
|
||||||
|
const show_password = this.get('show_password');
|
||||||
|
// hide/show password and update icon
|
||||||
|
$(this.dom_).find("input").attr("type", show_password ? "text" : "password");
|
||||||
|
$(this.dom_).find("#toggle-show-password").attr("src", show_password ? window.icons["eye-closed.svg"] : window.icons["eye-open.svg"])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineComponent('c-password-entry', PasswordEntry);
|
@ -1,4 +1,10 @@
|
|||||||
|
import TeePromise from "../../util/TeePromise.js";
|
||||||
|
import Button from "../Components/Button.js";
|
||||||
|
import Flexer from "../Components/Flexer.js";
|
||||||
|
import JustHTML from "../Components/JustHTML.js";
|
||||||
|
import PasswordEntry from "../Components/PasswordEntry.js";
|
||||||
import UIAlert from "../UIAlert.js";
|
import UIAlert from "../UIAlert.js";
|
||||||
|
import UIComponentWindow from "../UIComponentWindow.js";
|
||||||
import UIWindow2FASetup from "../UIWindow2FASetup.js";
|
import UIWindow2FASetup from "../UIWindow2FASetup.js";
|
||||||
import UIWindowQR from "../UIWindowQR.js";
|
import UIWindowQR from "../UIWindowQR.js";
|
||||||
|
|
||||||
@ -64,35 +70,88 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$el_window.find('.disable-2fa').on('click', async function (e) {
|
$el_window.find('.disable-2fa').on('click', async function (e) {
|
||||||
const confirmation = i18n('disable_2fa_confirm');
|
let win, password_entry;
|
||||||
const alert_resp = await UIAlert({
|
const password_confirm_promise = new TeePromise();
|
||||||
message: confirmation,
|
const try_password = async () => {
|
||||||
window_options: {
|
const value = password_entry.get('value');
|
||||||
parent_uuid: $el_window.attr('data-element_uuid'),
|
const resp = await fetch(`${window.api_origin}/user-protected/disable-2fa`, {
|
||||||
disable_parent_window: true,
|
method: 'POST',
|
||||||
parent_center: true,
|
headers: {
|
||||||
},
|
Authorization: `Bearer ${puter.authToken}`,
|
||||||
buttons:[
|
'Content-Type': 'application/json',
|
||||||
{
|
|
||||||
label: i18n('yes'),
|
|
||||||
value: true,
|
|
||||||
type: 'primary',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: i18n('cancel'),
|
|
||||||
value: false,
|
|
||||||
},
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
password: value,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if ( resp.status !== 200 ) {
|
||||||
|
/* eslint no-empty: ["error", { "allowEmptyCatch": true }] */
|
||||||
|
let message; try {
|
||||||
|
message = (await resp.json()).message;
|
||||||
|
} catch (e) {}
|
||||||
|
message = message || i18n('error_unknown_cause');
|
||||||
|
password_entry.set('error', message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
password_confirm_promise.resolve(true);
|
||||||
|
$(win).close();
|
||||||
|
}
|
||||||
|
const password_confirm = new Flexer({
|
||||||
|
children: [
|
||||||
|
new JustHTML({
|
||||||
|
html: /*html*/`
|
||||||
|
<h3 style="text-align:center; font-weight: 500; font-size: 20px;">${
|
||||||
|
i18n('disable_2fa_confirm')
|
||||||
|
}</h3>
|
||||||
|
<p style="text-align:center; padding: 0 20px;">${
|
||||||
|
i18n('disable_2fa_instructions')
|
||||||
|
}</p>
|
||||||
|
`
|
||||||
|
}),
|
||||||
|
new Flexer({
|
||||||
|
gap: '5pt',
|
||||||
|
children: [
|
||||||
|
new PasswordEntry({
|
||||||
|
_ref: me => password_entry = me,
|
||||||
|
on_submit: async () => {
|
||||||
|
try_password();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new Button({
|
||||||
|
label: i18n('disable_2fa'),
|
||||||
|
on_click: async () => {
|
||||||
|
try_password();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new Button({
|
||||||
|
label: i18n('cancel'),
|
||||||
|
style: 'secondary',
|
||||||
|
on_click: async () => {
|
||||||
|
password_confirm_promise.resolve(false);
|
||||||
|
$(win).close();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
})
|
|
||||||
if ( ! alert_resp ) return;
|
|
||||||
const resp = await fetch(`${window.api_origin}/auth/configure-2fa/disable`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${puter.authToken}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({}),
|
|
||||||
});
|
});
|
||||||
|
win = await UIComponentWindow({
|
||||||
|
component: password_confirm,
|
||||||
|
width: 500,
|
||||||
|
backdrop: true,
|
||||||
|
is_resizable: false,
|
||||||
|
body_css: {
|
||||||
|
width: 'initial',
|
||||||
|
height: '100%',
|
||||||
|
'background-color': 'rgb(245 247 249)',
|
||||||
|
'backdrop-filter': 'blur(3px)',
|
||||||
|
padding: '20px',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
password_entry.focus();
|
||||||
|
|
||||||
|
const ok = await password_confirm_promise;
|
||||||
|
if ( ! ok ) return;
|
||||||
|
|
||||||
$el_window.find('.enable-2fa').show();
|
$el_window.find('.enable-2fa').show();
|
||||||
$el_window.find('.disable-2fa').hide();
|
$el_window.find('.disable-2fa').hide();
|
||||||
@ -101,4 +160,4 @@ export default {
|
|||||||
$el_window.find('.settings-card-security').addClass('settings-card-warning');
|
$el_window.find('.settings-card-security').addClass('settings-card-warning');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,6 +97,7 @@ const en = {
|
|||||||
dir_published_as_website: `%strong% has been published to:`,
|
dir_published_as_website: `%strong% has been published to:`,
|
||||||
disable_2fa: 'Disable 2FA',
|
disable_2fa: 'Disable 2FA',
|
||||||
disable_2fa_confirm: "Are you sure you want to disable 2FA?",
|
disable_2fa_confirm: "Are you sure you want to disable 2FA?",
|
||||||
|
disable_2fa_instructions: "Enter your password to disable 2FA.",
|
||||||
disassociate_dir: "Disassociate Directory",
|
disassociate_dir: "Disassociate Directory",
|
||||||
download: 'Download',
|
download: 'Download',
|
||||||
download_file: 'Download File',
|
download_file: 'Download File',
|
||||||
|
Loading…
Reference in New Issue
Block a user