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:
Pulse Monitor
2025-08-15 09:35:40 +00:00
parent c6690d5392
commit b947eb8a98
10 changed files with 245 additions and 271 deletions
+10 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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.
-1
View File
@@ -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
+1
View File
@@ -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 */}
+16
View File
@@ -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,
+8
View File
@@ -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
View File
@@ -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 {