mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-22 19:23:31 +00:00
fix: improve security setup and pending restart detection
- Add pending restart detection when .env exists but not loaded - Update frontend to show pending state instead of re-showing setup - Fix QuickSecuritySetup to refresh security status after configuration - Remove auto-restart attempts from security setup - Show deployment-appropriate restart instructions - Update documentation to reflect new update mechanism Related to security setup issues after removing sudo/auto-restart capabilities
This commit is contained in:
@@ -221,18 +221,25 @@ pulse config import -i backup.enc
|
||||
|
||||
## Updates
|
||||
|
||||
Pulse shows when updates are available and provides deployment-specific instructions:
|
||||
|
||||
### ProxmoxVE LXC Container
|
||||
Type `update` in the LXC console - the script handles everything automatically
|
||||
|
||||
### Docker
|
||||
```bash
|
||||
docker pull rcourtman/pulse:latest
|
||||
docker stop pulse
|
||||
docker rm pulse
|
||||
# Run docker run command again
|
||||
# Run docker run command again with your settings
|
||||
```
|
||||
|
||||
### Manual Install
|
||||
Settings → System → Check for Updates
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | sudo bash
|
||||
```
|
||||
|
||||
After updates complete, refresh your browser (Ctrl+F5 or Cmd+Shift+R) to load the new version.
|
||||
The UI will detect your deployment type and show the appropriate update method when a new version is available.
|
||||
|
||||
## API
|
||||
|
||||
|
||||
+14
-5
@@ -368,21 +368,30 @@ curl -X POST http://localhost:7655/api/auto-register \
|
||||
## Updates
|
||||
|
||||
### Check for Updates
|
||||
Check if a new version is available.
|
||||
Check if a new version is available. Returns version info and deployment-specific update instructions.
|
||||
|
||||
```bash
|
||||
GET /api/updates/check
|
||||
```
|
||||
|
||||
### Apply Update
|
||||
Download and apply an available update.
|
||||
Response includes `deploymentType` field indicating how to update:
|
||||
- `proxmoxve`: Type `update` in LXC console
|
||||
- `docker`: Pull new image and recreate container
|
||||
- `systemd`: Re-run install script
|
||||
- `manual`: Re-run install script
|
||||
|
||||
### Apply Update (Deprecated)
|
||||
**⚠️ DEPRECATED**: This endpoint exists for backwards compatibility but is no longer used.
|
||||
Updates cannot be performed through the API due to security constraints (no sudo access,
|
||||
containers can't restart themselves). Use deployment-specific update methods instead.
|
||||
|
||||
```bash
|
||||
POST /api/updates/apply
|
||||
```
|
||||
|
||||
### Update Status
|
||||
Get current update operation status.
|
||||
### Update Status (Deprecated)
|
||||
**⚠️ DEPRECATED**: Since updates are no longer performed through the API, this endpoint
|
||||
is not used by the UI.
|
||||
|
||||
```bash
|
||||
GET /api/updates/status
|
||||
|
||||
+28
-4
@@ -60,7 +60,7 @@ Most settings are configured through the web interface at `http://<server>:7655/
|
||||
|
||||
- **Nodes**: Auto-discovery, one-click setup scripts, cluster detection
|
||||
- **Alerts**: Thresholds and notification rules
|
||||
- **Updates**: Update channels and auto-update settings
|
||||
- **Updates**: Update channels and deployment-specific update instructions
|
||||
- **Security**: Export/import encrypted configurations
|
||||
|
||||
## Understanding .env vs .enc Files
|
||||
@@ -113,9 +113,6 @@ Variables that ALWAYS override UI settings:
|
||||
- `PULSE_AUTH_USER` - Username for web UI authentication (overrides UI)
|
||||
- `PULSE_AUTH_PASS` - Bcrypt password hash - MUST be 60 chars in single quotes! (overrides UI)
|
||||
- `UPDATE_CHANNEL` - stable or rc (overrides UI)
|
||||
- `AUTO_UPDATE_ENABLED` - true/false (overrides UI)
|
||||
- `AUTO_UPDATE_CHECK_INTERVAL` - Hours between checks (overrides UI)
|
||||
- `AUTO_UPDATE_TIME` - Update time HH:MM (overrides UI)
|
||||
- `CONNECTION_TIMEOUT` - Connection timeout in seconds (overrides UI)
|
||||
- `ALLOWED_ORIGINS` - CORS origins (overrides UI, default: empty = same-origin only)
|
||||
- `LOG_LEVEL` - debug/info/warn/error (overrides UI)
|
||||
@@ -287,6 +284,33 @@ proxmox-backup-manager acl update / Admin --auth-id pulse-monitor@pbs
|
||||
proxmox-backup-manager user generate-token pulse-monitor@pbs pulse-token
|
||||
```
|
||||
|
||||
## Updates
|
||||
|
||||
Pulse automatically detects your deployment type and shows appropriate update instructions when a new version is available:
|
||||
|
||||
### ProxmoxVE LXC Containers
|
||||
- Type `update` in the LXC console
|
||||
- The community script handles everything automatically
|
||||
- No manual intervention required
|
||||
|
||||
### Docker
|
||||
- Pull the latest image: `docker pull rcourtman/pulse:latest`
|
||||
- Recreate the container with your existing settings
|
||||
- Data persists in the volume
|
||||
|
||||
### Manual/Systemd Installations
|
||||
- Re-run the installation script: `curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | sudo bash`
|
||||
- The script detects existing installations and updates them
|
||||
- Configuration is preserved
|
||||
|
||||
### Why No In-App Updates?
|
||||
Pulse cannot update itself from the UI due to security constraints:
|
||||
- **ProxmoxVE**: The pulse user has no sudo access (security best practice)
|
||||
- **Docker**: Containers cannot restart themselves
|
||||
- **Systemd**: Service cannot restart itself without privileges
|
||||
|
||||
This design ensures better security by requiring administrative access for updates.
|
||||
|
||||
## Reverse Proxy Configuration
|
||||
|
||||
Pulse requires WebSocket support for real-time updates. If using a reverse proxy (nginx, Apache, Caddy, etc.), you **MUST** enable WebSocket proxying.
|
||||
|
||||
@@ -105,7 +105,6 @@ volumes:
|
||||
| `CONNECTION_TIMEOUT` | Connection timeout in seconds | `10` |
|
||||
| `LOG_LEVEL` | Logging level | `info` |
|
||||
| `UPDATE_CHANNEL` | Update channel (stable/rc) | `stable` |
|
||||
| `AUTO_UPDATE_ENABLED` | Enable auto-updates | `false` |
|
||||
|
||||
## Volume Management
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface VersionInfo {
|
||||
channel?: string;
|
||||
isDocker: boolean;
|
||||
isDevelopment: boolean;
|
||||
deploymentType?: string;
|
||||
}
|
||||
|
||||
export class UpdatesAPI {
|
||||
|
||||
@@ -8,13 +8,15 @@ interface SecurityCredentials {
|
||||
apiToken?: string;
|
||||
}
|
||||
|
||||
export const QuickSecuritySetup: Component = () => {
|
||||
interface QuickSecuritySetupProps {
|
||||
onConfigured?: () => void;
|
||||
}
|
||||
|
||||
export const QuickSecuritySetup: Component<QuickSecuritySetupProps> = (props) => {
|
||||
const [isSettingUp, setIsSettingUp] = createSignal(false);
|
||||
const [credentials, setCredentials] = createSignal<SecurityCredentials | null>(null);
|
||||
const [showCredentials, setShowCredentials] = createSignal(false);
|
||||
const [copied, setCopied] = createSignal<'username' | 'password' | 'token' | null>(null);
|
||||
const [readyToRestart, setReadyToRestart] = createSignal(false);
|
||||
const [isRestarting, setIsRestarting] = createSignal(false);
|
||||
const [useCustomPassword, setUseCustomPassword] = createSignal(false);
|
||||
const [customUsername, setCustomUsername] = createSignal('admin');
|
||||
const [customPassword, setCustomPassword] = createSignal('');
|
||||
@@ -48,33 +50,6 @@ export const QuickSecuritySetup: Component = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const restartPulse = async () => {
|
||||
setIsRestarting(true);
|
||||
try {
|
||||
const response = await fetch('/api/security/apply-restart', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include' // Include cookies for CSRF token
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to restart Pulse');
|
||||
}
|
||||
|
||||
showSuccess('Restarting Pulse... You will be redirected to login.');
|
||||
|
||||
// Wait for restart then redirect to login
|
||||
setTimeout(() => {
|
||||
// Just reload - the auth check in App.tsx will show the login page
|
||||
window.location.reload();
|
||||
}, 5000);
|
||||
} catch (error) {
|
||||
showError(`Failed to restart: ${error}`);
|
||||
setIsRestarting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const setupSecurity = async () => {
|
||||
// Validate custom password if using
|
||||
@@ -114,7 +89,7 @@ export const QuickSecuritySetup: Component = () => {
|
||||
throw new Error(error || 'Failed to setup security');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
// Response is successful, no need to parse result
|
||||
setCredentials(newCredentials);
|
||||
setShowCredentials(true);
|
||||
|
||||
@@ -123,21 +98,12 @@ export const QuickSecuritySetup: Component = () => {
|
||||
sessionStorage.setItem('pulse_last_api_token', newCredentials.apiToken);
|
||||
}
|
||||
|
||||
// Store the command if manual action needed
|
||||
if (result.command) {
|
||||
(window as any).securityCommand = result.command;
|
||||
}
|
||||
// Show success message
|
||||
showSuccess('Security configured! Settings will apply after restart.');
|
||||
|
||||
// Check if we can auto-restart
|
||||
if (result.readyToRestart) {
|
||||
setReadyToRestart(true);
|
||||
showSuccess('Security configured! Save your credentials, then click "Restart Pulse" to apply.');
|
||||
} else if (result.method === 'systemd' && !result.automatic) {
|
||||
showSuccess('Security configured! Run the command shown below to apply settings.');
|
||||
} else if (result.method === 'docker') {
|
||||
showSuccess('Security configured! Please restart your Docker container with the credentials shown.');
|
||||
} else {
|
||||
showSuccess('Security configured! Please restart Pulse to apply settings.');
|
||||
// Notify parent component to refresh security status
|
||||
if (props.onConfigured) {
|
||||
props.onConfigured();
|
||||
}
|
||||
} catch (error) {
|
||||
showError(`Failed to setup security: ${error}`);
|
||||
@@ -392,61 +358,15 @@ Important:
|
||||
</div>
|
||||
|
||||
<div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-3">
|
||||
<Show
|
||||
when={readyToRestart()}
|
||||
fallback={
|
||||
<Show
|
||||
when={(window as any).securityCommand}
|
||||
fallback={
|
||||
<>
|
||||
<p class="text-sm font-semibold text-green-800 dark:text-green-200 mb-2">
|
||||
✅ Security configured successfully!
|
||||
</p>
|
||||
<p class="text-xs text-green-700 dark:text-green-300">
|
||||
Save your credentials above. Pulse will apply the security settings.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<p class="text-sm font-semibold text-green-800 dark:text-green-200 mb-2">
|
||||
✅ One more step to enable security:
|
||||
</p>
|
||||
<p class="text-xs text-green-700 dark:text-green-300 mb-2">
|
||||
Run this command in your terminal:
|
||||
</p>
|
||||
<div class="bg-gray-900 text-green-400 p-2 rounded font-mono text-xs overflow-x-auto">
|
||||
{(window as any).securityCommand}
|
||||
</div>
|
||||
<p class="text-xs text-green-700 dark:text-green-300 mt-2">
|
||||
This will apply the settings and restart Pulse with security enabled.
|
||||
</p>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<p class="text-sm font-semibold text-green-800 dark:text-green-200 mb-2">
|
||||
✅ Security configured! Ready to apply.
|
||||
</p>
|
||||
<p class="text-xs text-green-700 dark:text-green-300 mb-3">
|
||||
Make sure you've saved your credentials above before restarting.
|
||||
</p>
|
||||
<button
|
||||
onClick={restartPulse}
|
||||
disabled={isRestarting()}
|
||||
class="w-full px-4 py-2 bg-green-600 text-white text-sm font-medium rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{isRestarting() ? (
|
||||
<span class="flex items-center justify-center">
|
||||
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Restarting Pulse...
|
||||
</span>
|
||||
) : (
|
||||
'Restart Pulse to Apply Security'
|
||||
)}
|
||||
</button>
|
||||
</Show>
|
||||
<p class="text-sm font-semibold text-green-800 dark:text-green-200 mb-2">
|
||||
✅ Security configured successfully!
|
||||
</p>
|
||||
<p class="text-xs text-green-700 dark:text-green-300">
|
||||
The service needs to be restarted for security settings to take effect.
|
||||
</p>
|
||||
<p class="text-xs text-green-600 dark:text-green-400 mt-2 italic">
|
||||
Save your credentials above - they won't be shown again.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { SettingsAPI } from '@/api/settings';
|
||||
import { NodesAPI } from '@/api/nodes';
|
||||
import { UpdatesAPI } from '@/api/updates';
|
||||
import type { NodeConfig } from '@/types/nodes';
|
||||
import type { UpdateInfo, UpdateStatus, VersionInfo } from '@/api/updates';
|
||||
import type { UpdateInfo, VersionInfo } from '@/api/updates';
|
||||
import { eventBus } from '@/stores/events';
|
||||
import { notificationStore } from '@/stores/notifications';
|
||||
|
||||
@@ -85,7 +85,7 @@ type NodeConfigWithStatus = NodeConfig & {
|
||||
};
|
||||
|
||||
const Settings: Component = () => {
|
||||
const { state, connected, updateProgress } = useWebSocket();
|
||||
const { state, connected } = useWebSocket();
|
||||
const [activeTab, setActiveTab] = createSignal<SettingsTab>('pve');
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = createSignal(false);
|
||||
const [nodes, setNodes] = createSignal<NodeConfigWithStatus[]>([]);
|
||||
@@ -105,7 +105,6 @@ const Settings: Component = () => {
|
||||
// Update settings
|
||||
const [versionInfo, setVersionInfo] = createSignal<VersionInfo | null>(null);
|
||||
const [updateInfo, setUpdateInfo] = createSignal<UpdateInfo | null>(null);
|
||||
const [updateStatus, setUpdateStatus] = createSignal<UpdateStatus | null>(null);
|
||||
const [checkingForUpdates, setCheckingForUpdates] = createSignal(false);
|
||||
const [updateChannel, setUpdateChannel] = createSignal<'stable' | 'rc'>('stable');
|
||||
const [autoUpdateEnabled, setAutoUpdateEnabled] = createSignal(false);
|
||||
@@ -123,6 +122,7 @@ const Settings: Component = () => {
|
||||
exportProtected: boolean;
|
||||
unprotectedExportAllowed: boolean;
|
||||
hasAuthentication: boolean;
|
||||
configuredButPendingRestart?: boolean;
|
||||
hasAuditLogging: boolean;
|
||||
credentialsEncrypted: boolean;
|
||||
hasHTTPS: boolean;
|
||||
@@ -165,21 +165,6 @@ const Settings: Component = () => {
|
||||
}
|
||||
];
|
||||
|
||||
// Update status from WebSocket events
|
||||
createEffect(() => {
|
||||
const progress = updateProgress();
|
||||
if (progress) {
|
||||
setUpdateStatus(progress);
|
||||
|
||||
// Show appropriate messages based on status
|
||||
if (progress.status === 'completed') {
|
||||
showSuccess('Update completed! Please refresh the page (Ctrl+F5 or Cmd+Shift+R) to load the new version.');
|
||||
} else if (progress.status === 'restarting') {
|
||||
showSuccess('Service is restarting. Please wait a moment then refresh the page.');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Function to load nodes
|
||||
const loadNodes = async () => {
|
||||
try {
|
||||
@@ -461,85 +446,6 @@ const Settings: Component = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const applyUpdate = async () => {
|
||||
const info = updateInfo();
|
||||
if (!info || !info.downloadUrl) {
|
||||
showError('No update available');
|
||||
return;
|
||||
}
|
||||
|
||||
const previousVersion = versionInfo()?.version;
|
||||
let connectionLostCount = 0;
|
||||
let reconnectAttempts = 0;
|
||||
const maxReconnectAttempts = 30; // 30 seconds max
|
||||
|
||||
try {
|
||||
await UpdatesAPI.applyUpdate(info.downloadUrl);
|
||||
showSuccess('Update started. Pulse will restart automatically.');
|
||||
|
||||
// Start polling for update status
|
||||
const pollStatus = setInterval(async () => {
|
||||
try {
|
||||
const status = await UpdatesAPI.getUpdateStatus();
|
||||
setUpdateStatus(status);
|
||||
connectionLostCount = 0; // Reset on successful connection
|
||||
|
||||
if (status.status === 'completed' || status.status === 'error') {
|
||||
clearInterval(pollStatus);
|
||||
if (status.status === 'error') {
|
||||
showError(status.error || 'Update failed');
|
||||
} else if (status.status === 'completed') {
|
||||
showSuccess('Update completed! Please refresh the page (Ctrl+F5 or Cmd+Shift+R) to load the new version.');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Service might be restarting
|
||||
console.log('Status check failed, service may be restarting');
|
||||
connectionLostCount++;
|
||||
|
||||
// If we've lost connection for a few polls, assume service is restarting
|
||||
// Wait longer before trying to reconnect (service needs time to restart)
|
||||
if (connectionLostCount >= 5 && reconnectAttempts < maxReconnectAttempts) {
|
||||
reconnectAttempts++;
|
||||
|
||||
// Try to check if the service is back with new version
|
||||
try {
|
||||
const newVersion = await UpdatesAPI.getVersion();
|
||||
if (newVersion.version !== previousVersion) {
|
||||
clearInterval(pollStatus);
|
||||
setUpdateStatus({
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
message: `Update successful! Now running version ${newVersion.version}. Please refresh the page.`,
|
||||
updatedAt: new Date().toISOString()
|
||||
});
|
||||
setVersionInfo(newVersion);
|
||||
showSuccess(`Successfully updated to ${newVersion.version}! Please refresh the page (Ctrl+F5 or Cmd+Shift+R) to load the new interface.`);
|
||||
}
|
||||
} catch (e) {
|
||||
// Service still down, keep trying
|
||||
}
|
||||
}
|
||||
|
||||
// Give up after too many attempts
|
||||
if (reconnectAttempts >= maxReconnectAttempts) {
|
||||
clearInterval(pollStatus);
|
||||
setUpdateStatus({
|
||||
status: 'error',
|
||||
progress: 0,
|
||||
message: 'Update status unknown. Please refresh the page.',
|
||||
updatedAt: new Date().toISOString()
|
||||
});
|
||||
showError('Could not verify update status. Please refresh the page.');
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
showError('Failed to start update');
|
||||
console.error('Update error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
if (!exportPassphrase()) {
|
||||
const hasAuth = securityStatus()?.hasAuthentication;
|
||||
@@ -1367,7 +1273,7 @@ const Settings: Component = () => {
|
||||
</div>
|
||||
|
||||
{/* Docker Message */}
|
||||
<Show when={versionInfo()?.isDocker}>
|
||||
<Show when={versionInfo()?.isDocker && !updateInfo()?.available}>
|
||||
<div class="p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<p class="text-xs text-blue-800 dark:text-blue-200">
|
||||
<strong>Docker Installation:</strong> Updates are managed through Docker. Pull the latest image to update.
|
||||
@@ -1376,26 +1282,49 @@ const Settings: Component = () => {
|
||||
</Show>
|
||||
|
||||
{/* Update Available */}
|
||||
<Show when={updateInfo()?.available && !versionInfo()?.isDocker}>
|
||||
<Show when={updateInfo()?.available}>
|
||||
<div class="p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-green-800 dark:text-green-200">
|
||||
Update Available: {updateInfo()?.latestVersion}
|
||||
<div class="mb-2">
|
||||
<p class="text-sm font-medium text-green-800 dark:text-green-200">
|
||||
Update Available: {updateInfo()?.latestVersion}
|
||||
</p>
|
||||
<p class="text-xs text-green-700 dark:text-green-300 mt-1">
|
||||
Released: {updateInfo()?.releaseDate ? new Date(updateInfo()!.releaseDate).toLocaleDateString() : 'Unknown'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Update Instructions based on deployment type */}
|
||||
<div class="mt-3 p-2 bg-green-100 dark:bg-green-900/40 rounded">
|
||||
<p class="text-xs font-medium text-green-800 dark:text-green-200 mb-1">How to update:</p>
|
||||
<Show when={versionInfo()?.deploymentType === 'proxmoxve'}>
|
||||
<p class="text-xs text-green-700 dark:text-green-300">
|
||||
Type <code class="px-1 py-0.5 bg-green-200 dark:bg-green-800 rounded">update</code> in the LXC console
|
||||
</p>
|
||||
<p class="text-xs text-green-700 dark:text-green-300 mt-1">
|
||||
Released: {updateInfo()?.releaseDate ? new Date(updateInfo()!.releaseDate).toLocaleDateString() : 'Unknown'}
|
||||
</Show>
|
||||
<Show when={versionInfo()?.deploymentType === 'docker'}>
|
||||
<div class="text-xs text-green-700 dark:text-green-300 space-y-1">
|
||||
<p>Run these commands:</p>
|
||||
<code class="block p-1 bg-green-200 dark:bg-green-800 rounded text-xs">
|
||||
docker pull rcourtman/pulse:latest<br/>
|
||||
docker restart pulse
|
||||
</code>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={versionInfo()?.deploymentType === 'systemd' || versionInfo()?.deploymentType === 'manual'}>
|
||||
<div class="text-xs text-green-700 dark:text-green-300 space-y-1">
|
||||
<p>Run the install script:</p>
|
||||
<code class="block p-1 bg-green-200 dark:bg-green-800 rounded text-xs">
|
||||
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | sudo bash
|
||||
</code>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={versionInfo()?.deploymentType === 'development'}>
|
||||
<p class="text-xs text-green-700 dark:text-green-300">
|
||||
Pull latest changes and rebuild
|
||||
</p>
|
||||
</div>
|
||||
<Show when={!updateStatus() || updateStatus()?.status === 'idle' || updateStatus()?.status === 'available'}>
|
||||
<button
|
||||
onClick={applyUpdate}
|
||||
class="px-3 py-1.5 text-xs bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||||
>
|
||||
Apply Update
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={updateInfo()?.releaseNotes}>
|
||||
<details class="mt-2">
|
||||
<summary class="text-xs text-green-700 dark:text-green-300 cursor-pointer">Release Notes</summary>
|
||||
@@ -1407,24 +1336,6 @@ const Settings: Component = () => {
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Update Progress */}
|
||||
<Show when={updateStatus() && updateStatus()?.status !== 'idle'}>
|
||||
<div class="p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<div class="mb-2">
|
||||
<p class="text-sm font-medium text-blue-800 dark:text-blue-200">
|
||||
{updateStatus()?.message || 'Processing update...'}
|
||||
</p>
|
||||
</div>
|
||||
<Show when={updateStatus()?.progress}>
|
||||
<div class="w-full bg-blue-200 dark:bg-blue-800 rounded-full h-2">
|
||||
<div
|
||||
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
style={`width: ${updateStatus()?.progress}%`}
|
||||
></div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Update Settings */}
|
||||
<div class="border-t border-gray-200 dark:border-gray-600 pt-4 space-y-4">
|
||||
@@ -1589,9 +1500,44 @@ const Settings: Component = () => {
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Show setup when no auth */}
|
||||
<Show when={!securityStatus()?.hasAuthentication}>
|
||||
<QuickSecuritySetup />
|
||||
{/* Show pending restart message if configured but not loaded */}
|
||||
<Show when={securityStatus()?.configuredButPendingRestart}>
|
||||
<div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4">
|
||||
<div class="flex items-start space-x-3">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-6 w-6 text-amber-600 dark:text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-sm font-semibold text-amber-900 dark:text-amber-100">
|
||||
Security Configured - Restart Required
|
||||
</h4>
|
||||
<p class="text-xs text-amber-700 dark:text-amber-300 mt-1">
|
||||
Security settings have been configured but the service needs to be restarted to activate them.
|
||||
</p>
|
||||
<p class="text-xs text-amber-600 dark:text-amber-400 mt-2">
|
||||
After restarting, you'll need to log in with your saved credentials.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Show setup when no auth and not pending */}
|
||||
<Show when={!securityStatus()?.hasAuthentication && !securityStatus()?.configuredButPendingRestart}>
|
||||
<QuickSecuritySetup onConfigured={async () => {
|
||||
// Refresh security status after configuration
|
||||
try {
|
||||
const response = await fetch('/api/security/status');
|
||||
if (response.ok) {
|
||||
const status = await response.json();
|
||||
setSecurityStatus(status);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to refresh security status:', err);
|
||||
}
|
||||
}} />
|
||||
</Show>
|
||||
|
||||
{/* API Token - Show current token when auth is enabled */}
|
||||
|
||||
@@ -200,6 +200,21 @@ func (r *Router) setupRoutes() {
|
||||
// Check for basic auth configuration
|
||||
hasAuthentication := os.Getenv("PULSE_AUTH_USER") != "" || os.Getenv("REQUIRE_AUTH") == "true"
|
||||
|
||||
// Check if .env file exists but hasn't been loaded yet (pending restart)
|
||||
configuredButPendingRestart := false
|
||||
envPath := filepath.Join(r.config.ConfigPath, ".env")
|
||||
if envPath == "" || r.config.ConfigPath == "" {
|
||||
envPath = "/etc/pulse/.env"
|
||||
}
|
||||
|
||||
// If no auth is currently active but .env exists, security is pending restart
|
||||
if !hasAuthentication && r.config.AuthUser == "" && r.config.AuthPass == "" {
|
||||
if _, err := os.Stat(envPath); err == nil {
|
||||
// .env exists but auth not loaded - pending restart
|
||||
configuredButPendingRestart = true
|
||||
}
|
||||
}
|
||||
|
||||
// Check for audit logging
|
||||
hasAuditLogging := os.Getenv("PULSE_AUDIT_LOG") == "true" || os.Getenv("AUDIT_LOG_ENABLED") == "true"
|
||||
|
||||
@@ -227,6 +242,7 @@ func (r *Router) setupRoutes() {
|
||||
"exportProtected": r.config.APIToken != "" || os.Getenv("ALLOW_UNPROTECTED_EXPORT") != "true",
|
||||
"unprotectedExportAllowed": os.Getenv("ALLOW_UNPROTECTED_EXPORT") == "true",
|
||||
"hasAuthentication": hasAuthentication,
|
||||
"configuredButPendingRestart": configuredButPendingRestart,
|
||||
"hasAuditLogging": hasAuditLogging,
|
||||
"credentialsEncrypted": credentialsEncrypted,
|
||||
"hasHTTPS": req.TLS != nil,
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/auth"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/updates"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -148,6 +149,7 @@ ENABLE_AUDIT_LOG=true
|
||||
response := map[string]interface{}{
|
||||
"success": true,
|
||||
"method": "docker",
|
||||
"deploymentType": "docker",
|
||||
"requiresManualRestart": true,
|
||||
"message": "Security configuration saved. Restart your Docker container to apply settings.",
|
||||
"note": "Your credentials have been saved to /data/.env and will persist after restart.",
|
||||
@@ -189,6 +191,7 @@ ENABLE_AUDIT_LOG=true
|
||||
"method": "systemd-nonroot",
|
||||
"serviceName": serviceName,
|
||||
"envFile": envPath,
|
||||
"deploymentType": updates.GetDeploymentType(),
|
||||
"message": fmt.Sprintf("Security settings saved to %s. Restart the %s service to apply.", envPath, serviceName),
|
||||
"command": fmt.Sprintf("sudo systemctl restart %s", serviceName),
|
||||
"note": "You may need root privileges to restart the service.",
|
||||
@@ -232,6 +235,7 @@ Environment="ENABLE_AUDIT_LOG=true"
|
||||
"success": true,
|
||||
"method": "systemd-root",
|
||||
"serviceName": serviceName,
|
||||
"deploymentType": updates.GetDeploymentType(),
|
||||
"automatic": true,
|
||||
"readyToRestart": true,
|
||||
"message": fmt.Sprintf("Security configured! Restart %s service to apply settings.", serviceName),
|
||||
@@ -264,10 +268,14 @@ ENABLE_AUDIT_LOG=true
|
||||
// Still return success with manual instructions
|
||||
}
|
||||
|
||||
// Get deployment type for restart instructions
|
||||
deploymentType := updates.GetDeploymentType()
|
||||
|
||||
response := map[string]interface{}{
|
||||
"success": true,
|
||||
"method": "manual",
|
||||
"envFile": envPath,
|
||||
"deploymentType": deploymentType,
|
||||
"message": "Security configuration saved. Restart Pulse to apply settings.",
|
||||
"note": fmt.Sprintf("Configuration saved to %s", envPath),
|
||||
}
|
||||
|
||||
+68
-24
@@ -21,12 +21,13 @@ type Version struct {
|
||||
|
||||
// VersionInfo contains detailed version information
|
||||
type VersionInfo struct {
|
||||
Version string `json:"version"`
|
||||
Build string `json:"build"`
|
||||
Runtime string `json:"runtime"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
IsDocker bool `json:"isDocker"`
|
||||
IsDevelopment bool `json:"isDevelopment"`
|
||||
Version string `json:"version"`
|
||||
Build string `json:"build"`
|
||||
Runtime string `json:"runtime"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
IsDocker bool `json:"isDocker"`
|
||||
IsDevelopment bool `json:"isDevelopment"`
|
||||
DeploymentType string `json:"deploymentType"`
|
||||
}
|
||||
|
||||
// ParseVersion parses a version string into a Version struct
|
||||
@@ -124,12 +125,13 @@ func GetCurrentVersion() (*VersionInfo, error) {
|
||||
channel = "rc"
|
||||
}
|
||||
return &VersionInfo{
|
||||
Version: gitVersion,
|
||||
Build: "development",
|
||||
Runtime: "go",
|
||||
Channel: channel,
|
||||
IsDevelopment: true,
|
||||
IsDocker: isDockerEnvironment(),
|
||||
Version: gitVersion,
|
||||
Build: "development",
|
||||
Runtime: "go",
|
||||
Channel: channel,
|
||||
IsDevelopment: true,
|
||||
IsDocker: isDockerEnvironment(),
|
||||
DeploymentType: GetDeploymentType(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -150,12 +152,13 @@ func GetCurrentVersion() (*VersionInfo, error) {
|
||||
channel = "rc"
|
||||
}
|
||||
return &VersionInfo{
|
||||
Version: version,
|
||||
Build: "release",
|
||||
Runtime: "go",
|
||||
Channel: channel,
|
||||
IsDevelopment: false,
|
||||
IsDocker: isDockerEnvironment(),
|
||||
Version: version,
|
||||
Build: "release",
|
||||
Runtime: "go",
|
||||
Channel: channel,
|
||||
IsDevelopment: false,
|
||||
IsDocker: isDockerEnvironment(),
|
||||
DeploymentType: GetDeploymentType(),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -167,12 +170,13 @@ func GetCurrentVersion() (*VersionInfo, error) {
|
||||
channel = "rc"
|
||||
}
|
||||
return &VersionInfo{
|
||||
Version: version,
|
||||
Build: "release",
|
||||
Runtime: "go",
|
||||
Channel: channel,
|
||||
IsDevelopment: false,
|
||||
IsDocker: isDockerEnvironment(),
|
||||
Version: version,
|
||||
Build: "release",
|
||||
Runtime: "go",
|
||||
Channel: channel,
|
||||
IsDevelopment: false,
|
||||
IsDocker: isDockerEnvironment(),
|
||||
DeploymentType: GetDeploymentType(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -215,6 +219,46 @@ func fileExists(path string) bool {
|
||||
return cmd.Run() == nil
|
||||
}
|
||||
|
||||
// GetDeploymentType determines how Pulse was deployed
|
||||
func GetDeploymentType() string {
|
||||
// Check if running in Docker
|
||||
if isDockerEnvironment() {
|
||||
return "docker"
|
||||
}
|
||||
|
||||
// Check for ProxmoxVE LXC installation (has update command)
|
||||
if fileExists("/bin/update") {
|
||||
data, err := exec.Command("cat", "/bin/update").Output()
|
||||
if err == nil && strings.Contains(string(data), "pulse.sh") {
|
||||
return "proxmoxve"
|
||||
}
|
||||
}
|
||||
|
||||
// Check for systemd service to determine installation type
|
||||
if fileExists("/etc/systemd/system/pulse-backend.service") {
|
||||
// Check if it's a ProxmoxVE installation (specific user setup)
|
||||
data, err := exec.Command("cat", "/etc/systemd/system/pulse-backend.service").Output()
|
||||
if err == nil {
|
||||
content := string(data)
|
||||
if strings.Contains(content, "User=pulse") && strings.Contains(content, "/opt/pulse/bin/pulse") {
|
||||
return "proxmoxve"
|
||||
}
|
||||
}
|
||||
return "systemd"
|
||||
}
|
||||
|
||||
if fileExists("/etc/systemd/system/pulse.service") {
|
||||
return "systemd"
|
||||
}
|
||||
|
||||
// Development or manual run
|
||||
if strings.Contains(os.Args[0], "go-build") || fileExists(".git") {
|
||||
return "development"
|
||||
}
|
||||
|
||||
return "manual"
|
||||
}
|
||||
|
||||
// compareInts compares two integers
|
||||
func compareInts(a, b int) int {
|
||||
if a < b {
|
||||
|
||||
Reference in New Issue
Block a user