mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
refactor: Remove password auth, require API tokens
This commit is contained in:
+39
-87
@@ -1,98 +1,50 @@
|
||||
# Example .env file for Pulse configuration
|
||||
# Copy this file to .env and replace placeholder values with your actual credentials.
|
||||
# Pulse Configuration Example
|
||||
# ----------------------------
|
||||
|
||||
# --- Proxmox VE Endpoint 1 (Required) ---
|
||||
# Connection details for your primary Proxmox VE host or cluster endpoint.
|
||||
# --- Proxmox VE Primary Endpoint (Required) ---
|
||||
# Only API Token authentication is supported.
|
||||
PROXMOX_HOST=your-proxmox-ip-or-hostname
|
||||
PROXMOX_PORT=8006 # Optional, defaults to 8006 if omitted
|
||||
|
||||
# Authentication: Use *either* API Token (Recommended) *or* User/Password.
|
||||
# API Token (Recommended):
|
||||
PROXMOX_TOKEN_ID=your-api-token-id@pam!your-token-name # e.g., root@pam!pulse
|
||||
PROXMOX_TOKEN_SECRET=your-api-token-secret-uuid # The Secret UUID value
|
||||
|
||||
# User/Password (Less Recommended):
|
||||
# PROXMOX_USERNAME=your-proxmox-username # e.g., root
|
||||
# PROXMOX_PASSWORD=your-password
|
||||
# PROXMOX_REALM=pam # Optional, defaults to 'pam' if omitted (e.g., pam, pve, ad)
|
||||
|
||||
# Optional: Set a friendly name for this endpoint in logs/UI (defaults to host)
|
||||
PROXMOX_NODE_NAME=My Primary Proxmox
|
||||
|
||||
# Optional: Allow self-signed certificates (defaults to true if omitted)
|
||||
# Set to false if you have a valid certificate from a trusted CA.
|
||||
PROXMOX_ALLOW_SELF_SIGNED_CERTS=true
|
||||
|
||||
PROXMOX_TOKEN_ID=your-api-token-id@pam!your-token-name
|
||||
PROXMOX_TOKEN_SECRET=your-api-token-secret-uuid
|
||||
# Optional: Specify custom port if not default 8006
|
||||
# PROXMOX_PORT=8006
|
||||
# Optional: Provide a display name for this endpoint in the UI
|
||||
# PROXMOX_NODE_NAME=My Primary Proxmox
|
||||
# Optional: Set to true to allow self-signed certificates (default: true)
|
||||
# PROXMOX_ALLOW_SELF_SIGNED_CERTS=true
|
||||
# Optional: Enable/Disable this endpoint (defaults to true if omitted)
|
||||
# PROXMOX_ENABLED=true
|
||||
|
||||
|
||||
# --- Proxmox VE Endpoint 2 (Optional) ---
|
||||
# Duplicate these variables with a _2 suffix for a second PVE endpoint.
|
||||
# PROXMOX_HOST_2=second-proxmox-ip-or-hostname
|
||||
# --- Proxmox VE Additional Endpoints (Optional) ---
|
||||
# Use consecutive numbers (_2, _3, ...) for additional PVE instances
|
||||
# Only API Token authentication is supported.
|
||||
# PROXMOX_HOST_2=second-proxmox-ip
|
||||
# PROXMOX_TOKEN_ID_2=second-token-id@pve!my-token
|
||||
# PROXMOX_TOKEN_SECRET_2=second-token-secret
|
||||
# PROXMOX_PORT_2=8006
|
||||
# PROXMOX_TOKEN_ID_2=token-id@pam!token-name_2
|
||||
# PROXMOX_TOKEN_SECRET_2=token-secret-uuid_2
|
||||
# PROXMOX_NODE_NAME_2=My Second Proxmox
|
||||
# PROXMOX_NODE_NAME_2=My Secondary Proxmox
|
||||
# PROXMOX_ALLOW_SELF_SIGNED_CERTS_2=true
|
||||
# PROXMOX_ENABLED_2=true
|
||||
# ... User/Password_2 if needed ...
|
||||
|
||||
# --- Proxmox Backup Server (PBS) Integration (Optional) ---
|
||||
# Only API Token authentication is supported.
|
||||
# Use consecutive numbers (_2, _3, ...) for additional PBS instances
|
||||
# PBS_HOST=your-pbs-ip-or-hostname
|
||||
# PBS_TOKEN_ID=your-pbs-token-id@pbs!my-token
|
||||
# PBS_TOKEN_SECRET=your-pbs-token-secret
|
||||
# Optional: Specify custom port if not default 8007
|
||||
# PBS_PORT=8007
|
||||
# Optional: Provide a display name for this PBS instance
|
||||
# PBS_NODE_NAME=My Primary PBS
|
||||
# Optional: Set to true to allow self-signed certificates (default: true)
|
||||
# PBS_ALLOW_SELF_SIGNED_CERTS=true
|
||||
|
||||
# --- Proxmox VE Endpoint 3, 4, ... (Optional) ---
|
||||
# Continue the pattern with _3, _4, etc. for more endpoints.
|
||||
# PROXMOX_HOST_3=...
|
||||
# PROXMOX_TOKEN_ID_3=...
|
||||
# PROXMOX_TOKEN_SECRET_3=...
|
||||
# ...
|
||||
# --- Pulse Service Settings (Optional) ---
|
||||
# Interval in milliseconds for fetching detailed VM/Container metrics (default: 2000)
|
||||
# PULSE_METRIC_INTERVAL_MS=2000
|
||||
# Interval in milliseconds for fetching structural data (nodes, VM/CT list, storage) (default: 30000)
|
||||
# PULSE_DISCOVERY_INTERVAL_MS=30000
|
||||
|
||||
|
||||
# --- Proxmox Backup Server (PBS) Endpoint 1 (Optional) ---
|
||||
PBS_HOST=your-pbs-ip-or-hostname
|
||||
PBS_PORT=8007 # Optional, defaults to 8007 if omitted
|
||||
|
||||
# Authentication: Use *either* API Token *or* User/Password.
|
||||
# API Token (Recommended):
|
||||
# PBS_TOKEN_ID=your-pbs-api-token-id@pbs!your-token-name # e.g., pulse-reader@pbs!pulse-token
|
||||
# PBS_TOKEN_SECRET=your-pbs-api-token-secret-uuid
|
||||
|
||||
# User/Password (Less Recommended):
|
||||
PBS_USER=root@pam # e.g., root@pam or backup-user@pbs
|
||||
PBS_PASSWORD=your-password
|
||||
PBS_REALM=pbs # Optional, defaults to 'pbs' if using User/Password auth
|
||||
|
||||
# Optional: Set a friendly name for this endpoint in logs/UI (defaults to host)
|
||||
PBS_NODE_NAME=My Primary PBS
|
||||
|
||||
# Optional: Allow self-signed certificates (defaults to true if omitted)
|
||||
PBS_ALLOW_SELF_SIGNED_CERTS=true
|
||||
|
||||
|
||||
# --- Proxmox Backup Server (PBS) Endpoint 2 (Optional) ---
|
||||
# Duplicate these variables with a _2 suffix for a second PBS endpoint.
|
||||
# PBS_HOST_2=second-pbs-ip-or-hostname
|
||||
# PBS_PORT_2=8007
|
||||
# PBS_TOKEN_ID_2=token-id@pbs!token-name_2
|
||||
# PBS_TOKEN_SECRET_2=token-secret-uuid_2
|
||||
# PBS_NODE_NAME_2=My Second PBS
|
||||
# PBS_ALLOW_SELF_SIGNED_CERTS_2=true
|
||||
# ... User/Password_2 if needed ...
|
||||
|
||||
|
||||
# --- Proxmox Backup Server (PBS) Endpoint 3, 4, ... (Optional) ---
|
||||
# Continue the pattern with _3, _4, etc. for more endpoints.
|
||||
# PBS_HOST_3=...
|
||||
# PBS_TOKEN_ID_3=...
|
||||
# PBS_TOKEN_SECRET_3=...
|
||||
# ...
|
||||
|
||||
|
||||
# Development Settings
|
||||
ENABLE_HOT_RELOAD=true # Set to false to disable hot reload
|
||||
|
||||
# --- Pulse Interval Configuration (Optional) ---
|
||||
# Adjust how frequently Pulse fetches data. Lower values mean more real-time data
|
||||
# but increase load on PVE/PBS hosts. Values are in milliseconds.
|
||||
#
|
||||
# PULSE_METRIC_INTERVAL_MS=2000 # Default: 2000 (2 seconds) - Frequency for fetching guest CPU/Mem/IO stats.
|
||||
# PULSE_DISCOVERY_INTERVAL_MS=30000 # Default: 30000 (30 seconds) - Frequency for fetching node/guest lists and PBS data.
|
||||
# --- Development Settings (Optional) ---
|
||||
# Enable/disable hot reloading for frontend changes (default: true)
|
||||
# ENABLE_HOT_RELOAD=true
|
||||
+19
-132
@@ -1,11 +1,13 @@
|
||||
require('dotenv').config(); // Load environment variables from .env file
|
||||
|
||||
// --- BEGIN Environment Variable Validation ---
|
||||
// Define required primary variables (API Token Auth Only)
|
||||
const primaryRequiredEnvVars = [
|
||||
'PROXMOX_HOST',
|
||||
'PROXMOX_TOKEN_ID',
|
||||
'PROXMOX_TOKEN_SECRET'
|
||||
];
|
||||
|
||||
const placeholderValues = [
|
||||
'your-proxmox-ip-or-hostname',
|
||||
'your-api-token-id@pam!your-token-name',
|
||||
@@ -52,11 +54,6 @@ endpoints.push({
|
||||
tokenSecret: process.env.PROXMOX_TOKEN_SECRET,
|
||||
enabled: process.env.PROXMOX_ENABLED !== 'false', // Currently unused, but kept for potential future use
|
||||
allowSelfSignedCerts: process.env.PROXMOX_ALLOW_SELF_SIGNED_CERTS !== 'false',
|
||||
credentials: { // Keep password fallback possibility
|
||||
username: process.env.PROXMOX_USERNAME,
|
||||
password: process.env.PROXMOX_PASSWORD,
|
||||
realm: process.env.PROXMOX_REALM || 'pam'
|
||||
}
|
||||
});
|
||||
|
||||
// Load additional endpoints (PROXMOX_HOST_2, _3, ...)
|
||||
@@ -87,11 +84,6 @@ while (process.env[`PROXMOX_HOST_${i}`]) {
|
||||
tokenSecret: tokenSecret,
|
||||
enabled: process.env[`PROXMOX_ENABLED_${i}`] !== 'false',
|
||||
allowSelfSignedCerts: process.env[`PROXMOX_ALLOW_SELF_SIGNED_CERTS_${i}`] !== 'false',
|
||||
credentials: { // Allow fallback for additional endpoints too
|
||||
username: process.env[`PROXMOX_USERNAME_${i}`],
|
||||
password: process.env[`PROXMOX_PASSWORD_${i}`],
|
||||
realm: process.env[`PROXMOX_REALM_${i}`] || 'pam'
|
||||
}
|
||||
});
|
||||
i++;
|
||||
}
|
||||
@@ -132,9 +124,6 @@ const pbsApiClients = {}; // Object to hold initialized clients, keyed by pbsCon
|
||||
function loadPbsConfig(index = null) {
|
||||
const suffix = index ? `_${index}` : '';
|
||||
const hostVar = `PBS_HOST${suffix}`;
|
||||
const userVar = `PBS_USER${suffix}`;
|
||||
const passVar = `PBS_PASSWORD${suffix}`;
|
||||
const realmVar = `PBS_REALM${suffix}`;
|
||||
const tokenIdVar = `PBS_TOKEN_ID${suffix}`;
|
||||
const tokenSecretVar = `PBS_TOKEN_SECRET${suffix}`;
|
||||
const nodeNameVar = `PBS_NODE_NAME${suffix}`;
|
||||
@@ -157,40 +146,14 @@ function loadPbsConfig(index = null) {
|
||||
}
|
||||
// ---> END ADDED <---\
|
||||
|
||||
const pbsUser = process.env[userVar];
|
||||
const pbsPassword = process.env[passVar];
|
||||
const pbsTokenId = process.env[tokenIdVar];
|
||||
const pbsTokenSecret = process.env[tokenSecretVar];
|
||||
|
||||
let config = null;
|
||||
let idPrefix = index ? `pbs_endpoint_${index}` : 'pbs_primary';
|
||||
|
||||
// Check User/Password first
|
||||
if (pbsUser && pbsPassword) {
|
||||
const pbsPlaceholders = placeholderValues.filter(p =>
|
||||
pbsHostUrl.includes(p) || pbsUser.includes(p) || pbsPassword.includes(p) // Check against URL
|
||||
);
|
||||
if (pbsPlaceholders.length > 0) {
|
||||
console.warn(`WARN: Skipping PBS configuration ${index || 'primary'} (User/Pass). Placeholder values detected for: ${pbsPlaceholders.join(', ')}`);
|
||||
} else {
|
||||
config = {
|
||||
id: `${idPrefix}_userpass`,
|
||||
authMethod: 'userpass',
|
||||
name: process.env[nodeNameVar] || pbsHostname, // User-defined name or parsed hostname
|
||||
host: pbsHostUrl, // Keep original full URL here
|
||||
port: process.env[portVar] || '8007',
|
||||
user: pbsUser,
|
||||
password: pbsPassword,
|
||||
realm: process.env[realmVar] || 'pbs',
|
||||
nodeName: process.env[nodeNameVar], // Store explicitly set node name
|
||||
allowSelfSignedCerts: process.env[selfSignedVar] !== 'false',
|
||||
enabled: true // Assuming enabled if configured
|
||||
};
|
||||
console.log(`INFO: Found PBS configuration ${index || 'primary'} (User/Password): ${config.name} (${config.host})`);
|
||||
}
|
||||
}
|
||||
// Check Token second
|
||||
else if (pbsTokenId && pbsTokenSecret) {
|
||||
// Check Token ONLY
|
||||
if (pbsTokenId && pbsTokenSecret) {
|
||||
const pbsPlaceholders = placeholderValues.filter(p =>
|
||||
pbsHostUrl.includes(p) || pbsTokenId.includes(p) || pbsTokenSecret.includes(p) // Check against URL
|
||||
);
|
||||
@@ -199,7 +162,7 @@ function loadPbsConfig(index = null) {
|
||||
} else {
|
||||
config = {
|
||||
id: `${idPrefix}_token`,
|
||||
authMethod: 'token',
|
||||
authMethod: 'token', // Explicitly set auth method
|
||||
name: process.env[nodeNameVar] || pbsHostname,
|
||||
host: pbsHostUrl, // Keep original full URL here
|
||||
port: process.env[portVar] || '8007',
|
||||
@@ -214,14 +177,16 @@ function loadPbsConfig(index = null) {
|
||||
}
|
||||
// Warn if host is set but auth is incomplete
|
||||
else {
|
||||
console.warn(`WARN: Partial PBS configuration found for ${hostVar}. Please set either (${userVar} + ${passVar}) or (${tokenIdVar} + ${tokenSecretVar}) along with ${hostVar}.`);
|
||||
console.warn(`WARN: Partial PBS configuration found for ${hostVar}. Please set (${tokenIdVar} + ${tokenSecretVar}) along with ${hostVar}.`);
|
||||
}
|
||||
|
||||
if (config) {
|
||||
pbsConfigs.push(config);
|
||||
return true; // Indicate a config was found and added
|
||||
}
|
||||
return true; // Indicate we should check the next index even if this one was partial/invalid
|
||||
// If host was present but token details were missing/invalid, return true to check next index
|
||||
// but don't push an incomplete config.
|
||||
return !!pbsHostUrl; // Return true if host was set, false otherwise
|
||||
}
|
||||
|
||||
// Load primary PBS config (index=null)
|
||||
@@ -262,18 +227,14 @@ endpoints.forEach(endpoint => {
|
||||
|
||||
// Add request interceptor for authentication (specific to this endpoint)
|
||||
apiClient.interceptors.request.use(config => {
|
||||
// Add API token authentication
|
||||
// Add API token authentication ONLY
|
||||
if (endpoint.tokenId && endpoint.tokenSecret) {
|
||||
config.headers.Authorization = `PVEAPIToken=${endpoint.tokenId}=${endpoint.tokenSecret}`;
|
||||
}
|
||||
// Fallback to password auth if configured FOR THIS ENDPOINT
|
||||
else if (endpoint.credentials && endpoint.credentials.username && endpoint.credentials.password) {
|
||||
const { username, password, realm } = endpoint.credentials;
|
||||
config.headers.Authorization = `Basic ${Buffer.from(`${username}@${realm}:${password}`).toString('base64')}`;
|
||||
if (!realm) console.warn(`WARN: Using password auth for endpoint ${endpoint.name} without a realm specified. Defaulting to 'pam'.`);
|
||||
} else {
|
||||
// Should not happen due to validation, but log just in case
|
||||
console.error(`ERROR: Endpoint ${endpoint.name} has neither token nor full username/password credentials configured.`);
|
||||
// This should ideally not happen if validation passed, but log error.
|
||||
console.error(`ERROR: Endpoint ${endpoint.name} is missing required API token credentials.`);
|
||||
// Optionally, you could cancel the request here:
|
||||
// return Promise.reject(new Error(`Missing API token for endpoint ${endpoint.name}`));
|
||||
}
|
||||
return config;
|
||||
});
|
||||
@@ -300,75 +261,6 @@ endpoints.forEach(endpoint => {
|
||||
});
|
||||
|
||||
// --- Create PBS API Client (if configured) ---
|
||||
async function getPbsAuthTicketAndSetupClient(config) {
|
||||
const pbsBaseURL = config.host.includes('://')
|
||||
? `${config.host}` // Assume full URL if :// is present
|
||||
: `https://${config.host}:${config.port}`;
|
||||
|
||||
const loginUrl = `${pbsBaseURL}/api2/json/access/ticket`;
|
||||
const httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: !config.allowSelfSignedCerts
|
||||
});
|
||||
|
||||
try {
|
||||
console.log(`INFO: Attempting PBS login for user ${config.user}...`);
|
||||
const response = await axios.post(loginUrl, `username=${encodeURIComponent(config.user)}&password=${encodeURIComponent(config.password)}`, {
|
||||
httpsAgent: httpsAgent,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||
});
|
||||
|
||||
const ticket = response.data.data.ticket;
|
||||
const csrfToken = response.data.data.CSRFPreventionToken;
|
||||
const username = response.data.data.username;
|
||||
|
||||
if (!ticket || !csrfToken) {
|
||||
throw new Error("Login response missing ticket or CSRF token.");
|
||||
}
|
||||
|
||||
console.log(`INFO: PBS login successful for ${username}. Setting up authenticated client.`);
|
||||
|
||||
// Create the authenticated client instance
|
||||
const pbsAuthenticatedClient = axios.create({
|
||||
baseURL: `${pbsBaseURL}/api2/json`,
|
||||
httpsAgent: httpsAgent,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
// Apply interceptor for cookie and CSRF token
|
||||
pbsAuthenticatedClient.interceptors.request.use(reqConfig => {
|
||||
reqConfig.headers['CSRFPreventionToken'] = csrfToken;
|
||||
reqConfig.headers['Cookie'] = `PBSAuthCookie=${ticket}`;
|
||||
return reqConfig;
|
||||
});
|
||||
|
||||
// Apply retry logic
|
||||
axiosRetry(pbsAuthenticatedClient, {
|
||||
retries: 3,
|
||||
retryDelay: (retryCount, error) => {
|
||||
console.warn(`Retrying authenticated PBS API request (attempt ${retryCount}) due to error: ${error.message}`);
|
||||
return axiosRetry.exponentialDelay(retryCount);
|
||||
},
|
||||
retryCondition: (error) => {
|
||||
// Also retry on 401/403 in case ticket expires or CSRF mismatch?
|
||||
// For now, just standard retry conditions.
|
||||
return axiosRetry.isNetworkError(error) || axiosRetry.isRetryableError(error);
|
||||
},
|
||||
});
|
||||
|
||||
return { client: pbsAuthenticatedClient, config: config }; // Return the fully configured client object
|
||||
|
||||
} catch (error) {
|
||||
console.error(`ERROR: PBS login failed for user ${config.user}: ${error.message}`);
|
||||
if (error.response) {
|
||||
console.error(`ERROR: PBS login response status: ${error.response.status}`);
|
||||
console.error(`ERROR: PBS login response data: ${JSON.stringify(error.response.data)}`);
|
||||
}
|
||||
return null; // Indicate failure
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeAllPbsClients() {
|
||||
if (pbsConfigs.length === 0) return;
|
||||
|
||||
@@ -376,14 +268,8 @@ async function initializeAllPbsClients() {
|
||||
const initPromises = pbsConfigs.map(async (config) => {
|
||||
let clientData = null;
|
||||
try {
|
||||
if (config.authMethod === 'userpass') {
|
||||
clientData = await getPbsAuthTicketAndSetupClient(config);
|
||||
if (clientData) {
|
||||
console.log(`INFO: [PBS Init] Successfully initialized client for instance '${config.name}' (User/Password Auth)`);
|
||||
} else {
|
||||
console.error(`ERROR: Failed to initialize PBS client (User/Password) for: ${config.name} (${config.host})`);
|
||||
}
|
||||
} else if (config.authMethod === 'token') {
|
||||
// We only support token auth now
|
||||
if (config.authMethod === 'token') {
|
||||
// Token Auth Logic (adapted from old initializePbsClient)
|
||||
const pbsBaseURL = config.host.includes('://')
|
||||
? `${config.host}/api2/json`
|
||||
@@ -398,7 +284,7 @@ async function initializeAllPbsClients() {
|
||||
});
|
||||
|
||||
pbsAxiosInstance.interceptors.request.use(reqConfig => {
|
||||
reqConfig.headers.Authorization = `PBSAPIToken ${config.tokenId}:${config.tokenSecret}`;
|
||||
reqConfig.headers.Authorization = `PBSAPIToken ${config.tokenId}=${config.tokenSecret}`;
|
||||
return reqConfig;
|
||||
});
|
||||
|
||||
@@ -416,7 +302,8 @@ async function initializeAllPbsClients() {
|
||||
clientData = { client: pbsAxiosInstance, config: config };
|
||||
console.log(`INFO: [PBS Init] Successfully initialized client for instance '${config.name}' (Token Auth)`);
|
||||
} else {
|
||||
console.error(`ERROR: Unknown authMethod '${config.authMethod}' for PBS config: ${config.name}`);
|
||||
// This case should not be reachable anymore if loadPbsConfig only creates 'token' authMethod configs
|
||||
console.error(`ERROR: Unexpected authMethod '${config.authMethod}' found during PBS client initialization for: ${config.name}`);
|
||||
}
|
||||
|
||||
if (clientData) {
|
||||
|
||||
Reference in New Issue
Block a user