Add Security tab to dashboard with 2FA and session management

This commit is contained in:
jelveh
2025-12-10 12:10:20 -08:00
parent 357cb8bcca
commit ddf9bd1358
3 changed files with 246 additions and 0 deletions
+226
View File
@@ -0,0 +1,226 @@
/**
* Copyright (C) 2024-present Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import TeePromise from '../../util/TeePromise.js';
import UIComponentWindow from '../UIComponentWindow.js';
import UIWindow2FASetup from '../UIWindow2FASetup.js';
import UIWindowChangePassword from '../UIWindowChangePassword.js';
import UIWindowManageSessions from '../UIWindowManageSessions.js';
const TabSecurity = {
id: 'security',
label: i18n('security'),
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>`,
html () {
let h = '';
let user = window.user;
h += '<div class="dashboard-tab-content">';
// Section header
h += '<div class="dashboard-section-header">';
h += `<h2>${i18n('security')}</h2>`;
h += '<p>Manage your security settings and sessions</p>';
h += '</div>';
// Security settings cards
h += '<div class="dashboard-settings-grid">';
// Password card (only for non-temp users)
if ( !user.is_temp ) {
h += '<div class="dashboard-card dashboard-settings-card">';
h += '<div class="dashboard-settings-card-content">';
h += '<div class="dashboard-settings-card-icon">';
h += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>';
h += '</div>';
h += '<div class="dashboard-settings-card-info">';
h += `<strong>${i18n('password')}</strong>`;
h += '<span>••••••••</span>';
h += '</div>';
h += '</div>';
h += `<button class="button change-password">${i18n('change_password')}</button>`;
h += '</div>';
}
// Sessions card
h += '<div class="dashboard-card dashboard-settings-card">';
h += '<div class="dashboard-settings-card-content">';
h += '<div class="dashboard-settings-card-icon">';
h += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>';
h += '</div>';
h += '<div class="dashboard-settings-card-info">';
h += `<strong>${i18n('sessions')}</strong>`;
h += '<span>Manage active sessions</span>';
h += '</div>';
h += '</div>';
h += `<button class="button manage-sessions">${i18n('manage_sessions')}</button>`;
h += '</div>';
// 2FA card (only for non-temp users with confirmed email)
if ( !user.is_temp && user.email_confirmed ) {
const twoFaStatusClass = user.otp ? 'dashboard-settings-card-success' : 'dashboard-settings-card-warning';
h += `<div class="dashboard-card dashboard-settings-card dashboard-settings-card-2fa ${twoFaStatusClass}">`;
h += '<div class="dashboard-settings-card-content">';
h += '<div class="dashboard-settings-card-icon">';
h += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>';
h += '</div>';
h += '<div class="dashboard-settings-card-info">';
h += `<strong>${i18n('two_factor')}</strong>`;
h += `<span class="user-otp-state">${i18n(user.otp ? 'two_factor_enabled' : 'two_factor_disabled')}</span>`;
h += '</div>';
h += '</div>';
h += `<button class="button enable-2fa" style="${user.otp ? 'display:none;' : ''}">${i18n('enable_2fa')}</button>`;
h += `<button class="button disable-2fa" style="${user.otp ? '' : 'display:none;'}">${i18n('disable_2fa')}</button>`;
h += '</div>';
}
h += '</div>'; // end settings-grid
h += '</div>'; // end dashboard-tab-content
return h;
},
init ($el_window) {
$el_window.find('.dashboard-section-security .change-password').on('click', function (e) {
UIWindowChangePassword({
window_options: {
parent_uuid: $el_window.attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
},
});
});
$el_window.find('.dashboard-section-security .manage-sessions').on('click', function (e) {
UIWindowManageSessions({
window_options: {
parent_uuid: $el_window.attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
},
});
});
$el_window.find('.dashboard-section-security .enable-2fa').on('click', async function (e) {
const { promise } = await UIWindow2FASetup();
const tfa_was_enabled = await promise;
if ( tfa_was_enabled ) {
$el_window.find('.dashboard-section-security .enable-2fa').hide();
$el_window.find('.dashboard-section-security .disable-2fa').show();
$el_window.find('.dashboard-section-security .user-otp-state').text(i18n('two_factor_enabled'));
$el_window.find('.dashboard-section-security .dashboard-settings-card-2fa').removeClass('dashboard-settings-card-warning');
$el_window.find('.dashboard-section-security .dashboard-settings-card-2fa').addClass('dashboard-settings-card-success');
}
});
$el_window.find('.dashboard-section-security .disable-2fa').on('click', async function (e) {
let win;
const password_confirm_promise = new TeePromise();
const try_password = async () => {
const value = $win.find('.password-entry').val();
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');
$win.find('.password-entry').addClass('error');
$win.find('.error-message').text(message).show();
return;
}
password_confirm_promise.resolve(true);
$(win).close();
};
let h = '';
h += '<div style="display: flex; flex-direction: column; gap: 20pt; justify-content: center;">';
h += '<div>';
h += `<h3 style="text-align:center; font-weight: 500; font-size: 20px;">${i18n('disable_2fa_confirm')}</h3>`;
h += `<p style="text-align:center; padding: 0 20px;">${i18n('disable_2fa_instructions')}</p>`;
h += '</div>';
h += '<div style="display: flex; flex-direction: column; gap: 10pt;">';
h += '<input type="password" class="password-entry" />';
h += '<span class="error-message" style="color: red; display: none;"></span>';
h += '</div>';
h += '<div style="display: flex; gap: 5pt;">';
h += `<button class="button confirm-disable-2fa">${i18n('disable_2fa')}</button>`;
h += `<button class="button secondary cancel-disable-2fa">${i18n('cancel')}</button>`;
h += '</div>';
h += '</div>';
win = await UIComponentWindow({
html: h,
width: 500,
backdrop: true,
is_resizable: false,
body_css: {
width: 'initial',
'background-color': 'rgb(245 247 249)',
'backdrop-filter': 'blur(3px)',
padding: '20px',
},
});
// Set up event listeners
const $win = $(win);
const $password_entry = $win.find('.password-entry');
$password_entry.on('keypress', (e) => {
if ( e.which === 13 ) { // Enter key
try_password();
}
});
$win.find('.confirm-disable-2fa').on('click', () => {
try_password();
});
$win.find('.cancel-disable-2fa').on('click', () => {
password_confirm_promise.resolve(false);
$win.close();
});
$password_entry.focus();
const ok = await password_confirm_promise;
if ( ! ok ) return;
$el_window.find('.dashboard-section-security .enable-2fa').show();
$el_window.find('.dashboard-section-security .disable-2fa').hide();
$el_window.find('.dashboard-section-security .user-otp-state').text(i18n('two_factor_disabled'));
$el_window.find('.dashboard-section-security .dashboard-settings-card-2fa').removeClass('dashboard-settings-card-success');
$el_window.find('.dashboard-section-security .dashboard-settings-card-2fa').addClass('dashboard-settings-card-warning');
});
},
};
export default TabSecurity;
+2
View File
@@ -31,6 +31,7 @@ import TabFiles from './TabFiles.js';
import TabApps from './TabApps.js';
import TabUsage from './TabUsage.js';
import TabAccount from './TabAccount.js';
import TabSecurity from './TabSecurity.js';
// Registry of all available tabs
const tabs = [
@@ -39,6 +40,7 @@ const tabs = [
// TabFiles,
TabUsage,
TabAccount,
TabSecurity,
];
async function UIDashboard (options) {
+18
View File
@@ -6595,6 +6595,24 @@ html.dark-mode .usage-table-show-less:hover {
flex-shrink: 0;
}
.dashboard-settings-card-success {
border-color: #08bf4e;
background: #e6ffed;
}
.dashboard-settings-card-success .dashboard-settings-card-info span {
color: #03933a;
}
.dashboard-settings-card-warning {
border-color: #f0a500;
background: #fff7e6;
}
.dashboard-settings-card-warning .dashboard-settings-card-info span {
color: #c98900;
}
/* Danger zone */
.dashboard-danger-zone {
margin-top: 40px;