Add UI to view and copy auth token

Introduce a new UIWindowCopyToken component to display the user's auth token with a copy-to-clipboard button and confirmation message. Integrate the token UI into the account dashboard (TabAccount) by adding an auth token card and click handler to open the window. Add corresponding i18n keys (auth_token, token_copied, copy_token_message, copy_token_description) and import the new window in initgui. Also wire the global action 'copyauth' to show the token dialog and adjust session/login flow so the session list/login windows respect the action (avoid auto-reload when showing the token dialog). Minor UI string cleanup to use i18n('approve') directly.
This commit is contained in:
Nariman Jelveh
2026-02-01 13:02:25 -08:00
parent bfea66a1d6
commit 4b8c46efc8
4 changed files with 233 additions and 59 deletions
+80 -54
View File
@@ -21,34 +21,35 @@ import UIWindowChangePassword from '../UIWindowChangePassword.js';
import UIWindowChangeEmail from '../Settings/UIWindowChangeEmail.js';
import UIWindowChangeUsername from '../UIWindowChangeUsername.js';
import UIWindowConfirmUserDeletion from '../Settings/UIWindowConfirmUserDeletion.js';
import UIWindowCopyToken from '../UIWindowCopyToken.js';
import UIWindow from '../UIWindow.js';
const TabAccount = {
id: 'account',
label: i18n('account'),
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`,
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>',
html () {
let h = '';
h += '<div class="dashboard-tab-content">';
// Profile section header
h += '<div class="dashboard-section-header">';
h += '<h2>' + i18n('account') + '</h2>';
h += '<p>Manage your account settings and profile</p>';
h += `<h2>${ i18n('account') }</h2>`;
h += '<p>Manage your account settings and profile</p>';
h += '</div>';
// Profile picture card
h += '<div class="dashboard-card dashboard-profile-card">';
h += '<div class="dashboard-profile-picture-section">';
h += `<div class="profile-picture change-profile-picture dashboard-profile-avatar profile-pic" style="background-image: url('${html_encode(window.user?.profile?.picture ?? window.icons['profile.svg'])}');">`;
h += '</div>';
h += '<div class="dashboard-profile-info">';
h += `<h3>${html_encode(window.user?.username || 'User')}</h3>`;
h += `<p>${html_encode(window.user?.email || '')}</p>`;
h += '<span class="dashboard-profile-hint">Click the avatar to change your profile picture</span>';
h += '</div>';
h += '</div>';
h += '<div class="dashboard-profile-picture-section">';
h += `<div class="profile-picture change-profile-picture dashboard-profile-avatar profile-pic" style="background-image: url('${html_encode(window.user?.profile?.picture ?? window.icons['profile.svg'])}');">`;
h += '</div>';
h += '<div class="dashboard-profile-info">';
h += `<h3>${html_encode(window.user?.username || 'User')}</h3>`;
h += `<p>${html_encode(window.user?.email || '')}</p>`;
h += '<span class="dashboard-profile-hint">Click the avatar to change your profile picture</span>';
h += '</div>';
h += '</div>';
h += '</div>';
// Account settings cards
@@ -56,61 +57,75 @@ const TabAccount = {
// Username 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"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>';
h += '</div>';
h += '<div class="dashboard-settings-card-info">';
h += `<strong>${i18n('username')}</strong>`;
h += `<span class="username">${html_encode(window.user.username)}</span>`;
h += '</div>';
h += '</div>';
h += `<button class="button change-username">${i18n('change_username')}</button>`;
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="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>';
h += '</div>';
h += '<div class="dashboard-settings-card-info">';
h += `<strong>${i18n('username')}</strong>`;
h += `<span class="username">${html_encode(window.user.username)}</span>`;
h += '</div>';
h += '</div>';
h += `<button class="button change-username">${i18n('change_username')}</button>`;
h += '</div>';
// Password card (only for non-temp users)
if ( !window.user.is_temp ) {
if ( ! window.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 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>';
}
// Email card (only if email exists)
if ( window.user.email ) {
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"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>';
h += '</div>';
h += '<div class="dashboard-settings-card-info">';
h += `<strong>${i18n('email')}</strong>`;
h += `<span class="user-email">${html_encode(window.user.email)}</span>`;
h += '</div>';
h += '</div>';
h += `<button class="button change-email">${i18n('change_email')}</button>`;
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="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>';
h += '</div>';
h += '<div class="dashboard-settings-card-info">';
h += `<strong>${i18n('email')}</strong>`;
h += `<span class="user-email">${html_encode(window.user.email)}</span>`;
h += '</div>';
h += '</div>';
h += `<button class="button change-email">${i18n('change_email')}</button>`;
h += '</div>';
}
// Auth token 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"><path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg>';
h += '</div>';
h += '<div class="dashboard-settings-card-info">';
h += `<strong>${i18n('auth_token')}</strong>`;
h += `<span>${i18n('copy_token_description')}</span>`;
h += '</div>';
h += '</div>';
h += `<button class="button copy-auth-token">${i18n('copy') || 'Copy'}</button>`;
h += '</div>';
// Danger zone
h += '<div class="dashboard-danger-zone">';
h += '<div class="dashboard-card dashboard-danger-card">';
h += '<div class="dashboard-danger-card-content">';
h += '<div class="dashboard-danger-card-info">';
h += `<strong>${i18n('delete_account')}</strong>`;
h += '<span>Permanently delete your account and all associated data. This action cannot be undone.</span>';
h += '</div>';
h += '</div>';
h += `<button class="button button-danger delete-account">${i18n('delete_account')}</button>`;
h += '</div>';
h += '<div class="dashboard-card dashboard-danger-card">';
h += '<div class="dashboard-danger-card-content">';
h += '<div class="dashboard-danger-card-info">';
h += `<strong>${i18n('delete_account')}</strong>`;
h += '<span>Permanently delete your account and all associated data. This action cannot be undone.</span>';
h += '</div>';
h += '</div>';
h += `<button class="button button-danger delete-account">${i18n('delete_account')}</button>`;
h += '</div>';
h += '</div>';
h += '</div>'; // end settings-grid
@@ -156,6 +171,18 @@ const TabAccount = {
},
});
});
$el_window.find('.dashboard-section-account .copy-auth-token').on('click', function (e) {
UIWindowCopyToken({
show_header: true,
window_options: {
parent_uuid: $el_window.attr('data-element_uuid'),
backdrop: true,
close_on_backdrop_click: false,
parent_center: true,
stay_on_top: true,
},
});
});
$el_window.find('.dashboard-section-account .delete-account').on('click', function (e) {
UIWindowConfirmUserDeletion({
window_options: {
@@ -218,4 +245,3 @@ const TabAccount = {
};
export default TabAccount;
+119
View File
@@ -0,0 +1,119 @@
/**
* 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 UIWindow from './UIWindow.js';
function UIWindowCopyToken (options = {}) {
return new Promise(async (resolve) => {
let h = '';
if ( options.show_header ) {
h += `<div style="
display: flex;
flex-direction: column;
align-items: center;
padding: 30px 20px 20px;
background: linear-gradient(135deg, #3b82f6 0%, #6366f1 100%);
border-bottom: 1px solid #ced7e1;
">`;
h += `<div style="
width: 60px;
height: 60px;
background: rgba(255,255,255,0.2);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 14px;
">`;
h += `<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>
</svg>`;
h += '</div>';
h += `<h2 style="margin: 0; font-size: 17px; font-weight: 600; color: white;">${i18n('auth_token')}</h2>`;
h += `<p style="margin: 6px 0 0; font-size: 13px; color: rgba(255,255,255,0.8); text-align: center; line-height: 1.4;">${i18n('copy_token_message')}</p>`;
h += '</div>';
}
h += '<div class="copy-token" style="padding: 20px; border-bottom: 1px solid #ced7e1;">';
if ( ! options.show_header ) {
h += `<div class="form-label" style="margin-bottom: 5px; font-size: 13px; color: #666;">${i18n('copy_token_message')}</div>`;
}
h += `<div style="display: flex; gap: 8px; margin-top: ${options.show_header ? '0' : '15'}px; margin-bottom: 15px;">`;
h += `<input type="text" class="token-input" readonly value="${html_encode(window.auth_token)}" style="flex: 1; font-family: monospace; font-size: 13px;" />`;
h += `<button class="button button-primary copy-token-btn">${i18n('copy')}</button>`;
h += '</div>';
h += '<div class="token-copied-msg form-success-msg" style="display: none; text-align: center;">';
h += i18n('token_copied');
h += '</div>';
h += '</div>';
const el_window = await UIWindow({
title: i18n('auth_token'),
app: 'copy-token',
single_instance: true,
icon: null,
uid: null,
is_dir: false,
body_content: h,
has_head: !options.show_header,
selectable_body: false,
draggable_body: options.show_header,
allow_context_menu: false,
is_resizable: false,
is_droppable: false,
init_center: true,
allow_native_ctxmenu: false,
allow_user_select: false,
width: 450,
height: 'auto',
dominant: true,
show_in_taskbar: false,
window_class: 'window-publishWebsite',
body_css: {
width: 'initial',
height: '100%',
padding: '0',
'background-color': 'rgb(245 247 249)',
'backdrop-filter': 'blur(3px)',
},
...options.window_options,
});
$(el_window).find('.copy-token-btn').on('click', function () {
const $btn = $(this);
navigator.clipboard.writeText(window.auth_token).then(() => {
$(el_window).find('.token-copied-msg').fadeIn();
$btn.text(i18n('token_copied'));
setTimeout(() => {
$(el_window).find('.token-copied-msg').fadeOut();
$btn.text(i18n('copy'));
}, 2000);
});
});
$(el_window).on('close', () => {
resolve();
});
});
}
def(UIWindowCopyToken, 'ui.window.UIWindowCopyToken');
export default UIWindowCopyToken;
+8
View File
@@ -550,6 +550,14 @@ const en = {
'error_user_or_path_not_found': 'User or path not found.',
'error_invalid_username': 'Invalid username.',
// Auth token
auth_token: 'Auth Token',
token_copied: 'Token copied',
copy_auth_token: 'Copy Auth Token',
approve: 'Approve',
copy_token_message: 'Your authentication token is shown below. Keep it secret \u2014 anyone with this token can access your account.',
copy_token_description: 'View and copy your authentication token',
},
};
+26 -5
View File
@@ -30,6 +30,7 @@ import UIWindowRequestPermission from './UI/UIWindowRequestPermission.js';
import UIWindowSaveAccount from './UI/UIWindowSaveAccount.js';
import UIWindowSessionList from './UI/UIWindowSessionList.js';
import UIWindowSignup from './UI/UIWindowSignup.js';
import UIWindowCopyToken from './UI/UIWindowCopyToken.js';
import { PROCESS_RUNNING } from './definitions.js';
import item_icon from './helpers/item_icon.js';
import update_last_touch_coordinates from './helpers/update_last_touch_coordinates.js';
@@ -623,7 +624,7 @@ window.initgui = async function (options) {
const approved = await UIAlert({
message: `Do you want to authorize and redirect to <strong>${html_encode(redirectURL)}</strong>?`,
buttons: [
{ label: i18n('approve') || 'Approve', value: 'approve', type: 'primary' },
{ label: i18n('approve'), value: 'approve', type: 'primary' },
{ label: i18n('cancel'), value: 'cancel', type: 'secondary' },
],
type: 'confirm',
@@ -637,6 +638,13 @@ window.initgui = async function (options) {
}
}
// -------------------------------------------------------------------------------------
// Action: CopyAuth — show dialog to copy auth token
// -------------------------------------------------------------------------------------
if ( action === 'copyauth' ) {
await UIWindowCopyToken({ show_header: true });
}
// -------------------------------------------------------------------------------------
// Load desktop, only if we're not embedded in a popup and not on the dashboard page
// -------------------------------------------------------------------------------------
@@ -968,15 +976,18 @@ window.initgui = async function (options) {
// Un-authed but not first visit -> try to log in/sign up
// -------------------------------------------------------------------------------------
if ( !window.is_auth() && (!window.first_visit_ever || window.disable_temp_users) ) {
const needs_action = action === 'authme' || action === 'copyauth';
const reload_on_success = !needs_action;
if ( window.logged_in_users.length > 0 ) {
UIWindowSessionList();
await UIWindowSessionList({
reload_on_success,
});
}
else {
const resp = await fetch(`${window.gui_origin }/whoarewe`);
const whoarewe = await resp.json();
await UIWindowLogin({
// show_signup_button:
reload_on_success: true,
reload_on_success,
send_confirmation_code: false,
show_signup_button: ( !whoarewe.disable_user_signup ),
window_options: {
@@ -984,6 +995,9 @@ window.initgui = async function (options) {
},
});
}
if ( !reload_on_success && window.is_auth() ) {
document.dispatchEvent(new Event('login', { bubbles: true }));
}
}
// -------------------------------------------------------------------------------------
@@ -1153,7 +1167,7 @@ window.initgui = async function (options) {
const approved = await UIAlert({
message: `Do you want to authorize and redirect to <strong>${html_encode(redirectURL)}</strong>?`,
buttons: [
{ label: i18n('approve') || 'Approve', value: 'approve', type: 'primary' },
{ label: i18n('approve'), value: 'approve', type: 'primary' },
{ label: i18n('cancel'), value: 'cancel', type: 'secondary' },
],
type: 'confirm',
@@ -1167,6 +1181,13 @@ window.initgui = async function (options) {
}
}
// -------------------------------------------------------------------------------------
// Action: CopyAuth — show dialog to copy auth token
// -------------------------------------------------------------------------------------
if ( action === 'copyauth' ) {
await UIWindowCopyToken({ show_header: true });
}
// -------------------------------------------------------------------------------------
// Load desktop, if not embedded in a popup and not on the dashboard page
// -------------------------------------------------------------------------------------