mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
Adopt multi-token auth across docs, UI, and tooling
This commit is contained in:
@@ -11,7 +11,8 @@
|
||||
# Plain-text values are automatically hashed on startup, or supply a bcrypt hash.
|
||||
# PULSE_AUTH_USER=admin
|
||||
# PULSE_AUTH_PASS=super-secret-password
|
||||
# API_TOKEN=your-48-char-hex-token
|
||||
# API_TOKENS=host-token-1,host-token-2
|
||||
# API_TOKEN=legacy-fallback-token
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Optional security toggles
|
||||
|
||||
10
README.md
10
README.md
@@ -102,18 +102,19 @@ curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | b
|
||||
1. Open `http://<your-server>:7655`
|
||||
2. **Complete the mandatory security setup** (first-time only)
|
||||
3. Create your admin username and password
|
||||
4. Save the generated API token for automation
|
||||
4. Use **Settings → Security → API tokens** to mint dedicated tokens for automation (issue one token per integration so you can revoke credentials individually)
|
||||
|
||||
**Option B: Automated Setup (No UI)**
|
||||
For automated deployments, configure authentication via environment variables:
|
||||
```bash
|
||||
# Start Pulse with auth pre-configured - skips setup screen
|
||||
API_TOKEN=your-api-token ./pulse
|
||||
API_TOKENS="ansible-token,docker-agent-token" ./pulse
|
||||
|
||||
# Or use basic auth
|
||||
PULSE_AUTH_USER=admin PULSE_AUTH_PASS=password ./pulse
|
||||
|
||||
# Plain text credentials are automatically hashed for security
|
||||
# `API_TOKEN` is still accepted for back-compat, but `API_TOKENS` lets you manage multiple credentials
|
||||
# You can also provide pre-hashed values if preferred
|
||||
```
|
||||
See [Configuration Guide](docs/CONFIGURATION.md#automated-setup-skip-ui) for details.
|
||||
@@ -196,7 +197,7 @@ docker run -d \
|
||||
--name pulse \
|
||||
-p 7655:7655 \
|
||||
-v pulse_data:/data \
|
||||
-e API_TOKEN="your-secure-token" \
|
||||
-e API_TOKENS="ansible-token,docker-agent-token" \
|
||||
-e PULSE_AUTH_USER="admin" \
|
||||
-e PULSE_AUTH_PASS="your-password" \
|
||||
--restart unless-stopped \
|
||||
@@ -229,7 +230,8 @@ services:
|
||||
# Security (all optional - runs open by default)
|
||||
# - PULSE_AUTH_USER=admin # Username for web UI login
|
||||
# - PULSE_AUTH_PASS=your-password # Plain text or bcrypt hash (auto-hashed if plain)
|
||||
# - API_TOKEN=your-token # Plain text or SHA3-256 hash (auto-hashed if plain)
|
||||
# - API_TOKENS=token-a,token-b # Comma-separated tokens (plain or SHA3-256 hashed)
|
||||
# - API_TOKEN=legacy-token # Optional single-token fallback
|
||||
# - ALLOW_UNPROTECTED_EXPORT=false # Allow export without auth (default: false)
|
||||
|
||||
# Security: Plain text credentials are automatically hashed
|
||||
|
||||
68
docs/API.md
68
docs/API.md
@@ -26,25 +26,25 @@ docker run -e PULSE_AUTH_USER=admin -e PULSE_AUTH_PASS=your-password rcourtman/p
|
||||
Once set, users must login via the web UI. The password can be changed from Settings → Security.
|
||||
|
||||
### API Token Authentication
|
||||
For programmatic API access and automation. Tokens can be generated via the web UI (Settings → Security → Generate API Token).
|
||||
For programmatic API access and automation. Manage tokens via **Settings → Security → API tokens** or the `/api/security/tokens` endpoints.
|
||||
|
||||
**API-Only Mode**: If only API_TOKEN is configured (no password auth), the UI remains accessible in read-only mode while API modifications require the token.
|
||||
**API-Only Mode**: If at least one API token is configured (no password auth), the UI remains accessible in read-only mode while API modifications require a valid token.
|
||||
|
||||
```bash
|
||||
# Systemd
|
||||
sudo systemctl edit pulse-backend
|
||||
# Add:
|
||||
[Service]
|
||||
Environment="API_TOKEN=your-48-char-hex-token"
|
||||
Environment="API_TOKENS=token-a,token-b"
|
||||
|
||||
# Docker
|
||||
docker run -e API_TOKEN=your-48-char-hex-token rcourtman/pulse:latest
|
||||
docker run -e API_TOKENS=token-a,token-b rcourtman/pulse:latest
|
||||
```
|
||||
|
||||
### Using Authentication
|
||||
|
||||
```bash
|
||||
# With API Token (header)
|
||||
# With API token (header)
|
||||
curl -H "X-API-Token: your-secure-token" http://localhost:7655/api/health
|
||||
|
||||
# With API Token (query parameter, for export/import)
|
||||
@@ -54,6 +54,8 @@ curl "http://localhost:7655/api/export?token=your-secure-token"
|
||||
curl -b cookies.txt http://localhost:7655/api/health
|
||||
```
|
||||
|
||||
> Legacy note: The `API_TOKEN` environment variable is still honored for backwards compatibility. When both `API_TOKEN` and `API_TOKENS` are supplied, Pulse merges them and prefers the newest token when presenting hints.
|
||||
|
||||
### Security Features
|
||||
|
||||
When authentication is enabled, Pulse provides enterprise-grade security:
|
||||
@@ -431,13 +433,63 @@ Request body:
|
||||
```
|
||||
|
||||
#### API Token Management
|
||||
Manage API tokens for programmatic access.
|
||||
Manage API tokens for automation workflows, Docker agents, and tool integrations.
|
||||
|
||||
Authentication: Requires an admin session or an existing admin-scoped API token.
|
||||
|
||||
**List tokens**
|
||||
```bash
|
||||
POST /api/security/regenerate-token # Generate or regenerate API token
|
||||
GET /api/security/tokens
|
||||
```
|
||||
|
||||
Note: The old `/api/system/api-token` endpoints have been deprecated in favor of the simplified regenerate-token endpoint.
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"tokens": [
|
||||
{
|
||||
"id": "9bf9aa59-3b85-4fd8-9aad-3f19b2c9b6f0",
|
||||
"name": "ansible",
|
||||
"prefix": "pulse_1a2b",
|
||||
"suffix": "c3d4",
|
||||
"createdAt": "2025-10-14T12:12:34Z",
|
||||
"lastUsedAt": "2025-10-14T12:21:05Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Create a token**
|
||||
```bash
|
||||
POST /api/security/tokens
|
||||
Content-Type: application/json
|
||||
{
|
||||
"name": "ansible"
|
||||
}
|
||||
```
|
||||
|
||||
Response (token value is returned once):
|
||||
```json
|
||||
{
|
||||
"token": "pulse_1a2b3c4d5e6f7g8h9i0j",
|
||||
"record": {
|
||||
"id": "9bf9aa59-3b85-4fd8-9aad-3f19b2c9b6f0",
|
||||
"name": "ansible",
|
||||
"prefix": "pulse_1a2b",
|
||||
"suffix": "c3d4",
|
||||
"createdAt": "2025-10-14T12:12:34Z",
|
||||
"lastUsedAt": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Delete a token**
|
||||
```bash
|
||||
DELETE /api/security/tokens/{id}
|
||||
```
|
||||
|
||||
Returns `204 No Content` when the token is revoked.
|
||||
|
||||
> Legacy compatibility: `POST /api/security/regenerate-token` is still available but now replaces the entire token list with a single regenerated token. Prefer the endpoints above for multi-token environments.
|
||||
|
||||
#### Login
|
||||
Enhanced login endpoint with lockout feedback.
|
||||
|
||||
@@ -38,7 +38,8 @@ All configuration files are stored in `/etc/pulse/` (or `/data/` in Docker conta
|
||||
# User authentication
|
||||
PULSE_AUTH_USER='admin' # Admin username
|
||||
PULSE_AUTH_PASS='$2a$12$...' # Bcrypt hashed password (keep quotes!)
|
||||
API_TOKEN=abc123... # API token (Pulse hashes it automatically)
|
||||
API_TOKEN=abc123... # Optional: seed a primary API token (auto-hashed)
|
||||
API_TOKENS=token-one,token-two # Optional: comma-separated list of API tokens
|
||||
|
||||
# Security settings
|
||||
DISABLE_AUTH=true # Disable authentication entirely
|
||||
@@ -54,10 +55,11 @@ PROXY_AUTH_LOGOUT_URL=/logout # URL for SSO logout
|
||||
|
||||
**Important Notes:**
|
||||
- Password hash MUST be in single quotes to prevent shell expansion
|
||||
- API tokens are stored as SHA3-256 hashes on disk; provide a plain token and Pulse hashes it automatically
|
||||
- API tokens are stored as SHA3-256 hashes on disk; plain tokens listed in `API_TOKEN` or `API_TOKENS` are auto-hashed at startup
|
||||
- Multiple tokens can be pre-seeded via `API_TOKENS` (comma separated). Every token—plain text or pre-hashed—becomes a distinct credential.
|
||||
- This file should have restricted permissions (600)
|
||||
- Never commit this file to version control
|
||||
- ProxmoxVE installations may pre-configure API_TOKEN
|
||||
- ProxmoxVE installations may pre-configure `API_TOKEN`; you can now add additional tokens without touching the original value
|
||||
- Changes to this file are applied immediately without restart (v4.3.9+)
|
||||
- **DO NOT** put port configuration here - use system.json or systemd overrides
|
||||
- Copy `.env.example` from the repository for a ready-to-edit template
|
||||
@@ -317,7 +319,8 @@ These env vars override system.json values. When set, the UI will show a warning
|
||||
These should be set in the .env file for security:
|
||||
|
||||
- `PULSE_AUTH_USER`, `PULSE_AUTH_PASS` - Basic authentication
|
||||
- `API_TOKEN` - API token for authentication
|
||||
- `API_TOKEN` - Primary API token (auto-hashed if you supply the raw value)
|
||||
- `API_TOKENS` - Comma-separated list of additional API tokens (plain or SHA3-256 hashed)
|
||||
- `DISABLE_AUTH` - Set to `true` to disable authentication entirely
|
||||
|
||||
#### OIDC Variables (optional overrides)
|
||||
@@ -384,16 +387,20 @@ For automated deployments (CI/CD, infrastructure as code, ProxmoxVE scripts), yo
|
||||
|
||||
### Simple Automated Setup
|
||||
|
||||
**Option 1: API Token Authentication**
|
||||
**Option 1: API Tokens (single or multiple)**
|
||||
```bash
|
||||
# Start Pulse with API token - setup screen is skipped
|
||||
API_TOKEN=your-secure-api-token ./pulse
|
||||
# Start Pulse with API tokens - setup screen is skipped
|
||||
API_TOKENS="$ANSIBLE_TOKEN,$DOCKER_AGENT_TOKEN" ./pulse
|
||||
|
||||
# The token is hashed and stored securely
|
||||
# Use this same token for all API calls
|
||||
curl -H "X-API-Token: your-secure-api-token" http://localhost:7655/api/nodes
|
||||
# Each token is hashed and stored securely on startup
|
||||
curl -H "X-API-Token: $ANSIBLE_TOKEN" http://localhost:7655/api/nodes
|
||||
|
||||
# Legacy fallback (not recommended for new installs)
|
||||
# API_TOKEN=your-secure-api-token ./pulse
|
||||
```
|
||||
|
||||
> **Tip:** Generate a distinct token for each automation workflow (Ansible, Docker agents, CI runners, etc.) so you can revoke one credential without affecting the others.
|
||||
|
||||
**Option 2: Basic Authentication**
|
||||
```bash
|
||||
# Start Pulse with username/password - setup screen is skipped
|
||||
@@ -406,9 +413,10 @@ PULSE_AUTH_PASS=your-secure-password \
|
||||
```
|
||||
|
||||
**Option 3: Both (API + Basic Auth)**
|
||||
Set `PRIMARY_TOKEN` to the token value you want to reuse (plain text or SHA3-256 hash) before starting Pulse:
|
||||
```bash
|
||||
# Configure both authentication methods
|
||||
API_TOKEN=your-api-token \
|
||||
API_TOKENS="$PRIMARY_TOKEN" \
|
||||
PULSE_AUTH_USER=admin \
|
||||
PULSE_AUTH_PASS=your-password \
|
||||
./pulse
|
||||
@@ -430,23 +438,52 @@ PULSE_AUTH_PASS=your-password \
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Generate secure token
|
||||
API_TOKEN=$(openssl rand -hex 32)
|
||||
# Generate dedicated tokens for each integration
|
||||
ANSIBLE_TOKEN=$(openssl rand -hex 32)
|
||||
DOCKER_AGENT_TOKEN=$(openssl rand -hex 32)
|
||||
|
||||
# Deploy with authentication pre-configured
|
||||
docker run -d \
|
||||
--name pulse \
|
||||
-p 7655:7655 \
|
||||
-e API_TOKEN="$API_TOKEN" \
|
||||
-e API_TOKENS="$ANSIBLE_TOKEN,$DOCKER_AGENT_TOKEN" \
|
||||
-v pulse-data:/data \
|
||||
rcourtman/pulse:latest
|
||||
|
||||
echo "Pulse deployed! Use API token: $API_TOKEN"
|
||||
echo "Pulse deployed!"
|
||||
echo " Ansible token: $ANSIBLE_TOKEN"
|
||||
echo " Docker agent token: $DOCKER_AGENT_TOKEN"
|
||||
|
||||
# Immediately use the API - no setup needed
|
||||
curl -H "X-API-Token: $API_TOKEN" http://localhost:7655/api/nodes
|
||||
curl -H "X-API-Token: $ANSIBLE_TOKEN" http://localhost:7655/api/nodes
|
||||
```
|
||||
|
||||
Remember to store each token securely; the plain values above are displayed only once.
|
||||
|
||||
### Managing tokens via the REST API
|
||||
|
||||
Infrastructure-as-code workflows (Ansible, Terraform, etc.) can drive token lifecycle directly through the new `/api/security/tokens` endpoints:
|
||||
|
||||
- `GET /api/security/tokens` – list existing tokens (metadata only)
|
||||
- `POST /api/security/tokens` – create a new token; the raw value is returned once in the response
|
||||
- `DELETE /api/security/tokens/{id}` – revoke a token by its identifier
|
||||
|
||||
Example: create a token named `ansible` and capture the secret for later use.
|
||||
|
||||
```bash
|
||||
NEW_TOKEN_JSON=$(curl -sS -X POST http://localhost:7655/api/security/tokens \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Token: $ADMIN_TOKEN" \
|
||||
-d '{"name":"ansible"}')
|
||||
|
||||
NEW_TOKEN=$(echo "$NEW_TOKEN_JSON" | jq -r '.token')
|
||||
TOKEN_ID=$(echo "$NEW_TOKEN_JSON" | jq -r '.record.id')
|
||||
echo "New token value: $NEW_TOKEN"
|
||||
echo "Token id: $TOKEN_ID"
|
||||
```
|
||||
|
||||
Store `NEW_TOKEN` securely; future GET requests only expose token hints (`prefix`/`suffix`). To revoke the credential later, call `DELETE /api/security/tokens/$TOKEN_ID`.
|
||||
|
||||
---
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
@@ -78,7 +78,11 @@ services:
|
||||
# escape $ as $$ (e.g. $$2a$$12$$...) so docker compose does not treat it
|
||||
# as variable expansion.
|
||||
PULSE_AUTH_PASS: 'super-secret-password'
|
||||
API_TOKEN: 'your-48-char-hex-token' # Generate: openssl rand -hex 24
|
||||
# Provide one or more API tokens. Tokens can be raw values or SHA3-256 hashes.
|
||||
# Use distinct tokens per automation target for easier revocation.
|
||||
API_TOKENS: 'ansible-token,docker-agent-token'
|
||||
# Optional legacy variable kept for compatibility; newest token is used if both are set.
|
||||
# API_TOKEN: 'your-48-char-hex-token'
|
||||
PULSE_PUBLIC_URL: 'https://pulse.example.com' # Used for webhooks/links
|
||||
# TZ: 'UTC'
|
||||
restart: unless-stopped
|
||||
@@ -95,7 +99,10 @@ Create `.env` file (no escaping needed here). You can copy `.env.example` from t
|
||||
```env
|
||||
PULSE_AUTH_USER=admin
|
||||
PULSE_AUTH_PASS=super-secret-password # Plain text (auto-hashed) or bcrypt hash
|
||||
# Optional legacy token (used if API_TOKENS is empty)
|
||||
API_TOKEN=your-48-char-hex-token # Generate with: openssl rand -hex 24
|
||||
# Comma-separated list of tokens for automation/agents
|
||||
API_TOKENS=${ANSIBLE_TOKEN},${DOCKER_AGENT_TOKEN}
|
||||
PULSE_PUBLIC_URL=https://pulse.example.com # Recommended for webhooks
|
||||
TZ=Asia/Kolkata # Optional: matches host timezone
|
||||
```
|
||||
@@ -138,17 +145,21 @@ If you change anything in `.env`, run `docker compose up -d` again so the contai
|
||||
docker run -d \
|
||||
-e PULSE_AUTH_USER=admin \
|
||||
-e PULSE_AUTH_PASS=mypassword \
|
||||
-e API_TOKEN=mytoken123 \
|
||||
-e API_TOKENS="ansible-token,docker-agent-token" \
|
||||
rcourtman/pulse:latest
|
||||
```
|
||||
|
||||
> Tip: Create one token per automation workflow (Ansible, Docker agents, CI jobs, etc.) so you can revoke individual credentials without touching others. Use **Settings → Security → API tokens** or `POST /api/security/tokens` to mint tokens programmatically.
|
||||
|
||||
### Advanced: Pre-Hashing (Optional)
|
||||
```bash
|
||||
# Generate bcrypt hash for password
|
||||
docker run --rm -it rcourtman/pulse:latest pulse hash-password
|
||||
|
||||
# Generate random API token
|
||||
openssl rand -hex 32
|
||||
# Generate random API tokens
|
||||
ANSIBLE_TOKEN=$(openssl rand -hex 32)
|
||||
DOCKER_AGENT_TOKEN=$(openssl rand -hex 32)
|
||||
# Then pass them to the container via API_TOKENS
|
||||
```
|
||||
|
||||
## Data Persistence
|
||||
@@ -252,7 +263,8 @@ Common problems:
|
||||
|----------|-------------|-------------------|
|
||||
| `PULSE_AUTH_USER` | Admin username | `admin` |
|
||||
| `PULSE_AUTH_PASS` | Admin password (plain text auto-hashed or bcrypt hash) | `super-secret-password` or `$2a$12$...` |
|
||||
| `API_TOKEN` | API access token (plain text or SHA3-256 hash) | `openssl rand -hex 24` |
|
||||
| `API_TOKEN` | Legacy single API token (optional fallback) | `openssl rand -hex 24` |
|
||||
| `API_TOKENS` | Comma-separated list of API tokens (plain or SHA3-256 hashed) | `ansible-token,docker-agent-token` |
|
||||
| `DISABLE_AUTH` | Disable authentication entirely | `false` |
|
||||
| `PULSE_AUDIT_LOG` | Enable security audit logging | `false` |
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ docker run -d \
|
||||
--name pulse \
|
||||
-p 7655:7655 \
|
||||
-v pulse_data:/data \
|
||||
-e API_TOKEN="your-secure-token" \
|
||||
-e API_TOKENS="ansible-token,docker-agent-token" \
|
||||
-e PULSE_AUTH_USER="admin" \
|
||||
-e PULSE_AUTH_PASS="your-password" \
|
||||
--restart unless-stopped \
|
||||
@@ -141,7 +141,8 @@ services:
|
||||
# Security (all optional - runs open by default)
|
||||
# - PULSE_AUTH_USER=admin # Username for web UI login
|
||||
# - PULSE_AUTH_PASS=your-password # Plain text or bcrypt hash (auto-hashed if plain)
|
||||
# - API_TOKEN=your-token # Plain text or SHA3-256 hash (auto-hashed if plain)
|
||||
# - API_TOKENS=token-a,token-b # Comma-separated tokens (plain or SHA3-256 hashed)
|
||||
# - API_TOKEN=legacy-token # Optional single-token fallback
|
||||
# - ALLOW_UNPROTECTED_EXPORT=false # Allow export without auth (default: false)
|
||||
|
||||
# Security: Plain text credentials are automatically hashed
|
||||
@@ -167,7 +168,7 @@ volumes:
|
||||
1. Open `http://<your-server>:7655`
|
||||
2. **Complete the mandatory security setup** (first-time only)
|
||||
3. Create your admin username and password
|
||||
4. Save the generated API token for automation
|
||||
4. Use **Settings → Security → API tokens** to issue dedicated tokens for automation (one token per integration makes revocation painless)
|
||||
|
||||
## Configure Proxmox/PBS Nodes
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ Yes! When you add one cluster node, Pulse automatically discovers and monitors a
|
||||
|
||||
### Authentication issues?
|
||||
- Password auth: Check `PULSE_AUTH_USER` and `PULSE_AUTH_PASS` environment variables
|
||||
- API token: Verify `API_TOKEN` is set correctly
|
||||
- API tokens: Ensure `API_TOKENS` includes an active credential (or `API_TOKEN` for legacy setups)
|
||||
- Session expired: Log in again via web UI
|
||||
- Account locked: Wait 15 minutes after 5 failed attempts
|
||||
|
||||
|
||||
@@ -51,7 +51,8 @@ Environment variables always override configuration files.
|
||||
## Why not .env?
|
||||
|
||||
The `/etc/pulse/.env` file is reserved exclusively for authentication credentials:
|
||||
- `API_TOKEN` - API authentication token (hashed)
|
||||
- `API_TOKENS` - One or more API authentication tokens (hashed)
|
||||
- `API_TOKEN` - Legacy single API token (hashed)
|
||||
- `PULSE_AUTH_USER` - Web UI username
|
||||
- `PULSE_AUTH_PASS` - Web UI password (hashed)
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ proxy_set_header X-Authentik-Groups $http_x_authentik_groups;
|
||||
Proxy authentication can work alongside other authentication methods:
|
||||
|
||||
- If `PROXY_AUTH_SECRET` is set, proxy auth takes precedence
|
||||
- API tokens (`API_TOKEN`) still work for programmatic access
|
||||
- API tokens (`API_TOKENS` or legacy `API_TOKEN`) still work for programmatic access
|
||||
- Basic auth (`PULSE_AUTH_USER`/`PULSE_AUTH_PASS`) can be used as fallback
|
||||
|
||||
## Troubleshooting
|
||||
@@ -248,4 +248,4 @@ A: Currently, Pulse has admin and non-admin roles. Non-admin users have read-onl
|
||||
A: Yes, the authenticated username appears in the top-right corner of the UI.
|
||||
|
||||
**Q: Can I use both proxy auth and API tokens?**
|
||||
A: Yes! API tokens still work for automation/scripts. Proxy auth is for human users via the web UI.
|
||||
A: Yes! API tokens still work for automation/scripts. Proxy auth is for human users via the web UI.
|
||||
|
||||
@@ -67,7 +67,7 @@ If you're comfortable with your security setup, you can dismiss warnings:
|
||||
### Security Features
|
||||
- **Logs**: Token values masked with `***` in all outputs
|
||||
- **API**: Frontend receives only `hasToken: true`, never actual values
|
||||
- **Export**: Requires API_TOKEN authentication to extract credentials
|
||||
- **Export**: Requires a valid API token (via `X-API-Token` header or `token` query parameter) to extract credentials
|
||||
- **Migration**: Use passphrase-protected export/import (see [Migration Guide](MIGRATION.md))
|
||||
- **Auto-Migration**: Unencrypted configs automatically migrate to encrypted format
|
||||
|
||||
@@ -75,19 +75,20 @@ If you're comfortable with your security setup, you can dismiss warnings:
|
||||
|
||||
By default, configuration export/import is blocked for security. You have two options:
|
||||
|
||||
### Option 1: Set API Token (Recommended)
|
||||
### Option 1: Set API Tokens (Recommended)
|
||||
```bash
|
||||
# Using systemd (secure)
|
||||
sudo systemctl edit pulse-backend
|
||||
# Add:
|
||||
[Service]
|
||||
Environment="API_TOKEN=your-48-char-hex-token"
|
||||
Environment="API_TOKENS=ansible-token,docker-agent-token"
|
||||
Environment="API_TOKEN=legacy-token" # Optional fallback
|
||||
|
||||
# Then restart:
|
||||
sudo systemctl restart pulse-backend
|
||||
|
||||
# Docker
|
||||
docker run -e API_TOKEN=your-token rcourtman/pulse:latest
|
||||
docker run -e API_TOKENS=ansible-token,docker-agent-token rcourtman/pulse:latest
|
||||
```
|
||||
|
||||
### Option 2: Allow Unprotected Export (Homelab)
|
||||
@@ -217,26 +218,30 @@ The Quick Security Setup automatically:
|
||||
- Generates a cryptographically secure token
|
||||
- Hashes it with SHA3-256
|
||||
- Stores only the 64-character hash
|
||||
- Adds the token to the managed token list
|
||||
|
||||
#### Manual Token Setup
|
||||
```bash
|
||||
# Using systemd (use SHA3-256 hash, not plain text!)
|
||||
# Using systemd (plain text values are auto-hashed on startup)
|
||||
sudo systemctl edit pulse-backend
|
||||
# Add:
|
||||
[Service]
|
||||
Environment="API_TOKEN=<64-char-sha3-256-hash>"
|
||||
Environment="API_TOKENS=ansible-token,docker-agent-token"
|
||||
|
||||
# Docker
|
||||
docker run -e API_TOKEN=<64-char-sha3-256-hash> rcourtman/pulse:latest
|
||||
docker run -e API_TOKENS=ansible-token,docker-agent-token rcourtman/pulse:latest
|
||||
|
||||
# To provide pre-hashed tokens instead, list the SHA3-256 hashes
|
||||
# Environment="API_TOKENS=83c8...,b1de..."
|
||||
```
|
||||
|
||||
**Security Note**: API tokens are automatically hashed with SHA3-256. Never store plain text tokens in configuration.
|
||||
**Security Note**: Tokens defined via environment variables are hashed with SHA3-256 before being stored on disk. Plain values never persist beyond startup.
|
||||
|
||||
#### Token Management (Settings → Security → API Token)
|
||||
- Generate new tokens via web UI when authenticated
|
||||
- View existing token anytime (authenticated users only)
|
||||
- Regenerate tokens without disrupting service
|
||||
- Delete tokens to disable API access
|
||||
#### Token Management (Settings → Security → API tokens)
|
||||
- Issue dedicated tokens for automation/agents without sharing a global credential
|
||||
- View prefixes/suffixes and last-used timestamps for auditing
|
||||
- Revoke tokens individually without downtime
|
||||
- Regenerate tokens when rotating credentials (new value displayed once)
|
||||
- All tokens stored as SHA3-256 hashes
|
||||
|
||||
#### Usage
|
||||
@@ -258,7 +263,7 @@ curl "http://localhost:7655/api/export?token=your-original-token"
|
||||
#### Secure Mode
|
||||
- Require API token for all operations
|
||||
- Protects auto-registration endpoint
|
||||
- Enable by setting API_TOKEN environment variable
|
||||
- Enable by setting at least one API token via `API_TOKENS` (or legacy `API_TOKEN`) environment variable
|
||||
|
||||
## CORS (Cross-Origin Resource Sharing)
|
||||
|
||||
@@ -349,9 +354,9 @@ curl -X POST http://localhost:7655/api/security/reset-lockout \
|
||||
## Troubleshooting
|
||||
|
||||
**Account locked?** Wait 15 minutes or contact admin for manual reset
|
||||
**Export blocked?** You're on a public network - login with password, set API_TOKEN, or set ALLOW_UNPROTECTED_EXPORT=true
|
||||
**Export blocked?** You're on a public network - login with password, set an API token (`API_TOKENS`), or set ALLOW_UNPROTECTED_EXPORT=true
|
||||
**Rate limited?** Wait 1 minute and try again
|
||||
**Can't login?** Check PULSE_AUTH_USER and PULSE_AUTH_PASS environment variables
|
||||
**API access denied?** Verify API_TOKEN is correct (use original token, not hash)
|
||||
**API access denied?** Verify the token you supplied matches one of the values created in Settings → Security → API tokens (use the original token value, not the hash)
|
||||
**CORS errors?** Configure ALLOWED_ORIGINS for your domain
|
||||
**Forgot password?** Start fresh - delete your Pulse data and restart
|
||||
**Forgot password?** Start fresh - delete your Pulse data and restart
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import { Component, createSignal, Show } from 'solid-js';
|
||||
import { showSuccess, showError } from '@/utils/toast';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
|
||||
interface APIOnlySetupProps {
|
||||
onTokenGenerated?: () => void;
|
||||
}
|
||||
|
||||
export const APIOnlySetup: Component<APIOnlySetupProps> = (props) => {
|
||||
const [isGenerating, setIsGenerating] = createSignal(false);
|
||||
const [token, setToken] = createSignal<string | null>(null);
|
||||
const [showToken, setShowToken] = createSignal(false);
|
||||
const [copied, setCopied] = createSignal(false);
|
||||
|
||||
const generateToken = async () => {
|
||||
setIsGenerating(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/security/regenerate-token', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(error || 'Failed to generate token');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setToken(data.token);
|
||||
setShowToken(true);
|
||||
showSuccess("API token generated! Save it now - it won't be shown again.");
|
||||
} catch (error) {
|
||||
showError(`Failed to generate token: ${error}`);
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (!token()) return;
|
||||
|
||||
const success = await copyToClipboard(token()!);
|
||||
if (success) {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} else {
|
||||
showError('Failed to copy to clipboard');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="space-y-4">
|
||||
<Show when={!showToken()}>
|
||||
<div class="space-y-4">
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">
|
||||
Generate an API token for programmatic access. Use it for:
|
||||
</p>
|
||||
<ul class="list-disc list-inside text-xs text-gray-600 dark:text-gray-400 space-y-1">
|
||||
<li>Automation scripts and CI/CD pipelines</li>
|
||||
<li>Monitoring integrations</li>
|
||||
<li>Third-party applications</li>
|
||||
</ul>
|
||||
|
||||
<div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-3">
|
||||
<p class="text-xs text-amber-700 dark:text-amber-300">
|
||||
<strong>Note:</strong> Without password authentication enabled, the UI will remain
|
||||
publicly accessible.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={generateToken}
|
||||
disabled={isGenerating()}
|
||||
class="px-4 py-2 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{isGenerating() ? 'Generating...' : 'Generate API Token'}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={showToken() && token()}>
|
||||
<div class="space-y-4">
|
||||
<div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4">
|
||||
<h4 class="text-sm font-semibold text-green-800 dark:text-green-200 mb-2">
|
||||
✅ API Token Generated!
|
||||
</h4>
|
||||
<p class="text-xs text-green-700 dark:text-green-300">
|
||||
Save this token now - it will never be shown again!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-3">
|
||||
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Your API Token
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<code class="flex-1 font-mono text-sm bg-white dark:bg-gray-800 px-3 py-2 rounded border border-gray-200 dark:border-gray-700 break-all">
|
||||
{token()}
|
||||
</code>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
class="px-3 py-2 text-xs bg-gray-600 text-white rounded hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
{copied() ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowToken(false);
|
||||
setToken(null);
|
||||
if (props.onTokenGenerated) {
|
||||
props.onTokenGenerated();
|
||||
}
|
||||
}}
|
||||
class="px-4 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Done - I've Saved My Token
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import { formatRelativeTime } from '@/utils/format';
|
||||
|
||||
interface APITokenManagerProps {
|
||||
currentTokenHint?: string;
|
||||
onTokensChanged?: () => void;
|
||||
}
|
||||
|
||||
export const APITokenManager: Component<APITokenManagerProps> = (props) => {
|
||||
@@ -47,6 +48,7 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
|
||||
setNewTokenRecord(record);
|
||||
setNameInput('');
|
||||
showSuccess('New API token generated! Save it now – it will not be shown again.');
|
||||
props.onTokensChanged?.();
|
||||
|
||||
try {
|
||||
window.localStorage.setItem('apiToken', token);
|
||||
@@ -88,6 +90,7 @@ export const APITokenManager: Component<APITokenManagerProps> = (props) => {
|
||||
await SecurityAPI.deleteToken(record.id);
|
||||
setTokens((prev) => prev.filter((token) => token.id !== record.id));
|
||||
showSuccess('Token revoked');
|
||||
props.onTokensChanged?.();
|
||||
|
||||
const current = newTokenRecord();
|
||||
if (current && current.id === record.id) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component, createSignal, Show, createMemo, createEffect } from 'solid-js';
|
||||
import { apiFetch } from '@/utils/apiClient';
|
||||
import { SecurityAPI, type APITokenRecord } from '@/api/security';
|
||||
|
||||
interface CommandBuilderProps {
|
||||
command: string;
|
||||
@@ -7,7 +8,7 @@ interface CommandBuilderProps {
|
||||
storedToken?: string | null;
|
||||
currentTokenHint?: string; // Masked token preview (e.g., "abc12***...xyz89")
|
||||
onTokenChange?: (token: string) => void;
|
||||
onTokenGenerated?: (token: string) => void;
|
||||
onTokenGenerated?: (token: string, record: APITokenRecord) => void;
|
||||
requiresToken: boolean;
|
||||
hasExistingToken?: boolean;
|
||||
}
|
||||
@@ -21,10 +22,16 @@ export const CommandBuilder: Component<CommandBuilderProps> = (props) => {
|
||||
|
||||
// Token generation/revocation state
|
||||
const [showGenerateModal, setShowGenerateModal] = createSignal(false);
|
||||
const [showRevokeModal, setShowRevokeModal] = createSignal(false);
|
||||
const [isGenerating, setIsGenerating] = createSignal(false);
|
||||
const [newlyGeneratedToken, setNewlyGeneratedToken] = createSignal<string | null>(null);
|
||||
const [showNewTokenModal, setShowNewTokenModal] = createSignal(false);
|
||||
const [tokenLabel, setTokenLabel] = createSignal('Docker agent token');
|
||||
|
||||
const defaultTokenLabel = () => `Docker agent token ${new Date().toISOString().slice(0, 10)}`;
|
||||
const openGenerateModal = () => {
|
||||
setTokenLabel(defaultTokenLabel());
|
||||
setShowGenerateModal(true);
|
||||
};
|
||||
|
||||
// Initialize with stored token if available
|
||||
createEffect(() => {
|
||||
@@ -158,46 +165,41 @@ export const CommandBuilder: Component<CommandBuilderProps> = (props) => {
|
||||
|
||||
// Generate new token
|
||||
const generateNewToken = async () => {
|
||||
setShowGenerateModal(false);
|
||||
if (isGenerating()) return;
|
||||
setIsGenerating(true);
|
||||
|
||||
try {
|
||||
const response = await apiFetch('/api/security/regenerate-token', {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(error || 'Failed to generate token');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const newToken = data.token;
|
||||
const desiredName = tokenLabel().trim() || undefined;
|
||||
const { token: newToken, record } = await SecurityAPI.createToken(desiredName);
|
||||
|
||||
setShowGenerateModal(false);
|
||||
setNewlyGeneratedToken(newToken);
|
||||
setShowNewTokenModal(true);
|
||||
|
||||
// Auto-populate the command builder
|
||||
setTokenInput(newToken);
|
||||
if (props.onTokenGenerated) {
|
||||
props.onTokenGenerated(newToken);
|
||||
setTokenInput(newToken);
|
||||
if (props.onTokenGenerated) {
|
||||
props.onTokenGenerated(newToken, record);
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
window.localStorage.setItem('apiToken', newToken);
|
||||
window.dispatchEvent(new StorageEvent('storage', { key: 'apiToken', newValue: newToken }));
|
||||
} catch (storageErr) {
|
||||
console.warn('Unable to persist API token in localStorage', storageErr);
|
||||
}
|
||||
}
|
||||
|
||||
window.showToast('success', 'New API token generated successfully!');
|
||||
window.showToast('success', 'New API token generated. Save it now – it will not be shown again.');
|
||||
} catch (error) {
|
||||
console.error('Token generation failed:', error);
|
||||
window.showToast('error', `Failed to generate token: ${error}`);
|
||||
window.showToast('error', error instanceof Error ? error.message : 'Failed to generate token');
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Revoke and replace token
|
||||
const revokeAndReplace = async () => {
|
||||
setShowRevokeModal(false);
|
||||
await generateNewToken();
|
||||
};
|
||||
|
||||
// Use existing token
|
||||
const useExistingToken = () => {
|
||||
if (props.storedToken) {
|
||||
@@ -294,11 +296,11 @@ export const CommandBuilder: Component<CommandBuilderProps> = (props) => {
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowGenerateModal(true)}
|
||||
onClick={openGenerateModal}
|
||||
disabled={isGenerating()}
|
||||
class="px-3 py-1.5 text-xs font-medium text-white bg-blue-600 rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors whitespace-nowrap"
|
||||
>
|
||||
{isGenerating() ? 'Generating...' : 'Generate New Token'}
|
||||
{isGenerating() ? 'Generating...' : 'Generate API Token'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -341,15 +343,18 @@ export const CommandBuilder: Component<CommandBuilderProps> = (props) => {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowRevokeModal(true)}
|
||||
onClick={openGenerateModal}
|
||||
disabled={isGenerating()}
|
||||
class="px-3 py-1.5 text-xs font-medium text-red-700 dark:text-red-300 bg-red-100 dark:bg-red-900/40 rounded hover:bg-red-200 dark:hover:bg-red-900/60 disabled:opacity-50 disabled:cursor-not-allowed transition-colors whitespace-nowrap"
|
||||
title="Revoke current token and generate a new one"
|
||||
class="px-3 py-1.5 text-xs font-medium text-blue-700 dark:text-blue-300 bg-blue-100 dark:bg-blue-900/40 rounded hover:bg-blue-200 dark:hover:bg-blue-900/60 disabled:opacity-50 disabled:cursor-not-allowed transition-colors whitespace-nowrap"
|
||||
title="Generate another token for a new host or automation workflow"
|
||||
>
|
||||
Revoke & Replace
|
||||
Generate Token
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-600 dark:text-gray-400">
|
||||
Manage or revoke tokens from the table above whenever a credential is no longer needed.
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
@@ -479,18 +484,29 @@ export const CommandBuilder: Component<CommandBuilderProps> = (props) => {
|
||||
<Show when={showGenerateModal()}>
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Generate New API Token?</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Generate API Token</h3>
|
||||
<div class="space-y-3 mb-6">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
This will generate a new API token for Docker agent authentication.
|
||||
Create a dedicated token for this host or automation workflow. Tokens remain active until you revoke them from the API tokens list.
|
||||
</p>
|
||||
<Show when={props.hasExistingToken}>
|
||||
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded p-3">
|
||||
<p class="text-xs text-red-800 dark:text-red-300 font-medium">
|
||||
⚠️ This will immediately invalidate your existing token. All Docker agents using the old token will need to be updated with the new token.
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-gray-600 dark:text-gray-400" for="command-builder-token-name">
|
||||
Token name (optional)
|
||||
</label>
|
||||
<input
|
||||
id="command-builder-token-name"
|
||||
type="text"
|
||||
value={tokenLabel()}
|
||||
onInput={(event) => setTokenLabel(event.currentTarget.value)}
|
||||
class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder={defaultTokenLabel()}
|
||||
/>
|
||||
</div>
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded p-3">
|
||||
<p class="text-xs text-blue-800 dark:text-blue-300 font-medium">
|
||||
Tip: Issue one token per host so you can revoke compromised credentials without affecting other agents.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button
|
||||
@@ -503,44 +519,10 @@ export const CommandBuilder: Component<CommandBuilderProps> = (props) => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={generateNewToken}
|
||||
class="px-4 py-2 text-sm text-white bg-blue-600 rounded hover:bg-blue-700 transition-colors"
|
||||
disabled={isGenerating()}
|
||||
class="px-4 py-2 text-sm text-white bg-blue-600 rounded hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Generate Token
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Revoke & Replace Confirmation Modal */}
|
||||
<Show when={showRevokeModal()}>
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Revoke & Replace Token?</h3>
|
||||
<div class="space-y-3 mb-6">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
This will immediately invalidate your current token and generate a new one.
|
||||
</p>
|
||||
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded p-3">
|
||||
<p class="text-xs text-red-800 dark:text-red-300 font-medium">
|
||||
⚠️ All Docker agents using the old token will stop working immediately. You'll need to update them with the new token.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowRevokeModal(false)}
|
||||
class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={revokeAndReplace}
|
||||
class="px-4 py-2 text-sm text-white bg-red-600 rounded hover:bg-red-700 transition-colors"
|
||||
>
|
||||
Revoke & Replace
|
||||
{isGenerating() ? 'Generating…' : 'Generate Token'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -358,10 +358,31 @@ WantedBy=multi-user.target`;
|
||||
currentTokenHint={securityStatus()?.apiTokenHint}
|
||||
requiresToken={requiresToken()}
|
||||
hasExistingToken={Boolean(securityStatus()?.apiTokenConfigured)}
|
||||
onTokenGenerated={(token) => {
|
||||
onTokenGenerated={(token, record) => {
|
||||
setApiToken(token);
|
||||
if (typeof window !== 'undefined' && window.localStorage.getItem('apiToken')) {
|
||||
window.localStorage.setItem('apiToken', token);
|
||||
setAvailableTokens((prev) => {
|
||||
const filtered = prev.filter((existing) => existing.id !== record.id);
|
||||
return [record, ...filtered];
|
||||
});
|
||||
setSecurityStatus((prev) => {
|
||||
if (!prev) return prev;
|
||||
const hint =
|
||||
record.prefix && record.suffix
|
||||
? `${record.prefix}…${record.suffix}`
|
||||
: prev.apiTokenHint;
|
||||
return {
|
||||
...prev,
|
||||
apiTokenConfigured: true,
|
||||
apiTokenHint: hint || prev.apiTokenHint,
|
||||
};
|
||||
});
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
window.localStorage.setItem('apiToken', token);
|
||||
window.dispatchEvent(new StorageEvent('storage', { key: 'apiToken', newValue: token }));
|
||||
} catch (err) {
|
||||
console.warn('Unable to persist API token in localStorage', err);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -965,7 +965,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||
setShowApiTokenModal(true);
|
||||
return;
|
||||
}
|
||||
if (errorText.includes('API_TOKEN')) {
|
||||
if (errorText.includes('API_TOKEN') || errorText.includes('API_TOKENS')) {
|
||||
setApiTokenModalSource('export');
|
||||
setShowApiTokenModal(true);
|
||||
return;
|
||||
@@ -1082,7 +1082,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||
setShowApiTokenModal(true);
|
||||
return;
|
||||
}
|
||||
if (errorText.includes('API_TOKEN')) {
|
||||
if (errorText.includes('API_TOKEN') || errorText.includes('API_TOKENS')) {
|
||||
setApiTokenModalSource('import');
|
||||
setShowApiTokenModal(true);
|
||||
return;
|
||||
@@ -2661,7 +2661,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||
<div class="text-sm text-blue-800 dark:text-blue-200">
|
||||
<p class="font-medium mb-1">Configuration Priority</p>
|
||||
<ul class="space-y-1">
|
||||
<li>• Some env vars override settings (API_TOKEN, PORTS, AUTH)</li>
|
||||
<li>• Some env vars override settings (API_TOKENS, legacy API_TOKEN, PORTS, AUTH)</li>
|
||||
<li>• Changes made here are saved to system.json immediately</li>
|
||||
<li>• Settings persist unless overridden by env vars</li>
|
||||
</ul>
|
||||
@@ -3588,7 +3588,12 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||
|
||||
{/* Content */}
|
||||
<div class="p-6">
|
||||
<APITokenManager currentTokenHint={securityStatus()?.apiTokenHint} />
|
||||
<APITokenManager
|
||||
currentTokenHint={securityStatus()?.apiTokenHint}
|
||||
onTokensChanged={() => {
|
||||
void loadSecurityStatus();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Show>
|
||||
@@ -4980,7 +4985,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 rounded p-2">
|
||||
<p class="font-semibold mb-1">The API token is set as an environment variable:</p>
|
||||
<code class="block">API_TOKEN=your-secure-token</code>
|
||||
<code class="block">API_TOKENS=token-for-export,token-for-automation</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
export interface AuthConfig {
|
||||
PULSE_AUTH_USER: string; // Admin username
|
||||
PULSE_AUTH_PASS: string; // Bcrypt hashed password
|
||||
API_TOKEN: string; // API authentication token
|
||||
API_TOKEN: string; // Legacy API authentication token (hashed)
|
||||
API_TOKENS?: string; // Optional comma-separated list of hashed tokens
|
||||
ENABLE_AUDIT_LOG?: boolean; // @deprecated - use PULSE_AUDIT_LOG
|
||||
PULSE_AUDIT_LOG?: boolean; // Enable audit logging
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Package config manages Pulse configuration from multiple sources.
|
||||
//
|
||||
// Configuration File Separation:
|
||||
// - .env: Authentication credentials ONLY (PULSE_AUTH_USER, PULSE_AUTH_PASS, API_TOKEN)
|
||||
// - .env: Authentication credentials ONLY (PULSE_AUTH_USER, PULSE_AUTH_PASS, API_TOKEN/API_TOKENS)
|
||||
// - system.json: Application settings (polling interval, timeouts, update settings, etc.)
|
||||
// - nodes.enc: Encrypted node credentials (PVE/PBS passwords and tokens)
|
||||
//
|
||||
|
||||
@@ -11,6 +11,11 @@ NC='\033[0m'
|
||||
|
||||
MODE="full"
|
||||
API_TOKEN="${API_TOKEN:-}"
|
||||
# Allow new multi-token env var to provide the test credential
|
||||
if [ -z "$API_TOKEN" ] && [ -n "${API_TOKENS:-}" ]; then
|
||||
# Use the first token in the comma-separated list
|
||||
API_TOKEN="$(printf '%s\n' "$API_TOKENS" | tr ',' '\n' | head -n1)"
|
||||
fi
|
||||
PULSE_URL="${PULSE_URL:-http://localhost:7655}"
|
||||
FAILED=0
|
||||
PASSED=0
|
||||
@@ -139,4 +144,4 @@ else
|
||||
echo -e "${YELLOW}⚠️ Some tests failed. Check logs in /tmp/${NC}"
|
||||
echo -e "${BLUE}Note: Your production nodes were protected during testing${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user