From d522dfca5ed3c8e76da7048b1fdb7374bd81fdfa Mon Sep 17 00:00:00 2001 From: Pulse Monitor Date: Sun, 10 Aug 2025 21:44:10 +0000 Subject: [PATCH] fix: API token authentication for Docker users - Add UI modal to prompt for API token when required - Detect and handle invalid/expired tokens automatically - Clear invalid tokens and re-prompt user - Add ability to clear stored tokens from browser - Fix race condition in modal retry logic - Improve error messages for auth failures - Handle both export and import authentication consistently - Track operation source to ensure correct retry behavior Fixes issue reported by da99Beast where Docker users with API_TOKEN environment variable couldn't authenticate for export/import operations --- frontend-modern/VERSION | 1 - frontend-modern/index.html | 2 +- .../src/components/Settings/Settings.tsx | 127 +++++++++++++++++- 3 files changed, 125 insertions(+), 5 deletions(-) delete mode 100644 frontend-modern/VERSION diff --git a/frontend-modern/VERSION b/frontend-modern/VERSION deleted file mode 100644 index 69b740c59..000000000 --- a/frontend-modern/VERSION +++ /dev/null @@ -1 +0,0 @@ -4.1.0-rc.12 diff --git a/frontend-modern/index.html b/frontend-modern/index.html index ee0609cbb..78bd7f095 100644 --- a/frontend-modern/index.html +++ b/frontend-modern/index.html @@ -5,7 +5,7 @@ - Pulse Monitor - Modern + Pulse diff --git a/frontend-modern/src/components/Settings/Settings.tsx b/frontend-modern/src/components/Settings/Settings.tsx index f822f4f06..fbe373d42 100644 --- a/frontend-modern/src/components/Settings/Settings.tsx +++ b/frontend-modern/src/components/Settings/Settings.tsx @@ -121,6 +121,9 @@ const Settings: Component = () => { const [importFile, setImportFile] = createSignal(null); const [showExportDialog, setShowExportDialog] = createSignal(false); const [showImportDialog, setShowImportDialog] = createSignal(false); + const [showApiTokenModal, setShowApiTokenModal] = createSignal(false); + const [apiTokenInput, setApiTokenInput] = createSignal(''); + const [apiTokenModalSource, setApiTokenModalSource] = createSignal<'export' | 'import' | null>(null); const tabs: { id: SettingsTab; label: string; icon: string }[] = [ { @@ -513,6 +516,13 @@ const Settings: Component = () => { return; } + // Check if API token is required but not set + if (securityStatus()?.apiTokenConfigured && !localStorage.getItem('apiToken')) { + setApiTokenModalSource('export'); + setShowApiTokenModal(true); + return; + } + try { const headers: HeadersInit = { 'Content-Type': 'application/json', @@ -532,8 +542,17 @@ const Settings: Component = () => { if (!response.ok) { const errorText = await response.text(); - // Parse specific error messages for better user feedback - if (errorText.includes('API_TOKEN') || errorText.includes('ALLOW_UNPROTECTED_EXPORT')) { + // Handle authentication errors by clearing invalid token and re-prompting + if (response.status === 401 || response.status === 403 || errorText.includes('API_TOKEN') || errorText.includes('Unauthorized')) { + // Clear invalid token if we had one + const hadToken = localStorage.getItem('apiToken'); + if (hadToken) { + localStorage.removeItem('apiToken'); + showError('Invalid or expired API token. Please re-enter.'); + setApiTokenModalSource('export'); + setShowApiTokenModal(true); + return; + } throw new Error('Export requires authentication. Set API_TOKEN or ALLOW_UNPROTECTED_EXPORT=true in environment variables.'); } throw new Error(errorText || 'Export failed'); @@ -578,6 +597,13 @@ const Settings: Component = () => { return; } + // Check if API token is required but not set + if (securityStatus()?.apiTokenConfigured && !localStorage.getItem('apiToken')) { + setApiTokenModalSource('import'); + setShowApiTokenModal(true); + return; + } + try { const fileContent = await importFile()!.text(); let exportData; @@ -609,7 +635,21 @@ const Settings: Component = () => { }); if (!response.ok) { - throw new Error(await response.text()); + const errorText = await response.text(); + // Handle authentication errors by clearing invalid token and re-prompting + if (response.status === 401 || response.status === 403 || errorText.includes('API_TOKEN') || errorText.includes('Unauthorized')) { + // Clear invalid token if we had one + const hadToken = localStorage.getItem('apiToken'); + if (hadToken) { + localStorage.removeItem('apiToken'); + showError('Invalid or expired API token. Please re-enter.'); + setApiTokenModalSource('import'); + setShowApiTokenModal(true); + return; + } + throw new Error('Import requires authentication. Set API_TOKEN or ALLOW_UNPROTECTED_EXPORT=true in environment variables.'); + } + throw new Error(errorText || 'Import failed'); } showSuccess('Configuration imported successfully. Reloading...'); @@ -1384,6 +1424,17 @@ const Settings: Component = () => {

Your Pulse instance is protected with API token authentication.

+ + + @@ -1826,6 +1877,76 @@ docker run -d \ + {/* API Token Modal */} + +
+
+

+ API Token Required +

+ +
+

+ This Pulse instance requires an API token for export/import operations. Please enter the API token configured on the server. +

+ +
+ + setApiTokenInput(e.currentTarget.value)} + placeholder="Enter API token" + class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-200" + /> +
+ +
+

The API token is set as an environment variable:

+ API_TOKEN=your-secure-token +
+
+ +
+ + +
+
+
+
+ {/* Import Dialog */}