mirror of
https://github.com/HeyPuter/puter.git
synced 2025-01-23 14:20:22 +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 {};
|
||||
};
|
||||
|
||||
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] ) {
|
||||
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,
|
||||
window: HOUR,
|
||||
},
|
||||
['/user-protected/disable-2fa']: {
|
||||
limit: 10,
|
||||
window: HOUR,
|
||||
},
|
||||
['login-otp']: {
|
||||
limit: 15,
|
||||
window: 30 * MINUTE,
|
||||
|
@ -88,6 +88,10 @@ class UserProtectedEndpointsService extends BaseService {
|
||||
Endpoint(
|
||||
require('../../routers/user-protected/change-email.js'),
|
||||
).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 UIComponentWindow from "../UIComponentWindow.js";
|
||||
import UIWindow2FASetup from "../UIWindow2FASetup.js";
|
||||
import UIWindowQR from "../UIWindowQR.js";
|
||||
|
||||
@ -64,35 +70,88 @@ export default {
|
||||
});
|
||||
|
||||
$el_window.find('.disable-2fa').on('click', async function (e) {
|
||||
const confirmation = i18n('disable_2fa_confirm');
|
||||
const alert_resp = await UIAlert({
|
||||
message: confirmation,
|
||||
window_options: {
|
||||
parent_uuid: $el_window.attr('data-element_uuid'),
|
||||
disable_parent_window: true,
|
||||
parent_center: true,
|
||||
},
|
||||
buttons:[
|
||||
{
|
||||
label: i18n('yes'),
|
||||
value: true,
|
||||
type: 'primary',
|
||||
},
|
||||
{
|
||||
label: i18n('cancel'),
|
||||
value: false,
|
||||
let win, password_entry;
|
||||
const password_confirm_promise = new TeePromise();
|
||||
const try_password = async () => {
|
||||
const value = password_entry.get('value');
|
||||
const resp = await fetch(`${window.api_origin}/user-protected/disable-2fa`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${puter.authToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
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('.disable-2fa').hide();
|
||||
@ -101,4 +160,4 @@ export default {
|
||||
$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:`,
|
||||
disable_2fa: '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",
|
||||
download: 'Download',
|
||||
download_file: 'Download File',
|
||||
|
Loading…
Reference in New Issue
Block a user