fix: improve clipboard functionality and enhance security UI

- Fix clipboard copy buttons not working over HTTP by using fallback method
- Add proper clipboard utility with document.execCommand fallback
- Update all copy buttons to use the fallback-enabled utility
- Enhance Authentication section UI with better visual hierarchy
- Add colored headers with gradients for security sections
- Implement card-based buttons with icons and descriptions
- Show actual Pulse URL in API token examples instead of placeholder
- Improve overall security settings layout and accessibility
This commit is contained in:
Pulse Monitor
2025-08-14 08:45:39 +00:00
parent 5537245a52
commit a80129efea
4 changed files with 108 additions and 77 deletions
@@ -1,5 +1,6 @@
import { createSignal, Show, onMount } from 'solid-js';
import { SystemAPI, APITokenStatus } from '@/api/system';
import { copyToClipboard } from '@/utils/clipboard';
export function APITokenManager() {
const [tokenStatus, setTokenStatus] = createSignal<APITokenStatus | null>(null);
@@ -64,38 +65,15 @@ export function APITokenManager() {
}
};
const copyToClipboard = async () => {
if (currentToken()) {
try {
// Check if clipboard API is available (requires HTTPS in most browsers)
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(currentToken()!);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} else {
// Fallback for HTTP or unsupported browsers
const textArea = document.createElement('textarea');
textArea.value = currentToken()!;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Failed to copy:', err);
setError('Failed to copy - please select and copy manually');
} finally {
document.body.removeChild(textArea);
}
}
} catch (err) {
console.error('Failed to copy:', err);
setError('Failed to copy - please select and copy manually');
}
const handleCopy = async () => {
if (!currentToken()) return;
const success = await copyToClipboard(currentToken()!);
if (success) {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} else {
setError('Failed to copy - please select and copy manually');
}
};
@@ -153,15 +131,18 @@ export function APITokenManager() {
onClick={(e) => e.currentTarget.select()}
/>
<button
onClick={copyToClipboard}
onClick={handleCopy}
class="absolute right-2 top-1/2 -translate-y-1/2 px-3 py-1 text-xs bg-gray-600 text-white rounded hover:bg-gray-700 transition-colors"
>
{copied() ? 'Copied!' : 'Copy'}
</button>
</div>
<p class="text-xs text-gray-600 dark:text-gray-400 mt-2">
Use this token for API authentication. Keep it secure!
</p>
<div class="text-xs text-gray-600 dark:text-gray-400 mt-2 space-y-2">
<p>Use this token for API authentication:</p>
<code class="block bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded">
curl -H "X-API-Token: {currentToken()}" {window.location.origin}/api/health
</code>
</div>
</div>
</Show>
@@ -1,5 +1,6 @@
import { Component, createSignal, Show } from 'solid-js';
import { showError } from '@/utils/toast';
import { copyToClipboard } from '@/utils/clipboard';
export const CurrentAPIToken: Component = () => {
const [lastGeneratedToken, setLastGeneratedToken] = createSignal<string | null>(null);
@@ -15,14 +16,14 @@ export const CurrentAPIToken: Component = () => {
setLastGeneratedToken(storedToken);
}
const copyToClipboard = async () => {
const handleCopy = async () => {
if (!lastGeneratedToken()) return;
try {
await navigator.clipboard.writeText(lastGeneratedToken()!);
const success = await copyToClipboard(lastGeneratedToken()!);
if (success) {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
} else {
showError('Failed to copy to clipboard');
}
};
@@ -70,7 +71,7 @@ export const CurrentAPIToken: Component = () => {
{lastGeneratedToken()}
</code>
<button
onClick={copyToClipboard}
onClick={handleCopy}
class="px-3 py-2 text-sm bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
>
{copied() ? '✓ Copied' : 'Copy'}
@@ -80,7 +81,7 @@ export const CurrentAPIToken: Component = () => {
<div class="text-xs text-gray-500 dark:text-gray-400 space-y-1">
<p>Use this token with the X-API-Token header:</p>
<code class="block bg-gray-100 dark:bg-gray-800 px-2 py-1 rounded text-xs">
curl -H "X-API-Token: {lastGeneratedToken()}" http://your-pulse-url/api/health
curl -H "X-API-Token: {lastGeneratedToken()}" {window.location.origin}/api/health
</code>
</div>
</div>
@@ -1,5 +1,6 @@
import { Component, createSignal, Show } from 'solid-js';
import { showSuccess, showError } from '@/utils/toast';
import { copyToClipboard } from '@/utils/clipboard';
interface SecurityCredentials {
username: string;
@@ -37,12 +38,12 @@ export const QuickSecuritySetup: Component = () => {
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
};
const copyToClipboard = async (text: string, type: 'username' | 'password' | 'token') => {
try {
await navigator.clipboard.writeText(text);
const handleCopy = async (text: string, type: 'username' | 'password' | 'token') => {
const success = await copyToClipboard(text);
if (success) {
setCopied(type);
setTimeout(() => setCopied(null), 2000);
} catch (err) {
} else {
showError('Failed to copy to clipboard');
}
};
@@ -348,7 +349,7 @@ Important:
{credentials()!.username}
</code>
<button
onClick={() => copyToClipboard(credentials()!.username, 'username')}
onClick={() => handleCopy(credentials()!.username, 'username')}
class="px-3 py-2 text-xs bg-gray-600 text-white rounded hover:bg-gray-700 transition-colors"
>
{copied() === 'username' ? 'Copied!' : 'Copy'}
@@ -363,7 +364,7 @@ Important:
{credentials()!.password}
</code>
<button
onClick={() => copyToClipboard(credentials()!.password, 'password')}
onClick={() => handleCopy(credentials()!.password, 'password')}
class="px-3 py-2 text-xs bg-gray-600 text-white rounded hover:bg-gray-700 transition-colors"
>
{copied() === 'password' ? 'Copied!' : 'Copy'}
@@ -378,7 +379,7 @@ Important:
{credentials()!.apiToken}
</code>
<button
onClick={() => copyToClipboard(credentials()!.apiToken!, 'token')}
onClick={() => handleCopy(credentials()!.apiToken!, 'token')}
class="px-3 py-2 text-xs bg-gray-600 text-white rounded hover:bg-gray-700 transition-colors"
>
{copied() === 'token' ? 'Copied!' : 'Copy'}
@@ -1481,29 +1481,63 @@ const Settings: Component = () => {
<div class="space-y-6">
{/* Authentication Status */}
<Show when={securityStatus()?.hasAuthentication}>
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Authentication</h3>
<span class="px-2 py-1 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 text-xs rounded-md">
Enabled
</span>
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
{/* Header */}
<div class="bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="p-2 bg-green-100 dark:bg-green-900/50 rounded-lg">
<svg class="w-5 h-5 text-green-600 dark:text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-100">Authentication</h3>
<p class="text-xs text-gray-600 dark:text-gray-400">Password protection enabled</p>
</div>
</div>
<span class="px-3 py-1.5 bg-green-500 text-white text-xs font-medium rounded-full flex items-center gap-1">
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
Active
</span>
</div>
</div>
<p class="text-sm text-gray-600 dark:text-gray-400">
Login required to access Pulse
</p>
<div class="mt-4 flex gap-2">
<button
onClick={() => setShowPasswordModal(true)}
class="px-4 py-2 text-sm text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-md transition-colors"
>
Change Password
</button>
<button
onClick={() => setShowRemovePasswordModal(true)}
class="px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-md transition-colors"
>
Remove Password
</button>
{/* Content */}
<div class="p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<button
onClick={() => setShowPasswordModal(true)}
class="flex items-center gap-3 p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-all group"
>
<div class="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg group-hover:bg-blue-200 dark:group-hover:bg-blue-900/50 transition-colors">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
</div>
<div class="text-left">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">Change Password</div>
<div class="text-xs text-gray-500 dark:text-gray-400">Update your login credentials</div>
</div>
</button>
<button
onClick={() => setShowRemovePasswordModal(true)}
class="flex items-center gap-3 p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-900/50 transition-all group"
>
<div class="p-2 bg-red-100 dark:bg-red-900/30 rounded-lg group-hover:bg-red-200 dark:group-hover:bg-red-900/50 transition-colors">
<svg class="w-5 h-5 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<div class="text-left">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">Remove Password</div>
<div class="text-xs text-gray-500 dark:text-gray-400">Disable authentication</div>
</div>
</button>
</div>
</div>
</div>
</Show>
@@ -1515,12 +1549,26 @@ const Settings: Component = () => {
{/* API Token - Show current token when auth is enabled */}
<Show when={securityStatus()?.hasAuthentication && securityStatus()?.apiTokenConfigured}>
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-6">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">API Token</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
Your API token for automation and integrations
</p>
<CurrentAPIToken />
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
{/* Header */}
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center gap-3">
<div class="p-2 bg-blue-100 dark:bg-blue-900/50 rounded-lg">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
</div>
<div>
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-100">API Token</h3>
<p class="text-xs text-gray-600 dark:text-gray-400">For automation and integrations</p>
</div>
</div>
</div>
{/* Content */}
<div class="p-6">
<CurrentAPIToken />
</div>
</div>
</Show>