mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-22 11:13:26 +00:00
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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user