diff --git a/API.md b/API.md
index 9dbda36..bf36722 100644
--- a/API.md
+++ b/API.md
@@ -17,6 +17,7 @@ Complete API reference for the Web CLI application. All endpoints return JSON re
- [Environment Variables Management](#environment-variables-management)
- [Bash Scripts Management](#bash-scripts-management)
- [Script Presets Management](#script-presets-management)
+- [Vault Integration](#vault-integration)
- [Interactive Terminal](#interactive-terminal-websocket)
- [Error Responses](#error-responses)
- [Security Considerations](#security-considerations)
@@ -76,6 +77,19 @@ Default port is `7777`, configurable via `-port` flag or `PORT` environment vari
| `/script-presets/{id}` | GET | Get single script preset |
| `/script-presets/{id}` | PUT | Update script preset |
| `/script-presets/{id}` | DELETE | Delete script preset |
+| `/vault/config` | GET | Get Vault configuration |
+| `/vault/config` | POST | Create/update Vault configuration |
+| `/vault/config` | DELETE | Delete Vault configuration |
+| `/vault/status` | GET | Get Vault connection status |
+| `/vault/test` | POST | Test Vault connection |
+| `/vault/ssh-keys` | GET | List SSH keys from Vault |
+| `/vault/ssh-keys` | POST | Create SSH key in Vault |
+| `/vault/servers` | GET | List servers from Vault |
+| `/vault/servers` | POST | Create server in Vault |
+| `/vault/env-variables` | GET | List environment variables from Vault |
+| `/vault/env-variables` | POST | Create environment variable in Vault |
+| `/vault/bash-scripts` | GET | List bash scripts from Vault |
+| `/vault/bash-scripts` | POST | Create bash script in Vault |
## Authentication
@@ -2022,6 +2036,534 @@ curl -X DELETE http://localhost:7777/api/script-presets/1
---
+## Vault Integration
+
+HashiCorp Vault integration allows you to store and retrieve secrets (SSH keys, servers, environment variables, and bash scripts) from an external Vault server. This provides centralized secrets management with additional security features.
+
+### Get Vault Configuration
+
+Retrieve the current Vault configuration (token is never returned).
+
+**Endpoint**: `GET /vault/config`
+
+**Response**: `200 OK`
+
+```json
+{
+ "id": 1,
+ "address": "https://vault.example.com:8200",
+ "namespace": "admin",
+ "mount_path": "secret",
+ "enabled": true,
+ "has_token": true,
+ "created_at": "2025-01-15T10:00:00Z",
+ "updated_at": "2025-01-15T10:00:00Z"
+}
+```
+
+**Fields**:
+- `address` (string): Vault server URL
+- `namespace` (string): Optional Vault namespace
+- `mount_path` (string): KV secrets engine mount path (default: "secret")
+- `enabled` (boolean): Whether Vault integration is active
+- `has_token` (boolean): Indicates if a token is configured (token value never exposed)
+
+**Example**:
+
+```bash
+curl http://localhost:7777/api/vault/config
+```
+
+---
+
+### Create/Update Vault Configuration
+
+Configure or update the Vault connection settings.
+
+**Endpoint**: `POST /vault/config`
+
+**Request Headers**:
+- `Content-Type: application/json`
+
+**Request Body**:
+
+```json
+{
+ "address": "https://vault.example.com:8200",
+ "token": "hvs.your-vault-token",
+ "namespace": "admin",
+ "mount_path": "secret",
+ "enabled": true
+}
+```
+
+**Fields**:
+- `address` (string, required): Vault server URL
+- `token` (string, required for new config): Vault authentication token (encrypted with AES-256-GCM before storage)
+- `namespace` (string, optional): Vault namespace for enterprise deployments
+- `mount_path` (string, optional): KV secrets engine mount path (default: "secret")
+- `enabled` (boolean, optional): Enable/disable Vault integration
+
+**Response**: `200 OK`
+
+```json
+{
+ "id": 1,
+ "address": "https://vault.example.com:8200",
+ "namespace": "admin",
+ "mount_path": "secret",
+ "enabled": true,
+ "has_token": true,
+ "created_at": "2025-01-15T10:00:00Z",
+ "updated_at": "2025-01-15T10:00:00Z"
+}
+```
+
+**Error Responses**:
+- `400 Bad Request`: Missing required fields (address or token)
+
+**Example**:
+
+```bash
+curl -X POST http://localhost:7777/api/vault/config \
+ -H "Content-Type: application/json" \
+ -d '{
+ "address": "https://vault.example.com:8200",
+ "token": "hvs.your-vault-token",
+ "mount_path": "secret",
+ "enabled": true
+ }'
+```
+
+---
+
+### Delete Vault Configuration
+
+Remove the Vault configuration.
+
+**Endpoint**: `DELETE /vault/config`
+
+**Response**: `204 No Content`
+
+**Example**:
+
+```bash
+curl -X DELETE http://localhost:7777/api/vault/config
+```
+
+---
+
+### Get Vault Status
+
+Check the current Vault connection status.
+
+**Endpoint**: `GET /vault/status`
+
+**Response**: `200 OK`
+
+```json
+{
+ "configured": true,
+ "enabled": true,
+ "connected": true,
+ "address": "https://vault.example.com:8200",
+ "vault_sealed": false
+}
+```
+
+**Fields**:
+- `configured` (boolean): Whether Vault settings exist
+- `enabled` (boolean): Whether Vault integration is enabled
+- `connected` (boolean): Whether connection test passed
+- `address` (string): Vault server URL
+- `error` (string, optional): Error message if connection failed
+- `vault_sealed` (boolean, optional): Whether Vault is sealed
+
+**Example**:
+
+```bash
+curl http://localhost:7777/api/vault/status
+```
+
+---
+
+### Test Vault Connection
+
+Test the connection to Vault and initialize the secrets structure.
+
+**Endpoint**: `POST /vault/test`
+
+**Response**: `200 OK`
+
+```json
+{
+ "configured": true,
+ "enabled": true,
+ "connected": true,
+ "address": "https://vault.example.com:8200",
+ "vault_sealed": false
+}
+```
+
+**Note**: On successful connection, this endpoint automatically initializes the Vault secrets structure (`webcli/ssh-keys`, `webcli/servers`, `webcli/env-variables`, `webcli/bash-scripts`).
+
+**Example**:
+
+```bash
+curl -X POST http://localhost:7777/api/vault/test
+```
+
+---
+
+### List SSH Keys from Vault
+
+Retrieve all SSH keys stored in Vault.
+
+**Endpoint**: `GET /vault/ssh-keys`
+
+**Response**: `200 OK`
+
+```json
+[
+ {
+ "name": "production-key",
+ "private_key": "-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----",
+ "group": "production",
+ "created_at": "2025-01-15T10:00:00Z"
+ }
+]
+```
+
+**Error Responses**:
+- `400 Bad Request`: Vault not configured or disabled
+
+**Example**:
+
+```bash
+curl http://localhost:7777/api/vault/ssh-keys
+```
+
+---
+
+### Create SSH Key in Vault
+
+Store a new SSH key in Vault.
+
+**Endpoint**: `POST /vault/ssh-keys`
+
+**Request Headers**:
+- `Content-Type: application/json`
+
+**Request Body**:
+
+```json
+{
+ "name": "production-key",
+ "private_key": "-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----",
+ "group": "production"
+}
+```
+
+**Fields**:
+- `name` (string, required): Key identifier
+- `private_key` (string, required): PEM-encoded SSH private key
+- `group` (string, optional): Group for organization (default: "default")
+
+**Response**: `201 Created`
+
+```json
+{
+ "name": "production-key",
+ "group": "production",
+ "created_at": "2025-01-15T10:00:00Z",
+ "source": "vault"
+}
+```
+
+**Error Responses**:
+- `400 Bad Request`: Missing required fields or Vault not configured
+
+**Example**:
+
+```bash
+curl -X POST http://localhost:7777/api/vault/ssh-keys \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "production-key",
+ "private_key": "-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----",
+ "group": "production"
+ }'
+```
+
+---
+
+### List Servers from Vault
+
+Retrieve all server configurations stored in Vault.
+
+**Endpoint**: `GET /vault/servers`
+
+**Response**: `200 OK`
+
+```json
+[
+ {
+ "name": "prod-web-01",
+ "ip_address": "10.0.0.50",
+ "port": 22,
+ "username": "deploy",
+ "group": "production"
+ }
+]
+```
+
+**Error Responses**:
+- `400 Bad Request`: Vault not configured or disabled
+
+**Example**:
+
+```bash
+curl http://localhost:7777/api/vault/servers
+```
+
+---
+
+### Create Server in Vault
+
+Store a new server configuration in Vault.
+
+**Endpoint**: `POST /vault/servers`
+
+**Request Headers**:
+- `Content-Type: application/json`
+
+**Request Body**:
+
+```json
+{
+ "name": "prod-web-01",
+ "ip_address": "10.0.0.50",
+ "port": 22,
+ "username": "deploy",
+ "group": "production"
+}
+```
+
+**Fields**:
+- `name` (string, optional): Server name/alias (at least one of name or ip_address required)
+- `ip_address` (string, optional): Server IP address or hostname
+- `port` (integer, optional): SSH port (default: 22)
+- `username` (string, optional): SSH username (default: "root")
+- `group` (string, optional): Group for organization (default: "default")
+
+**Response**: `201 Created`
+
+```json
+{
+ "name": "prod-web-01",
+ "ip_address": "10.0.0.50",
+ "port": 22,
+ "username": "deploy",
+ "group": "production",
+ "source": "vault"
+}
+```
+
+**Error Responses**:
+- `400 Bad Request`: Missing required fields or Vault not configured
+
+**Example**:
+
+```bash
+curl -X POST http://localhost:7777/api/vault/servers \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "prod-web-01",
+ "ip_address": "10.0.0.50",
+ "port": 22,
+ "username": "deploy",
+ "group": "production"
+ }'
+```
+
+---
+
+### List Environment Variables from Vault
+
+Retrieve all environment variables stored in Vault.
+
+**Endpoint**: `GET /vault/env-variables`
+
+**Response**: `200 OK`
+
+```json
+[
+ {
+ "name": "API_SECRET",
+ "value": "super-secret-value",
+ "description": "External API authentication secret",
+ "group": "production"
+ }
+]
+```
+
+**Note**: Unlike local environment variables, Vault env variables are returned with full values (not masked).
+
+**Error Responses**:
+- `400 Bad Request`: Vault not configured or disabled
+
+**Example**:
+
+```bash
+curl http://localhost:7777/api/vault/env-variables
+```
+
+---
+
+### Create Environment Variable in Vault
+
+Store a new environment variable in Vault.
+
+**Endpoint**: `POST /vault/env-variables`
+
+**Request Headers**:
+- `Content-Type: application/json`
+
+**Request Body**:
+
+```json
+{
+ "name": "API_SECRET",
+ "value": "super-secret-value",
+ "description": "External API authentication secret",
+ "group": "production"
+}
+```
+
+**Fields**:
+- `name` (string, required): Variable name
+- `value` (string, required): Variable value
+- `description` (string, optional): Description
+- `group` (string, optional): Group for organization (default: "default")
+
+**Response**: `201 Created`
+
+```json
+{
+ "name": "API_SECRET",
+ "description": "External API authentication secret",
+ "group": "production",
+ "source": "vault"
+}
+```
+
+**Error Responses**:
+- `400 Bad Request`: Missing required fields or Vault not configured
+
+**Example**:
+
+```bash
+curl -X POST http://localhost:7777/api/vault/env-variables \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "API_SECRET",
+ "value": "super-secret-value",
+ "description": "External API authentication secret",
+ "group": "production"
+ }'
+```
+
+---
+
+### List Bash Scripts from Vault
+
+Retrieve all bash scripts stored in Vault.
+
+**Endpoint**: `GET /vault/bash-scripts`
+
+**Response**: `200 OK`
+
+```json
+[
+ {
+ "name": "deploy-script",
+ "description": "Production deployment script",
+ "content": "#!/bin/bash\necho \"Deploying...\"",
+ "filename": "deploy.sh",
+ "group": "production"
+ }
+]
+```
+
+**Error Responses**:
+- `400 Bad Request`: Vault not configured or disabled
+
+**Example**:
+
+```bash
+curl http://localhost:7777/api/vault/bash-scripts
+```
+
+---
+
+### Create Bash Script in Vault
+
+Store a new bash script in Vault.
+
+**Endpoint**: `POST /vault/bash-scripts`
+
+**Request Headers**:
+- `Content-Type: application/json`
+
+**Request Body**:
+
+```json
+{
+ "name": "deploy-script",
+ "description": "Production deployment script",
+ "content": "#!/bin/bash\nset -e\necho \"Deploying...\"\ncd /opt/app\ngit pull\nsystemctl restart app",
+ "filename": "deploy.sh",
+ "group": "production"
+}
+```
+
+**Fields**:
+- `name` (string, required): Script name
+- `content` (string, required): Bash script content
+- `description` (string, optional): Description
+- `filename` (string, optional): Original filename
+- `group` (string, optional): Group for organization (default: "default")
+
+**Response**: `201 Created`
+
+```json
+{
+ "name": "deploy-script",
+ "description": "Production deployment script",
+ "filename": "deploy.sh",
+ "group": "production",
+ "source": "vault"
+}
+```
+
+**Error Responses**:
+- `400 Bad Request`: Missing required fields or Vault not configured
+
+**Example**:
+
+```bash
+curl -X POST http://localhost:7777/api/vault/bash-scripts \
+ -H "Content-Type: application/json" \
+ -d '{
+ "name": "deploy-script",
+ "description": "Production deployment script",
+ "content": "#!/bin/bash\necho \"Deploying...\"",
+ "group": "production"
+ }'
+```
+
+---
+
## Error Responses
All API endpoints use standard HTTP status codes and return JSON error responses.
diff --git a/README.md b/README.md
index 67b2708..a91402a 100644
--- a/README.md
+++ b/README.md
@@ -9,699 +9,23 @@
A powerful web-based interface for executing commands on local and remote Linux servers. Built with Go and React with Material-UI for a professional, modern user experience.

-*Main dashboard with feature cards for easy navigation*
-
-## 📋 Table of Contents
-
-- [Features](#-features)
-- [Tech Stack](#-tech-stack)
-- [Screenshots](#-screenshots)
-- [Prerequisites](#-prerequisites)
-- [Installation](#-installation)
-- [Quick Start](#-quick-start)
-- [Usage](#-usage)
-- [API Documentation](#-api-documentation)
-- [Configuration](#-configuration)
-- [Security](#-security)
-- [Deployment](#-deployment)
-- [Development](#-development)
-- [Contributing](#-contributing)
-- [License](#-license)
-
-## ✨ Features
-
-### Core Functionality
-- **Web-Based Interface**: Professional Material-UI dashboard accessible from any browser
-- **Theme Support**: Dark and light mode toggle with preferences saved locally
-- **Multi-Platform**: Compiled binaries for Linux x64, macOS Intel, and macOS Apple Silicon
-
-### Interactive Terminal
-- **Full Browser-Based Terminal**: Real-time terminal emulator powered by xterm.js with WebSocket backend
-- **Multiple Shell Support**: Choose between Bash, Zsh, or Sh shells
-- **SSH Key Integration**: Select SSH keys from the dropdown - automatically injected into terminal sessions
-- **Server Name Resolution**: Servers from Admin Panel automatically available as SSH hostname aliases
-- **Dynamic Resize**: Terminal automatically resizes with proper 256-color support
-- **Fullscreen Mode**: Toggle fullscreen for distraction-free terminal access
-
-### Command Execution
-- **Local Commands**: Execute bash commands on your local server with real-time output
-- **Remote Commands**: Connect and execute commands on remote servers via SSH
-- **User Selection**: Run commands as different users (current, root, or custom)
-- **Sudo Support**: Secure password dialog for root command execution
-
-### Server & SSH Management
-- **Admin Panel**: Full management interface with tabbed sections
-- **SSH Key Management**: CRUD operations for SSH private keys
-- **Server Management**: Manage remote servers with hostname, IP, port, and username
-- **Hostname Validation**: Automatic validation according to hostname conventions
-
-### Command Templates & History
-- **Command Templates**: Save frequently-used commands for quick re-execution
-- **Command Type Management**: Visual indicators (Local/Remote) with type switching
-- **Smart Navigation**: Execute button routes to appropriate page based on command type
-- **Command History**: Complete execution history with output, exit codes, and timing
-
-### Environment Variables
-- **Secure Storage**: Store sensitive environment variables with AES-256-GCM encryption
-- **Masked Display**: Values masked by default in API responses
-- **Script Injection**: Inject stored env vars into script executions
-
-### Bash Scripts
-- **Script Library**: Store and manage reusable bash scripts
-- **Local & Remote Execution**: Execute scripts on local or remote servers
-- **Script Presets**: Save execution configurations (server, SSH key, user, env vars)
-- **Environment Integration**: Inject stored environment variables into scripts
-
-### Developer Tools
-- **YAML Validator**: Validate, format, and auto-fix YAML documents with Monaco Editor
-- **JSON Validator**: Validate, format, and auto-fix JSON data with Monaco Editor
-- **Auto-Fix**: Automatic repair of common issues (tabs, indentation, trailing commas, etc.)
-- **Diff View**: Side-by-side comparison showing exactly what was changed by auto-fix
-- **Syntax Highlighting**: Full VS Code-style editing experience
-
-### Security
-- **Authentication**: HTTP Basic Auth and Bearer token support (configurable)
-- **TLS/HTTPS Support**: Native TLS support with optional HTTPS enforcement
-- **Security Headers**: X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy
-- **SSH Host Key Verification**: Proper host key checking against known_hosts with TOFU support
-- **Input Validation**: Comprehensive validation for all user inputs
-- **Encrypted Database**: SQLite with AES-256-GCM encryption for sensitive data
-- **bcrypt Password Hashing**: Secure password hashing with cost factor 12
-- **HTTP Timeouts**: Protection against slowloris and DoS attacks
-- **CORS Policy**: Restrictive CORS with configurable origins
-- **Secure Password Handling**: SSH passwords never stored in command history
-- **Automatic Encryption**: All SSH keys, scripts, and env variables encrypted at rest
-- **Entropy Verification**: System entropy check before cryptographic key generation (Linux)
-- **Database Migrations**: Automatic schema versioning and migration system
-
-## 🛠 Tech Stack
-
-### Backend
-- **[Go 1.21+](https://golang.org/)**: High-performance backend server
-- **[Gorilla Mux](https://github.com/gorilla/mux)**: HTTP router for API endpoints
-- **[Gorilla WebSocket](https://github.com/gorilla/websocket)**: WebSocket support for interactive terminal
-- **[creack/pty](https://github.com/creack/pty)**: PTY (pseudo-terminal) handling for terminal emulation
-- **[Viper](https://github.com/spf13/viper)**: Configuration management with env, file, and flag support
-- **[Swaggo/swag](https://github.com/swaggo/swag)**: Swagger API documentation generator
-- **[SQLite](https://www.sqlite.org/)**: Embedded database with migration support
-- **[golang.org/x/crypto/ssh](https://pkg.go.dev/golang.org/x/crypto/ssh)**: SSH client with host key verification
-- **[golang.org/x/crypto/bcrypt](https://pkg.go.dev/golang.org/x/crypto/bcrypt)**: Secure password hashing
-- **AES-256-GCM**: Military-grade encryption for sensitive data
-- **Authentication Middleware**: HTTP Basic Auth and Bearer token support
-- **Security Middleware**: HTTPS enforcement and security headers
-- **Input Validation**: Comprehensive validation for security
-
-### Frontend
-- **[React 18](https://react.dev/)**: Modern UI library
-- **[React Router v6](https://reactrouter.com/)**: Client-side routing
-- **[Material-UI (MUI) v5](https://mui.com/)**: Professional component library
-- **[xterm.js](https://xtermjs.org/)**: Terminal emulator for the browser
-- **[Monaco Editor](https://microsoft.github.io/monaco-editor/)**: VS Code editor for YAML/JSON tools
-- **[Vite](https://vitejs.dev/)**: Fast build tool and dev server
-- **[Emotion](https://emotion.sh/)**: CSS-in-JS styling
-- **[Swagger UI](https://swagger.io/tools/swagger-ui/)**: Interactive API documentation
-- **[js-yaml](https://github.com/nodeca/js-yaml)**: YAML parsing and formatting
-- **[jsonrepair](https://github.com/josdejong/jsonrepair)**: JSON auto-fix and repair
-
-## 📸 Screenshots
-
-### Dashboard
-
-*Main dashboard with feature cards for easy navigation*
-
-### Local Command Execution
-
-*Execute commands on your local server with real-time output*
-
-### Remote Command Execution
-
-*Connect to remote servers via SSH and execute commands*
-
-### Remote Script Execution
-
-*Connect to remote servers via SSH and execute script*
-
-### Interactive Terminal
-
-*Full browser-based terminal with SSH key integration and server aliases*
-
-### Admin Panel - SSH Keys
-
-*Manage SSH private keys for server authentication*
-
-### Admin Panel - Servers
-
-*Configure and manage remote servers*
-
-### Saved Commands
-
-*Save and reuse frequently-used command templates with type indicators*
-
-### Command History
-
-*View complete execution history with output and timing*
-
-## 📦 Prerequisites
-
-- **Go**: Version 1.21 or higher ([Download](https://golang.org/dl/))
-- **Node.js**: Version 18 or higher ([Download](https://nodejs.org/))
-- **npm**: Version 8 or higher (comes with Node.js)
-
-## 🚀 Installation
-
-### Clone the Repository
-
-```bash
-git clone https://github.com/pozgo/web-cli.git
-cd web-cli
-```
-
-### Install Dependencies
-
-#### Backend
-```bash
-go mod download
-```
-
-#### Frontend
-```bash
-cd frontend
-npm install
-cd ..
-```
-
-## ⚡ Quick Start
-
-### Build and Run
-
-```bash
-# Build the application
-./build.sh
-
-# Run the server
-./web-cli
-```
-
-Access the application at `http://localhost:7777`
-
-### Using Management Script
-
-```bash
-# Start the server
-./manage.sh start
-
-# Stop the server
-./manage.sh stop
-
-# Check status
-./manage.sh status
-```
-
-## 📖 Usage
-
-### Interactive Terminal
-
-1. Navigate to **Interactive Terminal** from the dashboard
-2. Select your preferred shell (Bash, Zsh, or Sh)
-3. Optionally select an SSH key from the dropdown
-4. Use the terminal like a native terminal - full PTY support
-5. SSH to servers using aliases: `ssh myserver` (servers from Admin Panel)
-6. Toggle fullscreen with the fullscreen button
-
-**SSH Server Aliases:**
-When you add servers in the Admin Panel, they become available as SSH aliases in the terminal:
-```bash
-# If you added a server named "prod" with IP 10.0.0.5 and user "deploy"
-ssh prod # Automatically connects to deploy@10.0.0.5
-```
-
-### Local Command Execution
-
-1. Navigate to **Local Commands** from the dashboard
-2. Enter your bash command
-3. Select the user to run as (current, root, or custom)
-4. Click **Execute Command**
-5. View real-time output
-
-### Remote Command Execution
-
-1. First, add SSH keys and servers in the **Admin Panel**
-2. Navigate to **Remote Execution**
-3. Select a server from the dropdown
-4. Choose an SSH key (optional, password fallback available)
-5. Enter your command and click **Execute**
-
-### Saved Command Templates
-
-1. Execute any command and check "Save command as template"
-2. Access saved commands from the **Saved Commands** page
-3. Edit command type (Local/Remote) and details
-4. Execute saved commands with one click
-
-## 📚 API Documentation
-
-Web CLI provides a comprehensive RESTful API for programmatic access to all features. Perfect for automation, CI/CD pipelines, and integration with other tools.
-
-### 🚀 Quick Start
-
-**Base URL**: `http://localhost:7777/api`
-
-**Example Request**:
-```bash
-curl http://localhost:7777/api/health
-# Response: {"status":"ok"}
-```
-
-### 📋 API Endpoints Overview
-
-| Category | Endpoints | Description |
-|----------|-----------|-------------|
-| **Health** | 1 endpoint | Server health check |
-| **Terminal** | 1 WebSocket | Interactive terminal sessions |
-| **SSH Keys** | 5 endpoints | Manage SSH private keys |
-| **Servers** | 5 endpoints | Manage remote servers |
-| **Local Users** | 5 endpoints | Manage local user accounts |
-| **System Info** | 1 endpoint | Get current system user |
-| **Commands** | 1 endpoint | Execute local/remote commands |
-| **Saved Commands** | 5 endpoints | Manage command templates |
-| **History** | 2 endpoints | View execution history |
-| **Environment Variables** | 5 endpoints | Manage encrypted env variables |
-| **Bash Scripts** | 6 endpoints | Manage and execute scripts |
-| **Script Presets** | 5 endpoints | Manage execution presets |
-
-**Total: 43 endpoints (42 REST + 1 WebSocket)**
-
-### 📘 Swagger UI
-
-Interactive API documentation is available at `/swagger/` when the server is running.
-
-Access: `http://localhost:7777/swagger/`
-
-### 📖 Complete Documentation
-
-For detailed API documentation including:
-- ✅ All 41 endpoints with full descriptions
-- ✅ Request/response examples with cURL commands
-- ✅ Field descriptions and validation rules
-- ✅ Error response formats and status codes
-- ✅ Security considerations and best practices
-- ✅ Authentication guidance for production
-- ✅ Quick reference table for all endpoints
-
-**👉 See [API.md](API.md) for complete API reference**
-
-### 💡 Common Use Cases
-
-**Note:** If authentication is enabled, add `-u username:password` or `-H "Authorization: Bearer token"` to all requests.
-
-**Execute a local command**:
-```bash
-curl -u admin:password -X POST http://localhost:7777/api/commands/execute \
- -H "Content-Type: application/json" \
- -d '{"command": "df -h", "user": "root"}'
-```
-
-**Execute a remote command via SSH**:
-```bash
-curl -u admin:password -X POST http://localhost:7777/api/commands/execute \
- -H "Content-Type: application/json" \
- -d '{
- "command": "uptime",
- "is_remote": true,
- "server_id": 1,
- "ssh_key_id": 2
- }'
-```
-
-**List command history**:
-```bash
-curl -u admin:password "http://localhost:7777/api/history?limit=10&server=local"
-```
-
-**Using Bearer Token**:
-```bash
-curl -H "Authorization: Bearer your-api-token" \
- -X POST http://localhost:7777/api/commands/execute \
- -H "Content-Type: application/json" \
- -d '{"command": "uptime", "user": "current"}'
-```
-
-## ⚙️ Configuration
-
-### Command-Line Flags
-
-```bash
-./web-cli [options]
-
-Options:
- -port int Port to listen on (default: 7777)
- -host string Host to bind to (default: 0.0.0.0)
- -frontend string Path to frontend build files (default: ./frontend/dist)
- -db string Path to database file (default: ./data/web-cli.db)
- -encryption-key string Path to encryption key file (default: ./.encryption_key)
- -tls-cert string Path to TLS certificate file (enables HTTPS)
- -tls-key string Path to TLS private key file
- -require-https Require HTTPS when auth is enabled (reject HTTP requests)
-```
-
-### Environment Variables
-
-All configuration options can be set via environment variables:
-
-```bash
-# Standard environment variables
-PORT=8080 HOST=localhost ./web-cli
-
-# WEBCLI-prefixed variables (recommended)
-WEBCLI_PORT=8080
-WEBCLI_HOST=localhost
-WEBCLI_DATABASE_PATH=/var/lib/web-cli/web-cli.db
-WEBCLI_ENCRYPTION_KEY_PATH=/etc/web-cli/encryption.key
-WEBCLI_TLS_CERT_PATH=/etc/ssl/certs/web-cli.crt
-WEBCLI_TLS_KEY_PATH=/etc/ssl/private/web-cli.key
-WEBCLI_REQUIRE_HTTPS=true
-```
-
-### Configuration File
-
-Web CLI supports configuration files in YAML, JSON, or TOML format. Configuration files are searched in the following locations (first found is used):
-
-1. `./config.yaml` (current directory)
-2. `./config/config.yaml` (config subdirectory)
-3. `/etc/web-cli/config.yaml` (system config)
-4. `~/.config/web-cli/config.yaml` (user config)
-
-**Example config.yaml:**
-
-```yaml
-port: 7777
-host: "0.0.0.0"
-frontend_path: "./assets/frontend"
-database_path: "./data/web-cli.db"
-encryption_key_path: "./.encryption_key"
-tls_cert_path: "/etc/ssl/certs/web-cli.crt"
-tls_key_path: "/etc/ssl/private/web-cli.key"
-require_https: true
-```
-
-### Configuration Priority
-
-1. Command-line flags (highest priority)
-2. Environment variables
-3. Configuration file
-4. Default values (lowest priority)
-
-## 🔒 Security
-
-### Authentication (Production Ready)
-
-**Important: Authentication is disabled by default for development convenience.**
-
-For production deployments, enable authentication:
-
-```bash
-# Enable authentication
-export AUTH_ENABLED=true
-
-# Option 1: HTTP Basic Authentication
-export AUTH_USERNAME="admin"
-export AUTH_PASSWORD="your-secure-password"
-
-# Option 2: API Token (Bearer)
-export AUTH_API_TOKEN="your-api-token-here"
-```
-
-**Features:**
-- HTTP Basic Authentication support
-- Bearer token (API token) support
-- Constant-time credential comparison (prevents timing attacks)
-- Supports both methods simultaneously (token takes precedence)
-
-**Usage Examples:**
-
-```bash
-# Basic Auth
-curl -u admin:password http://localhost:7777/api/health
-
-# Bearer Token
-curl -H "Authorization: Bearer your-token" http://localhost:7777/api/health
-```
-
-### TLS/HTTPS Support
-
-**Native TLS support for encrypted connections:**
-
-```bash
-# Enable TLS with certificate and key
-./web-cli -tls-cert /path/to/cert.pem -tls-key /path/to/key.pem
-
-# Or via environment variables
-WEBCLI_TLS_CERT_PATH=/path/to/cert.pem \
-WEBCLI_TLS_KEY_PATH=/path/to/key.pem \
-./web-cli
-
-# Enforce HTTPS when authentication is enabled
-./web-cli -tls-cert cert.pem -tls-key key.pem -require-https
-```
-
-**Features:**
-- Native Go TLS implementation (no reverse proxy required)
-- Automatic HTTPS when certificate and key are provided
-- Optional HTTPS enforcement (rejects HTTP requests)
-- Works with any TLS certificate (self-signed, Let's Encrypt, etc.)
-
-**Generate self-signed certificate for testing:**
-
-```bash
-openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
- -days 365 -nodes -subj "/CN=localhost"
-```
-
-### SSH Host Key Verification
-
-**Proper host key verification for secure SSH connections:**
-
-- Verifies SSH host keys against `~/.ssh/known_hosts`
-- Supports "trust on first use" mode for development
-- Detects man-in-the-middle attacks (host key mismatch)
-- Automatically saves new trusted host keys
-- Thread-safe implementation
-
-**Configuration:**
-- **Strict Mode** (production): Rejects unknown hosts
-- **Trust-on-First-Use** (development): Automatically trusts new hosts
-
-### Input Validation
-
-**All user inputs are validated before processing:**
-
-- IP addresses (IPv4/IPv6)
-- Hostnames (RFC 1123 compliant)
-- Port numbers (1-65535)
-- SSH private keys (format validation)
-- Unix usernames (alphanumeric, dash, underscore)
-- Command names (no null bytes or newlines)
-
-### HTTP Security
-
-**Server configured with proper timeouts:**
-
-```go
-ReadTimeout: 15 * time.Second // Prevents slowloris attacks
-WriteTimeout: 15 * time.Second // Prevents slow writes
-IdleTimeout: 60 * time.Second // Prevents idle connections
-```
-
-**CORS Policy:**
-
-- Default: localhost only
-- Production: Configure via `CORS_ALLOWED_ORIGINS` environment variable
-
-```bash
-# Single origin
-export CORS_ALLOWED_ORIGINS="https://web-cli.example.com"
-# Multiple origins (comma-separated)
-export CORS_ALLOWED_ORIGINS="https://web-cli.example.com,https://admin.example.com"
-```
-
-### Database Encryption
-
-All sensitive data is encrypted using **AES-256-GCM**:
-- SSH private keys encrypted at rest
-- Command history (commands and output) encrypted
-- Encryption key auto-generated on first run
-- Key stored in `.encryption_key` with 600 permissions
+## Features
-### Encryption Key Management
+- **Interactive Terminal** - Full browser-based terminal with xterm.js, multi-tab support, SSH key integration
+- **Command Execution** - Execute commands locally or remotely via SSH with real-time output
+- **Server Management** - Manage SSH keys, servers, and connection settings
+- **Script Library** - Store and execute bash scripts with environment variable injection
+- **Command Templates** - Save frequently-used commands for quick re-execution
+- **Developer Tools** - YAML/JSON validators with Monaco Editor (VS Code engine)
+- **HashiCorp Vault** - Optional integration for external secrets management
+- **Security** - AES-256 encryption, TLS support, authentication, audit logging
-**Important:**
-- Backup your `.encryption_key` file - data cannot be recovered without it
-- For production, use environment variable instead of file
+## Quick Start
-**Generate a new encryption key:**
-
-```bash
-# macOS/Linux
-openssl rand -base64 32
+### Docker Compose (Recommended)
-# Or using dd and base64
-dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64
-
-# Output example: 7xK9mP2vQ8nL4wR6tY5uE3sA1zD0cF8bG7hJ9kM6nP4=
-```
-
-**Use the generated key:**
-
-```bash
-# Option 1: Environment variable (recommended for production)
-export ENCRYPTION_KEY="7xK9mP2vQ8nL4wR6tY5uE3sA1zD0cF8bG7hJ9kM6nP4="
-./web-cli
-
-# Option 2: Save to file (auto-generated on first run if not exists)
-echo "7xK9mP2vQ8nL4wR6tY5uE3sA1zD0cF8bG7hJ9kM6nP4=" > .encryption_key
-chmod 600 .encryption_key
-./web-cli
-```
-
-### Password Security
-
-- Sudo passwords only used for command execution
-- SSH passwords used for authentication fallback
-- **Passwords are never stored** in command history
-- Passwords cleared from memory after use
-
-### Database Migrations
-
-- Automatic schema versioning
-- Migrations run on startup
-- Safe to restart - migrations only run once
-- Current schema version: **14**
-
-## 🌐 Deployment
-
-### Linux Server
-
-```bash
-# Build for Linux
-./build.sh all
-
-# Copy to server
-scp bin/web-cli-linux-x64 user@server:/opt/web-cli/web-cli
-
-# Run on server
-ssh user@server
-cd /opt/web-cli
-./web-cli
-```
-
-### systemd Service
-
-Create `/etc/systemd/system/web-cli.service`:
-
-```ini
-[Unit]
-Description=Web CLI Service
-After=network.target
-
-[Service]
-Type=simple
-User=www-data
-WorkingDirectory=/opt/web-cli
-ExecStart=/opt/web-cli/web-cli
-Restart=on-failure
-
-# Security Configuration (REQUIRED for production)
-Environment=AUTH_ENABLED=true
-Environment=AUTH_USERNAME=admin
-Environment=AUTH_PASSWORD=your-secure-password-here
-Environment=CORS_ALLOWED_ORIGINS=https://web-cli.yourdomain.com
-Environment=ENCRYPTION_KEY=your-encryption-key-here
-
-[Install]
-WantedBy=multi-user.target
-```
-
-Enable and start:
-
-```bash
-sudo systemctl enable web-cli
-sudo systemctl start web-cli
-sudo systemctl status web-cli
-```
-
-### Production Deployment Checklist
-
-Before deploying to production, ensure:
-
-**Security Configuration:**
-- ✅ **Authentication enabled**: Set `AUTH_ENABLED=true`
-- ✅ **Strong credentials**: Configure `AUTH_USERNAME` and `AUTH_PASSWORD` (or `AUTH_API_TOKEN`)
-- ✅ **SSH host key verification**: Enabled by default (uses `~/.ssh/known_hosts`)
-- ✅ **CORS restricted**: Set `CORS_ALLOWED_ORIGINS` to your domain(s)
-- ✅ **HTTP timeouts**: Configured automatically (prevents DoS attacks)
-- ✅ **Input validation**: All inputs validated automatically
-
-**Additional Requirements:**
-- [ ] **HTTPS enabled**: Use native TLS (`-tls-cert`, `-tls-key`) or reverse proxy
-- [ ] **Encryption key backup**: Backup `.encryption_key` - data cannot be recovered without it
-- [ ] **Security scan**: Run `gosec ./...` or similar security scanner
-- [ ] **Monitor logs**: Check for authentication failures and errors
-- [ ] **Firewall configured**: Restrict access to authorized IPs if possible
-
-**Production Environment Variables:**
-
-```bash
-# Authentication (REQUIRED for production)
-AUTH_ENABLED=true
-AUTH_USERNAME=admin
-AUTH_PASSWORD=$(openssl rand -base64 32) # Or your secure password
-# OR use API token
-AUTH_API_TOKEN=$(openssl rand -base64 32)
-
-# CORS Policy (REQUIRED for production)
-CORS_ALLOWED_ORIGINS=https://web-cli.yourdomain.com
-
-# Encryption (REQUIRED)
-ENCRYPTION_KEY=$(openssl rand -base64 32)
-
-# TLS/HTTPS (RECOMMENDED for production)
-WEBCLI_TLS_CERT_PATH=/etc/ssl/certs/web-cli.crt
-WEBCLI_TLS_KEY_PATH=/etc/ssl/private/web-cli.key
-WEBCLI_REQUIRE_HTTPS=true
-
-# Server Config (Optional)
-WEBCLI_PORT=7777
-WEBCLI_HOST=0.0.0.0
-```
-
-**Testing Authentication:**
-
-```bash
-# Should fail (no auth)
-curl http://localhost:7777/api/health
-
-# Should succeed with Basic Auth
-curl -u admin:password http://localhost:7777/api/health
-
-# Should succeed with Bearer token
-curl -H "Authorization: Bearer your-token" http://localhost:7777/api/health
-```
-
-### Docker
-
-Web CLI is available as a Docker image for easy deployment.
-
-**Image Details:**
-- **Registry:** Docker Hub ([`polinux/web-cli`](https://hub.docker.com/r/polinux/web-cli))
-- **Base Image:** Debian Bookworm (slim) - provides full bash support for script execution
-- **Platforms:** `linux/amd64`, `linux/arm64`
-- **Size:** ~100MB compressed
-- **Tags:** `latest`, `dev`, `main`, version tags (e.g., `v0.2.3`)
-
-**Quick Start - Standalone Docker Compose:**
-
-Create a `docker-compose.yml` file anywhere on your system:
+Create a `docker-compose.yml`:
```yaml
services:
@@ -722,246 +46,141 @@ volumes:
web-cli-data:
```
-Then run:
+Run:
```bash
-# Start the container
docker compose up -d
+```
+
+Access at: **http://localhost:7777**
-# View logs
-docker compose logs -f
+### Docker Run
-# Access at http://localhost:7777
+```bash
+docker run -d \
+ --name web-cli \
+ -p 7777:7777 \
+ -v web-cli-data:/data \
+ -e AUTH_ENABLED=true \
+ -e AUTH_USERNAME=admin \
+ -e AUTH_PASSWORD=your-password \
+ polinux/web-cli:latest
```
-**Quick Start with Repository:**
+### From Source
```bash
-# Clone the repository
+# Clone and build
git clone https://github.com/pozgo/web-cli.git
cd web-cli
+./build.sh
-# Start with default settings
-docker compose up -d
-
-# View logs
-docker compose logs -f
+# Run
+./web-cli
```
-**Access:** `http://localhost:7777`
+## Screenshots
-**Build Locally:**
+| Interactive Terminal | Remote Commands |
+|:---:|:---:|
+|  |  |
-```bash
-# Build the image
-docker compose build
+| Admin Panel | Vault Integration |
+|:---:|:---:|
+|  |  |
-# Or build directly
-docker build -t web-cli .
-```
+## Documentation
-**Run with Custom Configuration:**
+| Document | Description |
+|----------|-------------|
+| **[API.md](API.md)** | Complete REST API reference (55 endpoints) |
+| **[docs/CONFIGURATION.md](docs/CONFIGURATION.md)** | Configuration options, env vars, config files |
+| **[docs/SECURITY.md](docs/SECURITY.md)** | Security features, authentication, TLS, audit logging |
+| **[docs/DEPLOYMENT.md](docs/DEPLOYMENT.md)** | Docker, systemd, production deployment guides |
+| **[docs/DEVELOPMENT.md](docs/DEVELOPMENT.md)** | Development setup, project structure, contributing |
-```bash
-# Copy example environment file
-cp .env.example .env
+## API
-# Edit .env with your settings
-nano .env
+Swagger UI available at `/swagger/` when the server is running.
-# Start with custom configuration
-docker compose up -d
+```bash
+# Health check
+curl http://localhost:7777/api/health
+
+# Execute command (with auth)
+curl -u admin:password -X POST http://localhost:7777/api/commands/execute \
+ -H "Content-Type: application/json" \
+ -d '{"command": "uptime", "user": "current"}'
```
-**Production Deployment:**
+See [API.md](API.md) for complete documentation.
-```bash
-# Create .env with production settings
-cat > .env << 'EOF'
-AUTH_ENABLED=true
-AUTH_USERNAME=admin
-AUTH_PASSWORD=$(openssl rand -base64 24)
-WEBCLI_PORT=7777
-EOF
+## Configuration
-# Start the container
-docker compose up -d
+### Essential Environment Variables
-# Check credentials
-cat .env | grep AUTH_
-```
-
-**Docker Run (without Compose):**
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `AUTH_ENABLED` | `false` | Enable authentication |
+| `AUTH_USERNAME` | - | Basic auth username |
+| `AUTH_PASSWORD` | - | Basic auth password |
+| `WEBCLI_PORT` | `7777` | Server port |
+| `WEBCLI_TLS_CERT_PATH` | - | TLS certificate (enables HTTPS) |
+| `WEBCLI_TLS_KEY_PATH` | - | TLS private key |
-```bash
-docker run -d \
- --name web-cli \
- -p 7777:7777 \
- -v web-cli-data:/data \
- -e AUTH_ENABLED=true \
- -e AUTH_USERNAME=admin \
- -e AUTH_PASSWORD=your-secure-password \
- polinux/web-cli:latest
-```
+See [docs/CONFIGURATION.md](docs/CONFIGURATION.md) for all options.
-**With TLS/HTTPS:**
+## Production Deployment
```bash
+# Generate secure credentials
+AUTH_PASSWORD=$(openssl rand -base64 24)
+ENCRYPTION_KEY=$(openssl rand -base64 32)
+
+# Run with TLS and authentication
docker run -d \
--name web-cli \
-p 7777:7777 \
-v web-cli-data:/data \
-v ./certs:/certs:ro \
- -e WEBCLI_TLS_CERT_PATH=/certs/cert.pem \
- -e WEBCLI_TLS_KEY_PATH=/certs/key.pem \
-e AUTH_ENABLED=true \
-e AUTH_USERNAME=admin \
- -e AUTH_PASSWORD=your-secure-password \
+ -e AUTH_PASSWORD="$AUTH_PASSWORD" \
+ -e ENCRYPTION_KEY="$ENCRYPTION_KEY" \
+ -e WEBCLI_TLS_CERT_PATH=/certs/cert.pem \
+ -e WEBCLI_TLS_KEY_PATH=/certs/key.pem \
+ -e CORS_ALLOWED_ORIGINS=https://web-cli.yourdomain.com \
polinux/web-cli:latest
```
-**Docker Environment Variables:**
-
-| Variable | Default | Description |
-|----------|---------|-------------|
-| `WEBCLI_PORT` | `7777` | Port to listen on |
-| `WEBCLI_HOST` | `0.0.0.0` | Host to bind to |
-| `WEBCLI_DATABASE_PATH` | `/data/web-cli.db` | Database file path |
-| `WEBCLI_ENCRYPTION_KEY_PATH` | `/data/.encryption_key` | Encryption key file path |
-| `ENCRYPTION_KEY` | (auto-generated) | Base64 encryption key |
-| `AUTH_ENABLED` | `false` | Enable authentication |
-| `AUTH_USERNAME` | `admin` | Basic auth username |
-| `AUTH_PASSWORD` | (none) | Basic auth password |
-| `AUTH_API_TOKEN` | (none) | Bearer token |
-| `WEBCLI_TLS_CERT_PATH` | (none) | TLS certificate path |
-| `WEBCLI_TLS_KEY_PATH` | (none) | TLS private key path |
-| `WEBCLI_REQUIRE_HTTPS` | `false` | Require HTTPS |
-| `CORS_ALLOWED_ORIGINS` | (localhost) | Allowed CORS origins |
-
-**Volumes:**
-
-| Path | Description |
-|------|-------------|
-| `/data` | Persistent data (database, encryption key) |
-| `/config` | Configuration files (optional) |
-| `/certs` | TLS certificates (optional) |
-
-## 💻 Development
-
-### Backend Development
-
-```bash
-# Run with hot reload (use air or similar)
-go run cmd/web-cli/main.go
-
-# Run tests
-go test ./...
-
-# Run tests with coverage
-go test -cover ./...
-
-# Format code
-go fmt ./...
-
-# Lint code
-go vet ./...
-```
-
-### Frontend Development
-
-```bash
-cd frontend
-
-# Start dev server with hot reload
-npm run dev
-
-# Run tests
-npm test
-
-# Lint code
-npm run lint
-
-# Build for production
-npm run build
-```
-
-### Development Workflow
-
-1. Start backend:
- ```bash
- go run cmd/web-cli/main.go
- ```
+See [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) for systemd service, production checklist, and more.
-2. Start frontend (new terminal):
- ```bash
- cd frontend && npm run dev
- ```
+## Tech Stack
-3. Open `http://localhost:3000` for hot-reload development
+**Backend:** Go 1.21+, Gorilla Mux, SQLite, AES-256-GCM encryption
-### Project Structure
+**Frontend:** React 18, Material-UI v5, xterm.js, Monaco Editor, Vite
-```
-web-cli/
-├── cmd/web-cli/ # Application entry point
-├── internal/ # Private application code
-│ ├── config/ # Configuration management
-│ ├── database/ # Database, migrations, encryption
-│ ├── executor/ # Command execution (local & remote)
-│ │ └── hostkeys.go # SSH host key verification
-│ ├── middleware/ # HTTP middleware (authentication)
-│ ├── models/ # Data models
-│ ├── repository/ # Data access layer
-│ ├── server/ # HTTP server and handlers
-│ ├── terminal/ # Interactive terminal (PTY + WebSocket)
-│ └── validation/ # Input validation functions
-├── frontend/ # React application
-│ ├── src/
-│ │ ├── components/ # React components
-│ │ │ └── Terminal.jsx # xterm.js terminal component
-│ │ ├── theme/ # MUI theme configuration
-│ │ └── App.jsx # Main app with routing
-│ └── vite.config.js # Vite configuration
-├── build.sh # Build script (all platforms)
-├── manage.sh # Server management script
-├── API.md # Complete API documentation
-└── go.mod # Go dependencies
-```
-
-## 🤝 Contributing
-
-Contributions are welcome! Please follow these steps:
+## Contributing
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
-3. Commit your changes (`git commit -m 'Add some amazing feature'`)
+3. Commit your changes (`git commit -m 'Add amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
-### Development Guidelines
-
-- Follow Go best practices and conventions
-- Write tests for new features
-- Update documentation as needed
-- Ensure all tests pass before submitting PR
-- Keep commits atomic and well-described
-
-## 📝 License
+See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for development setup.
-This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
+## License
-## 🆘 Support
+MIT License - see [LICENSE](LICENSE) for details.
-For issues and questions:
-- 📫 Open an issue on [GitHub Issues](https://github.com/pozgo/web-cli/issues)
-- 📖 Check the [API Documentation](API.md)
-- 🔒 Review [Security Improvements](SECURITY_IMPROVEMENTS.md) for security features
+## Support
-## 🙏 Acknowledgments
+- [GitHub Issues](https://github.com/pozgo/web-cli/issues)
+- [API Documentation](API.md)
+- [Security Guide](docs/SECURITY.md)
-- Built with [Go](https://golang.org/) - Fast, reliable, and efficient
-- UI powered by [React](https://react.dev/) and [Material-UI](https://mui.com/)
-- Build tool: [Vite](https://vitejs.dev/) - Next generation frontend tooling
-- SSH implementation: [golang.org/x/crypto/ssh](https://pkg.go.dev/golang.org/x/crypto/ssh)
+---
**Made with ❤️ by [Pozgo](https://github.com/pozgo)**
diff --git a/cmd/web-cli/main.go b/cmd/web-cli/main.go
index a4a9784..c9d4f94 100644
--- a/cmd/web-cli/main.go
+++ b/cmd/web-cli/main.go
@@ -4,6 +4,7 @@ import (
"log"
"github.com/pozgo/web-cli/assets"
+ "github.com/pozgo/web-cli/internal/audit"
"github.com/pozgo/web-cli/internal/config"
"github.com/pozgo/web-cli/internal/database"
"github.com/pozgo/web-cli/internal/server"
@@ -93,11 +94,27 @@ func main() {
log.Printf("Database schema version: %d", version)
}
+ // Initialize audit logging
+ if cfg.AuditLogPath != "" {
+ auditLogger, err := audit.Initialize(cfg.AuditLogPath)
+ if err != nil {
+ log.Printf("Warning: Failed to initialize audit logging: %v", err)
+ } else {
+ log.Printf("Audit logging enabled: %s", cfg.AuditLogPath)
+ defer auditLogger.Close()
+ }
+ } else {
+ log.Println("Audit logging is disabled (set AUDIT_LOG_PATH to enable)")
+ }
+
// Set embedded frontend
server.EmbeddedFrontend = assets.FrontendFS
// Create and start server
- srv := server.New(cfg, db)
+ srv, err := server.New(cfg, db)
+ if err != nil {
+ log.Fatalf("Failed to initialize server: %v", err)
+ }
log.Fatal(srv.Start())
}
diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md
new file mode 100644
index 0000000..ffe0eff
--- /dev/null
+++ b/docs/CONFIGURATION.md
@@ -0,0 +1,312 @@
+# Configuration Guide
+
+Web CLI supports multiple configuration methods with the following priority order:
+
+1. **Command-line flags** (highest priority)
+2. **Environment variables**
+3. **Configuration file**
+4. **Default values** (lowest priority)
+
+## Table of Contents
+
+- [Command-Line Flags](#command-line-flags)
+- [Environment Variables](#environment-variables)
+- [Configuration File](#configuration-file)
+- [Timeout Configuration](#timeout-configuration)
+- [Audit Logging](#audit-logging)
+- [TLS/HTTPS Configuration](#tlshttps-configuration)
+- [CORS Configuration](#cors-configuration)
+
+---
+
+## Command-Line Flags
+
+```bash
+./web-cli [options]
+
+Options:
+ -port int Port to listen on (default: 7777)
+ -host string Host to bind to (default: 0.0.0.0)
+ -frontend string Path to frontend build files (default: ./frontend/dist)
+ -db string Path to database file (default: ./data/web-cli.db)
+ -encryption-key string Path to encryption key file (default: ./.encryption_key)
+ -tls-cert string Path to TLS certificate file (enables HTTPS)
+ -tls-key string Path to TLS private key file
+ -require-https Require HTTPS when auth is enabled (reject HTTP requests)
+```
+
+---
+
+## Environment Variables
+
+All configuration options can be set via environment variables. Both standard and `WEBCLI_`-prefixed variables are supported (prefixed is recommended).
+
+### Server Configuration
+
+| Variable | WEBCLI Prefix | Default | Description |
+|----------|---------------|---------|-------------|
+| `PORT` | `WEBCLI_PORT` | `7777` | Port to listen on |
+| `HOST` | `WEBCLI_HOST` | `0.0.0.0` | Host to bind to |
+| `FRONTEND_PATH` | `WEBCLI_FRONTEND_PATH` | `./frontend/dist` | Frontend build files |
+| `DATABASE_PATH` | `WEBCLI_DATABASE_PATH` | `./data/web-cli.db` | SQLite database path |
+| `ENCRYPTION_KEY_PATH` | `WEBCLI_ENCRYPTION_KEY_PATH` | `./.encryption_key` | Encryption key file |
+
+### Authentication
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `AUTH_ENABLED` | `false` | Enable authentication |
+| `AUTH_USERNAME` | (none) | Basic auth username |
+| `AUTH_PASSWORD` | (none) | Basic auth password |
+| `AUTH_API_TOKEN` | (none) | Bearer token for API access |
+
+### TLS/HTTPS
+
+| Variable | WEBCLI Prefix | Default | Description |
+|----------|---------------|---------|-------------|
+| `TLS_CERT_PATH` | `WEBCLI_TLS_CERT_PATH` | (none) | TLS certificate file |
+| `TLS_KEY_PATH` | `WEBCLI_TLS_KEY_PATH` | (none) | TLS private key file |
+| `REQUIRE_HTTPS` | `WEBCLI_REQUIRE_HTTPS` | `false` | Reject HTTP when auth enabled |
+
+### Example Usage
+
+```bash
+# Standard environment variables
+PORT=8080 HOST=localhost ./web-cli
+
+# WEBCLI-prefixed variables (recommended)
+WEBCLI_PORT=8080 \
+WEBCLI_HOST=localhost \
+WEBCLI_DATABASE_PATH=/var/lib/web-cli/web-cli.db \
+./web-cli
+
+# Production example with authentication
+AUTH_ENABLED=true \
+AUTH_USERNAME=admin \
+AUTH_PASSWORD="secure-password" \
+WEBCLI_TLS_CERT_PATH=/etc/ssl/certs/web-cli.crt \
+WEBCLI_TLS_KEY_PATH=/etc/ssl/private/web-cli.key \
+./web-cli
+```
+
+---
+
+## Configuration File
+
+Web CLI supports configuration files in YAML, JSON, or TOML format.
+
+### Search Locations
+
+Configuration files are searched in the following order (first found is used):
+
+1. `./config.yaml` (current directory)
+2. `./config/config.yaml` (config subdirectory)
+3. `/etc/web-cli/config.yaml` (system config)
+4. `~/.config/web-cli/config.yaml` (user config)
+
+### Example config.yaml
+
+```yaml
+# Server Configuration
+port: 7777
+host: "0.0.0.0"
+frontend_path: "./assets/frontend"
+database_path: "./data/web-cli.db"
+encryption_key_path: "./.encryption_key"
+
+# TLS/HTTPS Configuration
+tls_cert_path: "/etc/ssl/certs/web-cli.crt"
+tls_key_path: "/etc/ssl/private/web-cli.key"
+require_https: true
+
+# Timeout Configuration (in seconds)
+read_timeout: 30
+write_timeout: 600
+idle_timeout: 60
+vault_timeout: 30
+command_timeout: 300
+ssh_connect_timeout: 30
+
+# Audit Logging (optional)
+audit_log_path: "/var/log/web-cli/audit.log"
+```
+
+### Example config.json
+
+```json
+{
+ "port": 7777,
+ "host": "0.0.0.0",
+ "database_path": "./data/web-cli.db",
+ "encryption_key_path": "./.encryption_key",
+ "read_timeout": 30,
+ "write_timeout": 600,
+ "command_timeout": 300
+}
+```
+
+---
+
+## Timeout Configuration
+
+All timeout values are configurable via environment variables (values in seconds):
+
+| Timeout | Default | Environment Variable | Description |
+|---------|---------|---------------------|-------------|
+| Read Timeout | 30s | `READ_TIMEOUT` or `WEBCLI_READ_TIMEOUT` | Time to read request headers |
+| Write Timeout | 600s (10m) | `WRITE_TIMEOUT` or `WEBCLI_WRITE_TIMEOUT` | Time to write response (high for streaming) |
+| Idle Timeout | 60s | `IDLE_TIMEOUT` or `WEBCLI_IDLE_TIMEOUT` | Keep-alive connection timeout |
+| Vault Timeout | 30s | `VAULT_TIMEOUT` or `WEBCLI_VAULT_TIMEOUT` | HashiCorp Vault operations |
+| Command Timeout | 300s (5m) | `COMMAND_TIMEOUT` or `WEBCLI_COMMAND_TIMEOUT` | Command execution timeout |
+| SSH Connect Timeout | 30s | `SSH_CONNECT_TIMEOUT` or `WEBCLI_SSH_CONNECT_TIMEOUT` | SSH connection establishment |
+
+### Example
+
+```bash
+# Increase command timeout to 10 minutes
+export COMMAND_TIMEOUT=600
+
+# Increase Vault timeout for slow connections
+export VAULT_TIMEOUT=60
+
+./web-cli
+```
+
+---
+
+## Audit Logging
+
+Enable comprehensive audit logging for security compliance and monitoring.
+
+### Enable Audit Logging
+
+```bash
+# Via environment variable
+export AUDIT_LOG_PATH=/var/log/web-cli/audit.log
+./web-cli
+
+# Or via WEBCLI prefix
+export WEBCLI_AUDIT_LOG_PATH=/var/log/web-cli/audit.log
+./web-cli
+```
+
+### Logged Events
+
+- Command executions (local and remote)
+- Script executions
+- Terminal sessions (start/end)
+- Authentication attempts
+
+### Log Format
+
+JSON Lines (JSONL) - one JSON object per line:
+
+```json
+{
+ "timestamp": "2025-01-19T10:30:00Z",
+ "event_type": "command_execution",
+ "outcome": "success",
+ "actor": "admin",
+ "source_ip": "192.168.1.100",
+ "target": "local",
+ "command": "df -h",
+ "user": "root",
+ "exit_code": 0,
+ "duration_ms": 150
+}
+```
+
+### Log Rotation
+
+Use logrotate to manage log file size:
+
+```bash
+# /etc/logrotate.d/web-cli
+/var/log/web-cli/audit.log {
+ daily
+ rotate 30
+ compress
+ delaycompress
+ missingok
+ notifempty
+ create 640 www-data www-data
+}
+```
+
+---
+
+## TLS/HTTPS Configuration
+
+### Enable TLS
+
+```bash
+# Using command-line flags
+./web-cli -tls-cert /path/to/cert.pem -tls-key /path/to/key.pem
+
+# Using environment variables
+WEBCLI_TLS_CERT_PATH=/path/to/cert.pem \
+WEBCLI_TLS_KEY_PATH=/path/to/key.pem \
+./web-cli
+
+# Enforce HTTPS (reject HTTP requests)
+./web-cli -tls-cert cert.pem -tls-key key.pem -require-https
+```
+
+### Generate Self-Signed Certificate (Testing)
+
+```bash
+openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
+ -days 365 -nodes -subj "/CN=localhost"
+```
+
+### Features
+
+- Native Go TLS implementation (no reverse proxy required)
+- Automatic HTTPS when certificate and key are provided
+- Optional HTTPS enforcement (rejects HTTP requests)
+- Works with any TLS certificate (self-signed, Let's Encrypt, etc.)
+
+---
+
+## CORS Configuration
+
+Configure Cross-Origin Resource Sharing for frontend access.
+
+### Default Behavior
+
+By default, only localhost origins are allowed:
+- `http://localhost:7777`
+- `http://127.0.0.1:7777`
+
+### Custom Origins
+
+```bash
+# Single origin
+export CORS_ALLOWED_ORIGINS="https://web-cli.example.com"
+
+# Multiple origins (comma-separated)
+export CORS_ALLOWED_ORIGINS="https://web-cli.example.com,https://admin.example.com"
+
+./web-cli
+```
+
+---
+
+## Complete Production Example
+
+```bash
+# /etc/systemd/system/web-cli.service environment
+AUTH_ENABLED=true
+AUTH_USERNAME=admin
+AUTH_PASSWORD=your-secure-password
+ENCRYPTION_KEY=your-base64-encryption-key
+CORS_ALLOWED_ORIGINS=https://web-cli.yourdomain.com
+WEBCLI_TLS_CERT_PATH=/etc/ssl/certs/web-cli.crt
+WEBCLI_TLS_KEY_PATH=/etc/ssl/private/web-cli.key
+WEBCLI_REQUIRE_HTTPS=true
+WEBCLI_AUDIT_LOG_PATH=/var/log/web-cli/audit.log
+WEBCLI_DATABASE_PATH=/var/lib/web-cli/web-cli.db
+```
+
+For more information on security configuration, see [SECURITY.md](SECURITY.md).
+For deployment instructions, see [DEPLOYMENT.md](DEPLOYMENT.md).
diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md
new file mode 100644
index 0000000..82aaf4b
--- /dev/null
+++ b/docs/DEPLOYMENT.md
@@ -0,0 +1,339 @@
+# Deployment Guide
+
+This document covers various deployment options for Web CLI.
+
+## Table of Contents
+
+- [Docker (Recommended)](#docker-recommended)
+- [Docker Compose](#docker-compose)
+- [Linux Server (Binary)](#linux-server-binary)
+- [systemd Service](#systemd-service)
+- [Production Checklist](#production-checklist)
+
+---
+
+## Docker (Recommended)
+
+Web CLI is available as a Docker image for easy deployment.
+
+### Image Details
+
+| Property | Value |
+|----------|-------|
+| Registry | Docker Hub ([`polinux/web-cli`](https://hub.docker.com/r/polinux/web-cli)) |
+| Base Image | Debian Bookworm (slim) |
+| Platforms | `linux/amd64`, `linux/arm64` |
+| Size | ~100MB compressed |
+| Tags | `latest`, `dev`, `main`, version tags (e.g., `v0.2.3`) |
+
+### Quick Start
+
+```bash
+docker run -d \
+ --name web-cli \
+ -p 7777:7777 \
+ -v web-cli-data:/data \
+ -e AUTH_ENABLED=true \
+ -e AUTH_USERNAME=admin \
+ -e AUTH_PASSWORD=your-secure-password \
+ polinux/web-cli:latest
+```
+
+Access at: `http://localhost:7777`
+
+### With TLS/HTTPS
+
+```bash
+docker run -d \
+ --name web-cli \
+ -p 7777:7777 \
+ -v web-cli-data:/data \
+ -v ./certs:/certs:ro \
+ -e WEBCLI_TLS_CERT_PATH=/certs/cert.pem \
+ -e WEBCLI_TLS_KEY_PATH=/certs/key.pem \
+ -e AUTH_ENABLED=true \
+ -e AUTH_USERNAME=admin \
+ -e AUTH_PASSWORD=your-secure-password \
+ polinux/web-cli:latest
+```
+
+### Docker Volumes
+
+| Path | Description |
+|------|-------------|
+| `/data` | Persistent data (database, encryption key) |
+| `/config` | Configuration files (optional) |
+| `/certs` | TLS certificates (optional) |
+
+### Docker Environment Variables
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `WEBCLI_PORT` | `7777` | Port to listen on |
+| `WEBCLI_HOST` | `0.0.0.0` | Host to bind to |
+| `WEBCLI_DATABASE_PATH` | `/data/web-cli.db` | Database file path |
+| `WEBCLI_ENCRYPTION_KEY_PATH` | `/data/.encryption_key` | Encryption key path |
+| `ENCRYPTION_KEY` | (auto-generated) | Base64 encryption key |
+| `AUTH_ENABLED` | `false` | Enable authentication |
+| `AUTH_USERNAME` | `admin` | Basic auth username |
+| `AUTH_PASSWORD` | (none) | Basic auth password |
+| `AUTH_API_TOKEN` | (none) | Bearer token |
+| `WEBCLI_TLS_CERT_PATH` | (none) | TLS certificate path |
+| `WEBCLI_TLS_KEY_PATH` | (none) | TLS private key path |
+| `WEBCLI_REQUIRE_HTTPS` | `false` | Require HTTPS |
+| `CORS_ALLOWED_ORIGINS` | (localhost) | Allowed CORS origins |
+| `WEBCLI_AUDIT_LOG_PATH` | (none) | Audit log file path |
+
+---
+
+## Docker Compose
+
+### Standalone docker-compose.yml
+
+Create a `docker-compose.yml` file anywhere on your system:
+
+```yaml
+services:
+ web-cli:
+ image: polinux/web-cli:latest
+ container_name: web-cli
+ restart: unless-stopped
+ ports:
+ - "7777:7777"
+ volumes:
+ - web-cli-data:/data
+ environment:
+ - AUTH_ENABLED=true
+ - AUTH_USERNAME=admin
+ - AUTH_PASSWORD=changeme123
+
+volumes:
+ web-cli-data:
+```
+
+### Run
+
+```bash
+# Start the container
+docker compose up -d
+
+# View logs
+docker compose logs -f
+
+# Stop
+docker compose down
+```
+
+### Production docker-compose.yml
+
+```yaml
+services:
+ web-cli:
+ image: polinux/web-cli:latest
+ container_name: web-cli
+ restart: unless-stopped
+ ports:
+ - "7777:7777"
+ volumes:
+ - web-cli-data:/data
+ - ./certs:/certs:ro
+ environment:
+ # Authentication
+ - AUTH_ENABLED=true
+ - AUTH_USERNAME=admin
+ - AUTH_PASSWORD=${WEB_CLI_PASSWORD}
+
+ # TLS/HTTPS
+ - WEBCLI_TLS_CERT_PATH=/certs/cert.pem
+ - WEBCLI_TLS_KEY_PATH=/certs/key.pem
+ - WEBCLI_REQUIRE_HTTPS=true
+
+ # CORS
+ - CORS_ALLOWED_ORIGINS=https://web-cli.yourdomain.com
+
+ # Audit Logging
+ - WEBCLI_AUDIT_LOG_PATH=/data/audit.log
+
+volumes:
+ web-cli-data:
+```
+
+### With .env File
+
+Create `.env` file:
+
+```bash
+WEB_CLI_PASSWORD=your-secure-password-here
+ENCRYPTION_KEY=your-base64-encryption-key
+```
+
+---
+
+## Linux Server (Binary)
+
+### Build and Deploy
+
+```bash
+# Build for Linux
+./build.sh all
+
+# Copy to server
+scp bin/web-cli-linux-x64 user@server:/opt/web-cli/web-cli
+
+# Run on server
+ssh user@server
+cd /opt/web-cli
+./web-cli
+```
+
+### Directory Structure
+
+```
+/opt/web-cli/
+├── web-cli # Binary
+├── data/
+│ └── web-cli.db # Database
+├── .encryption_key # Encryption key
+└── config.yaml # Configuration (optional)
+```
+
+### Permissions
+
+```bash
+# Create web-cli user
+sudo useradd -r -s /bin/false web-cli
+
+# Set ownership
+sudo chown -R web-cli:web-cli /opt/web-cli
+
+# Secure encryption key
+chmod 600 /opt/web-cli/.encryption_key
+```
+
+---
+
+## systemd Service
+
+Create `/etc/systemd/system/web-cli.service`:
+
+```ini
+[Unit]
+Description=Web CLI Service
+After=network.target
+
+[Service]
+Type=simple
+User=www-data
+WorkingDirectory=/opt/web-cli
+ExecStart=/opt/web-cli/web-cli
+Restart=on-failure
+RestartSec=5
+
+# Security Configuration (REQUIRED for production)
+Environment=AUTH_ENABLED=true
+Environment=AUTH_USERNAME=admin
+Environment=AUTH_PASSWORD=your-secure-password-here
+Environment=CORS_ALLOWED_ORIGINS=https://web-cli.yourdomain.com
+Environment=ENCRYPTION_KEY=your-encryption-key-here
+
+# Audit Logging (recommended)
+Environment=AUDIT_LOG_PATH=/var/log/web-cli/audit.log
+
+# TLS/HTTPS (recommended)
+# Environment=WEBCLI_TLS_CERT_PATH=/etc/ssl/certs/web-cli.crt
+# Environment=WEBCLI_TLS_KEY_PATH=/etc/ssl/private/web-cli.key
+# Environment=WEBCLI_REQUIRE_HTTPS=true
+
+# Custom timeouts (optional, values in seconds)
+# Environment=COMMAND_TIMEOUT=600
+# Environment=VAULT_TIMEOUT=60
+
+[Install]
+WantedBy=multi-user.target
+```
+
+### Enable and Start
+
+```bash
+# Create log directory
+sudo mkdir -p /var/log/web-cli
+sudo chown www-data:www-data /var/log/web-cli
+
+# Enable and start service
+sudo systemctl daemon-reload
+sudo systemctl enable web-cli
+sudo systemctl start web-cli
+
+# Check status
+sudo systemctl status web-cli
+
+# View logs
+sudo journalctl -u web-cli -f
+```
+
+---
+
+## Production Checklist
+
+### Before Deployment
+
+- [ ] Generate strong password: `openssl rand -base64 32`
+- [ ] Generate encryption key: `openssl rand -base64 32`
+- [ ] Obtain TLS certificate (Let's Encrypt or similar)
+- [ ] Configure firewall rules
+
+### Environment Variables
+
+```bash
+# Required
+AUTH_ENABLED=true
+AUTH_USERNAME=admin
+AUTH_PASSWORD=$(openssl rand -base64 32)
+ENCRYPTION_KEY=$(openssl rand -base64 32)
+CORS_ALLOWED_ORIGINS=https://web-cli.yourdomain.com
+
+# Recommended
+WEBCLI_TLS_CERT_PATH=/etc/ssl/certs/web-cli.crt
+WEBCLI_TLS_KEY_PATH=/etc/ssl/private/web-cli.key
+WEBCLI_REQUIRE_HTTPS=true
+WEBCLI_AUDIT_LOG_PATH=/var/log/web-cli/audit.log
+```
+
+### Post-Deployment
+
+- [ ] Verify authentication is working
+- [ ] Test HTTPS certificate
+- [ ] Configure log rotation
+- [ ] Set up monitoring/alerting
+- [ ] Backup encryption key securely
+- [ ] Document admin credentials securely
+
+### Backup Strategy
+
+Critical files to backup:
+
+| File | Importance | Description |
+|------|------------|-------------|
+| `.encryption_key` | **Critical** | Cannot recover data without it |
+| `web-cli.db` | Important | All configuration and history |
+| `config.yaml` | Optional | Configuration file |
+
+### Log Rotation
+
+```bash
+# /etc/logrotate.d/web-cli
+/var/log/web-cli/audit.log {
+ daily
+ rotate 30
+ compress
+ delaycompress
+ missingok
+ notifempty
+ create 640 www-data www-data
+}
+```
+
+---
+
+For more configuration options, see [CONFIGURATION.md](CONFIGURATION.md).
+For security information, see [SECURITY.md](SECURITY.md).
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
new file mode 100644
index 0000000..0eb8906
--- /dev/null
+++ b/docs/DEVELOPMENT.md
@@ -0,0 +1,375 @@
+# Development Guide
+
+This document covers the development setup and workflow for Web CLI.
+
+## Table of Contents
+
+- [Prerequisites](#prerequisites)
+- [Quick Setup](#quick-setup)
+- [Development Workflow](#development-workflow)
+- [Project Structure](#project-structure)
+- [Backend Development](#backend-development)
+- [Frontend Development](#frontend-development)
+- [Testing](#testing)
+- [Building](#building)
+- [Contributing](#contributing)
+
+---
+
+## Prerequisites
+
+- **Go**: Version 1.21 or higher ([Download](https://golang.org/dl/))
+- **Node.js**: Version 18 or higher ([Download](https://nodejs.org/))
+- **npm**: Version 8 or higher (comes with Node.js)
+
+---
+
+## Quick Setup
+
+```bash
+# Clone the repository
+git clone https://github.com/pozgo/web-cli.git
+cd web-cli
+
+# Install Go dependencies
+go mod download
+
+# Install frontend dependencies
+cd frontend
+npm install
+cd ..
+
+# Build and run
+./build.sh
+./web-cli
+```
+
+Access at: `http://localhost:7777`
+
+---
+
+## Development Workflow
+
+### Recommended: Two Terminal Setup
+
+**Terminal 1 - Backend:**
+
+```bash
+go run cmd/web-cli/main.go
+```
+
+**Terminal 2 - Frontend (with hot reload):**
+
+```bash
+cd frontend && npm run dev
+```
+
+Access frontend dev server at: `http://localhost:3000`
+(API calls are proxied to the Go backend on port 7777)
+
+### Using Management Script
+
+```bash
+# Start the server
+./manage.sh start
+
+# Stop the server
+./manage.sh stop
+
+# Check status
+./manage.sh status
+
+# Restart
+./manage.sh restart
+```
+
+---
+
+## Project Structure
+
+```
+web-cli/
+├── cmd/web-cli/ # Application entry point
+│ └── main.go # Main function
+├── internal/ # Private application code
+│ ├── audit/ # Audit logging
+│ ├── config/ # Configuration management
+│ ├── database/ # Database, migrations, encryption
+│ ├── executor/ # Command execution (local & remote)
+│ │ └── hostkeys.go # SSH host key verification
+│ ├── middleware/ # HTTP middleware (auth, security)
+│ ├── models/ # Data models
+│ ├── repository/ # Data access layer
+│ ├── server/ # HTTP server and handlers
+│ ├── terminal/ # Interactive terminal (PTY + WebSocket)
+│ ├── validation/ # Input validation
+│ └── vault/ # HashiCorp Vault client
+├── frontend/ # React application
+│ ├── src/
+│ │ ├── components/ # React components
+│ │ │ ├── Terminal.jsx # xterm.js terminal
+│ │ │ ├── AdminPanel.jsx # Admin interface
+│ │ │ ├── Dashboard.jsx # Main dashboard
+│ │ │ └── ...
+│ │ ├── theme/ # MUI theme configuration
+│ │ └── App.jsx # Main app with routing
+│ ├── public/ # Static assets
+│ ├── package.json # npm dependencies
+│ └── vite.config.js # Vite configuration
+├── docs/ # Documentation
+│ ├── images/ # Screenshots
+│ ├── CONFIGURATION.md # Configuration guide
+│ ├── SECURITY.md # Security guide
+│ ├── DEPLOYMENT.md # Deployment guide
+│ └── DEVELOPMENT.md # This file
+├── assets/ # Embedded frontend (production)
+├── build.sh # Build script (all platforms)
+├── manage.sh # Server management script
+├── API.md # Complete API documentation
+├── Dockerfile # Docker build file
+├── docker-compose.yml # Docker Compose configuration
+└── go.mod # Go dependencies
+```
+
+---
+
+## Backend Development
+
+### Running
+
+```bash
+# Run with go run
+go run cmd/web-cli/main.go
+
+# Run with custom options
+go run cmd/web-cli/main.go -port 8080 -host localhost
+
+# Run with air (hot reload)
+air
+```
+
+### Testing
+
+```bash
+# Run all tests
+go test ./...
+
+# Run tests with coverage
+go test -cover ./...
+
+# Run tests for a specific package
+go test ./internal/validation/...
+
+# Verbose output
+go test -v ./...
+```
+
+### Linting
+
+```bash
+# Format code
+go fmt ./...
+
+# Run vet
+go vet ./...
+
+# Run golangci-lint (if installed)
+golangci-lint run
+```
+
+### Generate Swagger Docs
+
+```bash
+# Install swag CLI
+go install github.com/swaggo/swag/cmd/swag@latest
+
+# Generate documentation
+swag init -g cmd/web-cli/main.go -o docs/
+```
+
+---
+
+## Frontend Development
+
+### Running
+
+```bash
+cd frontend
+
+# Start dev server with hot reload
+npm run dev
+
+# Build for production
+npm run build
+
+# Preview production build
+npm run preview
+```
+
+### Testing
+
+```bash
+cd frontend
+
+# Run tests
+npm test
+
+# Run tests with coverage
+npm test -- --coverage
+```
+
+### Linting
+
+```bash
+cd frontend
+
+# Run ESLint
+npm run lint
+
+# Fix auto-fixable issues
+npm run lint -- --fix
+```
+
+### Dependencies
+
+Key frontend dependencies:
+
+| Package | Purpose |
+|---------|---------|
+| `react` | UI library |
+| `react-router-dom` | Client-side routing |
+| `@mui/material` | Material-UI components |
+| `@emotion/react` | CSS-in-JS styling |
+| `xterm` | Terminal emulator |
+| `@monaco-editor/react` | Code editor for YAML/JSON tools |
+| `js-yaml` | YAML parsing |
+
+---
+
+## Testing
+
+### Backend Tests
+
+```bash
+# All tests
+go test ./...
+
+# With coverage report
+go test -coverprofile=coverage.out ./...
+go tool cover -html=coverage.out
+
+# Specific package
+go test ./internal/validation/
+
+# With race detection
+go test -race ./...
+```
+
+### Frontend Tests
+
+```bash
+cd frontend
+npm test
+```
+
+### Integration Testing
+
+```bash
+# Build and run
+./build.sh
+./web-cli &
+
+# Test API endpoints
+curl http://localhost:7777/api/health
+
+# Test with authentication
+curl -u admin:password http://localhost:7777/api/health
+```
+
+---
+
+## Building
+
+### Development Build
+
+```bash
+# Quick build for current platform
+go build -o web-cli cmd/web-cli/main.go
+```
+
+### Production Build (All Platforms)
+
+```bash
+# Build for all platforms
+./build.sh all
+
+# Outputs:
+# bin/web-cli-linux-x64
+# bin/web-cli-darwin-x64
+# bin/web-cli-darwin-arm64
+```
+
+### Docker Build
+
+```bash
+# Build Docker image
+docker build -t web-cli .
+
+# Or using docker compose
+docker compose build
+```
+
+### Build Script Options
+
+```bash
+./build.sh # Build for current platform
+./build.sh all # Build for all platforms
+./build.sh linux # Build for Linux only
+./build.sh darwin # Build for macOS only
+```
+
+---
+
+## Contributing
+
+### Getting Started
+
+1. Fork the repository
+2. Create a feature branch: `git checkout -b feature/amazing-feature`
+3. Make your changes
+4. Write/update tests
+5. Commit: `git commit -m 'Add some amazing feature'`
+6. Push: `git push origin feature/amazing-feature`
+7. Open a Pull Request
+
+### Code Style
+
+- **Go**: Follow standard Go formatting (`go fmt`)
+- **JavaScript/React**: Follow ESLint rules
+- Write tests for new features
+- Update documentation as needed
+
+### Commit Messages
+
+Use clear, descriptive commit messages:
+
+```
+feat: Add new feature X
+fix: Fix bug in component Y
+docs: Update README
+refactor: Restructure module Z
+test: Add tests for feature X
+```
+
+### Pull Request Guidelines
+
+- Ensure all tests pass
+- Update documentation if needed
+- Keep PRs focused (one feature/fix per PR)
+- Provide clear description of changes
+
+---
+
+For configuration options, see [CONFIGURATION.md](CONFIGURATION.md).
+For security information, see [SECURITY.md](SECURITY.md).
+For deployment instructions, see [DEPLOYMENT.md](DEPLOYMENT.md).
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
new file mode 100644
index 0000000..785dcad
--- /dev/null
+++ b/docs/SECURITY.md
@@ -0,0 +1,418 @@
+# Security Guide
+
+This document describes Web CLI's security features and best practices for secure deployment.
+
+## Table of Contents
+
+- [Authentication](#authentication)
+- [TLS/HTTPS](#tlshttps)
+- [SSH Host Key Verification](#ssh-host-key-verification)
+- [Input Validation](#input-validation)
+- [Database Encryption](#database-encryption)
+- [Encryption Key Management](#encryption-key-management)
+- [Password Security](#password-security)
+- [SSRF Protection](#ssrf-protection)
+- [Path Traversal Protection](#path-traversal-protection)
+- [HTTP Security](#http-security)
+- [Audit Logging](#audit-logging)
+- [Error Information Leakage Prevention](#error-information-leakage-prevention)
+- [Security Headers](#security-headers)
+- [Production Security Checklist](#production-security-checklist)
+
+---
+
+## Authentication
+
+**Important: Authentication is disabled by default for development convenience.**
+
+### Enable Authentication
+
+```bash
+# Enable authentication
+export AUTH_ENABLED=true
+
+# Option 1: HTTP Basic Authentication
+export AUTH_USERNAME="admin"
+export AUTH_PASSWORD="your-secure-password"
+
+# Option 2: API Token (Bearer)
+export AUTH_API_TOKEN="your-api-token-here"
+```
+
+### Features
+
+- HTTP Basic Authentication support
+- Bearer token (API token) support
+- Constant-time credential comparison (prevents timing attacks)
+- Supports both methods simultaneously (token takes precedence)
+- **Startup validation**: Server fails fast if auth is enabled but credentials are missing
+
+### Usage Examples
+
+```bash
+# Basic Auth
+curl -u admin:password http://localhost:7777/api/health
+
+# Bearer Token
+curl -H "Authorization: Bearer your-token" http://localhost:7777/api/health
+```
+
+### Testing Authentication
+
+```bash
+# Should fail (401 Unauthorized)
+curl http://localhost:7777/api/health
+
+# Should succeed with Basic Auth
+curl -u admin:password http://localhost:7777/api/health
+
+# Should succeed with Bearer token
+curl -H "Authorization: Bearer your-token" http://localhost:7777/api/health
+```
+
+---
+
+## TLS/HTTPS
+
+Native TLS support for encrypted connections without requiring a reverse proxy.
+
+### Enable TLS
+
+```bash
+# Using command-line flags
+./web-cli -tls-cert /path/to/cert.pem -tls-key /path/to/key.pem
+
+# Using environment variables
+WEBCLI_TLS_CERT_PATH=/path/to/cert.pem \
+WEBCLI_TLS_KEY_PATH=/path/to/key.pem \
+./web-cli
+
+# Enforce HTTPS when authentication is enabled
+./web-cli -tls-cert cert.pem -tls-key key.pem -require-https
+```
+
+### Generate Self-Signed Certificate (Testing)
+
+```bash
+openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
+ -days 365 -nodes -subj "/CN=localhost"
+```
+
+### Features
+
+- Native Go TLS implementation
+- Automatic HTTPS when certificate and key are provided
+- Optional HTTPS enforcement (rejects HTTP requests)
+- Works with any TLS certificate (self-signed, Let's Encrypt, etc.)
+
+---
+
+## SSH Host Key Verification
+
+Proper host key verification for secure SSH connections.
+
+### Features
+
+- Verifies SSH host keys against `~/.ssh/known_hosts`
+- Supports "trust on first use" (TOFU) mode for development
+- Detects man-in-the-middle attacks (host key mismatch)
+- Automatically saves new trusted host keys
+- Thread-safe implementation
+
+### Configuration
+
+- **Strict Mode** (production): Rejects unknown hosts
+- **Trust-on-First-Use** (development): Automatically trusts new hosts
+
+---
+
+## Input Validation
+
+All user inputs are validated before processing to prevent injection attacks.
+
+### Validated Inputs
+
+| Input Type | Validation |
+|------------|------------|
+| IP addresses | IPv4/IPv6 format validation |
+| Hostnames | RFC 1123 compliant |
+| Port numbers | 1-65535 range |
+| SSH private keys | PEM format validation |
+| Unix usernames | Alphanumeric, dash, underscore |
+| Command names | No null bytes or newlines |
+| Vault addresses | SSRF protection |
+| Vault secret paths | Path traversal protection |
+| Vault secret names | Alphanumeric, dash, underscore, dot |
+| Environment variable names | Unix standards |
+| Script content | Size limits, no null bytes |
+
+---
+
+## Database Encryption
+
+All sensitive data is encrypted using **AES-256-GCM** (military-grade encryption).
+
+### Encrypted Data
+
+- SSH private keys
+- Command history (commands and output)
+- Environment variable values
+- Vault tokens
+
+### Key Generation
+
+Encryption key is auto-generated on first run and stored with 600 permissions.
+
+---
+
+## Encryption Key Management
+
+### Generate a New Encryption Key
+
+```bash
+# macOS/Linux
+openssl rand -base64 32
+
+# Or using dd and base64
+dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64
+
+# Output example: 7xK9mP2vQ8nL4wR6tY5uE3sA1zD0cF8bG7hJ9kM6nP4=
+```
+
+### Use the Generated Key
+
+```bash
+# Option 1: Environment variable (recommended for production)
+export ENCRYPTION_KEY="7xK9mP2vQ8nL4wR6tY5uE3sA1zD0cF8bG7hJ9kM6nP4="
+./web-cli
+
+# Option 2: Save to file (auto-generated on first run)
+echo "7xK9mP2vQ8nL4wR6tY5uE3sA1zD0cF8bG7hJ9kM6nP4=" > .encryption_key
+chmod 600 .encryption_key
+./web-cli
+```
+
+### Important
+
+- **Backup your `.encryption_key` file** - data cannot be recovered without it
+- For production, use environment variable instead of file
+- System entropy is verified before key generation (Linux)
+
+---
+
+## Password Security
+
+- Sudo passwords only used for command execution
+- SSH passwords used for authentication fallback
+- **Passwords are never stored** in command history
+- Passwords cleared from memory after use
+- bcrypt password hashing with cost factor 12
+
+---
+
+## SSRF Protection
+
+When using HashiCorp Vault integration, the application includes SSRF (Server-Side Request Forgery) protection.
+
+### Allowed Addresses
+
+- Public IP addresses
+- Private IPs (10.x.x.x, 172.16.x.x, 192.168.x.x) for self-hosted Vault
+- Localhost (127.0.0.1) for local development
+- Valid hostnames
+
+### Blocked Addresses (Attack Vectors)
+
+- Link-local addresses (169.254.x.x) - used by cloud metadata services
+- Unspecified addresses (0.0.0.0)
+- Cloud metadata hostnames:
+ - `metadata.google.internal`
+ - `metadata.goog`
+ - `metadata`
+ - `instance-data`
+
+This allows you to use self-hosted Vault on private networks while preventing attacks against cloud metadata endpoints.
+
+---
+
+## Path Traversal Protection
+
+All Vault secret paths are validated to prevent path traversal attacks.
+
+### Blocked Patterns
+
+- Path traversal sequences (`..`)
+- Absolute paths starting with `/`
+- Backslashes (`\`)
+- URL-encoded characters (`%`)
+- Consecutive slashes (`//`)
+
+### Allowed Characters
+
+Only alphanumeric, dash, underscore, dot, and forward slash are allowed.
+
+---
+
+## HTTP Security
+
+### Configurable Timeouts
+
+Protection against slowloris and DoS attacks with customizable values:
+
+| Timeout | Default | Environment Variable | Description |
+|---------|---------|---------------------|-------------|
+| Read Timeout | 30s | `READ_TIMEOUT` | Time to read request headers |
+| Write Timeout | 600s | `WRITE_TIMEOUT` | Time to write response |
+| Idle Timeout | 60s | `IDLE_TIMEOUT` | Keep-alive connection timeout |
+| Command Timeout | 300s | `COMMAND_TIMEOUT` | Command execution timeout |
+| SSH Connect Timeout | 30s | `SSH_CONNECT_TIMEOUT` | SSH connection establishment |
+| Vault Timeout | 30s | `VAULT_TIMEOUT` | HashiCorp Vault operations |
+
+### CORS Policy
+
+- Default: localhost only
+- Production: Configure via `CORS_ALLOWED_ORIGINS`
+
+```bash
+# Single origin
+export CORS_ALLOWED_ORIGINS="https://web-cli.example.com"
+
+# Multiple origins (comma-separated)
+export CORS_ALLOWED_ORIGINS="https://web-cli.example.com,https://admin.example.com"
+```
+
+---
+
+## Audit Logging
+
+Comprehensive audit logging for security compliance and monitoring.
+
+### Enable Audit Logging
+
+```bash
+export AUDIT_LOG_PATH=/var/log/web-cli/audit.log
+./web-cli
+```
+
+### Logged Events
+
+- Command executions (local and remote)
+- Script executions
+- Terminal sessions (start/end)
+- Authentication attempts
+
+### Log Format
+
+JSON Lines (JSONL) - one JSON object per line:
+
+```json
+{
+ "timestamp": "2025-01-19T10:30:00Z",
+ "event_type": "command_execution",
+ "outcome": "success",
+ "actor": "admin",
+ "source_ip": "192.168.1.100",
+ "target": "local",
+ "command": "df -h",
+ "user": "root",
+ "exit_code": 0,
+ "duration_ms": 150
+}
+```
+
+### Log Rotation
+
+```bash
+# /etc/logrotate.d/web-cli
+/var/log/web-cli/audit.log {
+ daily
+ rotate 30
+ compress
+ delaycompress
+ missingok
+ notifempty
+ create 640 www-data www-data
+}
+```
+
+---
+
+## Error Information Leakage Prevention
+
+API error responses are sanitized to prevent information leakage:
+
+- Internal error details (IP addresses, paths, stack traces) are not exposed
+- Validation errors are returned with safe, descriptive messages
+- Generic "operation failed" messages for unexpected errors
+- Sensitive configuration details are never included in responses
+
+---
+
+## Security Headers
+
+The following security headers are set on all responses:
+
+| Header | Value | Purpose |
+|--------|-------|---------|
+| `X-Frame-Options` | `DENY` | Prevent clickjacking |
+| `X-Content-Type-Options` | `nosniff` | Prevent MIME sniffing |
+| `X-XSS-Protection` | `1; mode=block` | Enable XSS filter |
+| `Referrer-Policy` | `strict-origin-when-cross-origin` | Control referrer information |
+
+---
+
+## Production Security Checklist
+
+### Automatic (Built-in)
+
+- ✅ Authentication validated at startup (fails fast if misconfigured)
+- ✅ SSH host key verification enabled
+- ✅ Input validation for all user inputs
+- ✅ SSRF protection for Vault addresses
+- ✅ Path traversal protection for Vault paths
+- ✅ Error sanitization (no internal details leaked)
+- ✅ Security headers on all responses
+
+### Required Configuration
+
+- [ ] **Enable authentication**: Set `AUTH_ENABLED=true` with credentials
+- [ ] **Strong credentials**: Use secure `AUTH_USERNAME` and `AUTH_PASSWORD`
+- [ ] **CORS restricted**: Set `CORS_ALLOWED_ORIGINS` to your domain(s)
+- [ ] **Encryption key backup**: Backup `.encryption_key` file
+
+### Recommended
+
+- [ ] **HTTPS enabled**: Use native TLS or reverse proxy
+- [ ] **Audit logging enabled**: Set `AUDIT_LOG_PATH`
+- [ ] **Log rotation configured**: Use logrotate for audit logs
+- [ ] **Security scan**: Run `gosec ./...` or similar
+- [ ] **Monitor logs**: Check for authentication failures
+- [ ] **Firewall configured**: Restrict access to authorized IPs
+
+### Production Environment Variables
+
+```bash
+# Authentication (REQUIRED)
+AUTH_ENABLED=true
+AUTH_USERNAME=admin
+AUTH_PASSWORD=$(openssl rand -base64 32)
+
+# CORS Policy (REQUIRED)
+CORS_ALLOWED_ORIGINS=https://web-cli.yourdomain.com
+
+# Encryption (REQUIRED)
+ENCRYPTION_KEY=$(openssl rand -base64 32)
+
+# TLS/HTTPS (RECOMMENDED)
+WEBCLI_TLS_CERT_PATH=/etc/ssl/certs/web-cli.crt
+WEBCLI_TLS_KEY_PATH=/etc/ssl/private/web-cli.key
+WEBCLI_REQUIRE_HTTPS=true
+
+# Audit Logging (RECOMMENDED)
+WEBCLI_AUDIT_LOG_PATH=/var/log/web-cli/audit.log
+```
+
+---
+
+For more configuration options, see [CONFIGURATION.md](CONFIGURATION.md).
+For deployment instructions, see [DEPLOYMENT.md](DEPLOYMENT.md).
diff --git a/docs/docs.go b/docs/docs.go
index 294f798..ee2df02 100644
--- a/docs/docs.go
+++ b/docs/docs.go
@@ -42,20 +42,28 @@ const docTemplate = `{
"Bash Scripts"
],
"summary": "List all bash scripts",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Filter by group name",
+ "name": "group",
+ "in": "query"
+ }
+ ],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/models.BashScriptResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.BashScriptResponse"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -84,7 +92,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.BashScriptCreate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.BashScriptCreate"
}
}
],
@@ -92,19 +100,19 @@ const docTemplate = `{
"201": {
"description": "Created",
"schema": {
- "$ref": "#/definitions/models.BashScriptResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.BashScriptResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -135,7 +143,64 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.ScriptExecution"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptExecution"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptResult"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/bash-scripts/execute/stream": {
+ "post": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Execute a stored bash script locally or remotely with real-time output streaming via SSE",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "text/event-stream"
+ ],
+ "tags": [
+ "Bash Scripts"
+ ],
+ "summary": "Execute a bash script with streaming output",
+ "parameters": [
+ {
+ "description": "Script execution request",
+ "name": "execution",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptExecution"
}
}
],
@@ -143,25 +208,62 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.ScriptResult"
+ "$ref": "#/definitions/internal_server.StreamMessage"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/bash-scripts/groups": {
+ "get": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Get a list of all distinct group names for bash scripts",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Bash Scripts"
+ ],
+ "summary": "List all bash script groups",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -205,19 +307,19 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.BashScriptResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.BashScriptResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -253,7 +355,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.BashScriptUpdate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.BashScriptUpdate"
}
}
],
@@ -261,19 +363,19 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.BashScriptResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.BashScriptResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -311,13 +413,13 @@ const docTemplate = `{
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -356,26 +458,26 @@ const docTemplate = `{
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/models.ScriptPresetResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptPresetResponse"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -406,7 +508,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.CommandExecution"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.CommandExecution"
}
}
],
@@ -414,25 +516,25 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.CommandResult"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.CommandResult"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -462,6 +564,12 @@ const docTemplate = `{
"description": "Show actual values instead of masked values",
"name": "show_values",
"in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Filter by group name",
+ "name": "group",
+ "in": "query"
}
],
"responses": {
@@ -470,14 +578,14 @@ const docTemplate = `{
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/models.EnvVariableResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.EnvVariableResponse"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -506,7 +614,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.EnvVariableCreate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.EnvVariableCreate"
}
}
],
@@ -514,19 +622,56 @@ const docTemplate = `{
"201": {
"description": "Created",
"schema": {
- "$ref": "#/definitions/models.EnvVariableResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.EnvVariableResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/env-variables/groups": {
+ "get": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Get a list of all distinct group names for environment variables",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Environment Variables"
+ ],
+ "summary": "List all environment variable groups",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -569,19 +714,19 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.EnvVariableResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.EnvVariableResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -617,7 +762,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.EnvVariableUpdate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.EnvVariableUpdate"
}
}
],
@@ -625,19 +770,19 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.EnvVariableResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.EnvVariableResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -675,13 +820,13 @@ const docTemplate = `{
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -726,14 +871,14 @@ const docTemplate = `{
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/models.CommandHistory"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.CommandHistory"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -770,19 +915,19 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.CommandHistory"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.CommandHistory"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -806,20 +951,28 @@ const docTemplate = `{
"SSH Keys"
],
"summary": "List all SSH keys",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Filter by group name",
+ "name": "group",
+ "in": "query"
+ }
+ ],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/models.SSHKey"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SSHKey"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -848,7 +1001,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.SSHKeyCreate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SSHKeyCreate"
}
}
],
@@ -856,19 +1009,56 @@ const docTemplate = `{
"201": {
"description": "Created",
"schema": {
- "$ref": "#/definitions/models.SSHKey"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SSHKey"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/keys/groups": {
+ "get": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Get a list of all distinct group names for SSH keys",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "SSH Keys"
+ ],
+ "summary": "List all SSH key groups",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -905,19 +1095,19 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.SSHKey"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SSHKey"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -953,7 +1143,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.SSHKeyUpdate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SSHKeyUpdate"
}
}
],
@@ -961,19 +1151,19 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.SSHKey"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SSHKey"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1011,13 +1201,13 @@ const docTemplate = `{
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1047,14 +1237,14 @@ const docTemplate = `{
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/models.LocalUser"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.LocalUser"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1083,7 +1273,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.LocalUserCreate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.LocalUserCreate"
}
}
],
@@ -1091,19 +1281,19 @@ const docTemplate = `{
"201": {
"description": "Created",
"schema": {
- "$ref": "#/definitions/models.LocalUser"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.LocalUser"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1140,19 +1330,19 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.LocalUser"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.LocalUser"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1188,7 +1378,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.LocalUserUpdate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.LocalUserUpdate"
}
}
],
@@ -1196,19 +1386,19 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.LocalUser"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.LocalUser"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1246,13 +1436,13 @@ const docTemplate = `{
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1282,14 +1472,14 @@ const docTemplate = `{
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/models.SavedCommand"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SavedCommand"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1318,7 +1508,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.SavedCommandCreate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SavedCommandCreate"
}
}
],
@@ -1326,19 +1516,19 @@ const docTemplate = `{
"201": {
"description": "Created",
"schema": {
- "$ref": "#/definitions/models.SavedCommand"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SavedCommand"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1375,19 +1565,19 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.SavedCommand"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SavedCommand"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1423,7 +1613,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.SavedCommandUpdate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SavedCommandUpdate"
}
}
],
@@ -1431,19 +1621,19 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.SavedCommand"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SavedCommand"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1481,13 +1671,13 @@ const docTemplate = `{
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1517,14 +1707,14 @@ const docTemplate = `{
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/models.ScriptPresetResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptPresetResponse"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1553,7 +1743,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.ScriptPresetCreate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptPresetCreate"
}
}
],
@@ -1561,19 +1751,19 @@ const docTemplate = `{
"201": {
"description": "Created",
"schema": {
- "$ref": "#/definitions/models.ScriptPresetResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptPresetResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1610,19 +1800,19 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.ScriptPresetResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptPresetResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1658,7 +1848,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.ScriptPresetUpdate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptPresetUpdate"
}
}
],
@@ -1666,19 +1856,19 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.ScriptPresetResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptPresetResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1716,13 +1906,13 @@ const docTemplate = `{
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1746,20 +1936,28 @@ const docTemplate = `{
"Servers"
],
"summary": "List all servers",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Filter by group name",
+ "name": "group",
+ "in": "query"
+ }
+ ],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/models.Server"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.Server"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1788,7 +1986,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.ServerCreate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ServerCreate"
}
}
],
@@ -1796,19 +1994,56 @@ const docTemplate = `{
"201": {
"description": "Created",
"schema": {
- "$ref": "#/definitions/models.Server"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.Server"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/servers/groups": {
+ "get": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Get a list of all distinct group names for servers",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Servers"
+ ],
+ "summary": "List all server groups",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1845,19 +2080,19 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.Server"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.Server"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1893,7 +2128,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.ServerUpdate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ServerUpdate"
}
}
],
@@ -1901,19 +2136,19 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.Server"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.Server"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1951,13 +2186,13 @@ const docTemplate = `{
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1985,13 +2220,13 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/server.CurrentUserResponse"
+ "$ref": "#/definitions/internal_server.CurrentUserResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -2021,81 +2256,714 @@ const docTemplate = `{
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/server.ShellInfo"
+ "$ref": "#/definitions/internal_server.ShellInfo"
}
}
}
}
}
- }
- },
- "definitions": {
- "models.BashScriptCreate": {
- "type": "object",
- "required": [
- "content",
- "name"
- ],
- "properties": {
- "content": {
- "type": "string"
- },
- "description": {
- "type": "string"
- },
- "filename": {
- "type": "string"
- },
- "name": {
- "type": "string"
- }
- }
- },
- "models.BashScriptResponse": {
- "type": "object",
- "properties": {
- "content": {
- "description": "Only included when specifically requested",
- "type": "string"
- },
- "created_at": {
- "type": "string"
- },
- "description": {
- "type": "string"
- },
- "filename": {
- "type": "string"
- },
- "id": {
- "type": "integer"
- },
- "name": {
- "type": "string"
- },
- "updated_at": {
- "type": "string"
- }
- }
},
- "models.BashScriptUpdate": {
- "type": "object",
- "properties": {
- "content": {
- "type": "string"
- },
- "description": {
- "type": "string"
- },
- "filename": {
- "type": "string"
+ "/vault/bash-scripts": {
+ "get": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Retrieve all bash scripts stored in Vault",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "List bash scripts from Vault",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_vault.BashScript"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ },
+ "post": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Create a new bash script in HashiCorp Vault storage",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "Store bash script in Vault",
+ "parameters": [
+ {
+ "description": "Bash script data",
+ "name": "script",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "object"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Created script info with source field",
+ "schema": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ },
+ "400": {
+ "description": "Invalid request or Vault not configured",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "500": {
+ "description": "Failed to store script",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/vault/config": {
+ "get": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Retrieve the current Vault configuration (token is never returned)",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "Get Vault configuration",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.VaultConfigResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ },
+ "post": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Configure or update the Vault connection settings",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "Create or update Vault configuration",
+ "parameters": [
+ {
+ "description": "Vault configuration",
+ "name": "config",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.VaultConfigCreate"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.VaultConfigResponse"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ },
+ "delete": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Remove the Vault configuration",
+ "tags": [
+ "Vault"
+ ],
+ "summary": "Delete Vault configuration",
+ "responses": {
+ "204": {
+ "description": "No Content"
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/vault/env-variables": {
+ "get": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Retrieve all environment variables stored in Vault",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "List environment variables from Vault",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_vault.EnvVariable"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ },
+ "post": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Store a new environment variable in Vault",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "Create environment variable in Vault",
+ "parameters": [
+ {
+ "description": "Environment Variable",
+ "name": "envVar",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string"
+ },
+ "group": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Created",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string"
+ },
+ "group": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "source": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/vault/servers": {
+ "get": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Retrieve all server configurations stored in Vault",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "List servers from Vault",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_vault.Server"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ },
+ "post": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Store a new server configuration in Vault",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "Create server in Vault",
+ "parameters": [
+ {
+ "description": "Server",
+ "name": "server",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "object",
+ "properties": {
+ "group": {
+ "type": "string"
+ },
+ "ip_address": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "port": {
+ "type": "integer"
+ },
+ "username": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Created",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "group": {
+ "type": "string"
+ },
+ "ip_address": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "port": {
+ "type": "integer"
+ },
+ "source": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/vault/ssh-keys": {
+ "get": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Retrieve all SSH keys stored in Vault",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "List SSH keys from Vault",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_vault.SSHKey"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ },
+ "post": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Store a new SSH key in Vault",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "Create SSH key in Vault",
+ "parameters": [
+ {
+ "description": "SSH Key",
+ "name": "key",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "object",
+ "properties": {
+ "group": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "private_key": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Created",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "created_at": {
+ "type": "string"
+ },
+ "group": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "source": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/vault/status": {
+ "get": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Check the current Vault connection status",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "Get Vault connection status",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.VaultStatus"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/vault/test": {
+ "post": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Test the connection to Vault and initialize the secrets structure",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "Test Vault connection",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.VaultStatus"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "github_com_pozgo_web-cli_internal_models.BashScriptCreate": {
+ "type": "object",
+ "required": [
+ "content",
+ "name"
+ ],
+ "properties": {
+ "content": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "filename": {
+ "type": "string"
+ },
+ "group": {
+ "description": "Optional, defaults to \"default\"",
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ },
+ "github_com_pozgo_web-cli_internal_models.BashScriptResponse": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "description": "Only included when specifically requested",
+ "type": "string"
+ },
+ "created_at": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "filename": {
+ "type": "string"
+ },
+ "group": {
+ "description": "Group/category for organization",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "source": {
+ "description": "\"sqlite\" or \"vault\"",
+ "type": "string"
+ },
+ "updated_at": {
+ "type": "string"
+ }
+ }
+ },
+ "github_com_pozgo_web-cli_internal_models.BashScriptUpdate": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "filename": {
+ "type": "string"
+ },
+ "group": {
+ "type": "string"
},
"name": {
"type": "string"
}
}
},
- "models.CommandExecution": {
+ "github_com_pozgo_web-cli_internal_models.CommandExecution": {
"type": "object",
"required": [
"command"
@@ -2113,14 +2981,30 @@ const docTemplate = `{
"description": "Optional: save as template with this name",
"type": "string"
},
+ "server_group": {
+ "description": "Server group for remote execution (Vault)",
+ "type": "string"
+ },
"server_id": {
- "description": "Server ID for remote execution",
+ "description": "Server ID for remote execution (SQLite)",
"type": "integer"
},
+ "server_name": {
+ "description": "Server name for remote execution (Vault)",
+ "type": "string"
+ },
+ "ssh_key_group": {
+ "description": "SSH key group for remote execution (Vault)",
+ "type": "string"
+ },
"ssh_key_id": {
- "description": "SSH key ID for remote execution",
+ "description": "SSH key ID for remote execution (SQLite)",
"type": "integer"
},
+ "ssh_key_name": {
+ "description": "SSH key name for remote execution (Vault)",
+ "type": "string"
+ },
"ssh_password": {
"description": "SSH password (for remote, if key auth fails)",
"type": "string"
@@ -2135,7 +3019,7 @@ const docTemplate = `{
}
}
},
- "models.CommandHistory": {
+ "github_com_pozgo_web-cli_internal_models.CommandHistory": {
"type": "object",
"properties": {
"command": {
@@ -2168,7 +3052,7 @@ const docTemplate = `{
}
}
},
- "models.CommandResult": {
+ "github_com_pozgo_web-cli_internal_models.CommandResult": {
"type": "object",
"properties": {
"command": {
@@ -2192,7 +3076,7 @@ const docTemplate = `{
}
}
},
- "models.EnvVariableCreate": {
+ "github_com_pozgo_web-cli_internal_models.EnvVariableCreate": {
"type": "object",
"required": [
"name",
@@ -2202,6 +3086,10 @@ const docTemplate = `{
"description": {
"type": "string"
},
+ "group": {
+ "description": "Optional, defaults to \"default\"",
+ "type": "string"
+ },
"name": {
"type": "string"
},
@@ -2210,7 +3098,7 @@ const docTemplate = `{
}
}
},
- "models.EnvVariableResponse": {
+ "github_com_pozgo_web-cli_internal_models.EnvVariableResponse": {
"type": "object",
"properties": {
"created_at": {
@@ -2219,12 +3107,20 @@ const docTemplate = `{
"description": {
"type": "string"
},
+ "group": {
+ "description": "Group/category for organization",
+ "type": "string"
+ },
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
+ "source": {
+ "description": "\"sqlite\" or \"vault\"",
+ "type": "string"
+ },
"updated_at": {
"type": "string"
},
@@ -2234,12 +3130,15 @@ const docTemplate = `{
}
}
},
- "models.EnvVariableUpdate": {
+ "github_com_pozgo_web-cli_internal_models.EnvVariableUpdate": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
+ "group": {
+ "type": "string"
+ },
"name": {
"type": "string"
},
@@ -2248,7 +3147,7 @@ const docTemplate = `{
}
}
},
- "models.LocalUser": {
+ "github_com_pozgo_web-cli_internal_models.LocalUser": {
"type": "object",
"properties": {
"created_at": {
@@ -2266,7 +3165,7 @@ const docTemplate = `{
}
}
},
- "models.LocalUserCreate": {
+ "github_com_pozgo_web-cli_internal_models.LocalUserCreate": {
"type": "object",
"required": [
"name"
@@ -2278,7 +3177,7 @@ const docTemplate = `{
}
}
},
- "models.LocalUserUpdate": {
+ "github_com_pozgo_web-cli_internal_models.LocalUserUpdate": {
"type": "object",
"properties": {
"name": {
@@ -2287,12 +3186,16 @@ const docTemplate = `{
}
}
},
- "models.SSHKey": {
+ "github_com_pozgo_web-cli_internal_models.SSHKey": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
+ "group": {
+ "description": "Group/category for organization",
+ "type": "string"
+ },
"id": {
"type": "integer"
},
@@ -2303,18 +3206,26 @@ const docTemplate = `{
"description": "Decrypted value",
"type": "string"
},
+ "source": {
+ "description": "\"sqlite\" or \"vault\"",
+ "type": "string"
+ },
"updated_at": {
"type": "string"
}
}
},
- "models.SSHKeyCreate": {
+ "github_com_pozgo_web-cli_internal_models.SSHKeyCreate": {
"type": "object",
"required": [
"name",
"private_key"
],
"properties": {
+ "group": {
+ "description": "Optional, defaults to \"default\"",
+ "type": "string"
+ },
"name": {
"type": "string"
},
@@ -2323,9 +3234,12 @@ const docTemplate = `{
}
}
},
- "models.SSHKeyUpdate": {
+ "github_com_pozgo_web-cli_internal_models.SSHKeyUpdate": {
"type": "object",
"properties": {
+ "group": {
+ "type": "string"
+ },
"name": {
"type": "string"
},
@@ -2334,7 +3248,7 @@ const docTemplate = `{
}
}
},
- "models.SavedCommand": {
+ "github_com_pozgo_web-cli_internal_models.SavedCommand": {
"type": "object",
"properties": {
"command": {
@@ -2376,7 +3290,7 @@ const docTemplate = `{
}
}
},
- "models.SavedCommandCreate": {
+ "github_com_pozgo_web-cli_internal_models.SavedCommandCreate": {
"type": "object",
"required": [
"command",
@@ -2410,7 +3324,7 @@ const docTemplate = `{
}
}
},
- "models.SavedCommandUpdate": {
+ "github_com_pozgo_web-cli_internal_models.SavedCommandUpdate": {
"type": "object",
"properties": {
"command": {
@@ -2436,19 +3350,30 @@ const docTemplate = `{
}
}
},
- "models.ScriptExecution": {
+ "github_com_pozgo_web-cli_internal_models.ScriptExecution": {
"type": "object",
- "required": [
- "script_id"
- ],
"properties": {
+ "env_var_groups": {
+ "description": "Groups of env vars to include (Vault, paired with EnvVarNames)",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
"env_var_ids": {
- "description": "Specific env var IDs to include",
+ "description": "Specific env var IDs to include (SQLite)",
"type": "array",
"items": {
"type": "integer"
}
},
+ "env_var_names": {
+ "description": "Names of env vars to include (Vault)",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
"include_env_vars": {
"description": "Deprecated: use EnvVarIDs instead",
"type": "boolean"
@@ -2457,18 +3382,42 @@ const docTemplate = `{
"description": "True if remote execution",
"type": "boolean"
},
+ "script_group": {
+ "description": "Script group for execution (Vault)",
+ "type": "string"
+ },
"script_id": {
- "description": "ID of the script to execute",
+ "description": "ID of the script to execute (SQLite)",
"type": "integer"
},
+ "script_name": {
+ "description": "Name of the script to execute (Vault)",
+ "type": "string"
+ },
+ "server_group": {
+ "description": "Server group for remote execution (Vault)",
+ "type": "string"
+ },
"server_id": {
- "description": "Server ID for remote execution",
+ "description": "Server ID for remote execution (SQLite)",
"type": "integer"
},
+ "server_name": {
+ "description": "Server name for remote execution (Vault)",
+ "type": "string"
+ },
+ "ssh_key_group": {
+ "description": "SSH key group for remote execution (Vault)",
+ "type": "string"
+ },
"ssh_key_id": {
- "description": "SSH key ID for remote execution",
+ "description": "SSH key ID for remote execution (SQLite)",
"type": "integer"
},
+ "ssh_key_name": {
+ "description": "SSH key name for remote execution (Vault)",
+ "type": "string"
+ },
"ssh_password": {
"description": "SSH password (for remote, if key auth fails)",
"type": "string"
@@ -2483,7 +3432,7 @@ const docTemplate = `{
}
}
},
- "models.ScriptPresetCreate": {
+ "github_com_pozgo_web-cli_internal_models.ScriptPresetCreate": {
"type": "object",
"required": [
"name",
@@ -2519,7 +3468,7 @@ const docTemplate = `{
}
}
},
- "models.ScriptPresetResponse": {
+ "github_com_pozgo_web-cli_internal_models.ScriptPresetResponse": {
"type": "object",
"properties": {
"created_at": {
@@ -2560,7 +3509,7 @@ const docTemplate = `{
}
}
},
- "models.ScriptPresetUpdate": {
+ "github_com_pozgo_web-cli_internal_models.ScriptPresetUpdate": {
"type": "object",
"properties": {
"description": {
@@ -2592,7 +3541,7 @@ const docTemplate = `{
}
}
},
- "models.ScriptResult": {
+ "github_com_pozgo_web-cli_internal_models.ScriptResult": {
"type": "object",
"properties": {
"env_vars_injected": {
@@ -2624,12 +3573,16 @@ const docTemplate = `{
}
}
},
- "models.Server": {
+ "github_com_pozgo_web-cli_internal_models.Server": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
+ "group": {
+ "description": "Group/category for organization",
+ "type": "string"
+ },
"id": {
"type": "integer"
},
@@ -2645,6 +3598,10 @@ const docTemplate = `{
"description": "SSH port (default: 22)",
"type": "integer"
},
+ "source": {
+ "description": "\"sqlite\" or \"vault\"",
+ "type": "string"
+ },
"updated_at": {
"type": "string"
},
@@ -2654,9 +3611,13 @@ const docTemplate = `{
}
}
},
- "models.ServerCreate": {
+ "github_com_pozgo_web-cli_internal_models.ServerCreate": {
"type": "object",
"properties": {
+ "group": {
+ "description": "Optional, defaults to \"default\"",
+ "type": "string"
+ },
"ip_address": {
"type": "string"
},
@@ -2673,9 +3634,162 @@ const docTemplate = `{
}
}
},
- "models.ServerUpdate": {
+ "github_com_pozgo_web-cli_internal_models.ServerUpdate": {
+ "type": "object",
+ "properties": {
+ "group": {
+ "type": "string"
+ },
+ "ip_address": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "port": {
+ "type": "integer"
+ },
+ "username": {
+ "type": "string"
+ }
+ }
+ },
+ "github_com_pozgo_web-cli_internal_models.VaultConfigCreate": {
+ "type": "object",
+ "properties": {
+ "address": {
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "mount_path": {
+ "type": "string"
+ },
+ "namespace": {
+ "type": "string"
+ },
+ "token": {
+ "type": "string"
+ }
+ }
+ },
+ "github_com_pozgo_web-cli_internal_models.VaultConfigResponse": {
+ "type": "object",
+ "properties": {
+ "address": {
+ "type": "string"
+ },
+ "created_at": {
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "has_token": {
+ "description": "Indicates if a token is configured",
+ "type": "boolean"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "mount_path": {
+ "type": "string"
+ },
+ "namespace": {
+ "type": "string"
+ },
+ "updated_at": {
+ "type": "string"
+ }
+ }
+ },
+ "github_com_pozgo_web-cli_internal_models.VaultStatus": {
+ "type": "object",
+ "properties": {
+ "address": {
+ "type": "string"
+ },
+ "configured": {
+ "description": "Whether Vault is configured",
+ "type": "boolean"
+ },
+ "connected": {
+ "description": "Whether connection test passed",
+ "type": "boolean"
+ },
+ "enabled": {
+ "description": "Whether Vault is enabled",
+ "type": "boolean"
+ },
+ "error": {
+ "type": "string"
+ },
+ "vault_sealed": {
+ "type": "boolean"
+ }
+ }
+ },
+ "github_com_pozgo_web-cli_internal_vault.BashScript": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "filename": {
+ "type": "string"
+ },
+ "group": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ },
+ "github_com_pozgo_web-cli_internal_vault.EnvVariable": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string"
+ },
+ "group": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ }
+ },
+ "github_com_pozgo_web-cli_internal_vault.SSHKey": {
+ "type": "object",
+ "properties": {
+ "created_at": {
+ "type": "string"
+ },
+ "group": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "private_key": {
+ "type": "string"
+ }
+ }
+ },
+ "github_com_pozgo_web-cli_internal_vault.Server": {
"type": "object",
"properties": {
+ "group": {
+ "type": "string"
+ },
"ip_address": {
"type": "string"
},
@@ -2690,7 +3804,7 @@ const docTemplate = `{
}
}
},
- "server.CurrentUserResponse": {
+ "internal_server.CurrentUserResponse": {
"description": "Current system user information",
"type": "object",
"properties": {
@@ -2716,7 +3830,7 @@ const docTemplate = `{
}
}
},
- "server.ErrorResponse": {
+ "internal_server.ErrorResponse": {
"description": "Error response returned by the API",
"type": "object",
"properties": {
@@ -2726,7 +3840,7 @@ const docTemplate = `{
}
}
},
- "server.ShellInfo": {
+ "internal_server.ShellInfo": {
"description": "Information about an available shell",
"type": "object",
"properties": {
@@ -2739,6 +3853,27 @@ const docTemplate = `{
"example": "/bin/bash"
}
}
+ },
+ "internal_server.StreamMessage": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "output chunk or error message",
+ "type": "string"
+ },
+ "result": {
+ "description": "final result",
+ "allOf": [
+ {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptResult"
+ }
+ ]
+ },
+ "type": {
+ "description": "\"output\", \"result\", \"error\"",
+ "type": "string"
+ }
+ }
}
},
"securityDefinitions": {
diff --git a/docs/images/admin-servers.png b/docs/images/admin-servers.png
index b97f364..63a6bb5 100644
Binary files a/docs/images/admin-servers.png and b/docs/images/admin-servers.png differ
diff --git a/docs/images/admin-ssh-keys.png b/docs/images/admin-ssh-keys.png
index 2a9fa1d..b31e7c0 100644
Binary files a/docs/images/admin-ssh-keys.png and b/docs/images/admin-ssh-keys.png differ
diff --git a/docs/images/admin-vault-integration.png b/docs/images/admin-vault-integration.png
new file mode 100644
index 0000000..ef72c0c
Binary files /dev/null and b/docs/images/admin-vault-integration.png differ
diff --git a/docs/images/command-history.png b/docs/images/command-history.png
index 9b42044..23d9328 100644
Binary files a/docs/images/command-history.png and b/docs/images/command-history.png differ
diff --git a/docs/images/dashboard.png b/docs/images/dashboard.png
index bf11c37..4c217b3 100644
Binary files a/docs/images/dashboard.png and b/docs/images/dashboard.png differ
diff --git a/docs/images/interactive-terminal-dropdown.png b/docs/images/interactive-terminal-dropdown.png
new file mode 100644
index 0000000..3799294
Binary files /dev/null and b/docs/images/interactive-terminal-dropdown.png differ
diff --git a/docs/images/interactive-terminal.png b/docs/images/interactive-terminal.png
index 0be6783..40f0b32 100644
Binary files a/docs/images/interactive-terminal.png and b/docs/images/interactive-terminal.png differ
diff --git a/docs/images/local-commands.png b/docs/images/local-commands.png
index ec9f9b3..801e473 100644
Binary files a/docs/images/local-commands.png and b/docs/images/local-commands.png differ
diff --git a/docs/images/remote-commands.png b/docs/images/remote-commands.png
index 9d1a9ae..28a8700 100644
Binary files a/docs/images/remote-commands.png and b/docs/images/remote-commands.png differ
diff --git a/docs/images/remote-script.png b/docs/images/remote-script.png
index 72255d0..8ee01f3 100644
Binary files a/docs/images/remote-script.png and b/docs/images/remote-script.png differ
diff --git a/docs/images/saved-commands.png b/docs/images/saved-commands.png
index b63ae06..582fa7f 100644
Binary files a/docs/images/saved-commands.png and b/docs/images/saved-commands.png differ
diff --git a/docs/swagger.json b/docs/swagger.json
index 5ab8c04..0914d0d 100644
--- a/docs/swagger.json
+++ b/docs/swagger.json
@@ -36,20 +36,28 @@
"Bash Scripts"
],
"summary": "List all bash scripts",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Filter by group name",
+ "name": "group",
+ "in": "query"
+ }
+ ],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/models.BashScriptResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.BashScriptResponse"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -78,7 +86,7 @@
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.BashScriptCreate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.BashScriptCreate"
}
}
],
@@ -86,19 +94,19 @@
"201": {
"description": "Created",
"schema": {
- "$ref": "#/definitions/models.BashScriptResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.BashScriptResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -129,7 +137,64 @@
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.ScriptExecution"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptExecution"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptResult"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/bash-scripts/execute/stream": {
+ "post": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Execute a stored bash script locally or remotely with real-time output streaming via SSE",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "text/event-stream"
+ ],
+ "tags": [
+ "Bash Scripts"
+ ],
+ "summary": "Execute a bash script with streaming output",
+ "parameters": [
+ {
+ "description": "Script execution request",
+ "name": "execution",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptExecution"
}
}
],
@@ -137,25 +202,62 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.ScriptResult"
+ "$ref": "#/definitions/internal_server.StreamMessage"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/bash-scripts/groups": {
+ "get": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Get a list of all distinct group names for bash scripts",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Bash Scripts"
+ ],
+ "summary": "List all bash script groups",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -199,19 +301,19 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.BashScriptResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.BashScriptResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -247,7 +349,7 @@
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.BashScriptUpdate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.BashScriptUpdate"
}
}
],
@@ -255,19 +357,19 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.BashScriptResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.BashScriptResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -305,13 +407,13 @@
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -350,26 +452,26 @@
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/models.ScriptPresetResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptPresetResponse"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -400,7 +502,7 @@
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.CommandExecution"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.CommandExecution"
}
}
],
@@ -408,25 +510,25 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.CommandResult"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.CommandResult"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -456,6 +558,12 @@
"description": "Show actual values instead of masked values",
"name": "show_values",
"in": "query"
+ },
+ {
+ "type": "string",
+ "description": "Filter by group name",
+ "name": "group",
+ "in": "query"
}
],
"responses": {
@@ -464,14 +572,14 @@
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/models.EnvVariableResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.EnvVariableResponse"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -500,7 +608,7 @@
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.EnvVariableCreate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.EnvVariableCreate"
}
}
],
@@ -508,19 +616,56 @@
"201": {
"description": "Created",
"schema": {
- "$ref": "#/definitions/models.EnvVariableResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.EnvVariableResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/env-variables/groups": {
+ "get": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Get a list of all distinct group names for environment variables",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Environment Variables"
+ ],
+ "summary": "List all environment variable groups",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -563,19 +708,19 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.EnvVariableResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.EnvVariableResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -611,7 +756,7 @@
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.EnvVariableUpdate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.EnvVariableUpdate"
}
}
],
@@ -619,19 +764,19 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.EnvVariableResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.EnvVariableResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -669,13 +814,13 @@
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -720,14 +865,14 @@
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/models.CommandHistory"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.CommandHistory"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -764,19 +909,19 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.CommandHistory"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.CommandHistory"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -800,20 +945,28 @@
"SSH Keys"
],
"summary": "List all SSH keys",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Filter by group name",
+ "name": "group",
+ "in": "query"
+ }
+ ],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/models.SSHKey"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SSHKey"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -842,7 +995,7 @@
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.SSHKeyCreate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SSHKeyCreate"
}
}
],
@@ -850,19 +1003,56 @@
"201": {
"description": "Created",
"schema": {
- "$ref": "#/definitions/models.SSHKey"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SSHKey"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/keys/groups": {
+ "get": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Get a list of all distinct group names for SSH keys",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "SSH Keys"
+ ],
+ "summary": "List all SSH key groups",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -899,19 +1089,19 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.SSHKey"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SSHKey"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -947,7 +1137,7 @@
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.SSHKeyUpdate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SSHKeyUpdate"
}
}
],
@@ -955,19 +1145,19 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.SSHKey"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SSHKey"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1005,13 +1195,13 @@
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1041,14 +1231,14 @@
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/models.LocalUser"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.LocalUser"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1077,7 +1267,7 @@
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.LocalUserCreate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.LocalUserCreate"
}
}
],
@@ -1085,19 +1275,19 @@
"201": {
"description": "Created",
"schema": {
- "$ref": "#/definitions/models.LocalUser"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.LocalUser"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1134,19 +1324,19 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.LocalUser"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.LocalUser"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1182,7 +1372,7 @@
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.LocalUserUpdate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.LocalUserUpdate"
}
}
],
@@ -1190,19 +1380,19 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.LocalUser"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.LocalUser"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1240,13 +1430,13 @@
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1276,14 +1466,14 @@
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/models.SavedCommand"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SavedCommand"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1312,7 +1502,7 @@
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.SavedCommandCreate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SavedCommandCreate"
}
}
],
@@ -1320,19 +1510,19 @@
"201": {
"description": "Created",
"schema": {
- "$ref": "#/definitions/models.SavedCommand"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SavedCommand"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1369,19 +1559,19 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.SavedCommand"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SavedCommand"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1417,7 +1607,7 @@
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.SavedCommandUpdate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SavedCommandUpdate"
}
}
],
@@ -1425,19 +1615,19 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.SavedCommand"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.SavedCommand"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1475,13 +1665,13 @@
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1511,14 +1701,14 @@
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/models.ScriptPresetResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptPresetResponse"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1547,7 +1737,7 @@
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.ScriptPresetCreate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptPresetCreate"
}
}
],
@@ -1555,19 +1745,19 @@
"201": {
"description": "Created",
"schema": {
- "$ref": "#/definitions/models.ScriptPresetResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptPresetResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1604,19 +1794,19 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.ScriptPresetResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptPresetResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1652,7 +1842,7 @@
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.ScriptPresetUpdate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptPresetUpdate"
}
}
],
@@ -1660,19 +1850,19 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.ScriptPresetResponse"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptPresetResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1710,13 +1900,13 @@
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1740,20 +1930,28 @@
"Servers"
],
"summary": "List all servers",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "Filter by group name",
+ "name": "group",
+ "in": "query"
+ }
+ ],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/models.Server"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.Server"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1782,7 +1980,7 @@
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.ServerCreate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ServerCreate"
}
}
],
@@ -1790,19 +1988,56 @@
"201": {
"description": "Created",
"schema": {
- "$ref": "#/definitions/models.Server"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.Server"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/servers/groups": {
+ "get": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Get a list of all distinct group names for servers",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Servers"
+ ],
+ "summary": "List all server groups",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1839,19 +2074,19 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.Server"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.Server"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1887,7 +2122,7 @@
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/models.ServerUpdate"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ServerUpdate"
}
}
],
@@ -1895,19 +2130,19 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/models.Server"
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.Server"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1945,13 +2180,13 @@
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -1979,13 +2214,13 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/server.CurrentUserResponse"
+ "$ref": "#/definitions/internal_server.CurrentUserResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/server.ErrorResponse"
+ "$ref": "#/definitions/internal_server.ErrorResponse"
}
}
}
@@ -2015,81 +2250,714 @@
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/server.ShellInfo"
+ "$ref": "#/definitions/internal_server.ShellInfo"
}
}
}
}
}
- }
- },
- "definitions": {
- "models.BashScriptCreate": {
- "type": "object",
- "required": [
- "content",
- "name"
- ],
- "properties": {
- "content": {
- "type": "string"
- },
- "description": {
- "type": "string"
- },
- "filename": {
- "type": "string"
- },
- "name": {
- "type": "string"
- }
- }
- },
- "models.BashScriptResponse": {
- "type": "object",
- "properties": {
- "content": {
- "description": "Only included when specifically requested",
- "type": "string"
- },
- "created_at": {
- "type": "string"
- },
- "description": {
- "type": "string"
- },
- "filename": {
- "type": "string"
- },
- "id": {
- "type": "integer"
- },
- "name": {
- "type": "string"
- },
- "updated_at": {
- "type": "string"
- }
- }
},
- "models.BashScriptUpdate": {
- "type": "object",
- "properties": {
- "content": {
- "type": "string"
- },
- "description": {
- "type": "string"
- },
- "filename": {
- "type": "string"
+ "/vault/bash-scripts": {
+ "get": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Retrieve all bash scripts stored in Vault",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "List bash scripts from Vault",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_vault.BashScript"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ },
+ "post": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Create a new bash script in HashiCorp Vault storage",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "Store bash script in Vault",
+ "parameters": [
+ {
+ "description": "Bash script data",
+ "name": "script",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "object"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Created script info with source field",
+ "schema": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ },
+ "400": {
+ "description": "Invalid request or Vault not configured",
+ "schema": {
+ "type": "string"
+ }
+ },
+ "500": {
+ "description": "Failed to store script",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "/vault/config": {
+ "get": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Retrieve the current Vault configuration (token is never returned)",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "Get Vault configuration",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.VaultConfigResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ },
+ "post": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Configure or update the Vault connection settings",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "Create or update Vault configuration",
+ "parameters": [
+ {
+ "description": "Vault configuration",
+ "name": "config",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.VaultConfigCreate"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.VaultConfigResponse"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ },
+ "delete": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Remove the Vault configuration",
+ "tags": [
+ "Vault"
+ ],
+ "summary": "Delete Vault configuration",
+ "responses": {
+ "204": {
+ "description": "No Content"
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/vault/env-variables": {
+ "get": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Retrieve all environment variables stored in Vault",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "List environment variables from Vault",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_vault.EnvVariable"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ },
+ "post": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Store a new environment variable in Vault",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "Create environment variable in Vault",
+ "parameters": [
+ {
+ "description": "Environment Variable",
+ "name": "envVar",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string"
+ },
+ "group": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Created",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string"
+ },
+ "group": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "source": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/vault/servers": {
+ "get": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Retrieve all server configurations stored in Vault",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "List servers from Vault",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_vault.Server"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ },
+ "post": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Store a new server configuration in Vault",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "Create server in Vault",
+ "parameters": [
+ {
+ "description": "Server",
+ "name": "server",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "object",
+ "properties": {
+ "group": {
+ "type": "string"
+ },
+ "ip_address": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "port": {
+ "type": "integer"
+ },
+ "username": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Created",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "group": {
+ "type": "string"
+ },
+ "ip_address": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "port": {
+ "type": "integer"
+ },
+ "source": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/vault/ssh-keys": {
+ "get": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Retrieve all SSH keys stored in Vault",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "List SSH keys from Vault",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_vault.SSHKey"
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ },
+ "post": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Store a new SSH key in Vault",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "Create SSH key in Vault",
+ "parameters": [
+ {
+ "description": "SSH Key",
+ "name": "key",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "type": "object",
+ "properties": {
+ "group": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "private_key": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Created",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "created_at": {
+ "type": "string"
+ },
+ "group": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "source": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/vault/status": {
+ "get": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Check the current Vault connection status",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "Get Vault connection status",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.VaultStatus"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/vault/test": {
+ "post": {
+ "security": [
+ {
+ "BasicAuth": []
+ }
+ ],
+ "description": "Test the connection to Vault and initialize the secrets structure",
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Vault"
+ ],
+ "summary": "Test Vault connection",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.VaultStatus"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/internal_server.ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "github_com_pozgo_web-cli_internal_models.BashScriptCreate": {
+ "type": "object",
+ "required": [
+ "content",
+ "name"
+ ],
+ "properties": {
+ "content": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "filename": {
+ "type": "string"
+ },
+ "group": {
+ "description": "Optional, defaults to \"default\"",
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ },
+ "github_com_pozgo_web-cli_internal_models.BashScriptResponse": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "description": "Only included when specifically requested",
+ "type": "string"
+ },
+ "created_at": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "filename": {
+ "type": "string"
+ },
+ "group": {
+ "description": "Group/category for organization",
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ },
+ "source": {
+ "description": "\"sqlite\" or \"vault\"",
+ "type": "string"
+ },
+ "updated_at": {
+ "type": "string"
+ }
+ }
+ },
+ "github_com_pozgo_web-cli_internal_models.BashScriptUpdate": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "filename": {
+ "type": "string"
+ },
+ "group": {
+ "type": "string"
},
"name": {
"type": "string"
}
}
},
- "models.CommandExecution": {
+ "github_com_pozgo_web-cli_internal_models.CommandExecution": {
"type": "object",
"required": [
"command"
@@ -2107,14 +2975,30 @@
"description": "Optional: save as template with this name",
"type": "string"
},
+ "server_group": {
+ "description": "Server group for remote execution (Vault)",
+ "type": "string"
+ },
"server_id": {
- "description": "Server ID for remote execution",
+ "description": "Server ID for remote execution (SQLite)",
"type": "integer"
},
+ "server_name": {
+ "description": "Server name for remote execution (Vault)",
+ "type": "string"
+ },
+ "ssh_key_group": {
+ "description": "SSH key group for remote execution (Vault)",
+ "type": "string"
+ },
"ssh_key_id": {
- "description": "SSH key ID for remote execution",
+ "description": "SSH key ID for remote execution (SQLite)",
"type": "integer"
},
+ "ssh_key_name": {
+ "description": "SSH key name for remote execution (Vault)",
+ "type": "string"
+ },
"ssh_password": {
"description": "SSH password (for remote, if key auth fails)",
"type": "string"
@@ -2129,7 +3013,7 @@
}
}
},
- "models.CommandHistory": {
+ "github_com_pozgo_web-cli_internal_models.CommandHistory": {
"type": "object",
"properties": {
"command": {
@@ -2162,7 +3046,7 @@
}
}
},
- "models.CommandResult": {
+ "github_com_pozgo_web-cli_internal_models.CommandResult": {
"type": "object",
"properties": {
"command": {
@@ -2186,7 +3070,7 @@
}
}
},
- "models.EnvVariableCreate": {
+ "github_com_pozgo_web-cli_internal_models.EnvVariableCreate": {
"type": "object",
"required": [
"name",
@@ -2196,6 +3080,10 @@
"description": {
"type": "string"
},
+ "group": {
+ "description": "Optional, defaults to \"default\"",
+ "type": "string"
+ },
"name": {
"type": "string"
},
@@ -2204,7 +3092,7 @@
}
}
},
- "models.EnvVariableResponse": {
+ "github_com_pozgo_web-cli_internal_models.EnvVariableResponse": {
"type": "object",
"properties": {
"created_at": {
@@ -2213,12 +3101,20 @@
"description": {
"type": "string"
},
+ "group": {
+ "description": "Group/category for organization",
+ "type": "string"
+ },
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
+ "source": {
+ "description": "\"sqlite\" or \"vault\"",
+ "type": "string"
+ },
"updated_at": {
"type": "string"
},
@@ -2228,12 +3124,15 @@
}
}
},
- "models.EnvVariableUpdate": {
+ "github_com_pozgo_web-cli_internal_models.EnvVariableUpdate": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
+ "group": {
+ "type": "string"
+ },
"name": {
"type": "string"
},
@@ -2242,7 +3141,7 @@
}
}
},
- "models.LocalUser": {
+ "github_com_pozgo_web-cli_internal_models.LocalUser": {
"type": "object",
"properties": {
"created_at": {
@@ -2260,7 +3159,7 @@
}
}
},
- "models.LocalUserCreate": {
+ "github_com_pozgo_web-cli_internal_models.LocalUserCreate": {
"type": "object",
"required": [
"name"
@@ -2272,7 +3171,7 @@
}
}
},
- "models.LocalUserUpdate": {
+ "github_com_pozgo_web-cli_internal_models.LocalUserUpdate": {
"type": "object",
"properties": {
"name": {
@@ -2281,12 +3180,16 @@
}
}
},
- "models.SSHKey": {
+ "github_com_pozgo_web-cli_internal_models.SSHKey": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
+ "group": {
+ "description": "Group/category for organization",
+ "type": "string"
+ },
"id": {
"type": "integer"
},
@@ -2297,18 +3200,26 @@
"description": "Decrypted value",
"type": "string"
},
+ "source": {
+ "description": "\"sqlite\" or \"vault\"",
+ "type": "string"
+ },
"updated_at": {
"type": "string"
}
}
},
- "models.SSHKeyCreate": {
+ "github_com_pozgo_web-cli_internal_models.SSHKeyCreate": {
"type": "object",
"required": [
"name",
"private_key"
],
"properties": {
+ "group": {
+ "description": "Optional, defaults to \"default\"",
+ "type": "string"
+ },
"name": {
"type": "string"
},
@@ -2317,9 +3228,12 @@
}
}
},
- "models.SSHKeyUpdate": {
+ "github_com_pozgo_web-cli_internal_models.SSHKeyUpdate": {
"type": "object",
"properties": {
+ "group": {
+ "type": "string"
+ },
"name": {
"type": "string"
},
@@ -2328,7 +3242,7 @@
}
}
},
- "models.SavedCommand": {
+ "github_com_pozgo_web-cli_internal_models.SavedCommand": {
"type": "object",
"properties": {
"command": {
@@ -2370,7 +3284,7 @@
}
}
},
- "models.SavedCommandCreate": {
+ "github_com_pozgo_web-cli_internal_models.SavedCommandCreate": {
"type": "object",
"required": [
"command",
@@ -2404,7 +3318,7 @@
}
}
},
- "models.SavedCommandUpdate": {
+ "github_com_pozgo_web-cli_internal_models.SavedCommandUpdate": {
"type": "object",
"properties": {
"command": {
@@ -2430,19 +3344,30 @@
}
}
},
- "models.ScriptExecution": {
+ "github_com_pozgo_web-cli_internal_models.ScriptExecution": {
"type": "object",
- "required": [
- "script_id"
- ],
"properties": {
+ "env_var_groups": {
+ "description": "Groups of env vars to include (Vault, paired with EnvVarNames)",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
"env_var_ids": {
- "description": "Specific env var IDs to include",
+ "description": "Specific env var IDs to include (SQLite)",
"type": "array",
"items": {
"type": "integer"
}
},
+ "env_var_names": {
+ "description": "Names of env vars to include (Vault)",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
"include_env_vars": {
"description": "Deprecated: use EnvVarIDs instead",
"type": "boolean"
@@ -2451,18 +3376,42 @@
"description": "True if remote execution",
"type": "boolean"
},
+ "script_group": {
+ "description": "Script group for execution (Vault)",
+ "type": "string"
+ },
"script_id": {
- "description": "ID of the script to execute",
+ "description": "ID of the script to execute (SQLite)",
"type": "integer"
},
+ "script_name": {
+ "description": "Name of the script to execute (Vault)",
+ "type": "string"
+ },
+ "server_group": {
+ "description": "Server group for remote execution (Vault)",
+ "type": "string"
+ },
"server_id": {
- "description": "Server ID for remote execution",
+ "description": "Server ID for remote execution (SQLite)",
"type": "integer"
},
+ "server_name": {
+ "description": "Server name for remote execution (Vault)",
+ "type": "string"
+ },
+ "ssh_key_group": {
+ "description": "SSH key group for remote execution (Vault)",
+ "type": "string"
+ },
"ssh_key_id": {
- "description": "SSH key ID for remote execution",
+ "description": "SSH key ID for remote execution (SQLite)",
"type": "integer"
},
+ "ssh_key_name": {
+ "description": "SSH key name for remote execution (Vault)",
+ "type": "string"
+ },
"ssh_password": {
"description": "SSH password (for remote, if key auth fails)",
"type": "string"
@@ -2477,7 +3426,7 @@
}
}
},
- "models.ScriptPresetCreate": {
+ "github_com_pozgo_web-cli_internal_models.ScriptPresetCreate": {
"type": "object",
"required": [
"name",
@@ -2513,7 +3462,7 @@
}
}
},
- "models.ScriptPresetResponse": {
+ "github_com_pozgo_web-cli_internal_models.ScriptPresetResponse": {
"type": "object",
"properties": {
"created_at": {
@@ -2554,7 +3503,7 @@
}
}
},
- "models.ScriptPresetUpdate": {
+ "github_com_pozgo_web-cli_internal_models.ScriptPresetUpdate": {
"type": "object",
"properties": {
"description": {
@@ -2586,7 +3535,7 @@
}
}
},
- "models.ScriptResult": {
+ "github_com_pozgo_web-cli_internal_models.ScriptResult": {
"type": "object",
"properties": {
"env_vars_injected": {
@@ -2618,12 +3567,16 @@
}
}
},
- "models.Server": {
+ "github_com_pozgo_web-cli_internal_models.Server": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
+ "group": {
+ "description": "Group/category for organization",
+ "type": "string"
+ },
"id": {
"type": "integer"
},
@@ -2639,6 +3592,10 @@
"description": "SSH port (default: 22)",
"type": "integer"
},
+ "source": {
+ "description": "\"sqlite\" or \"vault\"",
+ "type": "string"
+ },
"updated_at": {
"type": "string"
},
@@ -2648,9 +3605,13 @@
}
}
},
- "models.ServerCreate": {
+ "github_com_pozgo_web-cli_internal_models.ServerCreate": {
"type": "object",
"properties": {
+ "group": {
+ "description": "Optional, defaults to \"default\"",
+ "type": "string"
+ },
"ip_address": {
"type": "string"
},
@@ -2667,9 +3628,162 @@
}
}
},
- "models.ServerUpdate": {
+ "github_com_pozgo_web-cli_internal_models.ServerUpdate": {
+ "type": "object",
+ "properties": {
+ "group": {
+ "type": "string"
+ },
+ "ip_address": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "port": {
+ "type": "integer"
+ },
+ "username": {
+ "type": "string"
+ }
+ }
+ },
+ "github_com_pozgo_web-cli_internal_models.VaultConfigCreate": {
+ "type": "object",
+ "properties": {
+ "address": {
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "mount_path": {
+ "type": "string"
+ },
+ "namespace": {
+ "type": "string"
+ },
+ "token": {
+ "type": "string"
+ }
+ }
+ },
+ "github_com_pozgo_web-cli_internal_models.VaultConfigResponse": {
+ "type": "object",
+ "properties": {
+ "address": {
+ "type": "string"
+ },
+ "created_at": {
+ "type": "string"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "has_token": {
+ "description": "Indicates if a token is configured",
+ "type": "boolean"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "mount_path": {
+ "type": "string"
+ },
+ "namespace": {
+ "type": "string"
+ },
+ "updated_at": {
+ "type": "string"
+ }
+ }
+ },
+ "github_com_pozgo_web-cli_internal_models.VaultStatus": {
+ "type": "object",
+ "properties": {
+ "address": {
+ "type": "string"
+ },
+ "configured": {
+ "description": "Whether Vault is configured",
+ "type": "boolean"
+ },
+ "connected": {
+ "description": "Whether connection test passed",
+ "type": "boolean"
+ },
+ "enabled": {
+ "description": "Whether Vault is enabled",
+ "type": "boolean"
+ },
+ "error": {
+ "type": "string"
+ },
+ "vault_sealed": {
+ "type": "boolean"
+ }
+ }
+ },
+ "github_com_pozgo_web-cli_internal_vault.BashScript": {
+ "type": "object",
+ "properties": {
+ "content": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "filename": {
+ "type": "string"
+ },
+ "group": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ },
+ "github_com_pozgo_web-cli_internal_vault.EnvVariable": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string"
+ },
+ "group": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ }
+ },
+ "github_com_pozgo_web-cli_internal_vault.SSHKey": {
+ "type": "object",
+ "properties": {
+ "created_at": {
+ "type": "string"
+ },
+ "group": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "private_key": {
+ "type": "string"
+ }
+ }
+ },
+ "github_com_pozgo_web-cli_internal_vault.Server": {
"type": "object",
"properties": {
+ "group": {
+ "type": "string"
+ },
"ip_address": {
"type": "string"
},
@@ -2684,7 +3798,7 @@
}
}
},
- "server.CurrentUserResponse": {
+ "internal_server.CurrentUserResponse": {
"description": "Current system user information",
"type": "object",
"properties": {
@@ -2710,7 +3824,7 @@
}
}
},
- "server.ErrorResponse": {
+ "internal_server.ErrorResponse": {
"description": "Error response returned by the API",
"type": "object",
"properties": {
@@ -2720,7 +3834,7 @@
}
}
},
- "server.ShellInfo": {
+ "internal_server.ShellInfo": {
"description": "Information about an available shell",
"type": "object",
"properties": {
@@ -2733,6 +3847,27 @@
"example": "/bin/bash"
}
}
+ },
+ "internal_server.StreamMessage": {
+ "type": "object",
+ "properties": {
+ "data": {
+ "description": "output chunk or error message",
+ "type": "string"
+ },
+ "result": {
+ "description": "final result",
+ "allOf": [
+ {
+ "$ref": "#/definitions/github_com_pozgo_web-cli_internal_models.ScriptResult"
+ }
+ ]
+ },
+ "type": {
+ "description": "\"output\", \"result\", \"error\"",
+ "type": "string"
+ }
+ }
}
},
"securityDefinitions": {
diff --git a/docs/swagger.yaml b/docs/swagger.yaml
index 0c5b5ea..07906ae 100644
--- a/docs/swagger.yaml
+++ b/docs/swagger.yaml
@@ -1,6 +1,6 @@
basePath: /api
definitions:
- models.BashScriptCreate:
+ github_com_pozgo_web-cli_internal_models.BashScriptCreate:
properties:
content:
type: string
@@ -8,13 +8,16 @@ definitions:
type: string
filename:
type: string
+ group:
+ description: Optional, defaults to "default"
+ type: string
name:
type: string
required:
- content
- name
type: object
- models.BashScriptResponse:
+ github_com_pozgo_web-cli_internal_models.BashScriptResponse:
properties:
content:
description: Only included when specifically requested
@@ -25,14 +28,20 @@ definitions:
type: string
filename:
type: string
+ group:
+ description: Group/category for organization
+ type: string
id:
type: integer
name:
type: string
+ source:
+ description: '"sqlite" or "vault"'
+ type: string
updated_at:
type: string
type: object
- models.BashScriptUpdate:
+ github_com_pozgo_web-cli_internal_models.BashScriptUpdate:
properties:
content:
type: string
@@ -40,10 +49,12 @@ definitions:
type: string
filename:
type: string
+ group:
+ type: string
name:
type: string
type: object
- models.CommandExecution:
+ github_com_pozgo_web-cli_internal_models.CommandExecution:
properties:
command:
description: Command to execute
@@ -54,12 +65,24 @@ definitions:
save_as:
description: 'Optional: save as template with this name'
type: string
+ server_group:
+ description: Server group for remote execution (Vault)
+ type: string
server_id:
- description: Server ID for remote execution
+ description: Server ID for remote execution (SQLite)
type: integer
+ server_name:
+ description: Server name for remote execution (Vault)
+ type: string
+ ssh_key_group:
+ description: SSH key group for remote execution (Vault)
+ type: string
ssh_key_id:
- description: SSH key ID for remote execution
+ description: SSH key ID for remote execution (SQLite)
type: integer
+ ssh_key_name:
+ description: SSH key name for remote execution (Vault)
+ type: string
ssh_password:
description: SSH password (for remote, if key auth fails)
type: string
@@ -72,7 +95,7 @@ definitions:
required:
- command
type: object
- models.CommandHistory:
+ github_com_pozgo_web-cli_internal_models.CommandHistory:
properties:
command:
description: Decrypted value
@@ -95,7 +118,7 @@ definitions:
description: User who executed the command (for local commands)
type: string
type: object
- models.CommandResult:
+ github_com_pozgo_web-cli_internal_models.CommandResult:
properties:
command:
type: string
@@ -111,10 +134,13 @@ definitions:
user:
type: string
type: object
- models.EnvVariableCreate:
+ github_com_pozgo_web-cli_internal_models.EnvVariableCreate:
properties:
description:
type: string
+ group:
+ description: Optional, defaults to "default"
+ type: string
name:
type: string
value:
@@ -123,32 +149,40 @@ definitions:
- name
- value
type: object
- models.EnvVariableResponse:
+ github_com_pozgo_web-cli_internal_models.EnvVariableResponse:
properties:
created_at:
type: string
description:
type: string
+ group:
+ description: Group/category for organization
+ type: string
id:
type: integer
name:
type: string
+ source:
+ description: '"sqlite" or "vault"'
+ type: string
updated_at:
type: string
value:
description: Will be masked unless explicitly requested
type: string
type: object
- models.EnvVariableUpdate:
+ github_com_pozgo_web-cli_internal_models.EnvVariableUpdate:
properties:
description:
type: string
+ group:
+ type: string
name:
type: string
value:
type: string
type: object
- models.LocalUser:
+ github_com_pozgo_web-cli_internal_models.LocalUser:
properties:
created_at:
type: string
@@ -160,7 +194,7 @@ definitions:
updated_at:
type: string
type: object
- models.LocalUserCreate:
+ github_com_pozgo_web-cli_internal_models.LocalUserCreate:
properties:
name:
description: Unix username
@@ -168,16 +202,19 @@ definitions:
required:
- name
type: object
- models.LocalUserUpdate:
+ github_com_pozgo_web-cli_internal_models.LocalUserUpdate:
properties:
name:
description: Unix username
type: string
type: object
- models.SSHKey:
+ github_com_pozgo_web-cli_internal_models.SSHKey:
properties:
created_at:
type: string
+ group:
+ description: Group/category for organization
+ type: string
id:
type: integer
name:
@@ -185,11 +222,17 @@ definitions:
private_key:
description: Decrypted value
type: string
+ source:
+ description: '"sqlite" or "vault"'
+ type: string
updated_at:
type: string
type: object
- models.SSHKeyCreate:
+ github_com_pozgo_web-cli_internal_models.SSHKeyCreate:
properties:
+ group:
+ description: Optional, defaults to "default"
+ type: string
name:
type: string
private_key:
@@ -198,14 +241,16 @@ definitions:
- name
- private_key
type: object
- models.SSHKeyUpdate:
+ github_com_pozgo_web-cli_internal_models.SSHKeyUpdate:
properties:
+ group:
+ type: string
name:
type: string
private_key:
type: string
type: object
- models.SavedCommand:
+ github_com_pozgo_web-cli_internal_models.SavedCommand:
properties:
command:
description: The actual command to execute
@@ -235,7 +280,7 @@ definitions:
description: 'User to run as (default: root)'
type: string
type: object
- models.SavedCommandCreate:
+ github_com_pozgo_web-cli_internal_models.SavedCommandCreate:
properties:
command:
type: string
@@ -259,7 +304,7 @@ definitions:
- command
- name
type: object
- models.SavedCommandUpdate:
+ github_com_pozgo_web-cli_internal_models.SavedCommandUpdate:
properties:
command:
type: string
@@ -276,28 +321,56 @@ definitions:
user:
type: string
type: object
- models.ScriptExecution:
+ github_com_pozgo_web-cli_internal_models.ScriptExecution:
properties:
+ env_var_groups:
+ description: Groups of env vars to include (Vault, paired with EnvVarNames)
+ items:
+ type: string
+ type: array
env_var_ids:
- description: Specific env var IDs to include
+ description: Specific env var IDs to include (SQLite)
items:
type: integer
type: array
+ env_var_names:
+ description: Names of env vars to include (Vault)
+ items:
+ type: string
+ type: array
include_env_vars:
description: 'Deprecated: use EnvVarIDs instead'
type: boolean
is_remote:
description: True if remote execution
type: boolean
+ script_group:
+ description: Script group for execution (Vault)
+ type: string
script_id:
- description: ID of the script to execute
+ description: ID of the script to execute (SQLite)
type: integer
+ script_name:
+ description: Name of the script to execute (Vault)
+ type: string
+ server_group:
+ description: Server group for remote execution (Vault)
+ type: string
server_id:
- description: Server ID for remote execution
+ description: Server ID for remote execution (SQLite)
type: integer
+ server_name:
+ description: Server name for remote execution (Vault)
+ type: string
+ ssh_key_group:
+ description: SSH key group for remote execution (Vault)
+ type: string
ssh_key_id:
- description: SSH key ID for remote execution
+ description: SSH key ID for remote execution (SQLite)
type: integer
+ ssh_key_name:
+ description: SSH key name for remote execution (Vault)
+ type: string
ssh_password:
description: SSH password (for remote, if key auth fails)
type: string
@@ -307,10 +380,8 @@ definitions:
user:
description: 'User to run as (default: root)'
type: string
- required:
- - script_id
type: object
- models.ScriptPresetCreate:
+ github_com_pozgo_web-cli_internal_models.ScriptPresetCreate:
properties:
description:
type: string
@@ -334,7 +405,7 @@ definitions:
- name
- script_id
type: object
- models.ScriptPresetResponse:
+ github_com_pozgo_web-cli_internal_models.ScriptPresetResponse:
properties:
created_at:
type: string
@@ -361,7 +432,7 @@ definitions:
user:
type: string
type: object
- models.ScriptPresetUpdate:
+ github_com_pozgo_web-cli_internal_models.ScriptPresetUpdate:
properties:
description:
type: string
@@ -382,7 +453,7 @@ definitions:
user:
type: string
type: object
- models.ScriptResult:
+ github_com_pozgo_web-cli_internal_models.ScriptResult:
properties:
env_vars_injected:
description: Number of env vars injected
@@ -404,10 +475,13 @@ definitions:
user:
type: string
type: object
- models.Server:
+ github_com_pozgo_web-cli_internal_models.Server:
properties:
created_at:
type: string
+ group:
+ description: Group/category for organization
+ type: string
id:
type: integer
ip_address:
@@ -419,14 +493,20 @@ definitions:
port:
description: 'SSH port (default: 22)'
type: integer
+ source:
+ description: '"sqlite" or "vault"'
+ type: string
updated_at:
type: string
username:
description: SSH username for remote connections
type: string
type: object
- models.ServerCreate:
+ github_com_pozgo_web-cli_internal_models.ServerCreate:
properties:
+ group:
+ description: Optional, defaults to "default"
+ type: string
ip_address:
type: string
name:
@@ -438,8 +518,10 @@ definitions:
description: SSH username for remote connections
type: string
type: object
- models.ServerUpdate:
+ github_com_pozgo_web-cli_internal_models.ServerUpdate:
properties:
+ group:
+ type: string
ip_address:
type: string
name:
@@ -449,7 +531,106 @@ definitions:
username:
type: string
type: object
- server.CurrentUserResponse:
+ github_com_pozgo_web-cli_internal_models.VaultConfigCreate:
+ properties:
+ address:
+ type: string
+ enabled:
+ type: boolean
+ mount_path:
+ type: string
+ namespace:
+ type: string
+ token:
+ type: string
+ type: object
+ github_com_pozgo_web-cli_internal_models.VaultConfigResponse:
+ properties:
+ address:
+ type: string
+ created_at:
+ type: string
+ enabled:
+ type: boolean
+ has_token:
+ description: Indicates if a token is configured
+ type: boolean
+ id:
+ type: integer
+ mount_path:
+ type: string
+ namespace:
+ type: string
+ updated_at:
+ type: string
+ type: object
+ github_com_pozgo_web-cli_internal_models.VaultStatus:
+ properties:
+ address:
+ type: string
+ configured:
+ description: Whether Vault is configured
+ type: boolean
+ connected:
+ description: Whether connection test passed
+ type: boolean
+ enabled:
+ description: Whether Vault is enabled
+ type: boolean
+ error:
+ type: string
+ vault_sealed:
+ type: boolean
+ type: object
+ github_com_pozgo_web-cli_internal_vault.BashScript:
+ properties:
+ content:
+ type: string
+ description:
+ type: string
+ filename:
+ type: string
+ group:
+ type: string
+ name:
+ type: string
+ type: object
+ github_com_pozgo_web-cli_internal_vault.EnvVariable:
+ properties:
+ description:
+ type: string
+ group:
+ type: string
+ name:
+ type: string
+ value:
+ type: string
+ type: object
+ github_com_pozgo_web-cli_internal_vault.SSHKey:
+ properties:
+ created_at:
+ type: string
+ group:
+ type: string
+ name:
+ type: string
+ private_key:
+ type: string
+ type: object
+ github_com_pozgo_web-cli_internal_vault.Server:
+ properties:
+ group:
+ type: string
+ ip_address:
+ type: string
+ name:
+ type: string
+ port:
+ type: integer
+ username:
+ type: string
+ type: object
+ internal_server.CurrentUserResponse:
description: Current system user information
properties:
gid:
@@ -468,14 +649,14 @@ definitions:
example: root
type: string
type: object
- server.ErrorResponse:
+ internal_server.ErrorResponse:
description: Error response returned by the API
properties:
error:
example: Invalid request body
type: string
type: object
- server.ShellInfo:
+ internal_server.ShellInfo:
description: Information about an available shell
properties:
name:
@@ -485,6 +666,19 @@ definitions:
example: /bin/bash
type: string
type: object
+ internal_server.StreamMessage:
+ properties:
+ data:
+ description: output chunk or error message
+ type: string
+ result:
+ allOf:
+ - $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.ScriptResult'
+ description: final result
+ type:
+ description: '"output", "result", "error"'
+ type: string
+ type: object
host: localhost:7777
info:
contact:
@@ -504,6 +698,11 @@ paths:
consumes:
- application/json
description: Get a list of all bash scripts (without content by default)
+ parameters:
+ - description: Filter by group name
+ in: query
+ name: group
+ type: string
produces:
- application/json
responses:
@@ -511,12 +710,12 @@ paths:
description: OK
schema:
items:
- $ref: '#/definitions/models.BashScriptResponse'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.BashScriptResponse'
type: array
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: List all bash scripts
@@ -532,22 +731,22 @@ paths:
name: script
required: true
schema:
- $ref: '#/definitions/models.BashScriptCreate'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.BashScriptCreate'
produces:
- application/json
responses:
"201":
description: Created
schema:
- $ref: '#/definitions/models.BashScriptResponse'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.BashScriptResponse'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Create a bash script
@@ -572,11 +771,11 @@ paths:
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Delete a bash script
@@ -603,15 +802,15 @@ paths:
"200":
description: OK
schema:
- $ref: '#/definitions/models.BashScriptResponse'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.BashScriptResponse'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"404":
description: Not Found
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Get a bash script by ID
@@ -632,22 +831,22 @@ paths:
name: script
required: true
schema:
- $ref: '#/definitions/models.BashScriptUpdate'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.BashScriptUpdate'
produces:
- application/json
responses:
"200":
description: OK
schema:
- $ref: '#/definitions/models.BashScriptResponse'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.BashScriptResponse'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Update a bash script
@@ -671,20 +870,20 @@ paths:
description: OK
schema:
items:
- $ref: '#/definitions/models.ScriptPresetResponse'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.ScriptPresetResponse'
type: array
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"404":
description: Not Found
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Get presets for a script
@@ -701,31 +900,91 @@ paths:
name: execution
required: true
schema:
- $ref: '#/definitions/models.ScriptExecution'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.ScriptExecution'
produces:
- application/json
responses:
"200":
description: OK
schema:
- $ref: '#/definitions/models.ScriptResult'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.ScriptResult'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"404":
description: Not Found
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Execute a bash script
tags:
- Bash Scripts
+ /bash-scripts/execute/stream:
+ post:
+ consumes:
+ - application/json
+ description: Execute a stored bash script locally or remotely with real-time
+ output streaming via SSE
+ parameters:
+ - description: Script execution request
+ in: body
+ name: execution
+ required: true
+ schema:
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.ScriptExecution'
+ produces:
+ - text/event-stream
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/internal_server.StreamMessage'
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ "404":
+ description: Not Found
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ security:
+ - BasicAuth: []
+ summary: Execute a bash script with streaming output
+ tags:
+ - Bash Scripts
+ /bash-scripts/groups:
+ get:
+ consumes:
+ - application/json
+ description: Get a list of all distinct group names for bash scripts
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ type: string
+ type: array
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ security:
+ - BasicAuth: []
+ summary: List all bash script groups
+ tags:
+ - Bash Scripts
/commands/execute:
post:
consumes:
@@ -737,26 +996,26 @@ paths:
name: command
required: true
schema:
- $ref: '#/definitions/models.CommandExecution'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.CommandExecution'
produces:
- application/json
responses:
"200":
description: OK
schema:
- $ref: '#/definitions/models.CommandResult'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.CommandResult'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"404":
description: Not Found
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Execute a command
@@ -772,6 +1031,10 @@ paths:
in: query
name: show_values
type: boolean
+ - description: Filter by group name
+ in: query
+ name: group
+ type: string
produces:
- application/json
responses:
@@ -779,12 +1042,12 @@ paths:
description: OK
schema:
items:
- $ref: '#/definitions/models.EnvVariableResponse'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.EnvVariableResponse'
type: array
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: List all environment variables
@@ -800,22 +1063,22 @@ paths:
name: envvar
required: true
schema:
- $ref: '#/definitions/models.EnvVariableCreate'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.EnvVariableCreate'
produces:
- application/json
responses:
"201":
description: Created
schema:
- $ref: '#/definitions/models.EnvVariableResponse'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.EnvVariableResponse'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Create an environment variable
@@ -840,11 +1103,11 @@ paths:
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Delete an environment variable
@@ -870,15 +1133,15 @@ paths:
"200":
description: OK
schema:
- $ref: '#/definitions/models.EnvVariableResponse'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.EnvVariableResponse'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"404":
description: Not Found
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Get an environment variable by ID
@@ -899,27 +1162,50 @@ paths:
name: envvar
required: true
schema:
- $ref: '#/definitions/models.EnvVariableUpdate'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.EnvVariableUpdate'
produces:
- application/json
responses:
"200":
description: OK
schema:
- $ref: '#/definitions/models.EnvVariableResponse'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.EnvVariableResponse'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Update an environment variable
tags:
- Environment Variables
+ /env-variables/groups:
+ get:
+ consumes:
+ - application/json
+ description: Get a list of all distinct group names for environment variables
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ type: string
+ type: array
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ security:
+ - BasicAuth: []
+ summary: List all environment variable groups
+ tags:
+ - Environment Variables
/history:
get:
consumes:
@@ -942,12 +1228,12 @@ paths:
description: OK
schema:
items:
- $ref: '#/definitions/models.CommandHistory'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.CommandHistory'
type: array
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: List command history
@@ -970,15 +1256,15 @@ paths:
"200":
description: OK
schema:
- $ref: '#/definitions/models.CommandHistory'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.CommandHistory'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"404":
description: Not Found
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Get a command history entry by ID
@@ -989,6 +1275,11 @@ paths:
consumes:
- application/json
description: Get a list of all SSH keys stored in the system
+ parameters:
+ - description: Filter by group name
+ in: query
+ name: group
+ type: string
produces:
- application/json
responses:
@@ -996,12 +1287,12 @@ paths:
description: OK
schema:
items:
- $ref: '#/definitions/models.SSHKey'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.SSHKey'
type: array
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: List all SSH keys
@@ -1017,22 +1308,22 @@ paths:
name: key
required: true
schema:
- $ref: '#/definitions/models.SSHKeyCreate'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.SSHKeyCreate'
produces:
- application/json
responses:
"201":
description: Created
schema:
- $ref: '#/definitions/models.SSHKey'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.SSHKey'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Create a new SSH key
@@ -1057,11 +1348,11 @@ paths:
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Delete an SSH key
@@ -1083,15 +1374,15 @@ paths:
"200":
description: OK
schema:
- $ref: '#/definitions/models.SSHKey'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.SSHKey'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"404":
description: Not Found
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Get an SSH key by ID
@@ -1112,27 +1403,50 @@ paths:
name: key
required: true
schema:
- $ref: '#/definitions/models.SSHKeyUpdate'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.SSHKeyUpdate'
produces:
- application/json
responses:
"200":
description: OK
schema:
- $ref: '#/definitions/models.SSHKey'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.SSHKey'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Update an SSH key
tags:
- SSH Keys
+ /keys/groups:
+ get:
+ consumes:
+ - application/json
+ description: Get a list of all distinct group names for SSH keys
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ type: string
+ type: array
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ security:
+ - BasicAuth: []
+ summary: List all SSH key groups
+ tags:
+ - SSH Keys
/local-users:
get:
consumes:
@@ -1145,12 +1459,12 @@ paths:
description: OK
schema:
items:
- $ref: '#/definitions/models.LocalUser'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.LocalUser'
type: array
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: List all local users
@@ -1166,22 +1480,22 @@ paths:
name: user
required: true
schema:
- $ref: '#/definitions/models.LocalUserCreate'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.LocalUserCreate'
produces:
- application/json
responses:
"201":
description: Created
schema:
- $ref: '#/definitions/models.LocalUser'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.LocalUser'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Create a local user
@@ -1206,11 +1520,11 @@ paths:
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Delete a local user
@@ -1232,15 +1546,15 @@ paths:
"200":
description: OK
schema:
- $ref: '#/definitions/models.LocalUser'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.LocalUser'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"404":
description: Not Found
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Get a local user by ID
@@ -1261,22 +1575,22 @@ paths:
name: user
required: true
schema:
- $ref: '#/definitions/models.LocalUserUpdate'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.LocalUserUpdate'
produces:
- application/json
responses:
"200":
description: OK
schema:
- $ref: '#/definitions/models.LocalUser'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.LocalUser'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Update a local user
@@ -1294,12 +1608,12 @@ paths:
description: OK
schema:
items:
- $ref: '#/definitions/models.SavedCommand'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.SavedCommand'
type: array
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: List all saved commands
@@ -1315,22 +1629,22 @@ paths:
name: command
required: true
schema:
- $ref: '#/definitions/models.SavedCommandCreate'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.SavedCommandCreate'
produces:
- application/json
responses:
"201":
description: Created
schema:
- $ref: '#/definitions/models.SavedCommand'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.SavedCommand'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Create a saved command
@@ -1355,11 +1669,11 @@ paths:
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Delete a saved command
@@ -1381,15 +1695,15 @@ paths:
"200":
description: OK
schema:
- $ref: '#/definitions/models.SavedCommand'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.SavedCommand'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"404":
description: Not Found
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Get a saved command by ID
@@ -1410,22 +1724,22 @@ paths:
name: command
required: true
schema:
- $ref: '#/definitions/models.SavedCommandUpdate'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.SavedCommandUpdate'
produces:
- application/json
responses:
"200":
description: OK
schema:
- $ref: '#/definitions/models.SavedCommand'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.SavedCommand'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Update a saved command
@@ -1443,12 +1757,12 @@ paths:
description: OK
schema:
items:
- $ref: '#/definitions/models.ScriptPresetResponse'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.ScriptPresetResponse'
type: array
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: List all script presets
@@ -1464,22 +1778,22 @@ paths:
name: preset
required: true
schema:
- $ref: '#/definitions/models.ScriptPresetCreate'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.ScriptPresetCreate'
produces:
- application/json
responses:
"201":
description: Created
schema:
- $ref: '#/definitions/models.ScriptPresetResponse'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.ScriptPresetResponse'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Create a script preset
@@ -1504,11 +1818,11 @@ paths:
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Delete a script preset
@@ -1530,15 +1844,15 @@ paths:
"200":
description: OK
schema:
- $ref: '#/definitions/models.ScriptPresetResponse'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.ScriptPresetResponse'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"404":
description: Not Found
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Get a script preset by ID
@@ -1559,22 +1873,22 @@ paths:
name: preset
required: true
schema:
- $ref: '#/definitions/models.ScriptPresetUpdate'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.ScriptPresetUpdate'
produces:
- application/json
responses:
"200":
description: OK
schema:
- $ref: '#/definitions/models.ScriptPresetResponse'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.ScriptPresetResponse'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Update a script preset
@@ -1585,6 +1899,11 @@ paths:
consumes:
- application/json
description: Get a list of all remote servers configured in the system
+ parameters:
+ - description: Filter by group name
+ in: query
+ name: group
+ type: string
produces:
- application/json
responses:
@@ -1592,12 +1911,12 @@ paths:
description: OK
schema:
items:
- $ref: '#/definitions/models.Server'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.Server'
type: array
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: List all servers
@@ -1613,22 +1932,22 @@ paths:
name: server
required: true
schema:
- $ref: '#/definitions/models.ServerCreate'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.ServerCreate'
produces:
- application/json
responses:
"201":
description: Created
schema:
- $ref: '#/definitions/models.Server'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.Server'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Create a new server
@@ -1653,11 +1972,11 @@ paths:
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Delete a server
@@ -1679,15 +1998,15 @@ paths:
"200":
description: OK
schema:
- $ref: '#/definitions/models.Server'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.Server'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"404":
description: Not Found
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Get a server by ID
@@ -1708,27 +2027,50 @@ paths:
name: server
required: true
schema:
- $ref: '#/definitions/models.ServerUpdate'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.ServerUpdate'
produces:
- application/json
responses:
"200":
description: OK
schema:
- $ref: '#/definitions/models.Server'
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.Server'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Update a server
tags:
- Servers
+ /servers/groups:
+ get:
+ consumes:
+ - application/json
+ description: Get a list of all distinct group names for servers
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ type: string
+ type: array
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ security:
+ - BasicAuth: []
+ summary: List all server groups
+ tags:
+ - Servers
/system/current-user:
get:
consumes:
@@ -1740,11 +2082,11 @@ paths:
"200":
description: OK
schema:
- $ref: '#/definitions/server.CurrentUserResponse'
+ $ref: '#/definitions/internal_server.CurrentUserResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/server.ErrorResponse'
+ $ref: '#/definitions/internal_server.ErrorResponse'
security:
- BasicAuth: []
summary: Get current system user
@@ -1762,13 +2104,402 @@ paths:
description: OK
schema:
items:
- $ref: '#/definitions/server.ShellInfo'
+ $ref: '#/definitions/internal_server.ShellInfo'
type: array
security:
- BasicAuth: []
summary: List available shells
tags:
- System
+ /vault/bash-scripts:
+ get:
+ description: Retrieve all bash scripts stored in Vault
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_vault.BashScript'
+ type: array
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ security:
+ - BasicAuth: []
+ summary: List bash scripts from Vault
+ tags:
+ - Vault
+ post:
+ consumes:
+ - application/json
+ description: Create a new bash script in HashiCorp Vault storage
+ parameters:
+ - description: Bash script data
+ in: body
+ name: script
+ required: true
+ schema:
+ type: object
+ produces:
+ - application/json
+ responses:
+ "201":
+ description: Created script info with source field
+ schema:
+ additionalProperties: true
+ type: object
+ "400":
+ description: Invalid request or Vault not configured
+ schema:
+ type: string
+ "500":
+ description: Failed to store script
+ schema:
+ type: string
+ security:
+ - BasicAuth: []
+ summary: Store bash script in Vault
+ tags:
+ - Vault
+ /vault/config:
+ delete:
+ description: Remove the Vault configuration
+ responses:
+ "204":
+ description: No Content
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ security:
+ - BasicAuth: []
+ summary: Delete Vault configuration
+ tags:
+ - Vault
+ get:
+ description: Retrieve the current Vault configuration (token is never returned)
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.VaultConfigResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ security:
+ - BasicAuth: []
+ summary: Get Vault configuration
+ tags:
+ - Vault
+ post:
+ consumes:
+ - application/json
+ description: Configure or update the Vault connection settings
+ parameters:
+ - description: Vault configuration
+ in: body
+ name: config
+ required: true
+ schema:
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.VaultConfigCreate'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.VaultConfigResponse'
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ security:
+ - BasicAuth: []
+ summary: Create or update Vault configuration
+ tags:
+ - Vault
+ /vault/env-variables:
+ get:
+ description: Retrieve all environment variables stored in Vault
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_vault.EnvVariable'
+ type: array
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ security:
+ - BasicAuth: []
+ summary: List environment variables from Vault
+ tags:
+ - Vault
+ post:
+ consumes:
+ - application/json
+ description: Store a new environment variable in Vault
+ parameters:
+ - description: Environment Variable
+ in: body
+ name: envVar
+ required: true
+ schema:
+ properties:
+ description:
+ type: string
+ group:
+ type: string
+ name:
+ type: string
+ value:
+ type: string
+ type: object
+ produces:
+ - application/json
+ responses:
+ "201":
+ description: Created
+ schema:
+ properties:
+ description:
+ type: string
+ group:
+ type: string
+ name:
+ type: string
+ source:
+ type: string
+ type: object
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ security:
+ - BasicAuth: []
+ summary: Create environment variable in Vault
+ tags:
+ - Vault
+ /vault/servers:
+ get:
+ description: Retrieve all server configurations stored in Vault
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_vault.Server'
+ type: array
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ security:
+ - BasicAuth: []
+ summary: List servers from Vault
+ tags:
+ - Vault
+ post:
+ consumes:
+ - application/json
+ description: Store a new server configuration in Vault
+ parameters:
+ - description: Server
+ in: body
+ name: server
+ required: true
+ schema:
+ properties:
+ group:
+ type: string
+ ip_address:
+ type: string
+ name:
+ type: string
+ port:
+ type: integer
+ username:
+ type: string
+ type: object
+ produces:
+ - application/json
+ responses:
+ "201":
+ description: Created
+ schema:
+ properties:
+ group:
+ type: string
+ ip_address:
+ type: string
+ name:
+ type: string
+ port:
+ type: integer
+ source:
+ type: string
+ username:
+ type: string
+ type: object
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ security:
+ - BasicAuth: []
+ summary: Create server in Vault
+ tags:
+ - Vault
+ /vault/ssh-keys:
+ get:
+ description: Retrieve all SSH keys stored in Vault
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ items:
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_vault.SSHKey'
+ type: array
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ security:
+ - BasicAuth: []
+ summary: List SSH keys from Vault
+ tags:
+ - Vault
+ post:
+ consumes:
+ - application/json
+ description: Store a new SSH key in Vault
+ parameters:
+ - description: SSH Key
+ in: body
+ name: key
+ required: true
+ schema:
+ properties:
+ group:
+ type: string
+ name:
+ type: string
+ private_key:
+ type: string
+ type: object
+ produces:
+ - application/json
+ responses:
+ "201":
+ description: Created
+ schema:
+ properties:
+ created_at:
+ type: string
+ group:
+ type: string
+ name:
+ type: string
+ source:
+ type: string
+ type: object
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ security:
+ - BasicAuth: []
+ summary: Create SSH key in Vault
+ tags:
+ - Vault
+ /vault/status:
+ get:
+ description: Check the current Vault connection status
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.VaultStatus'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ security:
+ - BasicAuth: []
+ summary: Get Vault connection status
+ tags:
+ - Vault
+ /vault/test:
+ post:
+ description: Test the connection to Vault and initialize the secrets structure
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/github_com_pozgo_web-cli_internal_models.VaultStatus'
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/internal_server.ErrorResponse'
+ security:
+ - BasicAuth: []
+ summary: Test Vault connection
+ tags:
+ - Vault
securityDefinitions:
BasicAuth:
type: basic
diff --git a/frontend/src/components/AddKeyDialog.jsx b/frontend/src/components/AddKeyDialog.jsx
index f0e9940..d6be1a6 100644
--- a/frontend/src/components/AddKeyDialog.jsx
+++ b/frontend/src/components/AddKeyDialog.jsx
@@ -9,6 +9,8 @@ import {
Alert,
Box,
} from '@mui/material';
+import StorageSelector from './shared/StorageSelector';
+import GroupInput from './shared/GroupInput';
/**
* AddKeyDialog component - dialog for adding new SSH keys
@@ -20,6 +22,8 @@ import {
const AddKeyDialog = ({ open, onClose, onKeyAdded }) => {
const [name, setName] = useState('');
const [privateKey, setPrivateKey] = useState('');
+ const [group, setGroup] = useState('default');
+ const [storage, setStorage] = useState('local');
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
@@ -42,7 +46,10 @@ const AddKeyDialog = ({ open, onClose, onKeyAdded }) => {
setLoading(true);
setError(null);
- const response = await fetch('/api/keys', {
+ // Choose API endpoint based on storage selection
+ const endpoint = storage === 'vault' ? '/api/vault/ssh-keys' : '/api/keys';
+
+ const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -50,6 +57,7 @@ const AddKeyDialog = ({ open, onClose, onKeyAdded }) => {
body: JSON.stringify({
name: name.trim(),
private_key: privateKey, // Don't trim - SSH keys need their newlines preserved
+ group: group.trim() || 'default',
}),
});
@@ -61,6 +69,8 @@ const AddKeyDialog = ({ open, onClose, onKeyAdded }) => {
// Success - reset form and notify parent
setName('');
setPrivateKey('');
+ setGroup('default');
+ setStorage('local');
setError(null);
onKeyAdded();
} catch (err) {
@@ -75,6 +85,8 @@ const AddKeyDialog = ({ open, onClose, onKeyAdded }) => {
if (!loading) {
setName('');
setPrivateKey('');
+ setGroup('default');
+ setStorage('local');
setError(null);
onClose();
}
@@ -129,6 +141,22 @@ b3BlbnNzaC1rZXktdjEAAAAABG5vbmUA..."
}}
/>
+
+
+
+
+
+
Security Note: Private keys are encrypted with AES-256-GCM before storage. Never share your private keys.
diff --git a/frontend/src/components/AddServerDialog.jsx b/frontend/src/components/AddServerDialog.jsx
index 7a609f3..f947e6c 100644
--- a/frontend/src/components/AddServerDialog.jsx
+++ b/frontend/src/components/AddServerDialog.jsx
@@ -9,6 +9,8 @@ import {
Alert,
Box,
} from '@mui/material';
+import StorageSelector from './shared/StorageSelector';
+import GroupInput from './shared/GroupInput';
/**
* AddServerDialog component - dialog for adding new servers
@@ -22,6 +24,8 @@ const AddServerDialog = ({ open, onClose, onServerAdded }) => {
const [ipAddress, setIPAddress] = useState('');
const [port, setPort] = useState('22');
const [username, setUsername] = useState('root');
+ const [group, setGroup] = useState('default');
+ const [storage, setStorage] = useState('local');
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
@@ -67,7 +71,10 @@ const AddServerDialog = ({ open, onClose, onServerAdded }) => {
setLoading(true);
setError(null);
- const response = await fetch('/api/servers', {
+ // Choose API endpoint based on storage selection
+ const endpoint = storage === 'vault' ? '/api/vault/servers' : '/api/servers';
+
+ const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -77,6 +84,7 @@ const AddServerDialog = ({ open, onClose, onServerAdded }) => {
ip_address: ipAddress.trim() || undefined,
port: portNum,
username: username.trim() || 'root',
+ group: group.trim() || 'default',
}),
});
@@ -90,6 +98,8 @@ const AddServerDialog = ({ open, onClose, onServerAdded }) => {
setIPAddress('');
setPort('22');
setUsername('root');
+ setGroup('default');
+ setStorage('local');
setError(null);
onServerAdded();
} catch (err) {
@@ -106,6 +116,8 @@ const AddServerDialog = ({ open, onClose, onServerAdded }) => {
setIPAddress('');
setPort('22');
setUsername('root');
+ setGroup('default');
+ setStorage('local');
setError(null);
onClose();
}
@@ -179,6 +191,22 @@ const AddServerDialog = ({ open, onClose, onServerAdded }) => {
disabled={loading}
/>
+
+
+
+
+
+
Note: At least one of Server Name or IP Address must be provided. Server names must follow hostname conventions (no spaces).
diff --git a/frontend/src/components/AdminPanel.jsx b/frontend/src/components/AdminPanel.jsx
index 28c2617..87017fa 100644
--- a/frontend/src/components/AdminPanel.jsx
+++ b/frontend/src/components/AdminPanel.jsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, Component } from 'react';
import {
Container,
Typography,
@@ -18,25 +18,59 @@ import {
Tab,
} from '@mui/material';
import { Add, Delete, Edit, ArrowBack } from '@mui/icons-material';
-import { useNavigate } from 'react-router-dom';
+import { useNavigate, useLocation } from 'react-router-dom';
import AddKeyDialog from './AddKeyDialog';
import EditKeyDialog from './EditKeyDialog';
import ServerList from './ServerList';
import LocalUserList from './LocalUserList';
import EnvVariableList from './EnvVariableList';
+import VaultSettings from './VaultSettings';
+import SourceChip from './shared/SourceChip';
+import GroupSelector from './shared/GroupSelector';
+
+/**
+ * Error Boundary for VaultSettings
+ */
+class VaultErrorBoundary extends Component {
+ constructor(props) {
+ super(props);
+ this.state = { hasError: false, error: null };
+ }
+
+ static getDerivedStateFromError(error) {
+ return { hasError: true, error };
+ }
+
+ componentDidCatch(error, errorInfo) {
+ console.error('VaultSettings error:', error, errorInfo);
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return (
+
+ Error loading Vault Settings: {this.state.error?.message || 'Unknown error'}
+
+ );
+ }
+ return this.props.children;
+ }
+}
/**
* AdminPanel component - displays and manages SSH keys and servers
*/
const AdminPanel = () => {
const navigate = useNavigate();
- const [tabValue, setTabValue] = useState(0);
+ const location = useLocation();
+ const [tabValue, setTabValue] = useState(location.state?.tab || 0);
const [keys, setKeys] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [openDialog, setOpenDialog] = useState(false);
const [openEditDialog, setOpenEditDialog] = useState(false);
const [selectedKey, setSelectedKey] = useState(null);
+ const [selectedGroup, setSelectedGroup] = useState('all');
// Handle tab change
const handleTabChange = (event, newValue) => {
@@ -48,7 +82,10 @@ const AdminPanel = () => {
try {
setLoading(true);
setError(null);
- const response = await fetch('/api/keys');
+ const url = selectedGroup === 'all'
+ ? '/api/keys'
+ : `/api/keys?group=${encodeURIComponent(selectedGroup)}`;
+ const response = await fetch(url);
if (!response.ok) {
throw new Error('Failed to fetch SSH keys');
@@ -63,10 +100,17 @@ const AdminPanel = () => {
}
};
- // Load keys on component mount
+ // Update tab when navigating with state (e.g., from VaultIcon)
+ useEffect(() => {
+ if (location.state?.tab !== undefined) {
+ setTabValue(location.state.tab);
+ }
+ }, [location.state]);
+
+ // Load keys on component mount or when group changes
useEffect(() => {
fetchKeys();
- }, []);
+ }, [selectedGroup]);
// Handle key deletion
const handleDelete = async (id) => {
@@ -130,6 +174,7 @@ const AdminPanel = () => {
+
@@ -139,13 +184,20 @@ const AdminPanel = () => {
SSH Key Management
- }
- onClick={() => setOpenDialog(true)}
- >
- Add New Key
-
+
+
+ }
+ onClick={() => setOpenDialog(true)}
+ >
+ Add New Key
+
+
{error && (
@@ -171,13 +223,15 @@ const AdminPanel = () => {
Name
Private Key
+ Group
+ Source
Created At
Actions
{keys.map((key) => (
-
+
{key.name}
{
{key.private_key}
+ {key.group || 'default'}
+
+
+
{new Date(key.created_at).toLocaleString()}
@@ -202,6 +260,8 @@ const AdminPanel = () => {
onClick={() => handleEdit(key)}
aria-label="edit"
sx={{ mr: 1 }}
+ disabled={key.source === 'vault'}
+ title={key.source === 'vault' ? 'Vault items cannot be edited here' : 'Edit'}
>
@@ -209,6 +269,8 @@ const AdminPanel = () => {
color="error"
onClick={() => handleDelete(key.id)}
aria-label="delete"
+ disabled={key.source === 'vault'}
+ title={key.source === 'vault' ? 'Vault items cannot be deleted here' : 'Delete'}
>
@@ -227,6 +289,12 @@ const AdminPanel = () => {
{tabValue === 2 && }
{tabValue === 3 && }
+
+ {tabValue === 4 && (
+
+
+
+ )}
{
const [name, setName] = useState('');
const [privateKey, setPrivateKey] = useState('');
+ const [group, setGroup] = useState('default');
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
@@ -29,6 +30,7 @@ const EditKeyDialog = ({ open, onClose, onKeyUpdated, keyData }) => {
if (keyData) {
setName(keyData.name || '');
setPrivateKey(keyData.private_key || '');
+ setGroup(keyData.group || 'default');
}
}, [keyData]);
@@ -59,6 +61,7 @@ const EditKeyDialog = ({ open, onClose, onKeyUpdated, keyData }) => {
body: JSON.stringify({
name: name.trim(),
private_key: privateKey, // Don't trim - SSH keys need their newlines preserved
+ group: group.trim() || 'default',
}),
});
@@ -134,6 +137,19 @@ b3BlbnNzaC1rZXktdjEAAAAABG5vbmUA..."
}}
/>
+ setGroup(e.target.value)}
+ placeholder="default"
+ helperText="Group for organizing SSH keys (default: default)"
+ disabled={loading}
+ />
+
Security Note: Private keys are encrypted with AES-256-GCM before storage. This will replace the existing key.
diff --git a/frontend/src/components/EditServerDialog.jsx b/frontend/src/components/EditServerDialog.jsx
index 82599bd..55d96b3 100644
--- a/frontend/src/components/EditServerDialog.jsx
+++ b/frontend/src/components/EditServerDialog.jsx
@@ -23,6 +23,7 @@ const EditServerDialog = ({ open, onClose, onServerUpdated, serverData }) => {
const [ipAddress, setIPAddress] = useState('');
const [port, setPort] = useState('22');
const [username, setUsername] = useState('root');
+ const [group, setGroup] = useState('default');
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
@@ -33,6 +34,7 @@ const EditServerDialog = ({ open, onClose, onServerUpdated, serverData }) => {
setIPAddress(serverData.ip_address || '');
setPort(serverData.port ? String(serverData.port) : '22');
setUsername(serverData.username || 'root');
+ setGroup(serverData.group || 'default');
}
}, [serverData]);
@@ -88,6 +90,7 @@ const EditServerDialog = ({ open, onClose, onServerUpdated, serverData }) => {
ip_address: ipAddress.trim() || undefined,
port: portNum,
username: username.trim() || 'root',
+ group: group.trim() || 'default',
}),
});
@@ -182,6 +185,19 @@ const EditServerDialog = ({ open, onClose, onServerUpdated, serverData }) => {
disabled={loading}
/>
+ setGroup(e.target.value)}
+ placeholder="default"
+ helperText="Group for organizing servers (default: default)"
+ disabled={loading}
+ />
+
Note: At least one of Server Name or IP Address must be provided. Server names must follow hostname conventions (no spaces).
diff --git a/frontend/src/components/EnvVariableList.jsx b/frontend/src/components/EnvVariableList.jsx
index 16b75f7..5f3a15f 100644
--- a/frontend/src/components/EnvVariableList.jsx
+++ b/frontend/src/components/EnvVariableList.jsx
@@ -22,6 +22,10 @@ import {
Tooltip,
} from '@mui/material';
import { Add, Delete, Edit, Visibility, VisibilityOff, ContentCopy } from '@mui/icons-material';
+import SourceChip from './shared/SourceChip';
+import GroupSelector from './shared/GroupSelector';
+import StorageSelector from './shared/StorageSelector';
+import GroupInput from './shared/GroupInput';
/**
* EnvVariableList component - displays and manages encrypted environment variables
@@ -38,15 +42,21 @@ const EnvVariableList = () => {
name: '',
value: '',
description: '',
+ group: 'default',
+ storage: 'local',
});
const [showFormValue, setShowFormValue] = useState(false);
+ const [selectedGroup, setSelectedGroup] = useState('all');
// Fetch environment variables from API
const fetchEnvVars = async () => {
try {
setLoading(true);
setError(null);
- const response = await fetch('/api/env-variables');
+ const url = selectedGroup === 'all'
+ ? '/api/env-variables'
+ : `/api/env-variables?group=${encodeURIComponent(selectedGroup)}`;
+ const response = await fetch(url);
if (!response.ok) {
throw new Error('Failed to fetch environment variables');
@@ -76,10 +86,10 @@ const EnvVariableList = () => {
}
};
- // Load env vars on component mount
+ // Load env vars on component mount or when group changes
useEffect(() => {
fetchEnvVars();
- }, []);
+ }, [selectedGroup]);
// Toggle value visibility for a specific variable
const handleToggleVisibility = async (id) => {
@@ -140,6 +150,7 @@ const EnvVariableList = () => {
name: envVar.name,
value: value || '',
description: envVar.description || '',
+ group: envVar.group || 'default',
});
setShowFormValue(false);
setOpenDialog(true);
@@ -152,6 +163,8 @@ const EnvVariableList = () => {
name: '',
value: '',
description: '',
+ group: 'default',
+ storage: 'local',
});
setShowFormValue(false);
setOpenDialog(true);
@@ -165,17 +178,25 @@ const EnvVariableList = () => {
}
try {
+ // Choose base API endpoint based on storage selection (only for new env vars)
+ const baseEndpoint = !editingVar && formData.storage === 'vault'
+ ? '/api/vault/env-variables'
+ : '/api/env-variables';
+
const url = editingVar
? `/api/env-variables/${editingVar.id}`
- : '/api/env-variables';
+ : baseEndpoint;
const method = editingVar ? 'PUT' : 'POST';
+ // Don't send storage field to the API
+ const { storage, ...dataToSend } = formData;
+
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
},
- body: JSON.stringify(formData),
+ body: JSON.stringify(dataToSend),
});
if (!response.ok) {
@@ -198,13 +219,20 @@ const EnvVariableList = () => {
Environment Variables
- }
- onClick={handleAdd}
- >
- Add Variable
-
+
+
+ }
+ onClick={handleAdd}
+ >
+ Add Variable
+
+
{error && (
@@ -241,13 +269,15 @@ const EnvVariableList = () => {
Name
Value
Description
+ Group
+ Source
Created At
Actions
{envVars.map((envVar) => (
-
+
{
{envVar.description || '-'}
+ {envVar.group || 'default'}
+
+
+
{new Date(envVar.created_at).toLocaleString()}
@@ -299,6 +333,8 @@ const EnvVariableList = () => {
onClick={() => handleEdit(envVar)}
aria-label="edit"
sx={{ mr: 1 }}
+ disabled={envVar.source === 'vault'}
+ title={envVar.source === 'vault' ? 'Vault items cannot be edited here' : 'Edit'}
>
@@ -306,6 +342,8 @@ const EnvVariableList = () => {
color="error"
onClick={() => handleDelete(envVar.id)}
aria-label="delete"
+ disabled={envVar.source === 'vault'}
+ title={envVar.source === 'vault' ? 'Vault items cannot be deleted here' : 'Delete'}
>
@@ -371,6 +409,20 @@ const EnvVariableList = () => {
placeholder="Brief description of this variable"
sx={{ mb: 2 }}
/>
+ setFormData({ ...formData, group: value })}
+ resourceType="env-variables"
+ helperText="Select an existing group or type a new one"
+ />
+ {!editingVar && (
+
+ setFormData({ ...formData, storage: value })}
+ />
+
+ )}
diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx
index 2e0a347..cbec7e0 100644
--- a/frontend/src/components/Header.jsx
+++ b/frontend/src/components/Header.jsx
@@ -1,9 +1,10 @@
-import React from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
import { AppBar, Toolbar, Typography, Box, IconButton, Tooltip } from '@mui/material';
import { Terminal, Api } from '@mui/icons-material';
import GitHubIcon from '@mui/icons-material/GitHub';
import { useNavigate } from 'react-router-dom';
import ThemeToggle from './ThemeToggle';
+import VaultIcon from './shared/VaultIcon';
/**
* Header component - displays the application header with logo and theme toggle
@@ -22,7 +23,6 @@ const Header = ({ mode, toggleTheme }) => {
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
- flexGrow: 1,
'&:hover': {
opacity: 0.8,
},
@@ -34,6 +34,8 @@ const Header = ({ mode, toggleTheme }) => {
Web CLI
+
+
{
if (response.ok) {
const data = await response.json();
setEnvVars(data || []);
- // Select all by default
- setSelectedEnvVarIds((data || []).map(v => v.id));
+ // Select all by default - use name for Vault items, ID for SQLite items
+ setSelectedEnvVarIds((data || []).map(v => v.source === 'vault' ? v.name : v.id));
}
} catch (err) {
console.error('Failed to fetch env vars:', err);
@@ -271,13 +271,49 @@ const LocalScripts = () => {
setSuccess(null);
setOutput('');
+ // Find the selected script object to determine its source
+ const selectedScriptObj = scripts.find(s =>
+ (s.source === 'vault' ? s.name === selectedScriptId : s.id === parseInt(selectedScriptId, 10))
+ );
+
const payload = {
- script_id: parseInt(selectedScriptId, 10),
user: user || 'root',
is_remote: false,
- env_var_ids: selectedEnvVarIds,
};
+ // For script: use name for Vault items, ID for SQLite items
+ if (selectedScriptObj) {
+ if (selectedScriptObj.source === 'vault') {
+ payload.script_name = selectedScriptObj.name;
+ payload.script_group = selectedScriptObj.group || 'default';
+ } else {
+ payload.script_id = selectedScriptObj.id;
+ }
+ }
+
+ // For env vars: separate SQLite IDs from Vault names (with groups)
+ const sqliteEnvVarIds = [];
+ const vaultEnvVarNames = [];
+ const vaultEnvVarGroups = [];
+ selectedEnvVarIds.forEach(id => {
+ const envVar = envVars.find(v => v.id === id || v.name === id);
+ if (envVar) {
+ if (envVar.source === 'vault') {
+ vaultEnvVarNames.push(envVar.name);
+ vaultEnvVarGroups.push(envVar.group || 'default');
+ } else if (envVar.id) {
+ sqliteEnvVarIds.push(envVar.id);
+ }
+ }
+ });
+ if (sqliteEnvVarIds.length > 0) {
+ payload.env_var_ids = sqliteEnvVarIds;
+ }
+ if (vaultEnvVarNames.length > 0) {
+ payload.env_var_names = vaultEnvVarNames;
+ payload.env_var_groups = vaultEnvVarGroups;
+ }
+
if (password) {
payload.sudo_password = password;
}
@@ -417,12 +453,21 @@ const LocalScripts = () => {
Choose a script...
{scripts.map((script) => (
-
@@ -301,6 +337,8 @@ const ScriptList = () => {
color="error"
onClick={() => handleDelete(script.id)}
aria-label="delete"
+ disabled={script.source === 'vault'}
+ title={script.source === 'vault' ? 'Vault items cannot be deleted here' : 'Delete'}
>
@@ -340,6 +378,20 @@ const ScriptList = () => {
placeholder="Brief description of what this script does"
sx={{ mb: 2 }}
/>
+ setFormData({ ...formData, group: value })}
+ resourceType="bash-scripts"
+ helperText="Select an existing group or type a new one"
+ />
+ {!editingScript && (
+
+ setFormData({ ...formData, storage: value })}
+ />
+
+ )}
{
const [openDialog, setOpenDialog] = useState(false);
const [openEditDialog, setOpenEditDialog] = useState(false);
const [selectedServer, setSelectedServer] = useState(null);
+ const [selectedGroup, setSelectedGroup] = useState('all');
// Fetch servers from API
const fetchServers = async () => {
try {
setLoading(true);
setError(null);
- const response = await fetch('/api/servers');
+ const url = selectedGroup === 'all'
+ ? '/api/servers'
+ : `/api/servers?group=${encodeURIComponent(selectedGroup)}`;
+ const response = await fetch(url);
if (!response.ok) {
throw new Error('Failed to fetch servers');
@@ -49,10 +55,10 @@ const ServerList = () => {
}
};
- // Load servers on component mount
+ // Load servers on component mount or when group changes
useEffect(() => {
fetchServers();
- }, []);
+ }, [selectedGroup]);
// Handle server deletion
const handleDelete = async (id) => {
@@ -101,13 +107,20 @@ const ServerList = () => {
Server Management
- }
- onClick={() => setOpenDialog(true)}
- >
- Add New Server
-
+
+
+ }
+ onClick={() => setOpenDialog(true)}
+ >
+ Add New Server
+
+
{error && (
@@ -135,17 +148,23 @@ const ServerList = () => {
IP Address / Hostname
Port
Username
+ Group
+ Source
Created At
Actions
{servers.map((server) => (
-
+
{server.name || '-'}
{server.ip_address || '-'}
{server.port || 22}
{server.username || 'root'}
+ {server.group || 'default'}
+
+
+
{new Date(server.created_at).toLocaleString()}
@@ -155,6 +174,8 @@ const ServerList = () => {
onClick={() => handleEdit(server)}
aria-label="edit"
sx={{ mr: 1 }}
+ disabled={server.source === 'vault'}
+ title={server.source === 'vault' ? 'Vault items cannot be edited here' : 'Edit'}
>
@@ -162,6 +183,8 @@ const ServerList = () => {
color="error"
onClick={() => handleDelete(server.id)}
aria-label="delete"
+ disabled={server.source === 'vault'}
+ title={server.source === 'vault' ? 'Vault items cannot be deleted here' : 'Delete'}
>
diff --git a/frontend/src/components/Terminal.jsx b/frontend/src/components/Terminal.jsx
index a74eea4..67f7bdd 100644
--- a/frontend/src/components/Terminal.jsx
+++ b/frontend/src/components/Terminal.jsx
@@ -10,6 +10,9 @@ import {
InputLabel,
Select,
MenuItem,
+ ListItemIcon,
+ ListItemText,
+ Chip,
} from '@mui/material';
import {
ArrowBack,
@@ -17,6 +20,8 @@ import {
FullscreenExit,
Refresh,
VpnKey,
+ Storage,
+ Lock,
} from '@mui/icons-material';
import { useNavigate } from 'react-router-dom';
import { TerminalProvider, useTerminal } from '../context/TerminalContext';
@@ -60,10 +65,22 @@ const TerminalContent = () => {
const fetchSshKeys = async () => {
try {
+ // /api/keys already returns merged local + vault keys with source field
const response = await fetch('/api/keys');
if (response.ok) {
const data = await response.json();
- setSshKeys(data || []);
+ const keysWithCompositeId = (data || []).map((key) => {
+ // Determine source: vault keys have source="vault", local have source="sqlite" or no source
+ const isVault = key.source === 'vault';
+ // Create composite ID: vault keys use name (no numeric id), local keys use id
+ const compositeId = isVault ? `vault:${key.name}` : `local:${key.id}`;
+ return {
+ ...key,
+ source: isVault ? 'vault' : 'local',
+ compositeId,
+ };
+ });
+ setSshKeys(keysWithCompositeId);
}
} catch (err) {
console.error('Failed to fetch SSH keys:', err);
@@ -103,7 +120,8 @@ const TerminalContent = () => {
// Handle SSH key change for active tab
const handleSshKeyChange = (e) => {
if (activeTab) {
- updateTabConfig(activeTab.id, { sshKeyId: e.target.value });
+ const compositeId = e.target.value;
+ updateTabConfig(activeTab.id, { sshKeyId: compositeId });
// Trigger reconnect
window.dispatchEvent(
new CustomEvent('terminal-reconnect', { detail: { tabId: activeTab.id } })
@@ -111,6 +129,12 @@ const TerminalContent = () => {
}
};
+ // Helper to get the selected key object from composite ID
+ const getSelectedKey = (compositeId) => {
+ if (!compositeId) return null;
+ return sshKeys.find((k) => k.compositeId === compositeId);
+ };
+
// Handle reconnect for active tab
const handleReconnect = () => {
if (activeTab) {
@@ -170,24 +194,46 @@ const TerminalContent = () => {
{/* SSH Key selector for active tab */}
-
+
SSH Key
- ) : null
- }
+ renderValue={(value) => {
+ if (!value) return None;
+ const key = getSelectedKey(value);
+ if (!key) return value;
+ return (
+
+
+ {key.name}
+ {key.source === 'vault' ? (
+
+ ) : (
+
+ )}
+
+ );
+ }}
>
None
{sshKeys.map((key) => (
-
- {key.name}
+
+
+
+
+
+ : }
+ label={key.source === 'vault' ? 'Vault' : 'Local'}
+ size="small"
+ variant="outlined"
+ color={key.source === 'vault' ? 'secondary' : 'default'}
+ sx={{ ml: 1, height: 24 }}
+ />
))}
@@ -250,10 +296,42 @@ const TerminalContent = () => {
Disconnected
)}
-
- Tabs: {tabs.length}/{maxTabs} | Shell: {activeTab?.shell || 'bash'}
- {activeTab?.sshKeyId &&
- ` | SSH Key: ${sshKeys.find((k) => k.id === activeTab.sshKeyId)?.name || 'Selected'}`}
+
+
+ Tabs: {tabs.length}/{maxTabs}
+ |
+ Shell: {activeTab?.shell || 'bash'}
+ {activeTab?.sshKeyId && (() => {
+ const selectedKey = getSelectedKey(activeTab.sshKeyId);
+ return selectedKey ? (
+ <>
+ |
+
+
+ {selectedKey.name}
+ {selectedKey.source === 'vault' ? (
+ }
+ label="Vault"
+ size="small"
+ variant="outlined"
+ color="secondary"
+ sx={{ height: 20, '& .MuiChip-label': { px: 0.5, fontSize: '0.7rem' } }}
+ />
+ ) : (
+ }
+ label="Local"
+ size="small"
+ variant="outlined"
+ sx={{ height: 20, '& .MuiChip-label': { px: 0.5, fontSize: '0.7rem' } }}
+ />
+ )}
+
+ >
+ ) : null;
+ })()}
+
diff --git a/frontend/src/components/TerminalPane.jsx b/frontend/src/components/TerminalPane.jsx
index 66be98e..d6b1cb1 100644
--- a/frontend/src/components/TerminalPane.jsx
+++ b/frontend/src/components/TerminalPane.jsx
@@ -59,8 +59,19 @@ const TerminalPane = ({
// Build WebSocket URL
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
let wsUrl = `${protocol}//${window.location.host}/api/terminal/ws?shell=${encodeURIComponent(currentShell)}`;
+
+ // Handle composite SSH key ID (format: "source:id" e.g., "local:123" or "vault:keyname")
if (currentSshKeyId) {
- wsUrl += `&sshKeyId=${encodeURIComponent(currentSshKeyId)}`;
+ const colonIndex = currentSshKeyId.indexOf(':');
+ if (colonIndex > 0) {
+ const source = currentSshKeyId.substring(0, colonIndex);
+ const keyIdentifier = currentSshKeyId.substring(colonIndex + 1);
+ wsUrl += `&sshKeyId=${encodeURIComponent(keyIdentifier)}`;
+ wsUrl += `&sshKeySource=${encodeURIComponent(source)}`;
+ } else {
+ // Fallback for legacy format (just the ID)
+ wsUrl += `&sshKeyId=${encodeURIComponent(currentSshKeyId)}`;
+ }
}
xterm.write('\r\n\x1b[33mConnecting to terminal...\x1b[0m\r\n');
diff --git a/frontend/src/components/VaultSettings.jsx b/frontend/src/components/VaultSettings.jsx
new file mode 100644
index 0000000..5b26b99
--- /dev/null
+++ b/frontend/src/components/VaultSettings.jsx
@@ -0,0 +1,581 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Typography,
+ Box,
+ Button,
+ Paper,
+ TextField,
+ Alert,
+ CircularProgress,
+ Switch,
+ FormControlLabel,
+ Chip,
+ Divider,
+ Card,
+ CardContent,
+ Grid,
+ InputAdornment,
+ IconButton,
+ Tooltip,
+ Collapse,
+} from '@mui/material';
+import {
+ Save,
+ Delete,
+ CheckCircle,
+ Error as ErrorIcon,
+ Refresh,
+ Visibility,
+ VisibilityOff,
+ Info,
+ Storage,
+ VpnKey,
+ Dns,
+ Code,
+ Settings,
+} from '@mui/icons-material';
+
+/**
+ * VaultSettings component - HashiCorp Vault integration configuration
+ */
+const VaultSettings = () => {
+ const [config, setConfig] = useState({
+ address: '',
+ token: '',
+ namespace: '',
+ mount_path: 'secret',
+ enabled: false,
+ });
+ const [status, setStatus] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [testing, setTesting] = useState(false);
+ const [error, setError] = useState(null);
+ const [success, setSuccess] = useState(null);
+ const [showToken, setShowToken] = useState(false);
+ const [hasExistingToken, setHasExistingToken] = useState(false);
+ const [showAdvanced, setShowAdvanced] = useState(false);
+ const [vaultData, setVaultData] = useState({
+ sshKeys: [],
+ servers: [],
+ envVars: [],
+ scripts: [],
+ });
+ const [loadingVaultData, setLoadingVaultData] = useState(false);
+
+ // Fetch current Vault configuration
+ const fetchConfig = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const response = await fetch('/api/vault/config');
+
+ if (!response.ok) {
+ // 404 is expected when no config exists yet
+ if (response.status === 404) {
+ setLoading(false);
+ return;
+ }
+ throw new Error('Failed to fetch Vault configuration');
+ }
+
+ const text = await response.text();
+ if (!text) {
+ setLoading(false);
+ return;
+ }
+
+ const data = JSON.parse(text);
+ if (data && data.id) {
+ setConfig({
+ address: data.address || '',
+ token: '', // Token is never returned
+ namespace: data.namespace || '',
+ mount_path: data.mount_path || 'secret',
+ enabled: data.enabled || false,
+ });
+ setHasExistingToken(data.has_token || false);
+ }
+ } catch (err) {
+ console.error('VaultSettings fetchConfig error:', err);
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Fetch Vault connection status
+ const fetchStatus = async () => {
+ try {
+ const response = await fetch('/api/vault/status');
+ if (response.ok) {
+ const text = await response.text();
+ if (text) {
+ const data = JSON.parse(text);
+ setStatus(data);
+ }
+ }
+ } catch (err) {
+ console.error('Failed to fetch Vault status:', err);
+ }
+ };
+
+ // Fetch Vault data (keys, servers, etc.)
+ const fetchVaultData = async () => {
+ if (!status?.connected) return;
+
+ setLoadingVaultData(true);
+ try {
+ const [keysRes, serversRes, envRes, scriptsRes] = await Promise.all([
+ fetch('/api/vault/ssh-keys').catch(() => ({ ok: false })),
+ fetch('/api/vault/servers').catch(() => ({ ok: false })),
+ fetch('/api/vault/env-variables').catch(() => ({ ok: false })),
+ fetch('/api/vault/scripts').catch(() => ({ ok: false })),
+ ]);
+
+ // Parse responses, ensuring we always get arrays (API might return null)
+ const parseResponse = async (res) => {
+ if (!res.ok) return [];
+ try {
+ const data = await res.json();
+ return Array.isArray(data) ? data : [];
+ } catch {
+ return [];
+ }
+ };
+
+ const sshKeys = await parseResponse(keysRes);
+ const servers = await parseResponse(serversRes);
+ const envVars = await parseResponse(envRes);
+ const scripts = await parseResponse(scriptsRes);
+
+ setVaultData({ sshKeys, servers, envVars, scripts });
+ } catch (err) {
+ console.error('Failed to fetch Vault data:', err);
+ } finally {
+ setLoadingVaultData(false);
+ }
+ };
+
+ // Load config and status on mount
+ useEffect(() => {
+ fetchConfig();
+ fetchStatus();
+ }, []);
+
+ // Fetch Vault data when connected
+ useEffect(() => {
+ if (status?.connected) {
+ fetchVaultData();
+ }
+ }, [status?.connected]);
+
+ // Handle form input changes
+ const handleChange = (field) => (event) => {
+ const value = field === 'enabled' ? event.target.checked : event.target.value;
+ setConfig((prev) => ({ ...prev, [field]: value }));
+ };
+
+ // Save Vault configuration
+ const handleSave = async () => {
+ try {
+ setSaving(true);
+ setError(null);
+ setSuccess(null);
+
+ // Validate
+ if (!config.address) {
+ throw new Error('Vault address is required');
+ }
+
+ if (!hasExistingToken && !config.token) {
+ throw new Error('Vault token is required');
+ }
+
+ const response = await fetch('/api/vault/config', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(config),
+ });
+
+ if (!response.ok) {
+ const text = await response.text();
+ throw new Error(text || 'Failed to save configuration');
+ }
+
+ setSuccess('Vault configuration saved successfully');
+ setHasExistingToken(true);
+ setConfig((prev) => ({ ...prev, token: '' })); // Clear token from form
+ fetchStatus();
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ // Test Vault connection
+ const handleTestConnection = async () => {
+ try {
+ setTesting(true);
+ setError(null);
+ setSuccess(null);
+
+ const response = await fetch('/api/vault/test', {
+ method: 'POST',
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to test connection');
+ }
+
+ const result = await response.json();
+ setStatus(result);
+
+ if (result.connected) {
+ setSuccess('Connection successful!');
+ fetchVaultData();
+ } else {
+ setError(result.error || 'Connection failed');
+ }
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setTesting(false);
+ }
+ };
+
+ // Delete Vault configuration
+ const handleDelete = async () => {
+ if (!window.confirm('Are you sure you want to remove the Vault configuration?')) {
+ return;
+ }
+
+ try {
+ setError(null);
+ setSuccess(null);
+
+ const response = await fetch('/api/vault/config', {
+ method: 'DELETE',
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to delete configuration');
+ }
+
+ setConfig({
+ address: '',
+ token: '',
+ namespace: '',
+ mount_path: 'secret',
+ enabled: false,
+ });
+ setHasExistingToken(false);
+ setStatus(null);
+ setVaultData({ sshKeys: [], servers: [], envVars: [], scripts: [] });
+ setSuccess('Vault configuration removed');
+ } catch (err) {
+ setError(err.message);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ HashiCorp Vault Integration
+
+
+ {error && (
+ setError(null)}>
+ {error}
+
+ )}
+
+ {success && (
+ setSuccess(null)}>
+ {success}
+
+ )}
+
+ {/* Connection Status */}
+ {status && (
+
+
+ Status:
+ {status.connected ? (
+ }
+ label="Connected"
+ color="success"
+ variant="outlined"
+ />
+ ) : status.configured ? (
+ }
+ label={status.enabled ? 'Disconnected' : 'Disabled'}
+ color={status.enabled ? 'error' : 'default'}
+ variant="outlined"
+ />
+ ) : (
+
+ )}
+ {status.vault_sealed && (
+
+ )}
+
+
+
+
+ {status.error && (
+
+ {status.error}
+
+ )}
+
+ )}
+
+ {/* Configuration Form */}
+
+
+
+
+
+ setShowToken(!showToken)}
+ edge="end"
+ >
+ {showToken ? : }
+
+
+ ),
+ }}
+ />
+
+ }
+ onClick={() => setShowAdvanced(!showAdvanced)}
+ sx={{ alignSelf: 'flex-start' }}
+ >
+ {showAdvanced ? 'Hide Advanced Settings' : 'Show Advanced Settings'}
+
+
+
+
+
+
+
+
+
+
+
+ }
+ label="Enable Vault Integration"
+ />
+
+
+ }
+ onClick={handleSave}
+ disabled={saving}
+ >
+ {saving ? 'Saving...' : 'Save Configuration'}
+
+
+ : }
+ onClick={handleTestConnection}
+ disabled={testing || !config.address}
+ >
+ {testing ? 'Testing...' : 'Test Connection'}
+
+
+ {hasExistingToken && (
+ }
+ onClick={handleDelete}
+ >
+ Remove
+
+ )}
+
+
+
+
+ {/* Vault Data Overview */}
+ {status?.connected && (
+
+
+ Vault Secrets Overview
+
+
+ {loadingVaultData ? (
+
+
+
+ ) : (
+
+
+
+
+
+
+
+ {vaultData.sshKeys.length}
+
+
+
+ SSH Keys
+
+
+
+
+
+
+
+
+
+
+
+ {vaultData.servers.length}
+
+
+
+ Servers
+
+
+
+
+
+
+
+
+
+
+
+ {vaultData.envVars.length}
+
+
+
+ Env Variables
+
+
+
+
+
+
+
+
+
+
+
+ {vaultData.scripts.length}
+
+
+
+ Scripts
+
+
+
+
+
+ )}
+
+
+
+ }>
+
+ Secrets stored in Vault will appear alongside your local SQLite data.
+ Look for the badge
+ in SSH Keys, Servers, and Environment Variables lists.
+
+
+
+ )}
+
+ {/* Setup Instructions */}
+ {!status?.configured && (
+
+
+ Getting Started with Vault
+
+
+ To use HashiCorp Vault integration, you need:
+
+
+
+ - A running HashiCorp Vault server (v1.0+)
+ - KV v2 secrets engine enabled
+ - A token with read/list permissions for
secret/data/web-cli/*
+
+
+
+ See the documentation for detailed setup instructions and Vault policy examples.
+
+
+ )}
+
+ );
+};
+
+export default VaultSettings;
diff --git a/frontend/src/components/shared/GroupInput.jsx b/frontend/src/components/shared/GroupInput.jsx
new file mode 100644
index 0000000..f010ec3
--- /dev/null
+++ b/frontend/src/components/shared/GroupInput.jsx
@@ -0,0 +1,198 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Autocomplete,
+ TextField,
+ CircularProgress,
+ Box,
+ Chip,
+ createFilterOptions,
+} from '@mui/material';
+import { Folder, Add } from '@mui/icons-material';
+
+const filter = createFilterOptions();
+
+/**
+ * GroupInput component - autocomplete input for selecting or creating groups
+ * Allows selecting from existing groups or typing a new group name
+ *
+ * @param {Object} props
+ * @param {string} props.value - Current group value
+ * @param {function} props.onChange - Callback when group changes
+ * @param {string} props.resourceType - The resource type to fetch groups from ('servers', 'keys', 'env-variables', 'bash-scripts')
+ * @param {boolean} props.disabled - Whether the input is disabled
+ * @param {string} props.helperText - Helper text to display
+ * @param {string} props.label - Label for the input
+ */
+const GroupInput = ({
+ value = 'default',
+ onChange,
+ resourceType = 'servers',
+ disabled = false,
+ helperText = 'Select an existing group or type a new one',
+ label = 'Group',
+}) => {
+ const [groups, setGroups] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [inputValue, setInputValue] = useState('');
+
+ // Fetch groups from all resource types to get a comprehensive list
+ const fetchAllGroups = async () => {
+ try {
+ setLoading(true);
+ const resourceTypes = ['servers', 'keys', 'env-variables', 'bash-scripts'];
+
+ const results = await Promise.all(
+ resourceTypes.map(async (type) => {
+ try {
+ const response = await fetch(`/api/${type}/groups`);
+ if (response.ok) {
+ return await response.json();
+ }
+ return [];
+ } catch {
+ return [];
+ }
+ })
+ );
+
+ // Combine and deduplicate groups from all resource types
+ const allGroups = [...new Set(results.flat())].filter(Boolean).sort();
+
+ // Ensure 'default' is always in the list
+ if (!allGroups.includes('default')) {
+ allGroups.unshift('default');
+ }
+
+ setGroups(allGroups);
+ } catch (err) {
+ console.error('Failed to fetch groups:', err);
+ setGroups(['default']);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Load groups on component mount
+ useEffect(() => {
+ fetchAllGroups();
+ }, []);
+
+ // Handle selection change
+ const handleChange = (event, newValue) => {
+ if (typeof newValue === 'string') {
+ // User typed and pressed enter
+ onChange(newValue);
+ } else if (newValue && newValue.inputValue) {
+ // User selected "Add new group" option
+ onChange(newValue.inputValue);
+ } else if (newValue) {
+ // User selected an existing option
+ onChange(newValue);
+ } else {
+ // Cleared
+ onChange('default');
+ }
+ };
+
+ return (
+ {
+ setInputValue(newInputValue);
+ }}
+ filterOptions={(options, params) => {
+ const filtered = filter(options, params);
+
+ const { inputValue } = params;
+ // Suggest creating a new group if the input doesn't match any existing group
+ const isExisting = options.some(
+ (option) => inputValue.toLowerCase() === option.toLowerCase()
+ );
+ if (inputValue !== '' && !isExisting) {
+ filtered.push({
+ inputValue,
+ title: `Add "${inputValue}"`,
+ });
+ }
+
+ return filtered;
+ }}
+ selectOnFocus
+ clearOnBlur
+ handleHomeEndKeys
+ freeSolo
+ options={groups}
+ getOptionLabel={(option) => {
+ // Value selected with enter from the input
+ if (typeof option === 'string') {
+ return option;
+ }
+ // Add new option created dynamically
+ if (option.inputValue) {
+ return option.inputValue;
+ }
+ return option;
+ }}
+ renderOption={(props, option) => {
+ const { key, ...otherProps } = props;
+ if (typeof option === 'object' && option.inputValue) {
+ // "Add new group" option
+ return (
+
+
+ {option.title}
+
+ );
+ }
+ // Existing group option
+ return (
+
+
+ {option}
+ {option === 'default' && (
+
+ )}
+
+ );
+ }}
+ loading={loading}
+ disabled={disabled}
+ renderInput={(params) => (
+
+ {loading ? : null}
+ {params.InputProps.endAdornment}
+ >
+ ),
+ }}
+ />
+ )}
+ />
+ );
+};
+
+export default GroupInput;
diff --git a/frontend/src/components/shared/GroupSelector.jsx b/frontend/src/components/shared/GroupSelector.jsx
new file mode 100644
index 0000000..659d873
--- /dev/null
+++ b/frontend/src/components/shared/GroupSelector.jsx
@@ -0,0 +1,140 @@
+import React, { useState, useEffect } from 'react';
+import {
+ FormControl,
+ InputLabel,
+ Select,
+ MenuItem,
+ Chip,
+ Box,
+ CircularProgress,
+ Typography,
+} from '@mui/material';
+import { Folder, FolderOpen } from '@mui/icons-material';
+
+/**
+ * GroupSelector component - allows filtering resources by group
+ * @param {Object} props
+ * @param {string} props.resourceType - The resource type ('servers', 'keys', 'env-variables', 'bash-scripts')
+ * @param {string} props.selectedGroup - Currently selected group ('all' for all groups)
+ * @param {function} props.onGroupChange - Callback when group selection changes
+ * @param {string} props.size - Select size ('small' or 'medium'), default 'small'
+ * @param {boolean} props.showCount - Show count of groups, default false
+ */
+const GroupSelector = ({
+ resourceType,
+ selectedGroup,
+ onGroupChange,
+ size = 'small',
+ showCount = false,
+}) => {
+ const [groups, setGroups] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ // Fetch groups from API
+ const fetchGroups = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const response = await fetch(`/api/${resourceType}/groups`);
+
+ if (!response.ok) {
+ throw new Error('Failed to fetch groups');
+ }
+
+ const data = await response.json();
+ setGroups(data || []);
+ } catch (err) {
+ setError(err.message);
+ setGroups([]);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Load groups on component mount or when resourceType changes
+ useEffect(() => {
+ fetchGroups();
+ }, [resourceType]);
+
+ // Handle selection change
+ const handleChange = (event) => {
+ onGroupChange(event.target.value);
+ };
+
+ if (loading) {
+ return (
+
+
+
+ Loading groups...
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ Failed to load groups
+
+ );
+ }
+
+ // If no groups found, don't render the selector
+ if (groups.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ Group
+
+
+
+
+ );
+};
+
+export default GroupSelector;
diff --git a/frontend/src/components/shared/SourceChip.jsx b/frontend/src/components/shared/SourceChip.jsx
new file mode 100644
index 0000000..a9b9eec
--- /dev/null
+++ b/frontend/src/components/shared/SourceChip.jsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { Chip, Tooltip } from '@mui/material';
+import { Storage, Lock } from '@mui/icons-material';
+
+/**
+ * SourceChip component - displays the data source (sqlite or vault)
+ * @param {Object} props
+ * @param {string} props.source - The data source ('sqlite' or 'vault')
+ * @param {string} props.size - Chip size ('small' or 'medium'), default 'small'
+ */
+const SourceChip = ({ source, size = 'small' }) => {
+ if (!source) return null;
+
+ const isVault = source === 'vault';
+
+ return (
+
+ : }
+ label={isVault ? 'Vault' : 'Local'}
+ size={size}
+ variant="outlined"
+ color={isVault ? 'secondary' : 'default'}
+ sx={{
+ fontWeight: 500,
+ '& .MuiChip-icon': {
+ fontSize: '1rem',
+ },
+ }}
+ />
+
+ );
+};
+
+export default SourceChip;
diff --git a/frontend/src/components/shared/StorageSelector.jsx b/frontend/src/components/shared/StorageSelector.jsx
new file mode 100644
index 0000000..9bb03e5
--- /dev/null
+++ b/frontend/src/components/shared/StorageSelector.jsx
@@ -0,0 +1,165 @@
+import React, { useState, useEffect } from 'react';
+import {
+ FormControl,
+ FormLabel,
+ RadioGroup,
+ FormControlLabel,
+ Radio,
+ Box,
+ Chip,
+ Typography,
+} from '@mui/material';
+import { Storage, Cloud } from '@mui/icons-material';
+
+/**
+ * Vault Logo SVG for the selector
+ */
+const VaultLogo = ({ size = 16 }) => (
+
+);
+
+/**
+ * StorageSelector component - allows choosing between local SQLite and Vault storage
+ * Only shows Vault option when Vault is configured and connected
+ *
+ * @param {string} value - Current storage value ('local' or 'vault')
+ * @param {function} onChange - Callback when storage changes
+ * @param {boolean} disabled - Whether the selector is disabled
+ */
+const StorageSelector = ({ value = 'local', onChange, disabled = false }) => {
+ const [vaultStatus, setVaultStatus] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ // Fetch Vault status to determine if Vault option should be available
+ useEffect(() => {
+ const fetchVaultStatus = async () => {
+ try {
+ const response = await fetch('/api/vault/status');
+ if (response.ok) {
+ const data = await response.json();
+ setVaultStatus(data);
+ } else {
+ setVaultStatus(null);
+ }
+ } catch (err) {
+ console.debug('Failed to fetch vault status:', err);
+ setVaultStatus(null);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchVaultStatus();
+ }, []);
+
+ // Check if Vault is available (configured, enabled, and connected)
+ const isVaultAvailable = vaultStatus?.configured && vaultStatus?.enabled && vaultStatus?.connected;
+
+ // If Vault is not available and current value is 'vault', reset to 'local'
+ useEffect(() => {
+ if (!loading && !isVaultAvailable && value === 'vault') {
+ onChange('local');
+ }
+ }, [loading, isVaultAvailable, value, onChange]);
+
+ // Don't show selector if Vault is not available - just use local storage
+ if (loading) {
+ return null;
+ }
+
+ if (!isVaultAvailable) {
+ return null;
+ }
+
+ return (
+
+
+ Storage Location
+
+ onChange(e.target.value)}
+ sx={{ gap: 2 }}
+ >
+ }
+ label={
+
+
+ Local Database
+
+ }
+ sx={{
+ border: '1px solid',
+ borderColor: value === 'local' ? 'primary.main' : 'divider',
+ borderRadius: 1,
+ px: 2,
+ py: 0.5,
+ m: 0,
+ backgroundColor: value === 'local' ? 'action.selected' : 'transparent',
+ transition: 'all 0.2s ease',
+ '&:hover': {
+ borderColor: 'primary.main',
+ backgroundColor: 'action.hover',
+ },
+ }}
+ />
+ }
+ label={
+
+
+ HashiCorp Vault
+
+
+ }
+ sx={{
+ border: '1px solid',
+ borderColor: value === 'vault' ? 'primary.main' : 'divider',
+ borderRadius: 1,
+ px: 2,
+ py: 0.5,
+ m: 0,
+ backgroundColor: value === 'vault' ? 'action.selected' : 'transparent',
+ transition: 'all 0.2s ease',
+ '&:hover': {
+ borderColor: 'primary.main',
+ backgroundColor: 'action.hover',
+ },
+ }}
+ />
+
+
+ );
+};
+
+export default StorageSelector;
diff --git a/frontend/src/components/shared/VaultIcon.jsx b/frontend/src/components/shared/VaultIcon.jsx
new file mode 100644
index 0000000..8111025
--- /dev/null
+++ b/frontend/src/components/shared/VaultIcon.jsx
@@ -0,0 +1,142 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import { Box, Tooltip, Typography, Chip } from '@mui/material';
+import { useNavigate } from 'react-router-dom';
+
+/**
+ * HashiCorp Vault Logo SVG
+ * Simplified filled vault logo for better visibility
+ */
+const VaultLogo = ({ color = 'currentColor', size = 20 }) => (
+
+);
+
+/**
+ * VaultIcon component - displays Vault connection status in header
+ * Only visible when Vault is configured in admin panel
+ * Shows green when connected, red when disconnected, orange when sealed
+ */
+const VaultIcon = ({ sx = {} }) => {
+ const navigate = useNavigate();
+ const [vaultStatus, setVaultStatus] = useState(null);
+
+ // Fetch Vault status
+ const fetchVaultStatus = useCallback(async () => {
+ try {
+ const response = await fetch('/api/vault/status');
+ if (response.ok) {
+ const data = await response.json();
+ setVaultStatus(data);
+ } else {
+ setVaultStatus(null);
+ }
+ } catch (err) {
+ // Silently fail - Vault might not be configured
+ console.debug('Failed to fetch vault status:', err);
+ setVaultStatus(null);
+ }
+ }, []);
+
+ // Initial fetch and periodic refresh
+ useEffect(() => {
+ fetchVaultStatus();
+ // Refresh every 30 seconds
+ const interval = setInterval(fetchVaultStatus, 30000);
+ return () => clearInterval(interval);
+ }, [fetchVaultStatus]);
+
+ // Don't render if Vault is not configured
+ if (!vaultStatus || !vaultStatus.configured) {
+ return null;
+ }
+
+ // Determine status color, text, and tooltip
+ const getStatusProps = () => {
+ if (!vaultStatus.enabled) {
+ return {
+ color: '#9e9e9e', // grey
+ statusText: 'Disabled',
+ tooltip: 'Vault integration is disabled',
+ };
+ }
+
+ if (vaultStatus.vault_sealed) {
+ return {
+ color: '#ff9800', // orange/warning
+ statusText: 'Sealed',
+ tooltip: 'Vault is sealed - secrets are inaccessible',
+ };
+ }
+
+ if (vaultStatus.connected) {
+ return {
+ color: '#4caf50', // green/success
+ statusText: 'Connected',
+ tooltip: `Vault connected: ${vaultStatus.address || 'HashiCorp Vault'}`,
+ };
+ }
+
+ return {
+ color: '#f44336', // red/error
+ statusText: 'Disconnected',
+ tooltip: vaultStatus.error || 'Unable to connect to Vault',
+ };
+ };
+
+ const { color, statusText, tooltip } = getStatusProps();
+
+ return (
+
+ }
+ label={statusText}
+ size="small"
+ onClick={() => navigate('/admin', { state: { tab: 4 } })}
+ sx={{
+ cursor: 'pointer',
+ backgroundColor: 'transparent',
+ border: `1px solid ${color}`,
+ color: color,
+ fontWeight: 500,
+ fontSize: '0.75rem',
+ height: 28,
+ transition: 'all 0.2s ease',
+ '& .MuiChip-icon': {
+ color: color,
+ marginLeft: '8px',
+ },
+ '&:hover': {
+ backgroundColor: `${color}20`,
+ transform: 'scale(1.02)',
+ },
+ ...sx,
+ }}
+ />
+
+ );
+};
+
+export default VaultIcon;
diff --git a/go.mod b/go.mod
index 6fa6cf7..b2130c2 100644
--- a/go.mod
+++ b/go.mod
@@ -11,10 +11,12 @@ require (
require (
github.com/KyleBanks/depth v1.2.1 // indirect
+ github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/creack/pty v1.1.21 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
+ github.com/go-jose/go-jose/v4 v4.1.1 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/spec v0.22.2 // indirect
@@ -29,13 +31,26 @@ require (
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
+ github.com/hashicorp/errwrap v1.1.0 // indirect
+ github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
+ github.com/hashicorp/go-multierror v1.1.1 // indirect
+ github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
+ github.com/hashicorp/go-rootcerts v1.0.2 // indirect
+ github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
+ github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
+ github.com/hashicorp/go-sockaddr v1.0.7 // indirect
+ github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
+ github.com/hashicorp/vault/api v1.22.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mitchellh/go-homedir v1.1.0 // indirect
+ github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
+ github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
@@ -57,6 +72,7 @@ require (
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
+ golang.org/x/time v0.12.0 // indirect
golang.org/x/tools v0.40.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
modernc.org/libc v1.66.10 // indirect
diff --git a/go.sum b/go.sum
index 99db067..3f929df 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,7 @@
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
@@ -9,6 +11,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI=
+github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
@@ -41,6 +45,27 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
+github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
+github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
+github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
+github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
+github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=
+github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=
+github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
+github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
+github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
+github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
+github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=
+github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
+github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0=
+github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
@@ -48,6 +73,10 @@ github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
@@ -59,6 +88,8 @@ github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo=
github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
+github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
@@ -140,6 +171,8 @@ golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
+golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
+golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
diff --git a/internal/audit/audit.go b/internal/audit/audit.go
new file mode 100644
index 0000000..4d7c710
--- /dev/null
+++ b/internal/audit/audit.go
@@ -0,0 +1,302 @@
+package audit
+
+import (
+ "encoding/json"
+ "log"
+ "net/http"
+ "os"
+ "sync"
+ "time"
+)
+
+// EventType represents the type of audited event
+type EventType string
+
+const (
+ EventTypeCommandExecution EventType = "COMMAND_EXECUTION"
+ EventTypeScriptExecution EventType = "SCRIPT_EXECUTION"
+ EventTypeSSHConnection EventType = "SSH_CONNECTION"
+ EventTypeTerminalSession EventType = "TERMINAL_SESSION"
+ EventTypeConfigChange EventType = "CONFIG_CHANGE"
+ EventTypeAuthAttempt EventType = "AUTH_ATTEMPT"
+)
+
+// EventOutcome represents the result of an audited event
+type EventOutcome string
+
+const (
+ OutcomeSuccess EventOutcome = "SUCCESS"
+ OutcomeFailure EventOutcome = "FAILURE"
+ OutcomeDenied EventOutcome = "DENIED"
+)
+
+// AuditEvent represents a single audit log entry
+type AuditEvent struct {
+ Timestamp time.Time `json:"timestamp"`
+ EventType EventType `json:"event_type"`
+ Outcome EventOutcome `json:"outcome"`
+ Actor string `json:"actor"` // Username or "anonymous"
+ SourceIP string `json:"source_ip"` // Client IP address
+ Target string `json:"target"` // What was acted upon (e.g., server name, script name)
+ Command string `json:"command,omitempty"`
+ User string `json:"user,omitempty"` // Execution user (e.g., root, ubuntu)
+ Server string `json:"server,omitempty"` // Target server for remote commands
+ ExitCode *int `json:"exit_code,omitempty"` // Command exit code
+ Duration int64 `json:"duration_ms,omitempty"` // Execution duration in milliseconds
+ ErrorMsg string `json:"error,omitempty"` // Error message if failed
+ Metadata map[string]string `json:"metadata,omitempty"` // Additional context
+}
+
+// Logger handles audit logging
+type Logger struct {
+ mu sync.Mutex
+ enabled bool
+ file *os.File
+ filePath string
+}
+
+var (
+ defaultLogger *Logger
+ once sync.Once
+)
+
+// Initialize creates or returns the singleton audit logger
+func Initialize(filePath string) (*Logger, error) {
+ var initErr error
+ once.Do(func() {
+ logger := &Logger{
+ enabled: filePath != "",
+ filePath: filePath,
+ }
+
+ if filePath != "" {
+ file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
+ if err != nil {
+ initErr = err
+ log.Printf("Warning: Failed to open audit log file %s: %v", filePath, err)
+ logger.enabled = false
+ } else {
+ logger.file = file
+ }
+ }
+
+ defaultLogger = logger
+ })
+
+ return defaultLogger, initErr
+}
+
+// GetLogger returns the default audit logger
+func GetLogger() *Logger {
+ if defaultLogger == nil {
+ // Return a disabled logger if not initialized
+ return &Logger{enabled: false}
+ }
+ return defaultLogger
+}
+
+// Close closes the audit log file
+func (l *Logger) Close() error {
+ l.mu.Lock()
+ defer l.mu.Unlock()
+
+ if l.file != nil {
+ return l.file.Close()
+ }
+ return nil
+}
+
+// Log writes an audit event
+func (l *Logger) Log(event *AuditEvent) {
+ if !l.enabled || l.file == nil {
+ return
+ }
+
+ // Ensure timestamp is set
+ if event.Timestamp.IsZero() {
+ event.Timestamp = time.Now().UTC()
+ }
+
+ l.mu.Lock()
+ defer l.mu.Unlock()
+
+ data, err := json.Marshal(event)
+ if err != nil {
+ log.Printf("Warning: Failed to marshal audit event: %v", err)
+ return
+ }
+
+ // Append newline for JSONL format
+ data = append(data, '\n')
+
+ if _, err := l.file.Write(data); err != nil {
+ log.Printf("Warning: Failed to write audit event: %v", err)
+ }
+}
+
+// LogCommandExecution logs a command execution event
+func (l *Logger) LogCommandExecution(r *http.Request, command, user, server string, exitCode int, durationMs int64, err error) {
+ event := &AuditEvent{
+ EventType: EventTypeCommandExecution,
+ Actor: getActorFromRequest(r),
+ SourceIP: getClientIP(r),
+ Target: server,
+ Command: sanitizeCommand(command),
+ User: user,
+ Server: server,
+ ExitCode: &exitCode,
+ Duration: durationMs,
+ }
+
+ if err != nil {
+ event.Outcome = OutcomeFailure
+ event.ErrorMsg = err.Error()
+ } else if exitCode != 0 {
+ event.Outcome = OutcomeFailure
+ } else {
+ event.Outcome = OutcomeSuccess
+ }
+
+ l.Log(event)
+}
+
+// LogScriptExecution logs a script execution event
+func (l *Logger) LogScriptExecution(r *http.Request, scriptName, user, server string, exitCode int, durationMs int64, err error) {
+ event := &AuditEvent{
+ EventType: EventTypeScriptExecution,
+ Actor: getActorFromRequest(r),
+ SourceIP: getClientIP(r),
+ Target: scriptName,
+ User: user,
+ Server: server,
+ ExitCode: &exitCode,
+ Duration: durationMs,
+ }
+
+ if err != nil {
+ event.Outcome = OutcomeFailure
+ event.ErrorMsg = err.Error()
+ } else if exitCode != 0 {
+ event.Outcome = OutcomeFailure
+ } else {
+ event.Outcome = OutcomeSuccess
+ }
+
+ l.Log(event)
+}
+
+// LogTerminalSession logs a terminal session start/end
+func (l *Logger) LogTerminalSession(r *http.Request, server, user string, outcome EventOutcome, metadata map[string]string) {
+ event := &AuditEvent{
+ EventType: EventTypeTerminalSession,
+ Outcome: outcome,
+ Actor: getActorFromRequest(r),
+ SourceIP: getClientIP(r),
+ Target: server,
+ User: user,
+ Server: server,
+ Metadata: metadata,
+ }
+
+ l.Log(event)
+}
+
+// LogAuthAttempt logs an authentication attempt
+func (l *Logger) LogAuthAttempt(r *http.Request, outcome EventOutcome, method string) {
+ event := &AuditEvent{
+ EventType: EventTypeAuthAttempt,
+ Outcome: outcome,
+ Actor: getActorFromRequest(r),
+ SourceIP: getClientIP(r),
+ Target: r.URL.Path,
+ Metadata: map[string]string{
+ "method": method,
+ },
+ }
+
+ l.Log(event)
+}
+
+// LogConfigChange logs a configuration change
+func (l *Logger) LogConfigChange(r *http.Request, configType, action string, outcome EventOutcome) {
+ event := &AuditEvent{
+ EventType: EventTypeConfigChange,
+ Outcome: outcome,
+ Actor: getActorFromRequest(r),
+ SourceIP: getClientIP(r),
+ Target: configType,
+ Metadata: map[string]string{
+ "action": action,
+ },
+ }
+
+ l.Log(event)
+}
+
+// getActorFromRequest extracts the actor (username) from the request
+func getActorFromRequest(r *http.Request) string {
+ if r == nil {
+ return "system"
+ }
+
+ // Check for basic auth username
+ if username, _, ok := r.BasicAuth(); ok && username != "" {
+ return username
+ }
+
+ // Check for custom header (if using token-based auth)
+ if actor := r.Header.Get("X-Auth-User"); actor != "" {
+ return actor
+ }
+
+ return "anonymous"
+}
+
+// getClientIP extracts the client IP from the request
+func getClientIP(r *http.Request) string {
+ if r == nil {
+ return ""
+ }
+
+ // Check X-Forwarded-For header first (for proxied requests)
+ if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
+ // Take the first IP in the chain
+ for i := 0; i < len(xff); i++ {
+ if xff[i] == ',' {
+ return xff[:i]
+ }
+ }
+ return xff
+ }
+
+ // Check X-Real-IP header
+ if xri := r.Header.Get("X-Real-IP"); xri != "" {
+ return xri
+ }
+
+ // Fall back to RemoteAddr
+ remoteAddr := r.RemoteAddr
+ // Strip port if present
+ for i := len(remoteAddr) - 1; i >= 0; i-- {
+ if remoteAddr[i] == ':' {
+ return remoteAddr[:i]
+ }
+ if remoteAddr[i] == ']' {
+ // IPv6 address with brackets
+ break
+ }
+ }
+
+ return remoteAddr
+}
+
+// sanitizeCommand removes potentially sensitive data from commands
+// This is a basic implementation - extend as needed
+func sanitizeCommand(cmd string) string {
+ // For audit purposes, we log the command but could add sanitization
+ // to remove obvious passwords or tokens if needed
+ if len(cmd) > 1000 {
+ return cmd[:1000] + "...[truncated]"
+ }
+ return cmd
+}
diff --git a/internal/config/config.go b/internal/config/config.go
index 851ceee..5e0d2d1 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -8,6 +8,7 @@ import (
"path/filepath"
"strconv"
"sync"
+ "time"
"github.com/spf13/viper"
)
@@ -27,6 +28,65 @@ type Config struct {
TLSCertPath string // Path to TLS certificate file (enables HTTPS)
TLSKeyPath string // Path to TLS private key file
RequireHTTPS bool // Require HTTPS when auth is enabled (reject HTTP requests)
+
+ // Timeout configurations (all in seconds)
+ ReadTimeout int // HTTP server read timeout (default: 30)
+ WriteTimeout int // HTTP server write timeout (default: 600 for streaming)
+ IdleTimeout int // HTTP server idle timeout (default: 60)
+ VaultTimeout int // Vault operation timeout (default: 30)
+ CommandTimeout int // Command execution timeout (default: 300)
+ SSHConnectTimeout int // SSH connection timeout (default: 30)
+
+ // Audit logging
+ AuditLogPath string // Path to audit log file (empty to disable)
+}
+
+// GetReadTimeout returns the read timeout as a time.Duration
+func (c *Config) GetReadTimeout() time.Duration {
+ if c.ReadTimeout <= 0 {
+ return 30 * time.Second
+ }
+ return time.Duration(c.ReadTimeout) * time.Second
+}
+
+// GetWriteTimeout returns the write timeout as a time.Duration
+func (c *Config) GetWriteTimeout() time.Duration {
+ if c.WriteTimeout <= 0 {
+ return 10 * time.Minute // Default for streaming
+ }
+ return time.Duration(c.WriteTimeout) * time.Second
+}
+
+// GetIdleTimeout returns the idle timeout as a time.Duration
+func (c *Config) GetIdleTimeout() time.Duration {
+ if c.IdleTimeout <= 0 {
+ return 60 * time.Second
+ }
+ return time.Duration(c.IdleTimeout) * time.Second
+}
+
+// GetVaultTimeout returns the Vault timeout as a time.Duration
+func (c *Config) GetVaultTimeout() time.Duration {
+ if c.VaultTimeout <= 0 {
+ return 30 * time.Second
+ }
+ return time.Duration(c.VaultTimeout) * time.Second
+}
+
+// GetCommandTimeout returns the command execution timeout as a time.Duration
+func (c *Config) GetCommandTimeout() time.Duration {
+ if c.CommandTimeout <= 0 {
+ return 5 * time.Minute
+ }
+ return time.Duration(c.CommandTimeout) * time.Second
+}
+
+// GetSSHConnectTimeout returns the SSH connection timeout as a time.Duration
+func (c *Config) GetSSHConnectTimeout() time.Duration {
+ if c.SSHConnectTimeout <= 0 {
+ return 30 * time.Second
+ }
+ return time.Duration(c.SSHConnectTimeout) * time.Second
}
// Load parses command-line flags and environment variables to load configuration
@@ -43,6 +103,15 @@ func Load() *Config {
v.SetDefault("tls_key_path", "")
v.SetDefault("require_https", false)
+ // Timeout defaults (in seconds)
+ v.SetDefault("read_timeout", 30)
+ v.SetDefault("write_timeout", 600) // 10 minutes for streaming
+ v.SetDefault("idle_timeout", 60)
+ v.SetDefault("vault_timeout", 30)
+ v.SetDefault("command_timeout", 300) // 5 minutes
+ v.SetDefault("ssh_connect_timeout", 30)
+ v.SetDefault("audit_log_path", "") // Empty to disable audit logging
+
// Enable environment variable support
v.SetEnvPrefix("WEBCLI") // Environment variables will be WEBCLI_PORT, WEBCLI_HOST, etc.
v.AutomaticEnv()
@@ -57,6 +126,17 @@ func Load() *Config {
v.BindEnv("tls_key_path", "TLS_KEY_PATH", "WEBCLI_TLS_KEY_PATH")
v.BindEnv("require_https", "REQUIRE_HTTPS", "WEBCLI_REQUIRE_HTTPS")
+ // Timeout environment variables
+ v.BindEnv("read_timeout", "READ_TIMEOUT", "WEBCLI_READ_TIMEOUT")
+ v.BindEnv("write_timeout", "WRITE_TIMEOUT", "WEBCLI_WRITE_TIMEOUT")
+ v.BindEnv("idle_timeout", "IDLE_TIMEOUT", "WEBCLI_IDLE_TIMEOUT")
+ v.BindEnv("vault_timeout", "VAULT_TIMEOUT", "WEBCLI_VAULT_TIMEOUT")
+ v.BindEnv("command_timeout", "COMMAND_TIMEOUT", "WEBCLI_COMMAND_TIMEOUT")
+ v.BindEnv("ssh_connect_timeout", "SSH_CONNECT_TIMEOUT", "WEBCLI_SSH_CONNECT_TIMEOUT")
+
+ // Audit logging
+ v.BindEnv("audit_log_path", "AUDIT_LOG_PATH", "WEBCLI_AUDIT_LOG_PATH")
+
// Config file support (optional)
v.SetConfigName("config") // config.yaml, config.json, config.toml
v.SetConfigType("yaml") // default to yaml
@@ -128,6 +208,17 @@ func Load() *Config {
TLSCertPath: v.GetString("tls_cert_path"),
TLSKeyPath: v.GetString("tls_key_path"),
RequireHTTPS: v.GetBool("require_https"),
+
+ // Timeout values
+ ReadTimeout: v.GetInt("read_timeout"),
+ WriteTimeout: v.GetInt("write_timeout"),
+ IdleTimeout: v.GetInt("idle_timeout"),
+ VaultTimeout: v.GetInt("vault_timeout"),
+ CommandTimeout: v.GetInt("command_timeout"),
+ SSHConnectTimeout: v.GetInt("ssh_connect_timeout"),
+
+ // Audit logging
+ AuditLogPath: v.GetString("audit_log_path"),
}
}
diff --git a/internal/database/database_test.go b/internal/database/database_test.go
index b2b7f66..8e68481 100644
--- a/internal/database/database_test.go
+++ b/internal/database/database_test.go
@@ -70,8 +70,8 @@ func TestDatabaseCreation(t *testing.T) {
t.Fatalf("Failed to get version: %v", err)
}
- if version != 14 {
- t.Errorf("Expected schema version 14, got %d", version)
+ if version != 16 {
+ t.Errorf("Expected schema version 16, got %d", version)
}
// Verify all tables exist
@@ -84,6 +84,7 @@ func TestDatabaseCreation(t *testing.T) {
"local_users",
"env_variables",
"bash_scripts",
+ "vault_config",
}
for _, table := range tables {
@@ -145,6 +146,15 @@ func TestDatabaseCreation(t *testing.T) {
t.Errorf("bash_scripts table should have %s column after migration 13", field)
}
}
+
+ // Verify group_name column exists on resource tables (migration 16)
+ groupNameTables := []string{"servers", "ssh_keys", "env_variables", "bash_scripts"}
+ for _, table := range groupNameTables {
+ err = db.conn.QueryRow("SELECT name FROM pragma_table_info(?) WHERE name='group_name'", table).Scan(&columnName)
+ if err != nil {
+ t.Errorf("%s table should have group_name column after migration 16", table)
+ }
+ }
}
func TestDatabasePersistence(t *testing.T) {
diff --git a/internal/database/migrations.go b/internal/database/migrations.go
index 2f2051f..3d44512 100644
--- a/internal/database/migrations.go
+++ b/internal/database/migrations.go
@@ -199,6 +199,37 @@ var migrations = []Migration{
CREATE INDEX IF NOT EXISTS idx_script_presets_script_id ON script_presets(script_id);
`,
},
+ {
+ Version: 15,
+ Description: "Create vault_config table for HashiCorp Vault integration",
+ SQL: `
+ CREATE TABLE IF NOT EXISTS vault_config (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ address TEXT NOT NULL,
+ token_encrypted BLOB NOT NULL,
+ namespace TEXT,
+ mount_path TEXT NOT NULL DEFAULT 'secret',
+ enabled INTEGER NOT NULL DEFAULT 1,
+ created_at DATETIME NOT NULL,
+ updated_at DATETIME NOT NULL
+ );
+ `,
+ },
+ {
+ Version: 16,
+ Description: "Add group_name column to resource tables for grouping/categorization",
+ SQL: `
+ ALTER TABLE servers ADD COLUMN group_name TEXT NOT NULL DEFAULT 'default';
+ ALTER TABLE ssh_keys ADD COLUMN group_name TEXT NOT NULL DEFAULT 'default';
+ ALTER TABLE env_variables ADD COLUMN group_name TEXT NOT NULL DEFAULT 'default';
+ ALTER TABLE bash_scripts ADD COLUMN group_name TEXT NOT NULL DEFAULT 'default';
+
+ CREATE INDEX IF NOT EXISTS idx_servers_group ON servers(group_name);
+ CREATE INDEX IF NOT EXISTS idx_ssh_keys_group ON ssh_keys(group_name);
+ CREATE INDEX IF NOT EXISTS idx_env_variables_group ON env_variables(group_name);
+ CREATE INDEX IF NOT EXISTS idx_bash_scripts_group ON bash_scripts(group_name);
+ `,
+ },
}
// runMigrations executes all pending migrations
diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go
index 81eb7a5..bcbf85b 100644
--- a/internal/middleware/auth.go
+++ b/internal/middleware/auth.go
@@ -2,11 +2,15 @@ package middleware
import (
"crypto/subtle"
+ "errors"
"net/http"
"os"
"strings"
)
+// ErrAuthMisconfigured is returned when authentication is enabled but credentials are missing
+var ErrAuthMisconfigured = errors.New("authentication is enabled but no credentials are configured: set AUTH_USERNAME and AUTH_PASSWORD, or AUTH_API_TOKEN")
+
// AuthConfig holds authentication configuration
type AuthConfig struct {
Enabled bool
@@ -29,6 +33,24 @@ func LoadAuthConfig() *AuthConfig {
}
}
+// Validate checks if the authentication configuration is valid
+// Returns an error if auth is enabled but no credentials are configured
+func (c *AuthConfig) Validate() error {
+ if !c.Enabled {
+ return nil // Auth disabled, no validation needed
+ }
+
+ // When auth is enabled, at least one auth method must be configured
+ hasBasicAuth := c.Username != "" && c.Password != ""
+ hasAPIToken := c.APIToken != ""
+
+ if !hasBasicAuth && !hasAPIToken {
+ return ErrAuthMisconfigured
+ }
+
+ return nil
+}
+
// BasicAuth provides HTTP Basic Authentication middleware
func BasicAuth(config *AuthConfig) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
diff --git a/internal/models/bash_script.go b/internal/models/bash_script.go
index 2db20a9..0706cb3 100644
--- a/internal/models/bash_script.go
+++ b/internal/models/bash_script.go
@@ -6,10 +6,12 @@ import "time"
// Script content is encrypted at rest using AES-256-GCM
type BashScript struct {
ID int64 `json:"id"`
- Name string `json:"name"` // Display name for the script
- Description string `json:"description"` // Optional description
- Content string `json:"content"` // Script content (encrypted in DB)
- Filename string `json:"filename"` // Original filename if uploaded
+ Name string `json:"name"` // Display name for the script
+ Description string `json:"description"` // Optional description
+ Content string `json:"content"` // Script content (encrypted in DB)
+ Filename string `json:"filename"` // Original filename if uploaded
+ Group string `json:"group"` // Group/category for organization
+ Source string `json:"source,omitempty"` // "sqlite" or "vault"
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -20,6 +22,7 @@ type BashScriptCreate struct {
Description string `json:"description,omitempty"`
Content string `json:"content" validate:"required"`
Filename string `json:"filename,omitempty"`
+ Group string `json:"group"` // Optional, defaults to "default"
}
// BashScriptUpdate represents the data that can be updated for a bash script
@@ -28,6 +31,7 @@ type BashScriptUpdate struct {
Description string `json:"description,omitempty"`
Content string `json:"content,omitempty"`
Filename string `json:"filename,omitempty"`
+ Group string `json:"group,omitempty"`
}
// BashScriptResponse is the API response format
@@ -37,6 +41,8 @@ type BashScriptResponse struct {
Description string `json:"description"`
Content string `json:"content,omitempty"` // Only included when specifically requested
Filename string `json:"filename"`
+ Group string `json:"group"` // Group/category for organization
+ Source string `json:"source,omitempty"` // "sqlite" or "vault"
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -54,6 +60,8 @@ func (s *BashScript) ToResponse(includeContent bool) *BashScriptResponse {
Description: s.Description,
Content: content,
Filename: s.Filename,
+ Group: s.Group,
+ Source: s.Source,
CreatedAt: s.CreatedAt,
UpdatedAt: s.UpdatedAt,
}
diff --git a/internal/models/env_variable.go b/internal/models/env_variable.go
index 62186fe..12940b8 100644
--- a/internal/models/env_variable.go
+++ b/internal/models/env_variable.go
@@ -6,9 +6,11 @@ import "time"
// Values are encrypted at rest using AES-256-GCM
type EnvVariable struct {
ID int64 `json:"id"`
- Name string `json:"name"` // Environment variable name (e.g., API_KEY)
- Value string `json:"value"` // Decrypted value (encrypted in DB)
- Description string `json:"description"` // Optional description
+ Name string `json:"name"` // Environment variable name (e.g., API_KEY)
+ Value string `json:"value"` // Decrypted value (encrypted in DB)
+ Description string `json:"description"` // Optional description
+ Group string `json:"group"` // Group/category for organization
+ Source string `json:"source,omitempty"` // "sqlite" or "vault"
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -18,6 +20,7 @@ type EnvVariableCreate struct {
Name string `json:"name" validate:"required"`
Value string `json:"value" validate:"required"`
Description string `json:"description,omitempty"`
+ Group string `json:"group"` // Optional, defaults to "default"
}
// EnvVariableUpdate represents the data that can be updated for an environment variable
@@ -25,6 +28,7 @@ type EnvVariableUpdate struct {
Name string `json:"name,omitempty"`
Value string `json:"value,omitempty"`
Description string `json:"description,omitempty"`
+ Group string `json:"group,omitempty"`
}
// EnvVariableResponse is the API response format (value masked by default)
@@ -33,6 +37,8 @@ type EnvVariableResponse struct {
Name string `json:"name"`
Value string `json:"value"` // Will be masked unless explicitly requested
Description string `json:"description"`
+ Group string `json:"group"` // Group/category for organization
+ Source string `json:"source,omitempty"` // "sqlite" or "vault"
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -48,6 +54,8 @@ func (e *EnvVariable) ToResponse(showValue bool) *EnvVariableResponse {
Name: e.Name,
Value: value,
Description: e.Description,
+ Group: e.Group,
+ Source: e.Source,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
diff --git a/internal/models/saved_command.go b/internal/models/saved_command.go
index 0161444..1d4d76c 100644
--- a/internal/models/saved_command.go
+++ b/internal/models/saved_command.go
@@ -47,8 +47,12 @@ type CommandExecution struct {
SSHPassword string `json:"ssh_password,omitempty"` // SSH password (for remote, if key auth fails)
SaveAs string `json:"save_as,omitempty"` // Optional: save as template with this name
IsRemote bool `json:"is_remote"` // True if remote execution
- ServerID *int64 `json:"server_id,omitempty"` // Server ID for remote execution
- SSHKeyID *int64 `json:"ssh_key_id,omitempty"` // SSH key ID for remote execution
+ ServerID *int64 `json:"server_id,omitempty"` // Server ID for remote execution (SQLite)
+ ServerName string `json:"server_name,omitempty"` // Server name for remote execution (Vault)
+ ServerGroup string `json:"server_group,omitempty"` // Server group for remote execution (Vault)
+ SSHKeyID *int64 `json:"ssh_key_id,omitempty"` // SSH key ID for remote execution (SQLite)
+ SSHKeyName string `json:"ssh_key_name,omitempty"` // SSH key name for remote execution (Vault)
+ SSHKeyGroup string `json:"ssh_key_group,omitempty"` // SSH key group for remote execution (Vault)
}
// CommandResult represents the result of a command execution
@@ -63,15 +67,23 @@ type CommandResult struct {
// ScriptExecution represents a request to execute a stored bash script
type ScriptExecution struct {
- ScriptID int64 `json:"script_id" validate:"required"` // ID of the script to execute
- User string `json:"user"` // User to run as (default: root)
- SudoPassword string `json:"sudo_password,omitempty"` // Sudo password (required when user != current for local)
- SSHPassword string `json:"ssh_password,omitempty"` // SSH password (for remote, if key auth fails)
- IsRemote bool `json:"is_remote"` // True if remote execution
- ServerID *int64 `json:"server_id,omitempty"` // Server ID for remote execution
- SSHKeyID *int64 `json:"ssh_key_id,omitempty"` // SSH key ID for remote execution
- IncludeEnvVars bool `json:"include_env_vars"` // Deprecated: use EnvVarIDs instead
- EnvVarIDs []int64 `json:"env_var_ids,omitempty"` // Specific env var IDs to include
+ ScriptID int64 `json:"script_id,omitempty"` // ID of the script to execute (SQLite)
+ ScriptName string `json:"script_name,omitempty"` // Name of the script to execute (Vault)
+ ScriptGroup string `json:"script_group,omitempty"` // Script group for execution (Vault)
+ User string `json:"user"` // User to run as (default: root)
+ SudoPassword string `json:"sudo_password,omitempty"` // Sudo password (required when user != current for local)
+ SSHPassword string `json:"ssh_password,omitempty"` // SSH password (for remote, if key auth fails)
+ IsRemote bool `json:"is_remote"` // True if remote execution
+ ServerID *int64 `json:"server_id,omitempty"` // Server ID for remote execution (SQLite)
+ ServerName string `json:"server_name,omitempty"` // Server name for remote execution (Vault)
+ ServerGroup string `json:"server_group,omitempty"` // Server group for remote execution (Vault)
+ SSHKeyID *int64 `json:"ssh_key_id,omitempty"` // SSH key ID for remote execution (SQLite)
+ SSHKeyName string `json:"ssh_key_name,omitempty"` // SSH key name for remote execution (Vault)
+ SSHKeyGroup string `json:"ssh_key_group,omitempty"` // SSH key group for remote execution (Vault)
+ IncludeEnvVars bool `json:"include_env_vars"` // Deprecated: use EnvVarIDs instead
+ EnvVarIDs []int64 `json:"env_var_ids,omitempty"` // Specific env var IDs to include (SQLite)
+ EnvVarNames []string `json:"env_var_names,omitempty"` // Names of env vars to include (Vault)
+ EnvVarGroups []string `json:"env_var_groups,omitempty"` // Groups of env vars to include (Vault, paired with EnvVarNames)
}
// ScriptResult represents the result of a script execution
diff --git a/internal/models/server.go b/internal/models/server.go
index 4a65076..7298c70 100644
--- a/internal/models/server.go
+++ b/internal/models/server.go
@@ -10,6 +10,8 @@ type Server struct {
IPAddress string `json:"ip_address,omitempty"` // IP address
Port int `json:"port"` // SSH port (default: 22)
Username string `json:"username"` // SSH username for remote connections
+ Group string `json:"group"` // Group/category for organization
+ Source string `json:"source,omitempty"` // "sqlite" or "vault"
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -21,6 +23,7 @@ type ServerCreate struct {
IPAddress string `json:"ip_address,omitempty"`
Port int `json:"port"` // Optional, defaults to 22 if not provided
Username string `json:"username"` // SSH username for remote connections
+ Group string `json:"group"` // Optional, defaults to "default"
}
// ServerUpdate represents the data that can be updated for a server
@@ -29,4 +32,5 @@ type ServerUpdate struct {
IPAddress string `json:"ip_address,omitempty"`
Port int `json:"port,omitempty"`
Username string `json:"username,omitempty"`
+ Group string `json:"group,omitempty"`
}
diff --git a/internal/models/ssh_key.go b/internal/models/ssh_key.go
index 65a31f6..6c8b748 100644
--- a/internal/models/ssh_key.go
+++ b/internal/models/ssh_key.go
@@ -6,7 +6,9 @@ import "time"
type SSHKey struct {
ID int64 `json:"id"`
Name string `json:"name"`
- PrivateKey string `json:"private_key"` // Decrypted value
+ PrivateKey string `json:"private_key"` // Decrypted value
+ Group string `json:"group"` // Group/category for organization
+ Source string `json:"source,omitempty"` // "sqlite" or "vault"
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -15,10 +17,12 @@ type SSHKey struct {
type SSHKeyCreate struct {
Name string `json:"name" validate:"required"`
PrivateKey string `json:"private_key" validate:"required"`
+ Group string `json:"group"` // Optional, defaults to "default"
}
// SSHKeyUpdate represents the data that can be updated for an SSH key
type SSHKeyUpdate struct {
Name string `json:"name,omitempty"`
PrivateKey string `json:"private_key,omitempty"`
+ Group string `json:"group,omitempty"`
}
diff --git a/internal/models/vault_config.go b/internal/models/vault_config.go
new file mode 100644
index 0000000..02a05a3
--- /dev/null
+++ b/internal/models/vault_config.go
@@ -0,0 +1,60 @@
+package models
+
+import "time"
+
+// VaultConfig represents the HashiCorp Vault configuration
+type VaultConfig struct {
+ ID int64 `json:"id"`
+ Address string `json:"address"` // Vault server address (e.g., https://vault.example.com:8200)
+ Token string `json:"token,omitempty"` // Vault token (decrypted, not included in responses)
+ Namespace string `json:"namespace,omitempty"` // Optional Vault namespace
+ MountPath string `json:"mount_path"` // KV secrets engine mount path (default: "secret")
+ Enabled bool `json:"enabled"` // Whether Vault integration is enabled
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
+
+// VaultConfigCreate represents the data needed to create/update Vault configuration
+type VaultConfigCreate struct {
+ Address string `json:"address"`
+ Token string `json:"token"`
+ Namespace string `json:"namespace,omitempty"`
+ MountPath string `json:"mount_path,omitempty"`
+ Enabled bool `json:"enabled"`
+}
+
+// VaultConfigResponse is the API response format (token masked)
+type VaultConfigResponse struct {
+ ID int64 `json:"id"`
+ Address string `json:"address"`
+ Namespace string `json:"namespace,omitempty"`
+ MountPath string `json:"mount_path"`
+ Enabled bool `json:"enabled"`
+ HasToken bool `json:"has_token"` // Indicates if a token is configured
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
+
+// ToResponse converts VaultConfig to a safe response (without token)
+func (v *VaultConfig) ToResponse() VaultConfigResponse {
+ return VaultConfigResponse{
+ ID: v.ID,
+ Address: v.Address,
+ Namespace: v.Namespace,
+ MountPath: v.MountPath,
+ Enabled: v.Enabled,
+ HasToken: v.Token != "",
+ CreatedAt: v.CreatedAt,
+ UpdatedAt: v.UpdatedAt,
+ }
+}
+
+// VaultStatus represents the current Vault connection status
+type VaultStatus struct {
+ Configured bool `json:"configured"` // Whether Vault is configured
+ Enabled bool `json:"enabled"` // Whether Vault is enabled
+ Connected bool `json:"connected"` // Whether connection test passed
+ Address string `json:"address,omitempty"`
+ Error string `json:"error,omitempty"`
+ VaultSealed bool `json:"vault_sealed,omitempty"`
+}
diff --git a/internal/repository/bash_script_repository.go b/internal/repository/bash_script_repository.go
index 79ccf4c..56d7ec8 100644
--- a/internal/repository/bash_script_repository.go
+++ b/internal/repository/bash_script_repository.go
@@ -29,6 +29,12 @@ func (r *BashScriptRepository) Create(script *models.BashScriptCreate) (*models.
return nil, fmt.Errorf("content is required")
}
+ // Default group to "default" if not provided
+ group := script.Group
+ if group == "" {
+ group = "default"
+ }
+
// Encrypt the content
encryptedContent, err := database.Encrypt(script.Content)
if err != nil {
@@ -38,11 +44,12 @@ func (r *BashScriptRepository) Create(script *models.BashScriptCreate) (*models.
now := time.Now().UTC()
result, err := r.db.GetConnection().Exec(
- "INSERT INTO bash_scripts (name, description, content_encrypted, filename, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
+ "INSERT INTO bash_scripts (name, description, content_encrypted, filename, group_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
script.Name,
script.Description,
encryptedContent,
script.Filename,
+ group,
now,
now,
)
@@ -61,6 +68,7 @@ func (r *BashScriptRepository) Create(script *models.BashScriptCreate) (*models.
Description: script.Description,
Content: script.Content, // Return unencrypted content
Filename: script.Filename,
+ Group: group,
CreatedAt: now,
UpdatedAt: now,
}, nil
@@ -73,9 +81,9 @@ func (r *BashScriptRepository) GetByID(id int64) (*models.BashScript, error) {
var description, filename sql.NullString
err := r.db.GetConnection().QueryRow(
- "SELECT id, name, description, content_encrypted, filename, created_at, updated_at FROM bash_scripts WHERE id = ?",
+ "SELECT id, name, description, content_encrypted, filename, group_name, created_at, updated_at FROM bash_scripts WHERE id = ?",
id,
- ).Scan(&script.ID, &script.Name, &description, &encryptedContent, &filename, &script.CreatedAt, &script.UpdatedAt)
+ ).Scan(&script.ID, &script.Name, &description, &encryptedContent, &filename, &script.Group, &script.CreatedAt, &script.UpdatedAt)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("bash script not found")
@@ -105,7 +113,53 @@ func (r *BashScriptRepository) GetByID(id int64) (*models.BashScript, error) {
// GetAll retrieves all bash scripts (without content for listing)
func (r *BashScriptRepository) GetAll() ([]*models.BashScript, error) {
rows, err := r.db.GetConnection().Query(
- "SELECT id, name, description, content_encrypted, filename, created_at, updated_at FROM bash_scripts ORDER BY name ASC",
+ "SELECT id, name, description, content_encrypted, filename, group_name, created_at, updated_at FROM bash_scripts ORDER BY group_name ASC, name ASC",
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to query bash scripts: %w", err)
+ }
+ defer rows.Close()
+
+ var scripts []*models.BashScript
+ for rows.Next() {
+ var script models.BashScript
+ var encryptedContent []byte
+ var description, filename sql.NullString
+
+ if err := rows.Scan(&script.ID, &script.Name, &description, &encryptedContent, &filename, &script.Group, &script.CreatedAt, &script.UpdatedAt); err != nil {
+ return nil, fmt.Errorf("failed to scan bash script: %w", err)
+ }
+
+ // Handle nullable fields
+ if description.Valid {
+ script.Description = description.String
+ }
+ if filename.Valid {
+ script.Filename = filename.String
+ }
+
+ // Decrypt the content
+ decryptedContent, err := database.Decrypt(encryptedContent)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decrypt content: %w", err)
+ }
+ script.Content = decryptedContent
+
+ scripts = append(scripts, &script)
+ }
+
+ if err := rows.Err(); err != nil {
+ return nil, fmt.Errorf("error iterating bash scripts: %w", err)
+ }
+
+ return scripts, nil
+}
+
+// GetByGroup retrieves all bash scripts in a specific group
+func (r *BashScriptRepository) GetByGroup(group string) ([]*models.BashScript, error) {
+ rows, err := r.db.GetConnection().Query(
+ "SELECT id, name, description, content_encrypted, filename, group_name, created_at, updated_at FROM bash_scripts WHERE group_name = ? ORDER BY name ASC",
+ group,
)
if err != nil {
return nil, fmt.Errorf("failed to query bash scripts: %w", err)
@@ -118,7 +172,7 @@ func (r *BashScriptRepository) GetAll() ([]*models.BashScript, error) {
var encryptedContent []byte
var description, filename sql.NullString
- if err := rows.Scan(&script.ID, &script.Name, &description, &encryptedContent, &filename, &script.CreatedAt, &script.UpdatedAt); err != nil {
+ if err := rows.Scan(&script.ID, &script.Name, &description, &encryptedContent, &filename, &script.Group, &script.CreatedAt, &script.UpdatedAt); err != nil {
return nil, fmt.Errorf("failed to scan bash script: %w", err)
}
@@ -147,6 +201,32 @@ func (r *BashScriptRepository) GetAll() ([]*models.BashScript, error) {
return scripts, nil
}
+// GetGroups retrieves all distinct group names
+func (r *BashScriptRepository) GetGroups() ([]string, error) {
+ rows, err := r.db.GetConnection().Query(
+ "SELECT DISTINCT group_name FROM bash_scripts ORDER BY group_name ASC",
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to query groups: %w", err)
+ }
+ defer rows.Close()
+
+ var groups []string
+ for rows.Next() {
+ var group string
+ if err := rows.Scan(&group); err != nil {
+ return nil, fmt.Errorf("failed to scan group: %w", err)
+ }
+ groups = append(groups, group)
+ }
+
+ if err := rows.Err(); err != nil {
+ return nil, fmt.Errorf("error iterating groups: %w", err)
+ }
+
+ return groups, nil
+}
+
// Update updates an existing bash script
func (r *BashScriptRepository) Update(id int64, update *models.BashScriptUpdate) (*models.BashScript, error) {
// Get existing script
@@ -173,6 +253,10 @@ func (r *BashScriptRepository) Update(id int64, update *models.BashScriptUpdate)
existing.Filename = update.Filename
}
+ if update.Group != "" {
+ existing.Group = update.Group
+ }
+
existing.UpdatedAt = time.Now().UTC()
// Encrypt the content
@@ -182,11 +266,12 @@ func (r *BashScriptRepository) Update(id int64, update *models.BashScriptUpdate)
}
_, err = r.db.GetConnection().Exec(
- "UPDATE bash_scripts SET name = ?, description = ?, content_encrypted = ?, filename = ?, updated_at = ? WHERE id = ?",
+ "UPDATE bash_scripts SET name = ?, description = ?, content_encrypted = ?, filename = ?, group_name = ?, updated_at = ? WHERE id = ?",
existing.Name,
existing.Description,
encryptedContent,
existing.Filename,
+ existing.Group,
existing.UpdatedAt,
id,
)
@@ -223,9 +308,9 @@ func (r *BashScriptRepository) GetByName(name string) (*models.BashScript, error
var description, filename sql.NullString
err := r.db.GetConnection().QueryRow(
- "SELECT id, name, description, content_encrypted, filename, created_at, updated_at FROM bash_scripts WHERE name = ?",
+ "SELECT id, name, description, content_encrypted, filename, group_name, created_at, updated_at FROM bash_scripts WHERE name = ?",
name,
- ).Scan(&script.ID, &script.Name, &description, &encryptedContent, &filename, &script.CreatedAt, &script.UpdatedAt)
+ ).Scan(&script.ID, &script.Name, &description, &encryptedContent, &filename, &script.Group, &script.CreatedAt, &script.UpdatedAt)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("bash script not found")
diff --git a/internal/repository/env_variable_repository.go b/internal/repository/env_variable_repository.go
index 471ddbd..5b3fe0e 100644
--- a/internal/repository/env_variable_repository.go
+++ b/internal/repository/env_variable_repository.go
@@ -29,6 +29,12 @@ func (r *EnvVariableRepository) Create(envVar *models.EnvVariableCreate) (*model
return nil, fmt.Errorf("value is required")
}
+ // Default group to "default" if not provided
+ group := envVar.Group
+ if group == "" {
+ group = "default"
+ }
+
// Encrypt the value
encryptedValue, err := database.Encrypt(envVar.Value)
if err != nil {
@@ -38,10 +44,11 @@ func (r *EnvVariableRepository) Create(envVar *models.EnvVariableCreate) (*model
now := time.Now().UTC()
result, err := r.db.GetConnection().Exec(
- "INSERT INTO env_variables (name, value_encrypted, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
+ "INSERT INTO env_variables (name, value_encrypted, description, group_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
envVar.Name,
encryptedValue,
envVar.Description,
+ group,
now,
now,
)
@@ -59,6 +66,7 @@ func (r *EnvVariableRepository) Create(envVar *models.EnvVariableCreate) (*model
Name: envVar.Name,
Value: envVar.Value, // Return unencrypted value
Description: envVar.Description,
+ Group: group,
CreatedAt: now,
UpdatedAt: now,
}, nil
@@ -70,9 +78,9 @@ func (r *EnvVariableRepository) GetByID(id int64) (*models.EnvVariable, error) {
var encryptedValue []byte
err := r.db.GetConnection().QueryRow(
- "SELECT id, name, value_encrypted, description, created_at, updated_at FROM env_variables WHERE id = ?",
+ "SELECT id, name, value_encrypted, description, group_name, created_at, updated_at FROM env_variables WHERE id = ?",
id,
- ).Scan(&envVar.ID, &envVar.Name, &encryptedValue, &envVar.Description, &envVar.CreatedAt, &envVar.UpdatedAt)
+ ).Scan(&envVar.ID, &envVar.Name, &encryptedValue, &envVar.Description, &envVar.Group, &envVar.CreatedAt, &envVar.UpdatedAt)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("environment variable not found")
@@ -97,9 +105,9 @@ func (r *EnvVariableRepository) GetByName(name string) (*models.EnvVariable, err
var encryptedValue []byte
err := r.db.GetConnection().QueryRow(
- "SELECT id, name, value_encrypted, description, created_at, updated_at FROM env_variables WHERE name = ?",
+ "SELECT id, name, value_encrypted, description, group_name, created_at, updated_at FROM env_variables WHERE name = ?",
name,
- ).Scan(&envVar.ID, &envVar.Name, &encryptedValue, &envVar.Description, &envVar.CreatedAt, &envVar.UpdatedAt)
+ ).Scan(&envVar.ID, &envVar.Name, &encryptedValue, &envVar.Description, &envVar.Group, &envVar.CreatedAt, &envVar.UpdatedAt)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("environment variable not found")
@@ -121,7 +129,7 @@ func (r *EnvVariableRepository) GetByName(name string) (*models.EnvVariable, err
// GetAll retrieves all environment variables
func (r *EnvVariableRepository) GetAll() ([]*models.EnvVariable, error) {
rows, err := r.db.GetConnection().Query(
- "SELECT id, name, value_encrypted, description, created_at, updated_at FROM env_variables ORDER BY name ASC",
+ "SELECT id, name, value_encrypted, description, group_name, created_at, updated_at FROM env_variables ORDER BY group_name ASC, name ASC",
)
if err != nil {
return nil, fmt.Errorf("failed to query environment variables: %w", err)
@@ -133,7 +141,7 @@ func (r *EnvVariableRepository) GetAll() ([]*models.EnvVariable, error) {
var envVar models.EnvVariable
var encryptedValue []byte
- if err := rows.Scan(&envVar.ID, &envVar.Name, &encryptedValue, &envVar.Description, &envVar.CreatedAt, &envVar.UpdatedAt); err != nil {
+ if err := rows.Scan(&envVar.ID, &envVar.Name, &encryptedValue, &envVar.Description, &envVar.Group, &envVar.CreatedAt, &envVar.UpdatedAt); err != nil {
return nil, fmt.Errorf("failed to scan environment variable: %w", err)
}
@@ -154,6 +162,69 @@ func (r *EnvVariableRepository) GetAll() ([]*models.EnvVariable, error) {
return envVars, nil
}
+// GetByGroup retrieves all environment variables in a specific group
+func (r *EnvVariableRepository) GetByGroup(group string) ([]*models.EnvVariable, error) {
+ rows, err := r.db.GetConnection().Query(
+ "SELECT id, name, value_encrypted, description, group_name, created_at, updated_at FROM env_variables WHERE group_name = ? ORDER BY name ASC",
+ group,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to query environment variables: %w", err)
+ }
+ defer rows.Close()
+
+ var envVars []*models.EnvVariable
+ for rows.Next() {
+ var envVar models.EnvVariable
+ var encryptedValue []byte
+
+ if err := rows.Scan(&envVar.ID, &envVar.Name, &encryptedValue, &envVar.Description, &envVar.Group, &envVar.CreatedAt, &envVar.UpdatedAt); err != nil {
+ return nil, fmt.Errorf("failed to scan environment variable: %w", err)
+ }
+
+ // Decrypt the value
+ decryptedValue, err := database.Decrypt(encryptedValue)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decrypt value: %w", err)
+ }
+ envVar.Value = decryptedValue
+
+ envVars = append(envVars, &envVar)
+ }
+
+ if err := rows.Err(); err != nil {
+ return nil, fmt.Errorf("error iterating environment variables: %w", err)
+ }
+
+ return envVars, nil
+}
+
+// GetGroups retrieves all distinct group names
+func (r *EnvVariableRepository) GetGroups() ([]string, error) {
+ rows, err := r.db.GetConnection().Query(
+ "SELECT DISTINCT group_name FROM env_variables ORDER BY group_name ASC",
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to query groups: %w", err)
+ }
+ defer rows.Close()
+
+ var groups []string
+ for rows.Next() {
+ var group string
+ if err := rows.Scan(&group); err != nil {
+ return nil, fmt.Errorf("failed to scan group: %w", err)
+ }
+ groups = append(groups, group)
+ }
+
+ if err := rows.Err(); err != nil {
+ return nil, fmt.Errorf("error iterating groups: %w", err)
+ }
+
+ return groups, nil
+}
+
// Update updates an existing environment variable
func (r *EnvVariableRepository) Update(id int64, update *models.EnvVariableUpdate) (*models.EnvVariable, error) {
// Get existing variable
@@ -176,6 +247,10 @@ func (r *EnvVariableRepository) Update(id int64, update *models.EnvVariableUpdat
existing.Description = update.Description
}
+ if update.Group != "" {
+ existing.Group = update.Group
+ }
+
existing.UpdatedAt = time.Now().UTC()
// Encrypt the value
@@ -185,10 +260,11 @@ func (r *EnvVariableRepository) Update(id int64, update *models.EnvVariableUpdat
}
_, err = r.db.GetConnection().Exec(
- "UPDATE env_variables SET name = ?, value_encrypted = ?, description = ?, updated_at = ? WHERE id = ?",
+ "UPDATE env_variables SET name = ?, value_encrypted = ?, description = ?, group_name = ?, updated_at = ? WHERE id = ?",
existing.Name,
encryptedValue,
existing.Description,
+ existing.Group,
existing.UpdatedAt,
id,
)
diff --git a/internal/repository/server_repository.go b/internal/repository/server_repository.go
index e141a2f..4b8e4ae 100644
--- a/internal/repository/server_repository.go
+++ b/internal/repository/server_repository.go
@@ -38,14 +38,21 @@ func (r *ServerRepository) Create(server *models.ServerCreate) (*models.Server,
username = "root"
}
+ // Default group to "default" if not provided
+ group := server.Group
+ if group == "" {
+ group = "default"
+ }
+
now := time.Now().UTC()
result, err := r.db.GetConnection().Exec(
- "INSERT INTO servers (name, ip_address, port, username, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
+ "INSERT INTO servers (name, ip_address, port, username, group_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
nullString(server.Name),
nullString(server.IPAddress),
port,
username,
+ group,
now,
now,
)
@@ -64,6 +71,7 @@ func (r *ServerRepository) Create(server *models.ServerCreate) (*models.Server,
IPAddress: server.IPAddress,
Port: port,
Username: username,
+ Group: group,
CreatedAt: now,
UpdatedAt: now,
}, nil
@@ -75,9 +83,9 @@ func (r *ServerRepository) GetByID(id int64) (*models.Server, error) {
var name, ipAddress sql.NullString
err := r.db.GetConnection().QueryRow(
- "SELECT id, name, ip_address, port, username, created_at, updated_at FROM servers WHERE id = ?",
+ "SELECT id, name, ip_address, port, username, group_name, created_at, updated_at FROM servers WHERE id = ?",
id,
- ).Scan(&server.ID, &name, &ipAddress, &server.Port, &server.Username, &server.CreatedAt, &server.UpdatedAt)
+ ).Scan(&server.ID, &name, &ipAddress, &server.Port, &server.Username, &server.Group, &server.CreatedAt, &server.UpdatedAt)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("server not found")
@@ -95,7 +103,39 @@ func (r *ServerRepository) GetByID(id int64) (*models.Server, error) {
// GetAll retrieves all servers
func (r *ServerRepository) GetAll() ([]*models.Server, error) {
rows, err := r.db.GetConnection().Query(
- "SELECT id, name, ip_address, port, username, created_at, updated_at FROM servers ORDER BY created_at DESC",
+ "SELECT id, name, ip_address, port, username, group_name, created_at, updated_at FROM servers ORDER BY group_name ASC, created_at DESC",
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to query servers: %w", err)
+ }
+ defer rows.Close()
+
+ var servers []*models.Server
+ for rows.Next() {
+ var server models.Server
+ var name, ipAddress sql.NullString
+
+ if err := rows.Scan(&server.ID, &name, &ipAddress, &server.Port, &server.Username, &server.Group, &server.CreatedAt, &server.UpdatedAt); err != nil {
+ return nil, fmt.Errorf("failed to scan server: %w", err)
+ }
+
+ server.Name = name.String
+ server.IPAddress = ipAddress.String
+ servers = append(servers, &server)
+ }
+
+ if err := rows.Err(); err != nil {
+ return nil, fmt.Errorf("error iterating servers: %w", err)
+ }
+
+ return servers, nil
+}
+
+// GetByGroup retrieves all servers in a specific group
+func (r *ServerRepository) GetByGroup(group string) ([]*models.Server, error) {
+ rows, err := r.db.GetConnection().Query(
+ "SELECT id, name, ip_address, port, username, group_name, created_at, updated_at FROM servers WHERE group_name = ? ORDER BY created_at DESC",
+ group,
)
if err != nil {
return nil, fmt.Errorf("failed to query servers: %w", err)
@@ -107,7 +147,7 @@ func (r *ServerRepository) GetAll() ([]*models.Server, error) {
var server models.Server
var name, ipAddress sql.NullString
- if err := rows.Scan(&server.ID, &name, &ipAddress, &server.Port, &server.Username, &server.CreatedAt, &server.UpdatedAt); err != nil {
+ if err := rows.Scan(&server.ID, &name, &ipAddress, &server.Port, &server.Username, &server.Group, &server.CreatedAt, &server.UpdatedAt); err != nil {
return nil, fmt.Errorf("failed to scan server: %w", err)
}
@@ -123,6 +163,32 @@ func (r *ServerRepository) GetAll() ([]*models.Server, error) {
return servers, nil
}
+// GetGroups retrieves all distinct group names
+func (r *ServerRepository) GetGroups() ([]string, error) {
+ rows, err := r.db.GetConnection().Query(
+ "SELECT DISTINCT group_name FROM servers ORDER BY group_name ASC",
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to query groups: %w", err)
+ }
+ defer rows.Close()
+
+ var groups []string
+ for rows.Next() {
+ var group string
+ if err := rows.Scan(&group); err != nil {
+ return nil, fmt.Errorf("failed to scan group: %w", err)
+ }
+ groups = append(groups, group)
+ }
+
+ if err := rows.Err(); err != nil {
+ return nil, fmt.Errorf("error iterating groups: %w", err)
+ }
+
+ return groups, nil
+}
+
// Update updates an existing server
func (r *ServerRepository) Update(id int64, update *models.ServerUpdate) (*models.Server, error) {
// Get existing server
@@ -148,6 +214,10 @@ func (r *ServerRepository) Update(id int64, update *models.ServerUpdate) (*model
existing.Username = update.Username
}
+ if update.Group != "" {
+ existing.Group = update.Group
+ }
+
// Validate that at least one field is set after update
if existing.Name == "" && existing.IPAddress == "" {
return nil, fmt.Errorf("at least one of name or ip_address must be provided")
@@ -166,11 +236,12 @@ func (r *ServerRepository) Update(id int64, update *models.ServerUpdate) (*model
existing.UpdatedAt = time.Now().UTC()
_, err = r.db.GetConnection().Exec(
- "UPDATE servers SET name = ?, ip_address = ?, port = ?, username = ?, updated_at = ? WHERE id = ?",
+ "UPDATE servers SET name = ?, ip_address = ?, port = ?, username = ?, group_name = ?, updated_at = ? WHERE id = ?",
nullString(existing.Name),
nullString(existing.IPAddress),
existing.Port,
existing.Username,
+ existing.Group,
existing.UpdatedAt,
id,
)
diff --git a/internal/repository/ssh_key_repository.go b/internal/repository/ssh_key_repository.go
index b49c746..77f1096 100644
--- a/internal/repository/ssh_key_repository.go
+++ b/internal/repository/ssh_key_repository.go
@@ -27,12 +27,19 @@ func (r *SSHKeyRepository) Create(key *models.SSHKeyCreate) (*models.SSHKey, err
return nil, fmt.Errorf("failed to encrypt private key: %w", err)
}
+ // Default group to "default" if not provided
+ group := key.Group
+ if group == "" {
+ group = "default"
+ }
+
now := time.Now().UTC()
result, err := r.db.GetConnection().Exec(
- "INSERT INTO ssh_keys (name, private_key_encrypted, created_at, updated_at) VALUES (?, ?, ?, ?)",
+ "INSERT INTO ssh_keys (name, private_key_encrypted, group_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
key.Name,
encryptedKey,
+ group,
now,
now,
)
@@ -49,6 +56,7 @@ func (r *SSHKeyRepository) Create(key *models.SSHKeyCreate) (*models.SSHKey, err
ID: id,
Name: key.Name,
PrivateKey: key.PrivateKey,
+ Group: group,
CreatedAt: now,
UpdatedAt: now,
}, nil
@@ -60,9 +68,9 @@ func (r *SSHKeyRepository) GetByID(id int64) (*models.SSHKey, error) {
var encryptedKey []byte
err := r.db.GetConnection().QueryRow(
- "SELECT id, name, private_key_encrypted, created_at, updated_at FROM ssh_keys WHERE id = ?",
+ "SELECT id, name, private_key_encrypted, group_name, created_at, updated_at FROM ssh_keys WHERE id = ?",
id,
- ).Scan(&key.ID, &key.Name, &encryptedKey, &key.CreatedAt, &key.UpdatedAt)
+ ).Scan(&key.ID, &key.Name, &encryptedKey, &key.Group, &key.CreatedAt, &key.UpdatedAt)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("SSH key not found")
@@ -84,7 +92,7 @@ func (r *SSHKeyRepository) GetByID(id int64) (*models.SSHKey, error) {
// GetAll retrieves all SSH keys
func (r *SSHKeyRepository) GetAll() ([]*models.SSHKey, error) {
rows, err := r.db.GetConnection().Query(
- "SELECT id, name, private_key_encrypted, created_at, updated_at FROM ssh_keys ORDER BY created_at DESC",
+ "SELECT id, name, private_key_encrypted, group_name, created_at, updated_at FROM ssh_keys ORDER BY group_name ASC, created_at DESC",
)
if err != nil {
return nil, fmt.Errorf("failed to query SSH keys: %w", err)
@@ -96,7 +104,7 @@ func (r *SSHKeyRepository) GetAll() ([]*models.SSHKey, error) {
var key models.SSHKey
var encryptedKey []byte
- if err := rows.Scan(&key.ID, &key.Name, &encryptedKey, &key.CreatedAt, &key.UpdatedAt); err != nil {
+ if err := rows.Scan(&key.ID, &key.Name, &encryptedKey, &key.Group, &key.CreatedAt, &key.UpdatedAt); err != nil {
return nil, fmt.Errorf("failed to scan SSH key: %w", err)
}
@@ -117,6 +125,69 @@ func (r *SSHKeyRepository) GetAll() ([]*models.SSHKey, error) {
return keys, nil
}
+// GetByGroup retrieves all SSH keys in a specific group
+func (r *SSHKeyRepository) GetByGroup(group string) ([]*models.SSHKey, error) {
+ rows, err := r.db.GetConnection().Query(
+ "SELECT id, name, private_key_encrypted, group_name, created_at, updated_at FROM ssh_keys WHERE group_name = ? ORDER BY created_at DESC",
+ group,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to query SSH keys: %w", err)
+ }
+ defer rows.Close()
+
+ var keys []*models.SSHKey
+ for rows.Next() {
+ var key models.SSHKey
+ var encryptedKey []byte
+
+ if err := rows.Scan(&key.ID, &key.Name, &encryptedKey, &key.Group, &key.CreatedAt, &key.UpdatedAt); err != nil {
+ return nil, fmt.Errorf("failed to scan SSH key: %w", err)
+ }
+
+ // Decrypt the private key
+ decryptedKey, err := database.Decrypt(encryptedKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decrypt private key: %w", err)
+ }
+
+ key.PrivateKey = decryptedKey
+ keys = append(keys, &key)
+ }
+
+ if err := rows.Err(); err != nil {
+ return nil, fmt.Errorf("error iterating SSH keys: %w", err)
+ }
+
+ return keys, nil
+}
+
+// GetGroups retrieves all distinct group names
+func (r *SSHKeyRepository) GetGroups() ([]string, error) {
+ rows, err := r.db.GetConnection().Query(
+ "SELECT DISTINCT group_name FROM ssh_keys ORDER BY group_name ASC",
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to query groups: %w", err)
+ }
+ defer rows.Close()
+
+ var groups []string
+ for rows.Next() {
+ var group string
+ if err := rows.Scan(&group); err != nil {
+ return nil, fmt.Errorf("failed to scan group: %w", err)
+ }
+ groups = append(groups, group)
+ }
+
+ if err := rows.Err(); err != nil {
+ return nil, fmt.Errorf("error iterating groups: %w", err)
+ }
+
+ return groups, nil
+}
+
// Update updates an existing SSH key
func (r *SSHKeyRepository) Update(id int64, update *models.SSHKeyUpdate) (*models.SSHKey, error) {
// Get existing key
@@ -134,6 +205,10 @@ func (r *SSHKeyRepository) Update(id int64, update *models.SSHKeyUpdate) (*model
existing.PrivateKey = update.PrivateKey
}
+ if update.Group != "" {
+ existing.Group = update.Group
+ }
+
// Encrypt the private key
encryptedKey, err := database.Encrypt(existing.PrivateKey)
if err != nil {
@@ -143,9 +218,10 @@ func (r *SSHKeyRepository) Update(id int64, update *models.SSHKeyUpdate) (*model
existing.UpdatedAt = time.Now().UTC()
_, err = r.db.GetConnection().Exec(
- "UPDATE ssh_keys SET name = ?, private_key_encrypted = ?, updated_at = ? WHERE id = ?",
+ "UPDATE ssh_keys SET name = ?, private_key_encrypted = ?, group_name = ?, updated_at = ? WHERE id = ?",
existing.Name,
encryptedKey,
+ existing.Group,
existing.UpdatedAt,
id,
)
diff --git a/internal/repository/vault_config_repository.go b/internal/repository/vault_config_repository.go
new file mode 100644
index 0000000..6570ffa
--- /dev/null
+++ b/internal/repository/vault_config_repository.go
@@ -0,0 +1,192 @@
+package repository
+
+import (
+ "database/sql"
+ "fmt"
+ "time"
+
+ "github.com/pozgo/web-cli/internal/database"
+ "github.com/pozgo/web-cli/internal/models"
+)
+
+// VaultConfigRepository handles Vault configuration database operations
+type VaultConfigRepository struct {
+ db *sql.DB
+}
+
+// NewVaultConfigRepository creates a new VaultConfigRepository
+func NewVaultConfigRepository(db *database.DB) *VaultConfigRepository {
+ return &VaultConfigRepository{db: db.GetConnection()}
+}
+
+// Get retrieves the Vault configuration (there should only be one)
+func (r *VaultConfigRepository) Get() (*models.VaultConfig, error) {
+ query := `
+ SELECT id, address, token_encrypted, namespace, mount_path, enabled, created_at, updated_at
+ FROM vault_config
+ LIMIT 1
+ `
+
+ var cfg models.VaultConfig
+ var tokenEncrypted []byte
+ var namespace sql.NullString
+
+ err := r.db.QueryRow(query).Scan(
+ &cfg.ID,
+ &cfg.Address,
+ &tokenEncrypted,
+ &namespace,
+ &cfg.MountPath,
+ &cfg.Enabled,
+ &cfg.CreatedAt,
+ &cfg.UpdatedAt,
+ )
+
+ if err == sql.ErrNoRows {
+ return nil, nil // No configuration exists
+ }
+ if err != nil {
+ return nil, fmt.Errorf("failed to get vault config: %w", err)
+ }
+
+ // Decrypt token
+ if len(tokenEncrypted) > 0 {
+ decrypted, err := database.Decrypt(tokenEncrypted)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decrypt vault token: %w", err)
+ }
+ cfg.Token = decrypted
+ }
+
+ if namespace.Valid {
+ cfg.Namespace = namespace.String
+ }
+
+ return &cfg, nil
+}
+
+// CreateOrUpdate creates or updates the Vault configuration
+func (r *VaultConfigRepository) CreateOrUpdate(create *models.VaultConfigCreate) (*models.VaultConfig, error) {
+ // Check if config exists
+ existing, err := r.Get()
+ if err != nil {
+ return nil, err
+ }
+
+ // Encrypt token
+ tokenEncrypted, err := database.Encrypt(create.Token)
+ if err != nil {
+ return nil, fmt.Errorf("failed to encrypt vault token: %w", err)
+ }
+
+ mountPath := create.MountPath
+ if mountPath == "" {
+ mountPath = "secret"
+ }
+
+ now := time.Now().UTC()
+
+ if existing == nil {
+ // Create new config
+ query := `
+ INSERT INTO vault_config (address, token_encrypted, namespace, mount_path, enabled, created_at, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ `
+
+ result, err := r.db.Exec(query,
+ create.Address,
+ tokenEncrypted,
+ nullString(create.Namespace),
+ mountPath,
+ create.Enabled,
+ now,
+ now,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to create vault config: %w", err)
+ }
+
+ id, _ := result.LastInsertId()
+ return &models.VaultConfig{
+ ID: id,
+ Address: create.Address,
+ Token: create.Token,
+ Namespace: create.Namespace,
+ MountPath: mountPath,
+ Enabled: create.Enabled,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }, nil
+ }
+
+ // Update existing config
+ query := `
+ UPDATE vault_config
+ SET address = ?, token_encrypted = ?, namespace = ?, mount_path = ?, enabled = ?, updated_at = ?
+ WHERE id = ?
+ `
+
+ _, err = r.db.Exec(query,
+ create.Address,
+ tokenEncrypted,
+ nullString(create.Namespace),
+ mountPath,
+ create.Enabled,
+ now,
+ existing.ID,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to update vault config: %w", err)
+ }
+
+ return &models.VaultConfig{
+ ID: existing.ID,
+ Address: create.Address,
+ Token: create.Token,
+ Namespace: create.Namespace,
+ MountPath: mountPath,
+ Enabled: create.Enabled,
+ CreatedAt: existing.CreatedAt,
+ UpdatedAt: now,
+ }, nil
+}
+
+// UpdateToken updates only the Vault token
+func (r *VaultConfigRepository) UpdateToken(token string) error {
+ tokenEncrypted, err := database.Encrypt(token)
+ if err != nil {
+ return fmt.Errorf("failed to encrypt vault token: %w", err)
+ }
+
+ query := `UPDATE vault_config SET token_encrypted = ?, updated_at = ?`
+ _, err = r.db.Exec(query, tokenEncrypted, time.Now().UTC())
+ if err != nil {
+ return fmt.Errorf("failed to update vault token: %w", err)
+ }
+
+ return nil
+}
+
+// SetEnabled enables or disables Vault integration
+func (r *VaultConfigRepository) SetEnabled(enabled bool) error {
+ query := `UPDATE vault_config SET enabled = ?, updated_at = ?`
+ _, err := r.db.Exec(query, enabled, time.Now().UTC())
+ if err != nil {
+ return fmt.Errorf("failed to update vault enabled status: %w", err)
+ }
+
+ return nil
+}
+
+// Delete removes the Vault configuration
+func (r *VaultConfigRepository) Delete() error {
+ query := `DELETE FROM vault_config`
+ _, err := r.db.Exec(query)
+ if err != nil {
+ return fmt.Errorf("failed to delete vault config: %w", err)
+ }
+
+ return nil
+}
+
+// Note: nullString is defined in server_repository.go
diff --git a/internal/server/handlers.go b/internal/server/handlers.go
index 0dc3dd6..3c6e54c 100644
--- a/internal/server/handlers.go
+++ b/internal/server/handlers.go
@@ -12,6 +12,7 @@ import (
"strings"
"github.com/gorilla/mux"
+ "github.com/pozgo/web-cli/internal/audit"
"github.com/pozgo/web-cli/internal/executor"
"github.com/pozgo/web-cli/internal/models"
"github.com/pozgo/web-cli/internal/repository"
@@ -49,19 +50,42 @@ type CurrentUserResponse struct {
// @Success 200 {array} models.SSHKey
// @Failure 500 {object} ErrorResponse
// @Security BasicAuth
+// @Param group query string false "Filter by group name"
// @Router /keys [get]
func (s *Server) handleListSSHKeys(w http.ResponseWriter, r *http.Request) {
repo := repository.NewSSHKeyRepository(s.db)
+ group := r.URL.Query().Get("group")
- keys, err := repo.GetAll()
+ var keys []*models.SSHKey
+ var err error
+
+ if group != "" {
+ keys, err = repo.GetByGroup(group)
+ } else {
+ keys, err = repo.GetAll()
+ }
if err != nil {
log.Printf("Error fetching SSH keys: %v", err)
http.Error(w, "Failed to fetch SSH keys", http.StatusInternalServerError)
return
}
+ // Merge with Vault keys if available
+ allKeys := s.mergeSSHKeysWithVault(r.Context(), keys)
+
+ // Filter Vault keys by group if specified
+ if group != "" {
+ filtered := make([]*models.SSHKey, 0)
+ for _, k := range allKeys {
+ if k.Group == group || (k.Group == "" && group == "default") {
+ filtered = append(filtered, k)
+ }
+ }
+ allKeys = filtered
+ }
+
w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(keys)
+ json.NewEncoder(w).Encode(allKeys)
}
// handleCreateSSHKey godoc
@@ -235,19 +259,42 @@ func (s *Server) handleDeleteSSHKey(w http.ResponseWriter, r *http.Request) {
// @Success 200 {array} models.Server
// @Failure 500 {object} ErrorResponse
// @Security BasicAuth
+// @Param group query string false "Filter by group name"
// @Router /servers [get]
func (s *Server) handleListServers(w http.ResponseWriter, r *http.Request) {
repo := repository.NewServerRepository(s.db)
+ group := r.URL.Query().Get("group")
+
+ var servers []*models.Server
+ var err error
- servers, err := repo.GetAll()
+ if group != "" {
+ servers, err = repo.GetByGroup(group)
+ } else {
+ servers, err = repo.GetAll()
+ }
if err != nil {
log.Printf("Error fetching servers: %v", err)
http.Error(w, "Failed to fetch servers", http.StatusInternalServerError)
return
}
+ // Merge with Vault servers if available
+ allServers := s.mergeServersWithVault(r.Context(), servers)
+
+ // Filter Vault servers by group if specified
+ if group != "" {
+ filtered := make([]*models.Server, 0)
+ for _, srv := range allServers {
+ if srv.Group == group || (srv.Group == "" && group == "default") {
+ filtered = append(filtered, srv)
+ }
+ }
+ allServers = filtered
+ }
+
w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(servers)
+ json.NewEncoder(w).Encode(allServers)
}
// handleCreateServer godoc
@@ -479,31 +526,63 @@ func (s *Server) handleExecuteCommand(w http.ResponseWriter, r *http.Request) {
if exec.IsRemote {
// Remote execution via SSH
- if exec.ServerID == nil {
- http.Error(w, "Server ID is required for remote execution", http.StatusBadRequest)
- return
- }
+ var server *models.Server
+ var err error
- // Get server details
- serverRepo := repository.NewServerRepository(s.db)
- server, err := serverRepo.GetByID(*exec.ServerID)
- if err != nil {
- log.Printf("Error fetching server: %v", err)
- http.Error(w, "Server not found", http.StatusNotFound)
+ // Get server details - support both ID (SQLite) and Name (Vault)
+ if exec.ServerID != nil && *exec.ServerID > 0 {
+ serverRepo := repository.NewServerRepository(s.db)
+ server, err = serverRepo.GetByID(*exec.ServerID)
+ if err != nil {
+ log.Printf("Error fetching server by ID: %v", err)
+ http.Error(w, "Server not found", http.StatusNotFound)
+ return
+ }
+ } else if exec.ServerName != "" {
+ // Try to find server by name from Vault
+ server, err = s.getServerByNameFromVault(r.Context(), exec.ServerGroup, exec.ServerName)
+ if err != nil {
+ log.Printf("Error fetching server from Vault: %v", err)
+ http.Error(w, "Server not found in Vault", http.StatusNotFound)
+ return
+ }
+ if server == nil {
+ http.Error(w, "Server not found in Vault", http.StatusNotFound)
+ return
+ }
+ } else {
+ http.Error(w, "Server ID or Server Name is required for remote execution", http.StatusBadRequest)
return
}
- // Get SSH key if provided
+ // Get SSH key if provided - support both ID (SQLite) and Name (Vault)
var privateKey string
- if exec.SSHKeyID != nil {
+ if exec.SSHKeyID != nil && *exec.SSHKeyID > 0 {
keyRepo := repository.NewSSHKeyRepository(s.db)
key, err := keyRepo.GetByID(*exec.SSHKeyID)
if err != nil {
- log.Printf("Error fetching SSH key: %v", err)
+ log.Printf("Error fetching SSH key by ID: %v", err)
http.Error(w, "SSH key not found", http.StatusNotFound)
return
}
privateKey = key.PrivateKey
+ } else if exec.SSHKeyName != "" {
+ // Try to find SSH key by name from Vault
+ key, err := s.getSSHKeyByNameFromVault(r.Context(), exec.SSHKeyGroup, exec.SSHKeyName)
+ if err != nil {
+ log.Printf("Error fetching SSH key from Vault: %v", err)
+ http.Error(w, "SSH key not found in Vault", http.StatusNotFound)
+ return
+ }
+ if key == nil {
+ http.Error(w, fmt.Sprintf("SSH key '%s' not found in Vault", exec.SSHKeyName), http.StatusNotFound)
+ return
+ }
+ if key.PrivateKey == "" {
+ http.Error(w, fmt.Sprintf("SSH key '%s' has no private key data in Vault", exec.SSHKeyName), http.StatusBadRequest)
+ return
+ }
+ privateKey = key.PrivateKey
}
// Set server name for history
@@ -545,6 +624,9 @@ func (s *Server) handleExecuteCommand(w http.ResponseWriter, r *http.Request) {
// Don't fail the request, just log the error
}
+ // Audit log the command execution
+ audit.GetLogger().LogCommandExecution(r, exec.Command, exec.User, serverName, exitCode, result.ExecutionTime, result.Error)
+
// Save as template if requested
if exec.SaveAs != "" {
savedCmdRepo := repository.NewSavedCommandRepository(s.db)
@@ -974,7 +1056,7 @@ func (s *Server) handleUpdateLocalUser(w http.ResponseWriter, r *http.Request) {
user, err := repo.Update(id, &userUpdate)
if err != nil {
log.Printf("Error updating local user: %v", err)
- http.Error(w, err.Error(), http.StatusBadRequest)
+ http.Error(w, "Failed to update local user", http.StatusBadRequest)
return
}
@@ -1095,26 +1177,49 @@ func (s *Server) handleListAvailableShells(w http.ResponseWriter, r *http.Reques
// @Accept json
// @Produce json
// @Param show_values query bool false "Show actual values instead of masked values"
+// @Param group query string false "Filter by group name"
// @Success 200 {array} models.EnvVariableResponse
// @Failure 500 {object} ErrorResponse
// @Security BasicAuth
// @Router /env-variables [get]
func (s *Server) handleListEnvVariables(w http.ResponseWriter, r *http.Request) {
repo := repository.NewEnvVariableRepository(s.db)
+ group := r.URL.Query().Get("group")
+
+ var envVars []*models.EnvVariable
+ var err error
- envVars, err := repo.GetAll()
+ if group != "" {
+ envVars, err = repo.GetByGroup(group)
+ } else {
+ envVars, err = repo.GetAll()
+ }
if err != nil {
log.Printf("Error fetching environment variables: %v", err)
http.Error(w, "Failed to fetch environment variables", http.StatusInternalServerError)
return
}
+ // Merge with Vault env variables if available
+ allEnvVars := s.mergeEnvVariablesWithVault(r.Context(), envVars)
+
+ // Filter Vault env vars by group if specified
+ if group != "" {
+ filtered := make([]*models.EnvVariable, 0)
+ for _, ev := range allEnvVars {
+ if ev.Group == group || (ev.Group == "" && group == "default") {
+ filtered = append(filtered, ev)
+ }
+ }
+ allEnvVars = filtered
+ }
+
// Check if full values are requested (for internal use)
showValues := r.URL.Query().Get("show_values") == "true"
// Convert to response format with masked values
- responses := make([]*models.EnvVariableResponse, len(envVars))
- for i, envVar := range envVars {
+ responses := make([]*models.EnvVariableResponse, len(allEnvVars))
+ for i, envVar := range allEnvVars {
responses[i] = envVar.ToResponse(showValues)
}
@@ -1304,20 +1409,43 @@ func (s *Server) handleDeleteEnvVariable(w http.ResponseWriter, r *http.Request)
// @Tags Bash Scripts
// @Accept json
// @Produce json
+// @Param group query string false "Filter by group name"
// @Success 200 {array} models.BashScriptResponse
// @Failure 500 {object} ErrorResponse
// @Security BasicAuth
// @Router /bash-scripts [get]
func (s *Server) handleListBashScripts(w http.ResponseWriter, r *http.Request) {
repo := repository.NewBashScriptRepository(s.db)
+ group := r.URL.Query().Get("group")
- scripts, err := repo.GetAll()
+ var scripts []*models.BashScript
+ var err error
+
+ if group != "" {
+ scripts, err = repo.GetByGroup(group)
+ } else {
+ scripts, err = repo.GetAll()
+ }
if err != nil {
log.Printf("Error fetching bash scripts: %v", err)
http.Error(w, "Failed to fetch bash scripts", http.StatusInternalServerError)
return
}
+ // Merge with Vault scripts
+ scripts = s.mergeScriptsWithVault(r.Context(), scripts)
+
+ // Filter Vault scripts by group if specified
+ if group != "" {
+ filtered := make([]*models.BashScript, 0)
+ for _, s := range scripts {
+ if s.Group == group || (s.Group == "" && group == "default") {
+ filtered = append(filtered, s)
+ }
+ }
+ scripts = filtered
+ }
+
// Convert to response format (without content for listing)
responses := models.BashScriptsToList(scripts)
@@ -1533,9 +1661,9 @@ func (s *Server) handleExecuteScript(w http.ResponseWriter, r *http.Request) {
return
}
- // Validate input
- if exec.ScriptID == 0 {
- http.Error(w, "Script ID is required", http.StatusBadRequest)
+ // Validate input - either ScriptID or ScriptName must be provided
+ if exec.ScriptID == 0 && exec.ScriptName == "" {
+ http.Error(w, "Script ID or Script Name is required", http.StatusBadRequest)
return
}
@@ -1547,13 +1675,28 @@ func (s *Server) handleExecuteScript(w http.ResponseWriter, r *http.Request) {
return
}
- // Fetch the script
- scriptRepo := repository.NewBashScriptRepository(s.db)
- script, err := scriptRepo.GetByID(exec.ScriptID)
- if err != nil {
- log.Printf("Error fetching script: %v", err)
- http.Error(w, "Script not found", http.StatusNotFound)
- return
+ // Fetch the script - support both ID (SQLite) and Name (Vault)
+ var script *models.BashScript
+ var err error
+ if exec.ScriptID > 0 {
+ scriptRepo := repository.NewBashScriptRepository(s.db)
+ script, err = scriptRepo.GetByID(exec.ScriptID)
+ if err != nil {
+ log.Printf("Error fetching script by ID: %v", err)
+ http.Error(w, "Script not found", http.StatusNotFound)
+ return
+ }
+ } else if exec.ScriptName != "" {
+ script, err = s.getScriptByNameFromVault(r.Context(), exec.ScriptGroup, exec.ScriptName)
+ if err != nil {
+ log.Printf("Error fetching script from Vault: %v", err)
+ http.Error(w, "Script not found in Vault", http.StatusNotFound)
+ return
+ }
+ if script == nil {
+ http.Error(w, "Script not found in Vault", http.StatusNotFound)
+ return
+ }
}
// Build the script content with optional env vars
@@ -1564,8 +1707,8 @@ func (s *Server) handleExecuteScript(w http.ResponseWriter, r *http.Request) {
// Priority: EnvVarIDs (specific selection) > IncludeEnvVars (all) > none
envRepo := repository.NewEnvVariableRepository(s.db)
- if len(exec.EnvVarIDs) > 0 {
- // Fetch specific environment variables by ID
+ if len(exec.EnvVarIDs) > 0 || len(exec.EnvVarNames) > 0 {
+ // Fetch specific environment variables by ID (SQLite)
for _, envVarID := range exec.EnvVarIDs {
envVar, err := envRepo.GetByID(envVarID)
if err != nil {
@@ -1577,6 +1720,26 @@ func (s *Server) handleExecuteScript(w http.ResponseWriter, r *http.Request) {
scriptContent.WriteString(fmt.Sprintf("export %s='%s'\n", envVar.Name, escapedValue))
envVarsCount++
}
+ // Fetch specific environment variables by Name (Vault)
+ for i, envVarName := range exec.EnvVarNames {
+ // Get group from EnvVarGroups if available, otherwise use default
+ envVarGroup := "default"
+ if i < len(exec.EnvVarGroups) {
+ envVarGroup = exec.EnvVarGroups[i]
+ }
+ envVar, err := s.getEnvVariableByNameFromVault(r.Context(), envVarGroup, envVarName)
+ if err != nil {
+ log.Printf("Warning: env variable '%s' not found in Vault: %v", envVarName, err)
+ continue
+ }
+ if envVar == nil {
+ log.Printf("Warning: env variable '%s' not found in Vault", envVarName)
+ continue
+ }
+ escapedValue := strings.ReplaceAll(envVar.Value, "'", "'\\''")
+ scriptContent.WriteString(fmt.Sprintf("export %s='%s'\n", envVar.Name, escapedValue))
+ envVarsCount++
+ }
} else if exec.IncludeEnvVars {
// Backwards compatibility: fetch all environment variables
envVars, err := envRepo.GetAll()
@@ -1605,31 +1768,60 @@ func (s *Server) handleExecuteScript(w http.ResponseWriter, r *http.Request) {
if exec.IsRemote {
// Remote execution via SSH
- if exec.ServerID == nil {
- http.Error(w, "Server ID is required for remote execution", http.StatusBadRequest)
- return
- }
+ var server *models.Server
- // Get server details
- serverRepo := repository.NewServerRepository(s.db)
- server, err := serverRepo.GetByID(*exec.ServerID)
- if err != nil {
- log.Printf("Error fetching server: %v", err)
- http.Error(w, "Server not found", http.StatusNotFound)
+ // Get server details - support both ID (SQLite) and Name (Vault)
+ if exec.ServerID != nil && *exec.ServerID > 0 {
+ serverRepo := repository.NewServerRepository(s.db)
+ server, err = serverRepo.GetByID(*exec.ServerID)
+ if err != nil {
+ log.Printf("Error fetching server by ID: %v", err)
+ http.Error(w, "Server not found", http.StatusNotFound)
+ return
+ }
+ } else if exec.ServerName != "" {
+ server, err = s.getServerByNameFromVault(r.Context(), exec.ServerGroup, exec.ServerName)
+ if err != nil {
+ log.Printf("Error fetching server from Vault: %v", err)
+ http.Error(w, "Server not found in Vault", http.StatusNotFound)
+ return
+ }
+ if server == nil {
+ http.Error(w, "Server not found in Vault", http.StatusNotFound)
+ return
+ }
+ } else {
+ http.Error(w, "Server ID or Server Name is required for remote execution", http.StatusBadRequest)
return
}
- // Get SSH key if provided
+ // Get SSH key if provided - support both ID (SQLite) and Name (Vault)
var privateKey string
- if exec.SSHKeyID != nil {
+ if exec.SSHKeyID != nil && *exec.SSHKeyID > 0 {
keyRepo := repository.NewSSHKeyRepository(s.db)
key, err := keyRepo.GetByID(*exec.SSHKeyID)
if err != nil {
- log.Printf("Error fetching SSH key: %v", err)
+ log.Printf("Error fetching SSH key by ID: %v", err)
http.Error(w, "SSH key not found", http.StatusNotFound)
return
}
privateKey = key.PrivateKey
+ } else if exec.SSHKeyName != "" {
+ key, err := s.getSSHKeyByNameFromVault(r.Context(), exec.SSHKeyGroup, exec.SSHKeyName)
+ if err != nil {
+ log.Printf("Error fetching SSH key from Vault: %v", err)
+ http.Error(w, "SSH key not found in Vault", http.StatusNotFound)
+ return
+ }
+ if key == nil {
+ http.Error(w, fmt.Sprintf("SSH key '%s' not found in Vault", exec.SSHKeyName), http.StatusNotFound)
+ return
+ }
+ if key.PrivateKey == "" {
+ http.Error(w, fmt.Sprintf("SSH key '%s' has no private key data in Vault", exec.SSHKeyName), http.StatusBadRequest)
+ return
+ }
+ privateKey = key.PrivateKey
}
// Set server name for response
@@ -1658,7 +1850,7 @@ func (s *Server) handleExecuteScript(w http.ResponseWriter, r *http.Request) {
// Store in command history
exitCode := result.ExitCode
historyRepo := repository.NewCommandHistoryRepository(s.db)
- _, err = historyRepo.Create(&models.CommandHistoryCreate{
+ _, histErr := historyRepo.Create(&models.CommandHistoryCreate{
Command: fmt.Sprintf("[Script: %s] %s", script.Name, script.Content[:min(100, len(script.Content))]),
Output: result.Output,
ExitCode: &exitCode,
@@ -1666,10 +1858,13 @@ func (s *Server) handleExecuteScript(w http.ResponseWriter, r *http.Request) {
User: exec.User,
ExecutionTimeMs: result.ExecutionTime,
})
- if err != nil {
- log.Printf("Warning: failed to save command history: %v", err)
+ if histErr != nil {
+ log.Printf("Warning: failed to save command history: %v", histErr)
}
+ // Audit log the script execution
+ audit.GetLogger().LogScriptExecution(r, script.Name, exec.User, serverName, exitCode, result.ExecutionTime, result.Error)
+
// Return result - include error in output if present
scriptOutput := result.Output
if result.Error != nil && scriptOutput == "" {
@@ -1717,9 +1912,9 @@ func (s *Server) handleExecuteScriptStream(w http.ResponseWriter, r *http.Reques
return
}
- // Validate input
- if exec.ScriptID == 0 {
- http.Error(w, "Script ID is required", http.StatusBadRequest)
+ // Validate input - either ScriptID or ScriptName must be provided
+ if exec.ScriptID == 0 && exec.ScriptName == "" {
+ http.Error(w, "Script ID or Script Name is required", http.StatusBadRequest)
return
}
@@ -1731,13 +1926,28 @@ func (s *Server) handleExecuteScriptStream(w http.ResponseWriter, r *http.Reques
return
}
- // Fetch the script
- scriptRepo := repository.NewBashScriptRepository(s.db)
- script, err := scriptRepo.GetByID(exec.ScriptID)
- if err != nil {
- log.Printf("Error fetching script: %v", err)
- http.Error(w, "Script not found", http.StatusNotFound)
- return
+ // Fetch the script - support both ID (SQLite) and Name (Vault)
+ var script *models.BashScript
+ var err error
+ if exec.ScriptID > 0 {
+ scriptRepo := repository.NewBashScriptRepository(s.db)
+ script, err = scriptRepo.GetByID(exec.ScriptID)
+ if err != nil {
+ log.Printf("Error fetching script by ID: %v", err)
+ http.Error(w, "Script not found", http.StatusNotFound)
+ return
+ }
+ } else if exec.ScriptName != "" {
+ script, err = s.getScriptByNameFromVault(r.Context(), exec.ScriptGroup, exec.ScriptName)
+ if err != nil {
+ log.Printf("Error fetching script from Vault: %v", err)
+ http.Error(w, "Script not found in Vault", http.StatusNotFound)
+ return
+ }
+ if script == nil {
+ http.Error(w, "Script not found in Vault", http.StatusNotFound)
+ return
+ }
}
// Build the script content with optional env vars
@@ -1747,7 +1957,8 @@ func (s *Server) handleExecuteScriptStream(w http.ResponseWriter, r *http.Reques
// Determine which env vars to include
envRepo := repository.NewEnvVariableRepository(s.db)
- if len(exec.EnvVarIDs) > 0 {
+ if len(exec.EnvVarIDs) > 0 || len(exec.EnvVarNames) > 0 {
+ // Fetch specific environment variables by ID (SQLite)
for _, envVarID := range exec.EnvVarIDs {
envVar, err := envRepo.GetByID(envVarID)
if err != nil {
@@ -1758,6 +1969,26 @@ func (s *Server) handleExecuteScriptStream(w http.ResponseWriter, r *http.Reques
scriptContent.WriteString(fmt.Sprintf("export %s='%s'\n", envVar.Name, escapedValue))
envVarsCount++
}
+ // Fetch specific environment variables by Name (Vault)
+ for i, envVarName := range exec.EnvVarNames {
+ // Get group from EnvVarGroups if available, otherwise use default
+ envVarGroup := "default"
+ if i < len(exec.EnvVarGroups) {
+ envVarGroup = exec.EnvVarGroups[i]
+ }
+ envVar, err := s.getEnvVariableByNameFromVault(r.Context(), envVarGroup, envVarName)
+ if err != nil {
+ log.Printf("Warning: env variable '%s' not found in Vault: %v", envVarName, err)
+ continue
+ }
+ if envVar == nil {
+ log.Printf("Warning: env variable '%s' not found in Vault", envVarName)
+ continue
+ }
+ escapedValue := strings.ReplaceAll(envVar.Value, "'", "'\\''")
+ scriptContent.WriteString(fmt.Sprintf("export %s='%s'\n", envVar.Name, escapedValue))
+ envVarsCount++
+ }
} else if exec.IncludeEnvVars {
envVars, err := envRepo.GetAll()
if err != nil {
@@ -1796,29 +2027,60 @@ func (s *Server) handleExecuteScriptStream(w http.ResponseWriter, r *http.Reques
if exec.IsRemote {
// Remote execution via SSH with streaming
- if exec.ServerID == nil {
- sendSSE(w, flusher, "error", "Server ID is required for remote execution")
- return
- }
+ var server *models.Server
- serverRepo := repository.NewServerRepository(s.db)
- server, err := serverRepo.GetByID(*exec.ServerID)
- if err != nil {
- log.Printf("Error fetching server: %v", err)
- sendSSE(w, flusher, "error", "Server not found")
+ // Get server details - support both ID (SQLite) and Name (Vault)
+ if exec.ServerID != nil && *exec.ServerID > 0 {
+ serverRepo := repository.NewServerRepository(s.db)
+ server, err = serverRepo.GetByID(*exec.ServerID)
+ if err != nil {
+ log.Printf("Error fetching server by ID: %v", err)
+ sendSSE(w, flusher, "error", "Server not found")
+ return
+ }
+ } else if exec.ServerName != "" {
+ server, err = s.getServerByNameFromVault(r.Context(), exec.ServerGroup, exec.ServerName)
+ if err != nil {
+ log.Printf("Error fetching server from Vault: %v", err)
+ sendSSE(w, flusher, "error", "Server not found in Vault")
+ return
+ }
+ if server == nil {
+ sendSSE(w, flusher, "error", "Server not found in Vault")
+ return
+ }
+ } else {
+ sendSSE(w, flusher, "error", "Server ID or Server Name is required for remote execution")
return
}
+ // Get SSH key if provided - support both ID (SQLite) and Name (Vault)
var privateKey string
- if exec.SSHKeyID != nil {
+ if exec.SSHKeyID != nil && *exec.SSHKeyID > 0 {
keyRepo := repository.NewSSHKeyRepository(s.db)
key, err := keyRepo.GetByID(*exec.SSHKeyID)
if err != nil {
- log.Printf("Error fetching SSH key: %v", err)
+ log.Printf("Error fetching SSH key by ID: %v", err)
sendSSE(w, flusher, "error", "SSH key not found")
return
}
privateKey = key.PrivateKey
+ } else if exec.SSHKeyName != "" {
+ key, err := s.getSSHKeyByNameFromVault(r.Context(), exec.SSHKeyGroup, exec.SSHKeyName)
+ if err != nil {
+ log.Printf("Error fetching SSH key from Vault: %v", err)
+ sendSSE(w, flusher, "error", "SSH key not found in Vault")
+ return
+ }
+ if key == nil {
+ sendSSE(w, flusher, "error", fmt.Sprintf("SSH key '%s' not found in Vault", exec.SSHKeyName))
+ return
+ }
+ if key.PrivateKey == "" {
+ sendSSE(w, flusher, "error", fmt.Sprintf("SSH key '%s' has no private key data in Vault", exec.SSHKeyName))
+ return
+ }
+ privateKey = key.PrivateKey
}
if server.Name != "" {
@@ -1866,6 +2128,9 @@ func (s *Server) handleExecuteScriptStream(w http.ResponseWriter, r *http.Reques
log.Printf("Warning: failed to save command history: %v", err)
}
+ // Audit log the script execution
+ audit.GetLogger().LogScriptExecution(r, script.Name, exec.User, serverName, exitCode, result.ExecutionTime, result.Error)
+
// Send final result
scriptResult := models.ScriptResult{
ScriptID: script.ID,
@@ -1909,6 +2174,9 @@ func (s *Server) handleExecuteScriptStream(w http.ResponseWriter, r *http.Reques
log.Printf("Warning: failed to save command history: %v", err)
}
+ // Audit log the script execution
+ audit.GetLogger().LogScriptExecution(r, script.Name, exec.User, serverName, exitCode, result.ExecutionTime, result.Error)
+
// Send final result
scriptOutput := result.Output
if result.Error != nil && scriptOutput == "" {
@@ -2262,3 +2530,168 @@ func (s *Server) handleGetScriptPresetsByScript(w http.ResponseWriter, r *http.R
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(responses)
}
+
+// handleListSSHKeyGroups godoc
+// @Summary List all SSH key groups
+// @Description Get a list of all distinct group names for SSH keys
+// @Tags SSH Keys
+// @Accept json
+// @Produce json
+// @Success 200 {array} string
+// @Failure 500 {object} ErrorResponse
+// @Security BasicAuth
+// @Router /keys/groups [get]
+func (s *Server) handleListSSHKeyGroups(w http.ResponseWriter, r *http.Request) {
+ repo := repository.NewSSHKeyRepository(s.db)
+
+ groups, err := repo.GetGroups()
+ if err != nil {
+ log.Printf("Error fetching SSH key groups: %v", err)
+ http.Error(w, "Failed to fetch groups", http.StatusInternalServerError)
+ return
+ }
+
+ // Merge with Vault groups if available
+ client := s.getVaultClientIfEnabled()
+ if client != nil {
+ vaultGroups, err := client.ListSSHKeyGroups(r.Context())
+ if err == nil {
+ // Add Vault groups that don't exist in SQLite
+ groupSet := make(map[string]bool)
+ for _, g := range groups {
+ groupSet[g] = true
+ }
+ for _, g := range vaultGroups {
+ if !groupSet[g] {
+ groups = append(groups, g)
+ }
+ }
+ }
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(groups)
+}
+
+// handleListServerGroups godoc
+// @Summary List all server groups
+// @Description Get a list of all distinct group names for servers
+// @Tags Servers
+// @Accept json
+// @Produce json
+// @Success 200 {array} string
+// @Failure 500 {object} ErrorResponse
+// @Security BasicAuth
+// @Router /servers/groups [get]
+func (s *Server) handleListServerGroups(w http.ResponseWriter, r *http.Request) {
+ repo := repository.NewServerRepository(s.db)
+
+ groups, err := repo.GetGroups()
+ if err != nil {
+ log.Printf("Error fetching server groups: %v", err)
+ http.Error(w, "Failed to fetch groups", http.StatusInternalServerError)
+ return
+ }
+
+ // Merge with Vault groups if available
+ client := s.getVaultClientIfEnabled()
+ if client != nil {
+ vaultGroups, err := client.ListServerGroups(r.Context())
+ if err == nil {
+ groupSet := make(map[string]bool)
+ for _, g := range groups {
+ groupSet[g] = true
+ }
+ for _, g := range vaultGroups {
+ if !groupSet[g] {
+ groups = append(groups, g)
+ }
+ }
+ }
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(groups)
+}
+
+// handleListEnvVariableGroups godoc
+// @Summary List all environment variable groups
+// @Description Get a list of all distinct group names for environment variables
+// @Tags Environment Variables
+// @Accept json
+// @Produce json
+// @Success 200 {array} string
+// @Failure 500 {object} ErrorResponse
+// @Security BasicAuth
+// @Router /env-variables/groups [get]
+func (s *Server) handleListEnvVariableGroups(w http.ResponseWriter, r *http.Request) {
+ repo := repository.NewEnvVariableRepository(s.db)
+
+ groups, err := repo.GetGroups()
+ if err != nil {
+ log.Printf("Error fetching environment variable groups: %v", err)
+ http.Error(w, "Failed to fetch groups", http.StatusInternalServerError)
+ return
+ }
+
+ // Merge with Vault groups if available
+ client := s.getVaultClientIfEnabled()
+ if client != nil {
+ vaultGroups, err := client.ListEnvVariableGroups(r.Context())
+ if err == nil {
+ groupSet := make(map[string]bool)
+ for _, g := range groups {
+ groupSet[g] = true
+ }
+ for _, g := range vaultGroups {
+ if !groupSet[g] {
+ groups = append(groups, g)
+ }
+ }
+ }
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(groups)
+}
+
+// handleListBashScriptGroups godoc
+// @Summary List all bash script groups
+// @Description Get a list of all distinct group names for bash scripts
+// @Tags Bash Scripts
+// @Accept json
+// @Produce json
+// @Success 200 {array} string
+// @Failure 500 {object} ErrorResponse
+// @Security BasicAuth
+// @Router /bash-scripts/groups [get]
+func (s *Server) handleListBashScriptGroups(w http.ResponseWriter, r *http.Request) {
+ repo := repository.NewBashScriptRepository(s.db)
+
+ groups, err := repo.GetGroups()
+ if err != nil {
+ log.Printf("Error fetching bash script groups: %v", err)
+ http.Error(w, "Failed to fetch groups", http.StatusInternalServerError)
+ return
+ }
+
+ // Merge with Vault groups if available
+ client := s.getVaultClientIfEnabled()
+ if client != nil {
+ vaultGroups, err := client.ListBashScriptGroups(r.Context())
+ if err == nil {
+ groupSet := make(map[string]bool)
+ for _, g := range groups {
+ groupSet[g] = true
+ }
+ for _, g := range vaultGroups {
+ if !groupSet[g] {
+ groups = append(groups, g)
+ }
+ }
+ }
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(groups)
+}
diff --git a/internal/server/handlers_terminal.go b/internal/server/handlers_terminal.go
index 892ca01..98db977 100644
--- a/internal/server/handlers_terminal.go
+++ b/internal/server/handlers_terminal.go
@@ -1,9 +1,11 @@
package server
import (
+ "context"
"log"
"net/http"
"strconv"
+ "time"
"github.com/gorilla/websocket"
"github.com/pozgo/web-cli/internal/repository"
@@ -46,13 +48,47 @@ func (s *Server) handleTerminalWebSocket(w http.ResponseWriter, r *http.Request)
// Check if SSH key is requested
var sshPrivateKey string
- if sshKeyIDStr := r.URL.Query().Get("sshKeyId"); sshKeyIDStr != "" {
- sshKeyID, err := strconv.ParseInt(sshKeyIDStr, 10, 64)
- if err == nil {
- repo := repository.NewSSHKeyRepository(s.db)
- key, err := repo.GetByID(sshKeyID)
+ sshKeyID := r.URL.Query().Get("sshKeyId")
+ sshKeySource := r.URL.Query().Get("sshKeySource")
+
+ if sshKeyID != "" {
+ if sshKeySource == "vault" {
+ // Fetch SSH key from Vault by name
+ client, err := s.getVaultClient()
+ if err == nil {
+ ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
+ defer cancel()
+ // Try to get key from default group first, then try without group
+ key, err := client.GetSSHKey(ctx, "default", sshKeyID)
+ if err != nil || key == nil {
+ // Try listing all groups and searching
+ groups, _ := client.ListSSHKeyGroups(ctx)
+ for _, group := range groups {
+ key, err = client.GetSSHKey(ctx, group, sshKeyID)
+ if err == nil && key != nil {
+ break
+ }
+ }
+ }
+ if key != nil {
+ sshPrivateKey = key.PrivateKey
+ log.Printf("Loaded SSH key '%s' from Vault", sshKeyID)
+ } else {
+ log.Printf("Failed to find SSH key '%s' in Vault", sshKeyID)
+ }
+ } else {
+ log.Printf("Failed to get Vault client for SSH key: %v", err)
+ }
+ } else {
+ // Fetch SSH key from local database by ID
+ keyID, err := strconv.ParseInt(sshKeyID, 10, 64)
if err == nil {
- sshPrivateKey = key.PrivateKey
+ repo := repository.NewSSHKeyRepository(s.db)
+ key, err := repo.GetByID(keyID)
+ if err == nil {
+ sshPrivateKey = key.PrivateKey
+ log.Printf("Loaded SSH key ID %d from local database", keyID)
+ }
}
}
}
diff --git a/internal/server/handlers_vault.go b/internal/server/handlers_vault.go
new file mode 100644
index 0000000..d7a3698
--- /dev/null
+++ b/internal/server/handlers_vault.go
@@ -0,0 +1,815 @@
+package server
+
+import (
+ "context"
+ "encoding/json"
+ "log"
+ "net/http"
+ "time"
+
+ "github.com/pozgo/web-cli/internal/models"
+ "github.com/pozgo/web-cli/internal/repository"
+ "github.com/pozgo/web-cli/internal/vault"
+)
+
+// handleGetVaultConfig godoc
+// @Summary Get Vault configuration
+// @Description Retrieve the current Vault configuration (token is never returned)
+// @Tags Vault
+// @Produce json
+// @Success 200 {object} models.VaultConfigResponse
+// @Failure 500 {object} ErrorResponse
+// @Security BasicAuth
+// @Router /vault/config [get]
+func (s *Server) handleGetVaultConfig(w http.ResponseWriter, r *http.Request) {
+ repo := repository.NewVaultConfigRepository(s.db)
+ cfg, err := repo.Get()
+ if err != nil {
+ log.Printf("Error getting vault config: %v", err)
+ http.Error(w, "Failed to get vault configuration", http.StatusInternalServerError)
+ return
+ }
+
+ if cfg == nil {
+ // Return empty response if not configured
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode(models.VaultConfigResponse{})
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(cfg.ToResponse())
+}
+
+// handleCreateOrUpdateVaultConfig godoc
+// @Summary Create or update Vault configuration
+// @Description Configure or update the Vault connection settings
+// @Tags Vault
+// @Accept json
+// @Produce json
+// @Param config body models.VaultConfigCreate true "Vault configuration"
+// @Success 200 {object} models.VaultConfigResponse
+// @Failure 400 {object} ErrorResponse
+// @Failure 500 {object} ErrorResponse
+// @Security BasicAuth
+// @Router /vault/config [post]
+func (s *Server) handleCreateOrUpdateVaultConfig(w http.ResponseWriter, r *http.Request) {
+ var create models.VaultConfigCreate
+ if err := json.NewDecoder(r.Body).Decode(&create); err != nil {
+ http.Error(w, "Invalid request body", http.StatusBadRequest)
+ return
+ }
+
+ // Validate required fields
+ if create.Address == "" {
+ http.Error(w, "Vault address is required", http.StatusBadRequest)
+ return
+ }
+
+ if create.Token == "" {
+ // If no token provided, check if we're updating existing config
+ repo := repository.NewVaultConfigRepository(s.db)
+ existing, _ := repo.Get()
+ if existing == nil || existing.Token == "" {
+ http.Error(w, "Vault token is required", http.StatusBadRequest)
+ return
+ }
+ // Use existing token
+ create.Token = existing.Token
+ }
+
+ // Set default mount path
+ if create.MountPath == "" {
+ create.MountPath = "secret"
+ }
+
+ repo := repository.NewVaultConfigRepository(s.db)
+ cfg, err := repo.CreateOrUpdate(&create)
+ if err != nil {
+ log.Printf("Error saving vault config: %v", err)
+ http.Error(w, "Failed to save vault configuration", http.StatusInternalServerError)
+ return
+ }
+
+ // If Vault is enabled, initialize the structure automatically
+ if cfg.Enabled {
+ go func() {
+ vaultCfg := &vault.Config{
+ Address: cfg.Address,
+ Token: cfg.Token,
+ Namespace: cfg.Namespace,
+ MountPath: cfg.MountPath,
+ }
+
+ client, err := vault.NewClient(vaultCfg)
+ if err != nil {
+ log.Printf("Warning: Failed to create Vault client for structure initialization: %v", err)
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+
+ if err := client.InitializeStructure(ctx); err != nil {
+ log.Printf("Warning: Failed to initialize Vault structure: %v", err)
+ }
+ }()
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode(cfg.ToResponse())
+}
+
+// handleDeleteVaultConfig godoc
+// @Summary Delete Vault configuration
+// @Description Remove the Vault configuration
+// @Tags Vault
+// @Success 204 "No Content"
+// @Failure 500 {object} ErrorResponse
+// @Security BasicAuth
+// @Router /vault/config [delete]
+func (s *Server) handleDeleteVaultConfig(w http.ResponseWriter, r *http.Request) {
+ repo := repository.NewVaultConfigRepository(s.db)
+ if err := repo.Delete(); err != nil {
+ log.Printf("Error deleting vault config: %v", err)
+ http.Error(w, "Failed to delete vault configuration", http.StatusInternalServerError)
+ return
+ }
+
+ w.WriteHeader(http.StatusNoContent)
+}
+
+// handleTestVaultConnection godoc
+// @Summary Test Vault connection
+// @Description Test the connection to Vault and initialize the secrets structure
+// @Tags Vault
+// @Produce json
+// @Success 200 {object} models.VaultStatus
+// @Failure 400 {object} ErrorResponse
+// @Failure 500 {object} ErrorResponse
+// @Security BasicAuth
+// @Router /vault/test [post]
+func (s *Server) handleTestVaultConnection(w http.ResponseWriter, r *http.Request) {
+ repo := repository.NewVaultConfigRepository(s.db)
+ cfg, err := repo.Get()
+ if err != nil {
+ log.Printf("Error getting vault config: %v", err)
+ http.Error(w, "Failed to get vault configuration", http.StatusInternalServerError)
+ return
+ }
+
+ if cfg == nil {
+ http.Error(w, "Vault is not configured", http.StatusBadRequest)
+ return
+ }
+
+ // Create Vault client
+ vaultCfg := &vault.Config{
+ Address: cfg.Address,
+ Token: cfg.Token,
+ Namespace: cfg.Namespace,
+ MountPath: cfg.MountPath,
+ }
+
+ client, err := vault.NewClient(vaultCfg)
+ if err != nil {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode(models.VaultStatus{
+ Configured: true,
+ Enabled: cfg.Enabled,
+ Connected: false,
+ Address: cfg.Address,
+ Error: err.Error(),
+ })
+ return
+ }
+
+ // Test connection with timeout
+ ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
+ defer cancel()
+
+ if err := client.TestConnection(ctx); err != nil {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode(models.VaultStatus{
+ Configured: true,
+ Enabled: cfg.Enabled,
+ Connected: false,
+ Address: cfg.Address,
+ Error: err.Error(),
+ })
+ return
+ }
+
+ // Connection successful - initialize structure in background
+ go func() {
+ initCtx, initCancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer initCancel()
+
+ if err := client.InitializeStructure(initCtx); err != nil {
+ log.Printf("Warning: Failed to initialize Vault structure: %v", err)
+ }
+ }()
+
+ // Check health status
+ var vaultSealed bool
+ health, err := client.GetHealth(ctx)
+ if err == nil && health != nil {
+ vaultSealed = health.Sealed
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(models.VaultStatus{
+ Configured: true,
+ Enabled: cfg.Enabled,
+ Connected: true,
+ Address: cfg.Address,
+ VaultSealed: vaultSealed,
+ })
+}
+
+// handleGetVaultStatus godoc
+// @Summary Get Vault connection status
+// @Description Check the current Vault connection status
+// @Tags Vault
+// @Produce json
+// @Success 200 {object} models.VaultStatus
+// @Failure 500 {object} ErrorResponse
+// @Security BasicAuth
+// @Router /vault/status [get]
+func (s *Server) handleGetVaultStatus(w http.ResponseWriter, r *http.Request) {
+ repo := repository.NewVaultConfigRepository(s.db)
+ cfg, err := repo.Get()
+ if err != nil {
+ log.Printf("Error getting vault config: %v", err)
+ http.Error(w, "Failed to get vault configuration", http.StatusInternalServerError)
+ return
+ }
+
+ if cfg == nil {
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(models.VaultStatus{
+ Configured: false,
+ Enabled: false,
+ Connected: false,
+ })
+ return
+ }
+
+ status := models.VaultStatus{
+ Configured: true,
+ Enabled: cfg.Enabled,
+ Connected: false,
+ Address: cfg.Address,
+ }
+
+ // Only test connection if enabled
+ if cfg.Enabled {
+ vaultCfg := &vault.Config{
+ Address: cfg.Address,
+ Token: cfg.Token,
+ Namespace: cfg.Namespace,
+ MountPath: cfg.MountPath,
+ }
+
+ client, err := vault.NewClient(vaultCfg)
+ if err != nil {
+ status.Error = err.Error()
+ } else {
+ ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
+ defer cancel()
+
+ if err := client.TestConnection(ctx); err != nil {
+ status.Error = err.Error()
+ } else {
+ status.Connected = true
+
+ // Check health
+ health, err := client.GetHealth(ctx)
+ if err == nil && health != nil {
+ status.VaultSealed = health.Sealed
+ }
+ }
+ }
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(status)
+}
+
+// handleListVaultSSHKeys godoc
+// @Summary List SSH keys from Vault
+// @Description Retrieve all SSH keys stored in Vault
+// @Tags Vault
+// @Produce json
+// @Success 200 {array} vault.SSHKey
+// @Failure 400 {object} ErrorResponse
+// @Failure 500 {object} ErrorResponse
+// @Security BasicAuth
+// @Router /vault/ssh-keys [get]
+func (s *Server) handleListVaultSSHKeys(w http.ResponseWriter, r *http.Request) {
+ client, err := s.getVaultClient()
+ if err != nil {
+ log.Printf("Error getting vault client: %v", err)
+ http.Error(w, sanitizeVaultError(err), http.StatusBadRequest)
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
+ defer cancel()
+
+ keys, err := client.ListSSHKeys(ctx)
+ if err != nil {
+ log.Printf("Error listing vault SSH keys: %v", err)
+ http.Error(w, "Failed to list SSH keys from Vault", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(keys)
+}
+
+// handleListVaultServers godoc
+// @Summary List servers from Vault
+// @Description Retrieve all server configurations stored in Vault
+// @Tags Vault
+// @Produce json
+// @Success 200 {array} vault.Server
+// @Failure 400 {object} ErrorResponse
+// @Failure 500 {object} ErrorResponse
+// @Security BasicAuth
+// @Router /vault/servers [get]
+func (s *Server) handleListVaultServers(w http.ResponseWriter, r *http.Request) {
+ client, err := s.getVaultClient()
+ if err != nil {
+ http.Error(w, sanitizeVaultError(err), http.StatusBadRequest)
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
+ defer cancel()
+
+ servers, err := client.ListServers(ctx)
+ if err != nil {
+ log.Printf("Error listing vault servers: %v", err)
+ http.Error(w, "Failed to list servers from Vault", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(servers)
+}
+
+// handleListVaultEnvVariables godoc
+// @Summary List environment variables from Vault
+// @Description Retrieve all environment variables stored in Vault
+// @Tags Vault
+// @Produce json
+// @Success 200 {array} vault.EnvVariable
+// @Failure 400 {object} ErrorResponse
+// @Failure 500 {object} ErrorResponse
+// @Security BasicAuth
+// @Router /vault/env-variables [get]
+func (s *Server) handleListVaultEnvVariables(w http.ResponseWriter, r *http.Request) {
+ client, err := s.getVaultClient()
+ if err != nil {
+ http.Error(w, sanitizeVaultError(err), http.StatusBadRequest)
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
+ defer cancel()
+
+ vars, err := client.ListEnvVariables(ctx)
+ if err != nil {
+ log.Printf("Error listing vault env variables: %v", err)
+ http.Error(w, "Failed to list environment variables from Vault", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(vars)
+}
+
+// handleListVaultScripts godoc
+// @Summary List bash scripts from Vault
+// @Description Retrieve all bash scripts stored in Vault
+// @Tags Vault
+// @Produce json
+// @Success 200 {array} vault.BashScript
+// @Failure 400 {object} ErrorResponse
+// @Failure 500 {object} ErrorResponse
+// @Security BasicAuth
+// @Router /vault/bash-scripts [get]
+func (s *Server) handleListVaultScripts(w http.ResponseWriter, r *http.Request) {
+ client, err := s.getVaultClient()
+ if err != nil {
+ http.Error(w, sanitizeVaultError(err), http.StatusBadRequest)
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
+ defer cancel()
+
+ scripts, err := client.ListBashScripts(ctx)
+ if err != nil {
+ log.Printf("Error listing vault scripts: %v", err)
+ http.Error(w, "Failed to list scripts from Vault", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(scripts)
+}
+
+// getVaultClient creates a Vault client from stored configuration
+func (s *Server) getVaultClient() (*vault.Client, error) {
+ repo := repository.NewVaultConfigRepository(s.db)
+ cfg, err := repo.Get()
+ if err != nil {
+ return nil, err
+ }
+
+ if cfg == nil {
+ return nil, &VaultNotConfiguredError{}
+ }
+
+ if !cfg.Enabled {
+ return nil, &VaultDisabledError{}
+ }
+
+ vaultCfg := &vault.Config{
+ Address: cfg.Address,
+ Token: cfg.Token,
+ Namespace: cfg.Namespace,
+ MountPath: cfg.MountPath,
+ }
+
+ return vault.NewClient(vaultCfg)
+}
+
+// VaultNotConfiguredError is returned when Vault is not configured
+type VaultNotConfiguredError struct{}
+
+func (e *VaultNotConfiguredError) Error() string {
+ return "Vault is not configured"
+}
+
+// VaultDisabledError is returned when Vault is disabled
+type VaultDisabledError struct{}
+
+func (e *VaultDisabledError) Error() string {
+ return "Vault integration is disabled"
+}
+
+// sanitizeVaultError returns a safe error message for API responses
+// This prevents leaking internal details like IP addresses, paths, or stack traces
+func sanitizeVaultError(err error) string {
+ if err == nil {
+ return "Unknown error"
+ }
+
+ // Check for known safe error types
+ switch err.(type) {
+ case *VaultNotConfiguredError:
+ return err.Error()
+ case *VaultDisabledError:
+ return err.Error()
+ }
+
+ // Check error message for safe patterns
+ errMsg := err.Error()
+
+ // Safe messages that can be returned directly
+ safeMessages := []string{
+ "Vault is not configured",
+ "Vault integration is disabled",
+ "vault address is required",
+ "vault token is required",
+ "vault config is nil",
+ }
+
+ for _, safe := range safeMessages {
+ if errMsg == safe {
+ return errMsg
+ }
+ }
+
+ // For validation errors, return them (they don't contain sensitive info)
+ if containsAny(errMsg, []string{
+ "invalid secret type",
+ "invalid group name",
+ "invalid secret name",
+ "invalid vault address",
+ "cannot be empty",
+ "cannot contain",
+ "too long",
+ }) {
+ return errMsg
+ }
+
+ // Generic message for all other errors to prevent information leakage
+ return "Vault operation failed"
+}
+
+// containsAny checks if the string contains any of the substrings
+func containsAny(s string, substrs []string) bool {
+ for _, substr := range substrs {
+ if len(s) >= len(substr) {
+ for i := 0; i <= len(s)-len(substr); i++ {
+ if s[i:i+len(substr)] == substr {
+ return true
+ }
+ }
+ }
+ }
+ return false
+}
+
+// handleCreateVaultSSHKey godoc
+// @Summary Create SSH key in Vault
+// @Description Store a new SSH key in Vault
+// @Tags Vault
+// @Accept json
+// @Produce json
+// @Param key body object{name=string,private_key=string,group=string} true "SSH Key"
+// @Success 201 {object} object{name=string,group=string,created_at=string,source=string}
+// @Failure 400 {object} ErrorResponse
+// @Failure 500 {object} ErrorResponse
+// @Security BasicAuth
+// @Router /vault/ssh-keys [post]
+func (s *Server) handleCreateVaultSSHKey(w http.ResponseWriter, r *http.Request) {
+ client, err := s.getVaultClient()
+ if err != nil {
+ http.Error(w, sanitizeVaultError(err), http.StatusBadRequest)
+ return
+ }
+
+ var req struct {
+ Name string `json:"name"`
+ PrivateKey string `json:"private_key"`
+ Group string `json:"group"`
+ }
+
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, "Invalid request body", http.StatusBadRequest)
+ return
+ }
+
+ if req.Name == "" || req.PrivateKey == "" {
+ http.Error(w, "Name and private_key are required", http.StatusBadRequest)
+ return
+ }
+
+ if req.Group == "" {
+ req.Group = "default"
+ }
+
+ ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
+ defer cancel()
+
+ key := &vault.SSHKey{
+ Name: req.Name,
+ PrivateKey: req.PrivateKey,
+ Group: req.Group,
+ CreatedAt: time.Now(),
+ }
+
+ if err := client.SaveSSHKey(ctx, key); err != nil {
+ log.Printf("Error saving SSH key to Vault: %v", err)
+ http.Error(w, "Failed to save SSH key to Vault", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "name": key.Name,
+ "group": key.Group,
+ "created_at": key.CreatedAt,
+ "source": "vault",
+ })
+}
+
+// handleCreateVaultServer godoc
+// @Summary Create server in Vault
+// @Description Store a new server configuration in Vault
+// @Tags Vault
+// @Accept json
+// @Produce json
+// @Param server body object{name=string,ip_address=string,port=int,username=string,group=string} true "Server"
+// @Success 201 {object} object{name=string,ip_address=string,port=int,username=string,group=string,source=string}
+// @Failure 400 {object} ErrorResponse
+// @Failure 500 {object} ErrorResponse
+// @Security BasicAuth
+// @Router /vault/servers [post]
+func (s *Server) handleCreateVaultServer(w http.ResponseWriter, r *http.Request) {
+ client, err := s.getVaultClient()
+ if err != nil {
+ http.Error(w, sanitizeVaultError(err), http.StatusBadRequest)
+ return
+ }
+
+ var req struct {
+ Name string `json:"name"`
+ IPAddress string `json:"ip_address"`
+ Port int `json:"port"`
+ Username string `json:"username"`
+ Group string `json:"group"`
+ }
+
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, "Invalid request body", http.StatusBadRequest)
+ return
+ }
+
+ // At least one of name or ip_address is required
+ if req.Name == "" && req.IPAddress == "" {
+ http.Error(w, "At least one of name or ip_address is required", http.StatusBadRequest)
+ return
+ }
+
+ // Use name as identifier, fallback to ip_address
+ identifier := req.Name
+ if identifier == "" {
+ identifier = req.IPAddress
+ }
+
+ if req.Group == "" {
+ req.Group = "default"
+ }
+
+ if req.Port == 0 {
+ req.Port = 22
+ }
+
+ if req.Username == "" {
+ req.Username = "root"
+ }
+
+ ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
+ defer cancel()
+
+ srv := &vault.Server{
+ Name: identifier,
+ IPAddress: req.IPAddress,
+ Port: req.Port,
+ Username: req.Username,
+ Group: req.Group,
+ }
+
+ if err := client.SaveServer(ctx, srv); err != nil {
+ log.Printf("Error saving server to Vault: %v", err)
+ http.Error(w, "Failed to save server to Vault", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "name": srv.Name,
+ "ip_address": srv.IPAddress,
+ "port": srv.Port,
+ "username": srv.Username,
+ "group": srv.Group,
+ "source": "vault",
+ })
+}
+
+// handleCreateVaultEnvVariable godoc
+// @Summary Create environment variable in Vault
+// @Description Store a new environment variable in Vault
+// @Tags Vault
+// @Accept json
+// @Produce json
+// @Param envVar body object{name=string,value=string,description=string,group=string} true "Environment Variable"
+// @Success 201 {object} object{name=string,description=string,group=string,source=string}
+// @Failure 400 {object} ErrorResponse
+// @Failure 500 {object} ErrorResponse
+// @Security BasicAuth
+// @Router /vault/env-variables [post]
+func (s *Server) handleCreateVaultEnvVariable(w http.ResponseWriter, r *http.Request) {
+ client, err := s.getVaultClient()
+ if err != nil {
+ http.Error(w, sanitizeVaultError(err), http.StatusBadRequest)
+ return
+ }
+
+ var req struct {
+ Name string `json:"name"`
+ Value string `json:"value"`
+ Description string `json:"description"`
+ Group string `json:"group"`
+ }
+
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, "Invalid request body", http.StatusBadRequest)
+ return
+ }
+
+ if req.Name == "" || req.Value == "" {
+ http.Error(w, "Name and value are required", http.StatusBadRequest)
+ return
+ }
+
+ if req.Group == "" {
+ req.Group = "default"
+ }
+
+ ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
+ defer cancel()
+
+ envVar := &vault.EnvVariable{
+ Name: req.Name,
+ Value: req.Value,
+ Description: req.Description,
+ Group: req.Group,
+ }
+
+ if err := client.SaveEnvVariable(ctx, envVar); err != nil {
+ log.Printf("Error saving env variable to Vault: %v", err)
+ http.Error(w, "Failed to save environment variable to Vault", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "name": envVar.Name,
+ "description": envVar.Description,
+ "group": envVar.Group,
+ "source": "vault",
+ })
+}
+
+// handleCreateVaultScript godoc
+// @Summary Store bash script in Vault
+// @Description Create a new bash script in HashiCorp Vault storage
+// @Tags Vault
+// @Accept json
+// @Produce json
+// @Param script body object true "Bash script data" example({"name":"deploy-script","description":"Production deployment script","content":"#!/bin/bash\necho 'Deploying...'","filename":"deploy.sh","group":"production"})
+// @Success 201 {object} map[string]interface{} "Created script info with source field"
+// @Failure 400 {string} string "Invalid request or Vault not configured"
+// @Failure 500 {string} string "Failed to store script"
+// @Security BasicAuth
+// @Router /vault/bash-scripts [post]
+func (s *Server) handleCreateVaultScript(w http.ResponseWriter, r *http.Request) {
+ client, err := s.getVaultClient()
+ if err != nil {
+ http.Error(w, sanitizeVaultError(err), http.StatusBadRequest)
+ return
+ }
+
+ var req struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Content string `json:"content"`
+ Filename string `json:"filename"`
+ Group string `json:"group"`
+ }
+
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, "Invalid request body", http.StatusBadRequest)
+ return
+ }
+
+ if req.Name == "" || req.Content == "" {
+ http.Error(w, "Name and content are required", http.StatusBadRequest)
+ return
+ }
+
+ if req.Group == "" {
+ req.Group = "default"
+ }
+
+ ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
+ defer cancel()
+
+ script := &vault.BashScript{
+ Name: req.Name,
+ Description: req.Description,
+ Content: req.Content,
+ Filename: req.Filename,
+ Group: req.Group,
+ }
+
+ if err := client.SaveBashScript(ctx, script); err != nil {
+ log.Printf("Error saving script to Vault: %v", err)
+ http.Error(w, "Failed to save script to Vault", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "name": script.Name,
+ "description": script.Description,
+ "filename": script.Filename,
+ "group": script.Group,
+ "source": "vault",
+ })
+}
diff --git a/internal/server/server.go b/internal/server/server.go
index cbbe97c..60534c5 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -8,7 +8,6 @@ import (
"os"
"path/filepath"
"strings"
- "time"
"github.com/gorilla/mux"
"github.com/pozgo/web-cli/internal/config"
@@ -29,7 +28,14 @@ type Server struct {
}
// New creates a new Server instance
-func New(cfg *config.Config, db *database.DB) *Server {
+// Returns an error if critical configuration validation fails (e.g., auth misconfigured)
+func New(cfg *config.Config, db *database.DB) (*Server, error) {
+ // Validate authentication configuration at startup
+ authConfig := middleware.LoadAuthConfig()
+ if err := authConfig.Validate(); err != nil {
+ return nil, err
+ }
+
s := &Server{
config: cfg,
router: mux.NewRouter(),
@@ -38,7 +44,7 @@ func New(cfg *config.Config, db *database.DB) *Server {
s.setupRoutes()
- return s
+ return s, nil
}
// setupRoutes configures all HTTP routes
@@ -58,6 +64,7 @@ func (s *Server) setupRoutes() {
// SSH Keys endpoints
api.HandleFunc("/keys", s.handleListSSHKeys).Methods("GET")
api.HandleFunc("/keys", s.handleCreateSSHKey).Methods("POST")
+ api.HandleFunc("/keys/groups", s.handleListSSHKeyGroups).Methods("GET")
api.HandleFunc("/keys/{id}", s.handleGetSSHKey).Methods("GET")
api.HandleFunc("/keys/{id}", s.handleUpdateSSHKey).Methods("PUT")
api.HandleFunc("/keys/{id}", s.handleDeleteSSHKey).Methods("DELETE")
@@ -65,6 +72,7 @@ func (s *Server) setupRoutes() {
// Servers endpoints
api.HandleFunc("/servers", s.handleListServers).Methods("GET")
api.HandleFunc("/servers", s.handleCreateServer).Methods("POST")
+ api.HandleFunc("/servers/groups", s.handleListServerGroups).Methods("GET")
api.HandleFunc("/servers/{id}", s.handleGetServer).Methods("GET")
api.HandleFunc("/servers/{id}", s.handleUpdateServer).Methods("PUT")
api.HandleFunc("/servers/{id}", s.handleDeleteServer).Methods("DELETE")
@@ -97,6 +105,7 @@ func (s *Server) setupRoutes() {
// Environment variables endpoints
api.HandleFunc("/env-variables", s.handleListEnvVariables).Methods("GET")
api.HandleFunc("/env-variables", s.handleCreateEnvVariable).Methods("POST")
+ api.HandleFunc("/env-variables/groups", s.handleListEnvVariableGroups).Methods("GET")
api.HandleFunc("/env-variables/{id}", s.handleGetEnvVariable).Methods("GET")
api.HandleFunc("/env-variables/{id}", s.handleUpdateEnvVariable).Methods("PUT")
api.HandleFunc("/env-variables/{id}", s.handleDeleteEnvVariable).Methods("DELETE")
@@ -104,11 +113,12 @@ func (s *Server) setupRoutes() {
// Bash scripts endpoints
api.HandleFunc("/bash-scripts", s.handleListBashScripts).Methods("GET")
api.HandleFunc("/bash-scripts", s.handleCreateBashScript).Methods("POST")
+ api.HandleFunc("/bash-scripts/groups", s.handleListBashScriptGroups).Methods("GET")
+ api.HandleFunc("/bash-scripts/execute", s.handleExecuteScript).Methods("POST")
+ api.HandleFunc("/bash-scripts/execute/stream", s.handleExecuteScriptStream).Methods("POST")
api.HandleFunc("/bash-scripts/{id}", s.handleGetBashScript).Methods("GET")
api.HandleFunc("/bash-scripts/{id}", s.handleUpdateBashScript).Methods("PUT")
api.HandleFunc("/bash-scripts/{id}", s.handleDeleteBashScript).Methods("DELETE")
- api.HandleFunc("/bash-scripts/execute", s.handleExecuteScript).Methods("POST")
- api.HandleFunc("/bash-scripts/execute/stream", s.handleExecuteScriptStream).Methods("POST")
api.HandleFunc("/bash-scripts/{id}/presets", s.handleGetScriptPresetsByScript).Methods("GET")
// Script preset endpoints
@@ -118,6 +128,22 @@ func (s *Server) setupRoutes() {
api.HandleFunc("/script-presets/{id}", s.handleUpdateScriptPreset).Methods("PUT")
api.HandleFunc("/script-presets/{id}", s.handleDeleteScriptPreset).Methods("DELETE")
+ // Vault integration endpoints
+ api.HandleFunc("/vault/config", s.handleGetVaultConfig).Methods("GET")
+ api.HandleFunc("/vault/config", s.handleCreateOrUpdateVaultConfig).Methods("POST")
+ api.HandleFunc("/vault/config", s.handleDeleteVaultConfig).Methods("DELETE")
+ api.HandleFunc("/vault/test", s.handleTestVaultConnection).Methods("POST")
+ api.HandleFunc("/vault/status", s.handleGetVaultStatus).Methods("GET")
+ api.HandleFunc("/vault/ssh-keys", s.handleListVaultSSHKeys).Methods("GET")
+ api.HandleFunc("/vault/ssh-keys", s.handleCreateVaultSSHKey).Methods("POST")
+ api.HandleFunc("/vault/servers", s.handleListVaultServers).Methods("GET")
+ api.HandleFunc("/vault/servers", s.handleCreateVaultServer).Methods("POST")
+ api.HandleFunc("/vault/env-variables", s.handleListVaultEnvVariables).Methods("GET")
+ api.HandleFunc("/vault/env-variables", s.handleCreateVaultEnvVariable).Methods("POST")
+ api.HandleFunc("/vault/bash-scripts", s.handleListVaultScripts).Methods("GET")
+ api.HandleFunc("/vault/bash-scripts", s.handleCreateVaultScript).Methods("POST")
+ api.HandleFunc("/vault/scripts", s.handleListVaultScripts).Methods("GET") // Backward compatibility
+
// Terminal WebSocket endpoint (for interactive shell)
api.HandleFunc("/terminal/ws", s.handleTerminalWebSocket)
@@ -281,9 +307,9 @@ func (s *Server) Start() error {
server := &http.Server{
Addr: addr,
Handler: handler,
- ReadTimeout: 30 * time.Second,
- WriteTimeout: 10 * time.Minute, // Allow long-running script streams
- IdleTimeout: 60 * time.Second,
+ ReadTimeout: s.config.GetReadTimeout(),
+ WriteTimeout: s.config.GetWriteTimeout(),
+ IdleTimeout: s.config.GetIdleTimeout(),
}
// Start with TLS if configured
diff --git a/internal/server/vault_helpers.go b/internal/server/vault_helpers.go
new file mode 100644
index 0000000..07bfc01
--- /dev/null
+++ b/internal/server/vault_helpers.go
@@ -0,0 +1,340 @@
+package server
+
+import (
+ "context"
+ "log"
+ "time"
+
+ "github.com/pozgo/web-cli/internal/models"
+ "github.com/pozgo/web-cli/internal/repository"
+ "github.com/pozgo/web-cli/internal/vault"
+)
+
+// getVaultClientIfEnabled returns a Vault client if Vault is configured and enabled
+// Returns nil if Vault is not available (no error)
+func (s *Server) getVaultClientIfEnabled() *vault.Client {
+ repo := repository.NewVaultConfigRepository(s.db)
+ cfg, err := repo.Get()
+ if err != nil || cfg == nil || !cfg.Enabled {
+ return nil
+ }
+
+ vaultCfg := &vault.Config{
+ Address: cfg.Address,
+ Token: cfg.Token,
+ Namespace: cfg.Namespace,
+ MountPath: cfg.MountPath,
+ }
+
+ client, err := vault.NewClient(vaultCfg)
+ if err != nil {
+ log.Printf("Warning: Failed to create Vault client: %v", err)
+ return nil
+ }
+
+ return client
+}
+
+// mergeSSHKeysWithVault combines SQLite SSH keys with Vault SSH keys
+func (s *Server) mergeSSHKeysWithVault(ctx context.Context, sqliteKeys []*models.SSHKey) []*models.SSHKey {
+ // Mark SQLite keys
+ for _, k := range sqliteKeys {
+ k.Source = "sqlite"
+ }
+
+ // Try to get Vault client
+ client := s.getVaultClientIfEnabled()
+ if client == nil {
+ return sqliteKeys
+ }
+
+ // Set timeout for Vault operations
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+
+ // Get keys from Vault
+ vaultKeys, err := client.ListSSHKeys(ctx)
+ if err != nil {
+ log.Printf("Warning: Failed to list Vault SSH keys: %v", err)
+ return sqliteKeys
+ }
+
+ // Convert Vault keys to models and append
+ allKeys := make([]*models.SSHKey, 0, len(sqliteKeys)+len(vaultKeys))
+ allKeys = append(allKeys, sqliteKeys...)
+
+ for _, vk := range vaultKeys {
+ allKeys = append(allKeys, &models.SSHKey{
+ ID: 0, // Vault keys don't have numeric IDs
+ Name: vk.Name,
+ PrivateKey: vk.PrivateKey,
+ Group: vk.Group,
+ Source: "vault",
+ CreatedAt: vk.CreatedAt,
+ UpdatedAt: vk.CreatedAt,
+ })
+ }
+
+ return allKeys
+}
+
+// mergeServersWithVault combines SQLite servers with Vault servers
+func (s *Server) mergeServersWithVault(ctx context.Context, sqliteServers []*models.Server) []*models.Server {
+ // Mark SQLite servers
+ for _, srv := range sqliteServers {
+ srv.Source = "sqlite"
+ }
+
+ // Try to get Vault client
+ client := s.getVaultClientIfEnabled()
+ if client == nil {
+ return sqliteServers
+ }
+
+ // Set timeout for Vault operations
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+
+ // Get servers from Vault
+ vaultServers, err := client.ListServers(ctx)
+ if err != nil {
+ log.Printf("Warning: Failed to list Vault servers: %v", err)
+ return sqliteServers
+ }
+
+ // Convert Vault servers to models and append
+ allServers := make([]*models.Server, 0, len(sqliteServers)+len(vaultServers))
+ allServers = append(allServers, sqliteServers...)
+
+ now := time.Now()
+ for _, vs := range vaultServers {
+ allServers = append(allServers, &models.Server{
+ ID: 0, // Vault servers don't have numeric IDs
+ Name: vs.Name,
+ IPAddress: vs.IPAddress,
+ Port: vs.Port,
+ Username: vs.Username,
+ Group: vs.Group,
+ Source: "vault",
+ CreatedAt: now,
+ UpdatedAt: now,
+ })
+ }
+
+ return allServers
+}
+
+// mergeEnvVariablesWithVault combines SQLite env variables with Vault env variables
+func (s *Server) mergeEnvVariablesWithVault(ctx context.Context, sqliteVars []*models.EnvVariable) []*models.EnvVariable {
+ // Mark SQLite vars
+ for _, v := range sqliteVars {
+ v.Source = "sqlite"
+ }
+
+ // Try to get Vault client
+ client := s.getVaultClientIfEnabled()
+ if client == nil {
+ return sqliteVars
+ }
+
+ // Set timeout for Vault operations
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+
+ // Get env vars from Vault
+ vaultVars, err := client.ListEnvVariables(ctx)
+ if err != nil {
+ log.Printf("Warning: Failed to list Vault env variables: %v", err)
+ return sqliteVars
+ }
+
+ // Convert Vault vars to models and append
+ allVars := make([]*models.EnvVariable, 0, len(sqliteVars)+len(vaultVars))
+ allVars = append(allVars, sqliteVars...)
+
+ now := time.Now()
+ for _, vv := range vaultVars {
+ allVars = append(allVars, &models.EnvVariable{
+ ID: 0, // Vault vars don't have numeric IDs
+ Name: vv.Name,
+ Value: vv.Value,
+ Description: vv.Description,
+ Group: vv.Group,
+ Source: "vault",
+ CreatedAt: now,
+ UpdatedAt: now,
+ })
+ }
+
+ return allVars
+}
+
+// getSSHKeyByNameFromVault retrieves an SSH key from Vault by name and group
+func (s *Server) getSSHKeyByNameFromVault(ctx context.Context, group, name string) (*models.SSHKey, error) {
+ client := s.getVaultClientIfEnabled()
+ if client == nil {
+ return nil, nil
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+
+ vk, err := client.GetSSHKey(ctx, group, name)
+ if err != nil {
+ return nil, err
+ }
+ if vk == nil {
+ return nil, nil
+ }
+
+ return &models.SSHKey{
+ ID: 0,
+ Name: vk.Name,
+ PrivateKey: vk.PrivateKey,
+ Group: vk.Group,
+ Source: "vault",
+ CreatedAt: vk.CreatedAt,
+ UpdatedAt: vk.CreatedAt,
+ }, nil
+}
+
+// getServerByNameFromVault retrieves a server from Vault by name and group
+func (s *Server) getServerByNameFromVault(ctx context.Context, group, name string) (*models.Server, error) {
+ client := s.getVaultClientIfEnabled()
+ if client == nil {
+ return nil, nil
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+
+ vs, err := client.GetServer(ctx, group, name)
+ if err != nil {
+ return nil, err
+ }
+ if vs == nil {
+ return nil, nil
+ }
+
+ now := time.Now()
+ return &models.Server{
+ ID: 0,
+ Name: vs.Name,
+ IPAddress: vs.IPAddress,
+ Port: vs.Port,
+ Username: vs.Username,
+ Group: vs.Group,
+ Source: "vault",
+ CreatedAt: now,
+ UpdatedAt: now,
+ }, nil
+}
+
+// getEnvVariableByNameFromVault retrieves an env variable from Vault by name and group
+func (s *Server) getEnvVariableByNameFromVault(ctx context.Context, group, name string) (*models.EnvVariable, error) {
+ client := s.getVaultClientIfEnabled()
+ if client == nil {
+ return nil, nil
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+
+ vv, err := client.GetEnvVariable(ctx, group, name)
+ if err != nil {
+ return nil, err
+ }
+ if vv == nil {
+ return nil, nil
+ }
+
+ now := time.Now()
+ return &models.EnvVariable{
+ ID: 0,
+ Name: vv.Name,
+ Value: vv.Value,
+ Description: vv.Description,
+ Group: vv.Group,
+ Source: "vault",
+ CreatedAt: now,
+ UpdatedAt: now,
+ }, nil
+}
+
+// mergeScriptsWithVault combines SQLite scripts with Vault scripts
+func (s *Server) mergeScriptsWithVault(ctx context.Context, sqliteScripts []*models.BashScript) []*models.BashScript {
+ // Mark SQLite scripts
+ for _, script := range sqliteScripts {
+ script.Source = "sqlite"
+ }
+
+ // Try to get Vault client
+ client := s.getVaultClientIfEnabled()
+ if client == nil {
+ return sqliteScripts
+ }
+
+ // Set timeout for Vault operations
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+
+ // Get scripts from Vault
+ vaultScripts, err := client.ListBashScripts(ctx)
+ if err != nil {
+ log.Printf("Warning: Failed to list Vault scripts: %v", err)
+ return sqliteScripts
+ }
+
+ // Convert Vault scripts to models and append
+ allScripts := make([]*models.BashScript, 0, len(sqliteScripts)+len(vaultScripts))
+ allScripts = append(allScripts, sqliteScripts...)
+
+ now := time.Now()
+ for _, vs := range vaultScripts {
+ allScripts = append(allScripts, &models.BashScript{
+ ID: 0, // Vault scripts don't have numeric IDs
+ Name: vs.Name,
+ Description: vs.Description,
+ Content: vs.Content,
+ Filename: vs.Filename,
+ Group: vs.Group,
+ Source: "vault",
+ CreatedAt: now,
+ UpdatedAt: now,
+ })
+ }
+
+ return allScripts
+}
+
+// getScriptByNameFromVault retrieves a script from Vault by name and group
+func (s *Server) getScriptByNameFromVault(ctx context.Context, group, name string) (*models.BashScript, error) {
+ client := s.getVaultClientIfEnabled()
+ if client == nil {
+ return nil, nil
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ defer cancel()
+
+ vs, err := client.GetBashScript(ctx, group, name)
+ if err != nil {
+ return nil, err
+ }
+ if vs == nil {
+ return nil, nil
+ }
+
+ now := time.Now()
+ return &models.BashScript{
+ ID: 0,
+ Name: vs.Name,
+ Description: vs.Description,
+ Content: vs.Content,
+ Filename: vs.Filename,
+ Group: vs.Group,
+ Source: "vault",
+ CreatedAt: now,
+ UpdatedAt: now,
+ }, nil
+}
diff --git a/internal/validation/validators.go b/internal/validation/validators.go
index 5cfc9ae..f86f8cf 100644
--- a/internal/validation/validators.go
+++ b/internal/validation/validators.go
@@ -3,6 +3,7 @@ package validation
import (
"fmt"
"net"
+ "net/url"
"regexp"
"strings"
@@ -251,3 +252,221 @@ func ValidateCommand(command string) error {
return nil
}
+
+// ValidateVaultAddress validates a Vault server address to prevent SSRF attacks
+// Only allows HTTPS URLs with valid public hostnames/IPs
+func ValidateVaultAddress(address string) error {
+ if address == "" {
+ return fmt.Errorf("vault address cannot be empty")
+ }
+
+ // Parse the URL
+ parsedURL, err := url.Parse(address)
+ if err != nil {
+ return fmt.Errorf("invalid vault address URL: %w", err)
+ }
+
+ // Require HTTP or HTTPS scheme
+ if parsedURL.Scheme != "https" && parsedURL.Scheme != "http" {
+ return fmt.Errorf("vault address must use http or https scheme")
+ }
+
+ // Get the hostname
+ hostname := parsedURL.Hostname()
+ if hostname == "" {
+ return fmt.Errorf("vault address must include a hostname")
+ }
+
+ // Check for dangerous endpoints (SSRF protection)
+ // We allow private IPs for self-hosted Vault but block cloud metadata endpoints
+ if ip := net.ParseIP(hostname); ip != nil {
+ // Block link-local addresses (169.254.x.x) which include cloud metadata endpoints
+ if ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
+ return fmt.Errorf("vault address cannot be a link-local address (potential metadata endpoint)")
+ }
+ // Block unspecified addresses (0.0.0.0)
+ if ip.IsUnspecified() {
+ return fmt.Errorf("vault address cannot be an unspecified address")
+ }
+ } else {
+ // It's a hostname, validate format
+ if err := ValidateHostname(hostname); err != nil {
+ return fmt.Errorf("invalid vault hostname: %w", err)
+ }
+
+ // Block cloud metadata hostnames that could be used for SSRF
+ lowercaseHost := strings.ToLower(hostname)
+ blockedHosts := []string{
+ "metadata.google.internal",
+ "metadata.goog",
+ "metadata",
+ "instance-data",
+ }
+ for _, blocked := range blockedHosts {
+ if lowercaseHost == blocked || strings.HasSuffix(lowercaseHost, "."+blocked) {
+ return fmt.Errorf("vault address hostname is not allowed: %s", hostname)
+ }
+ }
+ }
+
+ // Validate port if specified
+ if portStr := parsedURL.Port(); portStr != "" {
+ // Port is already validated by url.Parse, but verify it's reasonable
+ var port int
+ if _, err := fmt.Sscanf(portStr, "%d", &port); err == nil {
+ if err := ValidatePort(port); err != nil {
+ return fmt.Errorf("invalid vault port: %w", err)
+ }
+ }
+ }
+
+ return nil
+}
+
+// isPrivateOrReservedIP checks if an IP address is private, loopback, or reserved
+func isPrivateOrReservedIP(ip net.IP) bool {
+ // Check for loopback (127.0.0.0/8, ::1)
+ if ip.IsLoopback() {
+ return true
+ }
+
+ // Check for private ranges
+ if ip.IsPrivate() {
+ return true
+ }
+
+ // Check for link-local (169.254.0.0/16, fe80::/10)
+ if ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
+ return true
+ }
+
+ // Check for other reserved ranges
+ // 0.0.0.0/8 - Current network
+ // 10.0.0.0/8 - Private (handled by IsPrivate)
+ // 100.64.0.0/10 - Carrier-grade NAT
+ // 172.16.0.0/12 - Private (handled by IsPrivate)
+ // 192.0.0.0/24 - IETF Protocol Assignments
+ // 192.0.2.0/24 - Documentation
+ // 192.168.0.0/16 - Private (handled by IsPrivate)
+ // 198.51.100.0/24 - Documentation
+ // 203.0.113.0/24 - Documentation
+
+ ip4 := ip.To4()
+ if ip4 != nil {
+ // Carrier-grade NAT: 100.64.0.0/10
+ if ip4[0] == 100 && ip4[1] >= 64 && ip4[1] <= 127 {
+ return true
+ }
+ // Current network: 0.0.0.0/8
+ if ip4[0] == 0 {
+ return true
+ }
+ // IETF Protocol Assignments: 192.0.0.0/24
+ if ip4[0] == 192 && ip4[1] == 0 && ip4[2] == 0 {
+ return true
+ }
+ }
+
+ return false
+}
+
+// ValidateVaultSecretPath validates a Vault secret path component to prevent path traversal
+func ValidateVaultSecretPath(path string) error {
+ if path == "" {
+ return fmt.Errorf("vault path cannot be empty")
+ }
+
+ // Check for path traversal sequences
+ if strings.Contains(path, "..") {
+ return fmt.Errorf("vault path cannot contain path traversal sequences (..)")
+ }
+
+ // Check for absolute path indicators
+ if strings.HasPrefix(path, "/") {
+ return fmt.Errorf("vault path cannot start with /")
+ }
+
+ // Check for backslashes (Windows-style paths)
+ if strings.Contains(path, "\\") {
+ return fmt.Errorf("vault path cannot contain backslashes")
+ }
+
+ // Check for null bytes
+ if strings.Contains(path, "\x00") {
+ return fmt.Errorf("vault path cannot contain null characters")
+ }
+
+ // Check for URL encoding that could be used to bypass filters
+ if strings.Contains(path, "%") {
+ return fmt.Errorf("vault path cannot contain URL-encoded characters")
+ }
+
+ // Validate allowed characters (alphanumeric, dash, underscore, dot, forward slash)
+ validPathRegex := regexp.MustCompile(`^[a-zA-Z0-9._/-]+$`)
+ if !validPathRegex.MatchString(path) {
+ return fmt.Errorf("vault path contains invalid characters (allowed: alphanumeric, dash, underscore, dot, forward slash)")
+ }
+
+ // Check for consecutive slashes
+ if strings.Contains(path, "//") {
+ return fmt.Errorf("vault path cannot contain consecutive slashes")
+ }
+
+ // Limit path length
+ if len(path) > 512 {
+ return fmt.Errorf("vault path too long (max 512 characters)")
+ }
+
+ return nil
+}
+
+// ValidateVaultSecretName validates a Vault secret name to prevent path traversal
+func ValidateVaultSecretName(name string) error {
+ if name == "" {
+ return fmt.Errorf("vault secret name cannot be empty")
+ }
+
+ // Secret names should not contain path separators
+ if strings.Contains(name, "/") || strings.Contains(name, "\\") {
+ return fmt.Errorf("vault secret name cannot contain path separators")
+ }
+
+ // Check for path traversal
+ if strings.Contains(name, "..") {
+ return fmt.Errorf("vault secret name cannot contain path traversal sequences")
+ }
+
+ // Check for null bytes
+ if strings.Contains(name, "\x00") {
+ return fmt.Errorf("vault secret name cannot contain null characters")
+ }
+
+ // Check for URL encoding
+ if strings.Contains(name, "%") {
+ return fmt.Errorf("vault secret name cannot contain URL-encoded characters")
+ }
+
+ // Validate allowed characters
+ validNameRegex := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`)
+ if !validNameRegex.MatchString(name) {
+ return fmt.Errorf("vault secret name contains invalid characters (allowed: alphanumeric, dash, underscore, dot)")
+ }
+
+ // Limit name length
+ if len(name) > 255 {
+ return fmt.Errorf("vault secret name too long (max 255 characters)")
+ }
+
+ return nil
+}
+
+// ValidateVaultGroupName validates a Vault group name
+func ValidateVaultGroupName(group string) error {
+ // Empty group is allowed (defaults to "default")
+ if group == "" {
+ return nil
+ }
+
+ // Group names follow same rules as secret names
+ return ValidateVaultSecretName(group)
+}
diff --git a/internal/validation/validators_test.go b/internal/validation/validators_test.go
index d499dab..e14c614 100644
--- a/internal/validation/validators_test.go
+++ b/internal/validation/validators_test.go
@@ -1,358 +1,308 @@
package validation
import (
- "strings"
"testing"
)
-func TestValidateIPAddress(t *testing.T) {
+func TestValidateVaultAddress(t *testing.T) {
tests := []struct {
name string
- ip string
+ address string
wantErr bool
+ errMsg string
}{
- {"valid IPv4", "192.168.1.1", false},
- {"valid IPv4 localhost", "127.0.0.1", false},
- {"valid IPv6", "2001:0db8:85a3:0000:0000:8a2e:0370:7334", false},
- {"valid IPv6 short", "::1", false},
- {"empty string", "", true},
- {"invalid IPv4", "256.1.1.1", true},
- {"invalid format", "not-an-ip", true},
- {"hostname not IP", "example.com", true},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- err := ValidateIPAddress(tt.ip)
- if (err != nil) != tt.wantErr {
- t.Errorf("ValidateIPAddress() error = %v, wantErr %v", err, tt.wantErr)
- }
- })
- }
-}
+ // Valid addresses
+ {
+ name: "valid https public URL",
+ address: "https://vault.example.com:8200",
+ wantErr: false,
+ },
+ {
+ name: "valid https with path",
+ address: "https://vault.example.com/v1",
+ wantErr: false,
+ },
+ {
+ name: "valid http local development",
+ address: "http://localhost:8200",
+ wantErr: false,
+ },
+ {
+ name: "valid https localhost",
+ address: "https://127.0.0.1:8200",
+ wantErr: false,
+ },
+ {
+ name: "valid private IP 10.x.x.x",
+ address: "https://10.0.0.5:8200",
+ wantErr: false,
+ },
+ {
+ name: "valid private IP 172.16.x.x",
+ address: "https://172.16.0.10:8200",
+ wantErr: false,
+ },
+ {
+ name: "valid private IP 192.168.x.x",
+ address: "https://192.168.1.100:8200",
+ wantErr: false,
+ },
-func TestValidateHostname(t *testing.T) {
- tests := []struct {
- name string
- hostname string
- wantErr bool
- }{
- {"valid simple hostname", "server1", false},
- {"valid FQDN", "mail.example.com", false},
- {"valid with dash", "my-server", false},
- {"valid with numbers", "server123", false},
- {"empty string", "", true},
- {"starts with dash", "-server", true},
- {"ends with dash", "server-", true},
- {"double dash", "my--server", false}, // Actually valid in RFC 1123
- {"too long", strings.Repeat("a", 254), true},
- {"invalid chars", "my_server", true}, // Underscore not allowed in hostname
- {"valid subdomain", "api.v1.example.com", false},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- err := ValidateHostname(tt.hostname)
- if (err != nil) != tt.wantErr {
- t.Errorf("ValidateHostname() error = %v, wantErr %v", err, tt.wantErr)
- }
- })
- }
-}
+ // Invalid addresses - SSRF protection
+ {
+ name: "block link-local address 169.254.x.x",
+ address: "http://169.254.169.254/latest/meta-data",
+ wantErr: true,
+ errMsg: "link-local address",
+ },
+ {
+ name: "block unspecified address 0.0.0.0",
+ address: "http://0.0.0.0:8200",
+ wantErr: true,
+ errMsg: "unspecified address",
+ },
+ {
+ name: "block metadata.google.internal",
+ address: "http://metadata.google.internal/computeMetadata/v1/",
+ wantErr: true,
+ errMsg: "hostname is not allowed",
+ },
+ {
+ name: "block metadata hostname",
+ address: "http://metadata/computeMetadata/v1/",
+ wantErr: true,
+ errMsg: "hostname is not allowed",
+ },
+ {
+ name: "block instance-data hostname",
+ address: "http://instance-data/latest/meta-data",
+ wantErr: true,
+ errMsg: "hostname is not allowed",
+ },
-func TestValidateIPOrHostname(t *testing.T) {
- tests := []struct {
- name string
- value string
- wantErr bool
- }{
- {"valid IP", "192.168.1.1", false},
- {"valid hostname", "example.com", false},
- {"valid IPv6", "::1", false},
- {"empty string", "", true},
- {"invalid both", "not_valid-256", true},
+ // Invalid addresses - other validation
+ {
+ name: "empty address",
+ address: "",
+ wantErr: true,
+ errMsg: "cannot be empty",
+ },
+ {
+ name: "invalid scheme ftp",
+ address: "ftp://vault.example.com",
+ wantErr: true,
+ errMsg: "must use http or https",
+ },
+ {
+ name: "invalid scheme file",
+ address: "file:///etc/passwd",
+ wantErr: true,
+ errMsg: "must use http or https",
+ },
+ {
+ name: "missing hostname",
+ address: "https:///path",
+ wantErr: true,
+ errMsg: "must include a hostname",
+ },
+ {
+ name: "invalid port",
+ address: "https://vault.example.com:99999",
+ wantErr: true,
+ errMsg: "invalid vault port",
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- err := ValidateIPOrHostname(tt.value)
+ err := ValidateVaultAddress(tt.address)
if (err != nil) != tt.wantErr {
- t.Errorf("ValidateIPOrHostname() error = %v, wantErr %v", err, tt.wantErr)
+ t.Errorf("ValidateVaultAddress(%q) error = %v, wantErr %v", tt.address, err, tt.wantErr)
+ return
}
- })
- }
-}
-
-func TestValidatePort(t *testing.T) {
- tests := []struct {
- name string
- port int
- wantErr bool
- }{
- {"valid port 22", 22, false},
- {"valid port 80", 80, false},
- {"valid port 443", 443, false},
- {"valid port 65535", 65535, false},
- {"valid port 1", 1, false},
- {"zero", 0, true},
- {"negative", -1, true},
- {"too large", 65536, true},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- err := ValidatePort(tt.port)
- if (err != nil) != tt.wantErr {
- t.Errorf("ValidatePort() error = %v, wantErr %v", err, tt.wantErr)
+ if tt.wantErr && tt.errMsg != "" && err != nil {
+ if !contains(err.Error(), tt.errMsg) {
+ t.Errorf("ValidateVaultAddress(%q) error = %v, want error containing %q", tt.address, err, tt.errMsg)
+ }
}
})
}
}
-func TestValidateSSHPrivateKey(t *testing.T) {
- // Note: Testing with actual SSH key parsing is complex
- // We test the basic validation logic here
+func TestValidateVaultSecretPath(t *testing.T) {
tests := []struct {
name string
- key string
+ path string
wantErr bool
+ errMsg string
}{
- {"empty string", "", true},
- {"missing BEGIN header", "some random text", true},
- {"whitespace only", " \n\t ", true},
- // Real key validation happens in ssh.ParsePrivateKey
- // which requires a properly formatted key
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- err := ValidateSSHPrivateKey(tt.key)
- if (err != nil) != tt.wantErr {
- t.Errorf("ValidateSSHPrivateKey() error = %v, wantErr %v", err, tt.wantErr)
- }
- })
- }
-}
-
-func TestValidateUsername(t *testing.T) {
- tests := []struct {
- name string
- username string
- wantErr bool
- }{
- {"valid simple", "john", false},
- {"valid with underscore", "john_doe", false},
- {"valid with dash", "john-doe", false},
- {"valid with numbers", "user123", false},
- {"special case root", "root", false},
- {"special case current", "current", false},
- {"empty string", "", true},
- {"starts with number", "1john", true},
- {"too long", strings.Repeat("a", 33), true},
- {"uppercase", "John", true}, // Unix usernames are lowercase
- {"with spaces", "john doe", true},
- {"special chars", "john@doe", true},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- err := ValidateUsername(tt.username)
- if (err != nil) != tt.wantErr {
- t.Errorf("ValidateUsername() error = %v, wantErr %v", err, tt.wantErr)
- }
- })
- }
-}
-
-func TestValidateCommandName(t *testing.T) {
- tests := []struct {
- name string
- commandName string
- wantErr bool
- }{
- {"valid simple", "backup-db", false},
- {"valid with spaces", "My Command", false},
- {"valid special chars", "command-123_test", false},
- {"empty string", "", true},
- {"too long", strings.Repeat("a", 256), true},
- {"with null byte", "cmd\x00name", true},
- {"with newline", "cmd\nname", true},
- {"with carriage return", "cmd\rname", true},
- {"valid unicode", "备份数据库", false},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- err := ValidateCommandName(tt.commandName)
- if (err != nil) != tt.wantErr {
- t.Errorf("ValidateCommandName() error = %v, wantErr %v", err, tt.wantErr)
- }
- })
- }
-}
+ // Valid paths
+ {
+ name: "simple path",
+ path: "web-cli/ssh-keys",
+ wantErr: false,
+ },
+ {
+ name: "path with underscores",
+ path: "web_cli/ssh_keys",
+ wantErr: false,
+ },
+ {
+ name: "path with dashes",
+ path: "web-cli/ssh-keys/my-key",
+ wantErr: false,
+ },
+ {
+ name: "path with dots",
+ path: "web-cli/config.json",
+ wantErr: false,
+ },
-func TestValidateEnvVarName(t *testing.T) {
- tests := []struct {
- name string
- varName string
- wantErr bool
- }{
- {"valid simple", "MY_VAR", false},
- {"valid with numbers", "VAR123", false},
- {"valid starts with underscore", "_PRIVATE_VAR", false},
- {"valid lowercase", "my_var", false},
- {"valid mixed case", "MyVar", false},
- {"valid single letter", "A", false},
- {"valid single underscore", "_", false},
- {"empty string", "", true},
- {"starts with number", "1VAR", true},
- {"contains dash", "MY-VAR", true},
- {"contains space", "MY VAR", true},
- {"contains dot", "MY.VAR", true},
- {"contains special chars", "MY$VAR", true},
- {"too long", strings.Repeat("A", 256), true},
- {"common env var PATH", "PATH", false},
- {"common env var HOME", "HOME", false},
- {"common env var API_KEY", "API_KEY", false},
- {"common env var DATABASE_URL", "DATABASE_URL", false},
+ // Invalid paths - path traversal
+ {
+ name: "path traversal with ..",
+ path: "../../../etc/passwd",
+ wantErr: true,
+ errMsg: "path traversal",
+ },
+ {
+ name: "path traversal in middle",
+ path: "web-cli/../../../etc/passwd",
+ wantErr: true,
+ errMsg: "path traversal",
+ },
+ {
+ name: "absolute path",
+ path: "/etc/passwd",
+ wantErr: true,
+ errMsg: "cannot start with /",
+ },
+ {
+ name: "backslash path",
+ path: "web-cli\\ssh-keys",
+ wantErr: true,
+ errMsg: "cannot contain backslashes",
+ },
+ {
+ name: "null byte",
+ path: "web-cli\x00/ssh-keys",
+ wantErr: true,
+ errMsg: "null characters",
+ },
+ {
+ name: "URL encoded",
+ path: "web-cli%2Fssh-keys",
+ wantErr: true,
+ errMsg: "URL-encoded",
+ },
+ {
+ name: "consecutive slashes",
+ path: "web-cli//ssh-keys",
+ wantErr: true,
+ errMsg: "consecutive slashes",
+ },
+ {
+ name: "empty path",
+ path: "",
+ wantErr: true,
+ errMsg: "cannot be empty",
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- err := ValidateEnvVarName(tt.varName)
+ err := ValidateVaultSecretPath(tt.path)
if (err != nil) != tt.wantErr {
- t.Errorf("ValidateEnvVarName() error = %v, wantErr %v", err, tt.wantErr)
+ t.Errorf("ValidateVaultSecretPath(%q) error = %v, wantErr %v", tt.path, err, tt.wantErr)
+ return
}
- })
- }
-}
-
-func TestValidateEnvVarValue(t *testing.T) {
- tests := []struct {
- name string
- value string
- wantErr bool
- }{
- {"valid simple", "my-value", false},
- {"valid with spaces", "my value with spaces", false},
- {"valid with special chars", "pass!@#$%^&*()", false},
- {"valid with newlines", "line1\nline2", false},
- {"valid JSON", `{"key": "value"}`, false},
- {"valid URL", "https://example.com/path?query=1", false},
- {"valid long string", strings.Repeat("a", 1000), false},
- {"empty string", "", true},
- {"contains null byte", "value\x00here", true},
- {"valid unicode", "密码123", false},
- {"valid base64", "SGVsbG8gV29ybGQh", false},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- err := ValidateEnvVarValue(tt.value)
- if (err != nil) != tt.wantErr {
- t.Errorf("ValidateEnvVarValue() error = %v, wantErr %v", err, tt.wantErr)
+ if tt.wantErr && tt.errMsg != "" && err != nil {
+ if !contains(err.Error(), tt.errMsg) {
+ t.Errorf("ValidateVaultSecretPath(%q) error = %v, want error containing %q", tt.path, err, tt.errMsg)
+ }
}
})
}
}
-func TestValidateBashScriptName(t *testing.T) {
+func TestValidateVaultSecretName(t *testing.T) {
tests := []struct {
name string
- scriptName string
+ secretName string
wantErr bool
+ errMsg string
}{
- {"valid simple", "My Script", false},
- {"valid with dash", "backup-script", false},
- {"valid with underscore", "db_backup", false},
- {"valid with numbers", "script123", false},
- {"valid unicode", "备份脚本", false},
- {"valid long name", strings.Repeat("a", 255), false},
- {"empty string", "", true},
- {"too long", strings.Repeat("a", 256), true},
- {"contains null byte", "script\x00name", true},
- {"contains newline", "script\nname", true},
- {"contains carriage return", "script\rname", true},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- err := ValidateBashScriptName(tt.scriptName)
- if (err != nil) != tt.wantErr {
- t.Errorf("ValidateBashScriptName() error = %v, wantErr %v", err, tt.wantErr)
- }
- })
- }
-}
-
-func TestValidateBashScriptContent(t *testing.T) {
- tests := []struct {
- name string
- content string
- wantErr bool
- }{
- {"valid simple", "#!/bin/bash\necho hello", false},
- {"valid multiline", "#!/bin/bash\necho hello\necho world", false},
- {"valid with comments", "#!/bin/bash\n# This is a comment\necho hello", false},
- {"valid with variables", "#!/bin/bash\nNAME=\"World\"\necho \"Hello $NAME\"", false},
- {"valid long script", strings.Repeat("echo hello\n", 1000), false},
- {"empty string", "", true},
- {"contains null byte", "echo hello\x00world", true},
- {"valid without shebang", "echo hello", false},
- {"valid complex script", `#!/bin/bash
-set -euo pipefail
-
-if [ "$#" -lt 1 ]; then
- echo "Usage: $0 "
- exit 1
-fi
+ // Valid names
+ {
+ name: "simple name",
+ secretName: "my-secret",
+ wantErr: false,
+ },
+ {
+ name: "name with underscores",
+ secretName: "my_secret_key",
+ wantErr: false,
+ },
+ {
+ name: "name with dots",
+ secretName: "config.json",
+ wantErr: false,
+ },
-for i in "$@"; do
- echo "Processing: $i"
-done
-`, false},
+ // Invalid names
+ {
+ name: "path separator forward slash",
+ secretName: "path/to/secret",
+ wantErr: true,
+ errMsg: "path separators",
+ },
+ {
+ name: "path separator backslash",
+ secretName: "path\\to\\secret",
+ wantErr: true,
+ errMsg: "path separators",
+ },
+ {
+ name: "path traversal",
+ secretName: "..",
+ wantErr: true,
+ errMsg: "path traversal",
+ },
+ {
+ name: "empty name",
+ secretName: "",
+ wantErr: true,
+ errMsg: "cannot be empty",
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- err := ValidateBashScriptContent(tt.content)
+ err := ValidateVaultSecretName(tt.secretName)
if (err != nil) != tt.wantErr {
- t.Errorf("ValidateBashScriptContent() error = %v, wantErr %v", err, tt.wantErr)
+ t.Errorf("ValidateVaultSecretName(%q) error = %v, wantErr %v", tt.secretName, err, tt.wantErr)
+ return
+ }
+ if tt.wantErr && tt.errMsg != "" && err != nil {
+ if !contains(err.Error(), tt.errMsg) {
+ t.Errorf("ValidateVaultSecretName(%q) error = %v, want error containing %q", tt.secretName, err, tt.errMsg)
+ }
}
})
}
}
-func TestValidateBashScriptFilename(t *testing.T) {
- tests := []struct {
- name string
- filename string
- wantErr bool
- }{
- {"valid simple", "script.sh", false},
- {"valid with dash", "my-script.sh", false},
- {"valid with underscore", "my_script.sh", false},
- {"valid bash extension", "script.bash", false},
- {"valid no extension", "script", false},
- {"empty string", "", false}, // Filename is optional
- {"too long", strings.Repeat("a", 256), true},
- {"path traversal", "../script.sh", true},
- {"absolute path", "/etc/script.sh", true},
- {"windows path", "C:\\script.sh", true},
- {"contains asterisk", "script*.sh", true},
- {"contains question mark", "script?.sh", true},
- {"contains quotes", "script\".sh", true},
- {"contains pipe", "script|.sh", true},
- {"contains null byte", "script\x00.sh", true},
- {"contains newline", "script\n.sh", true},
- }
+// contains checks if substr is contained in s
+func contains(s, substr string) bool {
+ return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
+ (len(s) > 0 && len(substr) > 0 && findSubstr(s, substr)))
+}
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- err := ValidateBashScriptFilename(tt.filename)
- if (err != nil) != tt.wantErr {
- t.Errorf("ValidateBashScriptFilename() error = %v, wantErr %v", err, tt.wantErr)
- }
- })
+func findSubstr(s, substr string) bool {
+ for i := 0; i <= len(s)-len(substr); i++ {
+ if s[i:i+len(substr)] == substr {
+ return true
+ }
}
+ return false
}
diff --git a/internal/vault/client.go b/internal/vault/client.go
new file mode 100644
index 0000000..12d1329
--- /dev/null
+++ b/internal/vault/client.go
@@ -0,0 +1,856 @@
+package vault
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/hashicorp/vault/api"
+ "github.com/pozgo/web-cli/internal/validation"
+)
+
+// Client wraps the Vault API client with convenience methods
+type Client struct {
+ client *api.Client
+ mountPath string
+}
+
+// Config holds the configuration for connecting to Vault
+type Config struct {
+ Address string `json:"address"`
+ Token string `json:"token"`
+ Namespace string `json:"namespace,omitempty"`
+ MountPath string `json:"mount_path"`
+}
+
+// NewClient creates a new Vault client with the given configuration
+func NewClient(cfg *Config) (*Client, error) {
+ if cfg == nil {
+ return nil, fmt.Errorf("vault config is nil")
+ }
+
+ if cfg.Address == "" {
+ return nil, fmt.Errorf("vault address is required")
+ }
+
+ // Validate Vault address to prevent SSRF attacks
+ if err := validation.ValidateVaultAddress(cfg.Address); err != nil {
+ return nil, fmt.Errorf("invalid vault address: %w", err)
+ }
+
+ if cfg.Token == "" {
+ return nil, fmt.Errorf("vault token is required")
+ }
+
+ vaultCfg := api.DefaultConfig()
+ vaultCfg.Address = cfg.Address
+
+ client, err := api.NewClient(vaultCfg)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create vault client: %w", err)
+ }
+
+ client.SetToken(cfg.Token)
+
+ if cfg.Namespace != "" {
+ client.SetNamespace(cfg.Namespace)
+ }
+
+ mountPath := cfg.MountPath
+ if mountPath == "" {
+ mountPath = "secret"
+ }
+
+ return &Client{
+ client: client,
+ mountPath: mountPath,
+ }, nil
+}
+
+// TestConnection verifies the Vault connection and token validity
+func (c *Client) TestConnection(ctx context.Context) error {
+ // Try to look up the token to verify it's valid
+ _, err := c.client.Auth().Token().LookupSelfWithContext(ctx)
+ if err != nil {
+ return fmt.Errorf("vault connection test failed: %w", err)
+ }
+ return nil
+}
+
+// InitializeStructure creates the web-cli directory structure in Vault
+// This ensures paths exist for listing operations
+func (c *Client) InitializeStructure(ctx context.Context) error {
+ // Create a .initialized marker in each path to ensure the paths exist
+ // This makes listing work even for empty paths
+ paths := []string{"ssh-keys", "servers", "env", "scripts"}
+
+ for _, secretType := range paths {
+ // Check if default group already has entries
+ secrets, err := c.ListSecrets(ctx, secretType, "default")
+ if err != nil {
+ // If list fails with actual error, try to create anyway
+ continue
+ }
+
+ // If path is empty, create .initialized marker
+ if len(secrets) == 0 {
+ data := map[string]interface{}{
+ "initialized": true,
+ "created_at": time.Now().Format(time.RFC3339),
+ }
+ // Ignore errors - token may not have write permission
+ _ = c.WriteSecret(ctx, secretType, "default", ".initialized", data)
+ }
+ }
+
+ return nil
+}
+
+// GetHealth returns the Vault server health status
+func (c *Client) GetHealth(ctx context.Context) (*api.HealthResponse, error) {
+ return c.client.Sys().HealthWithContext(ctx)
+}
+
+// secretPath constructs the full path for a secret
+// Path format: {mount}/data/{secretType}/{group}/{name}
+// Example: web-cli/data/ssh-keys/production/my-key
+func (c *Client) secretPath(secretType, group, name string) (string, error) {
+ if group == "" {
+ group = "default"
+ }
+
+ // Validate path components to prevent path traversal
+ if err := validation.ValidateVaultSecretName(secretType); err != nil {
+ return "", fmt.Errorf("invalid secret type: %w", err)
+ }
+ if err := validation.ValidateVaultGroupName(group); err != nil {
+ return "", fmt.Errorf("invalid group name: %w", err)
+ }
+ if err := validation.ValidateVaultSecretName(name); err != nil {
+ return "", fmt.Errorf("invalid secret name: %w", err)
+ }
+
+ return fmt.Sprintf("%s/data/%s/%s/%s", c.mountPath, secretType, group, name), nil
+}
+
+// secretPathFlat constructs the path for backward compatibility (no group)
+// Path format: {mount}/data/{secretType}/{name}
+// Example: web-cli/data/ssh-keys/my-key
+func (c *Client) secretPathFlat(secretType, name string) (string, error) {
+ // Validate path components to prevent path traversal
+ if err := validation.ValidateVaultSecretName(secretType); err != nil {
+ return "", fmt.Errorf("invalid secret type: %w", err)
+ }
+ if err := validation.ValidateVaultSecretName(name); err != nil {
+ return "", fmt.Errorf("invalid secret name: %w", err)
+ }
+
+ return fmt.Sprintf("%s/data/%s/%s", c.mountPath, secretType, name), nil
+}
+
+// metadataPath constructs the metadata path for listing a group
+// Path format: {mount}/metadata/{secretType}/{group}
+// Example: web-cli/metadata/ssh-keys/production
+func (c *Client) metadataPath(secretType, group string) (string, error) {
+ // Validate path components to prevent path traversal
+ if err := validation.ValidateVaultSecretName(secretType); err != nil {
+ return "", fmt.Errorf("invalid secret type: %w", err)
+ }
+ if group != "" {
+ if err := validation.ValidateVaultGroupName(group); err != nil {
+ return "", fmt.Errorf("invalid group name: %w", err)
+ }
+ return fmt.Sprintf("%s/metadata/%s/%s", c.mountPath, secretType, group), nil
+ }
+ return fmt.Sprintf("%s/metadata/%s", c.mountPath, secretType), nil
+}
+
+// metadataPathFlat constructs the metadata path for listing all groups
+// Path format: {mount}/metadata/{secretType}
+// Example: web-cli/metadata/ssh-keys
+func (c *Client) metadataPathFlat(secretType string) (string, error) {
+ // Validate path components to prevent path traversal
+ if err := validation.ValidateVaultSecretName(secretType); err != nil {
+ return "", fmt.Errorf("invalid secret type: %w", err)
+ }
+ return fmt.Sprintf("%s/metadata/%s", c.mountPath, secretType), nil
+}
+
+// ReadSecret reads a secret from Vault (with group support)
+func (c *Client) ReadSecret(ctx context.Context, secretType, group, name string) (map[string]interface{}, error) {
+ path, err := c.secretPath(secretType, group, name)
+ if err != nil {
+ return nil, err
+ }
+
+ secret, err := c.client.Logical().ReadWithContext(ctx, path)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read secret %s/%s/%s: %w", secretType, group, name, err)
+ }
+
+ if secret == nil || secret.Data == nil {
+ return nil, nil // Secret doesn't exist
+ }
+
+ // KV v2 stores data under "data" key
+ data, ok := secret.Data["data"].(map[string]interface{})
+ if !ok {
+ return nil, fmt.Errorf("unexpected secret format for %s/%s/%s", secretType, group, name)
+ }
+
+ return data, nil
+}
+
+// ReadSecretFlat reads a secret from Vault without group (backward compatibility)
+func (c *Client) ReadSecretFlat(ctx context.Context, secretType, name string) (map[string]interface{}, error) {
+ path, err := c.secretPathFlat(secretType, name)
+ if err != nil {
+ return nil, err
+ }
+
+ secret, err := c.client.Logical().ReadWithContext(ctx, path)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read secret %s/%s: %w", secretType, name, err)
+ }
+
+ if secret == nil || secret.Data == nil {
+ return nil, nil // Secret doesn't exist
+ }
+
+ // KV v2 stores data under "data" key
+ data, ok := secret.Data["data"].(map[string]interface{})
+ if !ok {
+ return nil, fmt.Errorf("unexpected secret format for %s/%s", secretType, name)
+ }
+
+ return data, nil
+}
+
+// WriteSecret writes a secret to Vault (with group support)
+func (c *Client) WriteSecret(ctx context.Context, secretType, group, name string, data map[string]interface{}) error {
+ path, err := c.secretPath(secretType, group, name)
+ if err != nil {
+ return err
+ }
+
+ // KV v2 requires data to be wrapped
+ wrappedData := map[string]interface{}{
+ "data": data,
+ }
+
+ _, err = c.client.Logical().WriteWithContext(ctx, path, wrappedData)
+ if err != nil {
+ return fmt.Errorf("failed to write secret %s/%s/%s: %w", secretType, group, name, err)
+ }
+
+ return nil
+}
+
+// WriteSecretFlat writes a secret to Vault without group (backward compatibility)
+func (c *Client) WriteSecretFlat(ctx context.Context, secretType, name string, data map[string]interface{}) error {
+ path, err := c.secretPathFlat(secretType, name)
+ if err != nil {
+ return err
+ }
+
+ // KV v2 requires data to be wrapped
+ wrappedData := map[string]interface{}{
+ "data": data,
+ }
+
+ _, err = c.client.Logical().WriteWithContext(ctx, path, wrappedData)
+ if err != nil {
+ return fmt.Errorf("failed to write secret %s/%s: %w", secretType, name, err)
+ }
+
+ return nil
+}
+
+// DeleteSecret deletes a secret from Vault (with group support)
+func (c *Client) DeleteSecret(ctx context.Context, secretType, group, name string) error {
+ if group == "" {
+ group = "default"
+ }
+
+ // Validate path components to prevent path traversal
+ if err := validation.ValidateVaultSecretName(secretType); err != nil {
+ return fmt.Errorf("invalid secret type: %w", err)
+ }
+ if err := validation.ValidateVaultGroupName(group); err != nil {
+ return fmt.Errorf("invalid group name: %w", err)
+ }
+ if err := validation.ValidateVaultSecretName(name); err != nil {
+ return fmt.Errorf("invalid secret name: %w", err)
+ }
+
+ // For KV v2, we delete the metadata to permanently remove
+ metaPath := fmt.Sprintf("%s/metadata/%s/%s/%s", c.mountPath, secretType, group, name)
+
+ _, err := c.client.Logical().DeleteWithContext(ctx, metaPath)
+ if err != nil {
+ return fmt.Errorf("failed to delete secret %s/%s/%s: %w", secretType, group, name, err)
+ }
+
+ return nil
+}
+
+// DeleteSecretFlat deletes a secret from Vault without group (backward compatibility)
+func (c *Client) DeleteSecretFlat(ctx context.Context, secretType, name string) error {
+ // Validate path components to prevent path traversal
+ if err := validation.ValidateVaultSecretName(secretType); err != nil {
+ return fmt.Errorf("invalid secret type: %w", err)
+ }
+ if err := validation.ValidateVaultSecretName(name); err != nil {
+ return fmt.Errorf("invalid secret name: %w", err)
+ }
+
+ // For KV v2, we delete the metadata to permanently remove
+ metaPath := fmt.Sprintf("%s/metadata/%s/%s", c.mountPath, secretType, name)
+
+ _, err := c.client.Logical().DeleteWithContext(ctx, metaPath)
+ if err != nil {
+ return fmt.Errorf("failed to delete secret %s/%s: %w", secretType, name, err)
+ }
+
+ return nil
+}
+
+// ListSecrets lists all secrets of a given type (optionally filtered by group)
+func (c *Client) ListSecrets(ctx context.Context, secretType, group string) ([]string, error) {
+ path, err := c.metadataPath(secretType, group)
+ if err != nil {
+ return nil, err
+ }
+
+ secret, err := c.client.Logical().ListWithContext(ctx, path)
+ if err != nil {
+ // Check if it's a 403 (permission denied) or 404 (path doesn't exist)
+ // In both cases, return empty list - the path may not exist yet or
+ // the token may not have list permissions (but might have read/write)
+ if strings.Contains(err.Error(), "403") || strings.Contains(err.Error(), "404") ||
+ strings.Contains(err.Error(), "permission denied") {
+ return []string{}, nil
+ }
+ return nil, fmt.Errorf("failed to list secrets for %s/%s: %w", secretType, group, err)
+ }
+
+ if secret == nil || secret.Data == nil {
+ return []string{}, nil // No secrets
+ }
+
+ keys, ok := secret.Data["keys"].([]interface{})
+ if !ok {
+ return []string{}, nil
+ }
+
+ result := make([]string, 0, len(keys))
+ for _, k := range keys {
+ if s, ok := k.(string); ok {
+ // Skip directory entries (end with /) and internal .initialized marker
+ if !strings.HasSuffix(s, "/") && s != ".initialized" {
+ result = append(result, s)
+ }
+ }
+ }
+
+ return result, nil
+}
+
+// ListGroups lists all groups for a given secret type
+func (c *Client) ListGroups(ctx context.Context, secretType string) ([]string, error) {
+ path, err := c.metadataPathFlat(secretType)
+ if err != nil {
+ return nil, err
+ }
+
+ secret, err := c.client.Logical().ListWithContext(ctx, path)
+ if err != nil {
+ if strings.Contains(err.Error(), "403") || strings.Contains(err.Error(), "404") ||
+ strings.Contains(err.Error(), "permission denied") {
+ return []string{}, nil
+ }
+ return nil, fmt.Errorf("failed to list groups for %s: %w", secretType, err)
+ }
+
+ if secret == nil || secret.Data == nil {
+ return []string{}, nil
+ }
+
+ keys, ok := secret.Data["keys"].([]interface{})
+ if !ok {
+ return []string{}, nil
+ }
+
+ result := make([]string, 0, len(keys))
+ for _, k := range keys {
+ if s, ok := k.(string); ok {
+ // Groups are directories (end with /)
+ if strings.HasSuffix(s, "/") {
+ result = append(result, strings.TrimSuffix(s, "/"))
+ }
+ }
+ }
+
+ return result, nil
+}
+
+// SSHKey represents an SSH key stored in Vault
+type SSHKey struct {
+ Name string `json:"name"`
+ PrivateKey string `json:"private_key"`
+ Group string `json:"group"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
+// ListSSHKeys returns all SSH keys from Vault (across all groups)
+func (c *Client) ListSSHKeys(ctx context.Context) ([]SSHKey, error) {
+ groups, err := c.ListGroups(ctx, "ssh-keys")
+ if err != nil {
+ return nil, err
+ }
+
+ // If no groups exist, check default group
+ if len(groups) == 0 {
+ groups = []string{"default"}
+ }
+
+ var keys []SSHKey
+ for _, group := range groups {
+ groupKeys, err := c.ListSSHKeysByGroup(ctx, group)
+ if err != nil {
+ continue
+ }
+ keys = append(keys, groupKeys...)
+ }
+
+ return keys, nil
+}
+
+// ListSSHKeysByGroup returns all SSH keys in a specific group
+func (c *Client) ListSSHKeysByGroup(ctx context.Context, group string) ([]SSHKey, error) {
+ names, err := c.ListSecrets(ctx, "ssh-keys", group)
+ if err != nil {
+ return nil, err
+ }
+
+ keys := make([]SSHKey, 0, len(names))
+ for _, name := range names {
+ key, err := c.GetSSHKey(ctx, group, name)
+ if err != nil {
+ continue // Skip keys that can't be read
+ }
+ if key != nil {
+ keys = append(keys, *key)
+ }
+ }
+
+ return keys, nil
+}
+
+// GetSSHKey retrieves a specific SSH key from Vault
+func (c *Client) GetSSHKey(ctx context.Context, group, name string) (*SSHKey, error) {
+ if group == "" {
+ group = "default"
+ }
+ data, err := c.ReadSecret(ctx, "ssh-keys", group, name)
+ if err != nil {
+ return nil, err
+ }
+ if data == nil {
+ return nil, nil
+ }
+
+ key := &SSHKey{Name: name, Group: group}
+
+ // Try standard field name first, then fall back to checking all string values
+ if pk, ok := data["private_key"].(string); ok {
+ key.PrivateKey = pk
+ } else {
+ // Flexible parsing: find any string value that looks like a private key
+ for _, v := range data {
+ if s, ok := v.(string); ok && strings.Contains(s, "PRIVATE KEY") {
+ key.PrivateKey = s
+ break
+ }
+ }
+ }
+
+ if ca, ok := data["created_at"].(string); ok {
+ if t, err := time.Parse(time.RFC3339, ca); err == nil {
+ key.CreatedAt = t
+ }
+ }
+
+ return key, nil
+}
+
+// SaveSSHKey saves an SSH key to Vault
+func (c *Client) SaveSSHKey(ctx context.Context, key *SSHKey) error {
+ if key.Group == "" {
+ key.Group = "default"
+ }
+ data := map[string]interface{}{
+ "private_key": key.PrivateKey,
+ "created_at": key.CreatedAt.Format(time.RFC3339),
+ }
+ return c.WriteSecret(ctx, "ssh-keys", key.Group, key.Name, data)
+}
+
+// DeleteSSHKey deletes an SSH key from Vault
+func (c *Client) DeleteSSHKey(ctx context.Context, group, name string) error {
+ if group == "" {
+ group = "default"
+ }
+ return c.DeleteSecret(ctx, "ssh-keys", group, name)
+}
+
+// ListSSHKeyGroups returns all groups for SSH keys
+func (c *Client) ListSSHKeyGroups(ctx context.Context) ([]string, error) {
+ return c.ListGroups(ctx, "ssh-keys")
+}
+
+// Server represents a server configuration stored in Vault
+type Server struct {
+ Name string `json:"name"`
+ IPAddress string `json:"ip_address"`
+ Port int `json:"port"`
+ Username string `json:"username"`
+ Group string `json:"group"`
+}
+
+// ListServers returns all servers from Vault (across all groups)
+func (c *Client) ListServers(ctx context.Context) ([]Server, error) {
+ groups, err := c.ListGroups(ctx, "servers")
+ if err != nil {
+ return nil, err
+ }
+
+ // If no groups exist, check default group
+ if len(groups) == 0 {
+ groups = []string{"default"}
+ }
+
+ var servers []Server
+ for _, group := range groups {
+ groupServers, err := c.ListServersByGroup(ctx, group)
+ if err != nil {
+ continue
+ }
+ servers = append(servers, groupServers...)
+ }
+
+ return servers, nil
+}
+
+// ListServersByGroup returns all servers in a specific group
+func (c *Client) ListServersByGroup(ctx context.Context, group string) ([]Server, error) {
+ names, err := c.ListSecrets(ctx, "servers", group)
+ if err != nil {
+ return nil, err
+ }
+
+ servers := make([]Server, 0, len(names))
+ for _, name := range names {
+ srv, err := c.GetServer(ctx, group, name)
+ if err != nil {
+ continue
+ }
+ if srv != nil {
+ servers = append(servers, *srv)
+ }
+ }
+
+ return servers, nil
+}
+
+// GetServer retrieves a specific server from Vault
+func (c *Client) GetServer(ctx context.Context, group, name string) (*Server, error) {
+ if group == "" {
+ group = "default"
+ }
+ data, err := c.ReadSecret(ctx, "servers", group, name)
+ if err != nil {
+ return nil, err
+ }
+ if data == nil {
+ return nil, nil
+ }
+
+ srv := &Server{Name: name, Group: group}
+
+ // Try standard field names first
+ if ip, ok := data["ip_address"].(string); ok {
+ srv.IPAddress = ip
+ } else if ip, ok := data["ip"].(string); ok {
+ srv.IPAddress = ip
+ } else if ip, ok := data["host"].(string); ok {
+ srv.IPAddress = ip
+ } else if ip, ok := data["address"].(string); ok {
+ srv.IPAddress = ip
+ } else {
+ // Flexible parsing: find any string value that looks like an IP or hostname
+ for _, v := range data {
+ if s, ok := v.(string); ok && s != "" {
+ srv.IPAddress = s
+ break
+ }
+ }
+ }
+
+ if port, ok := data["port"].(float64); ok {
+ srv.Port = int(port)
+ } else if port, ok := data["port"].(int); ok {
+ srv.Port = port
+ } else {
+ srv.Port = 22 // Default port
+ }
+
+ if user, ok := data["username"].(string); ok {
+ srv.Username = user
+ } else if user, ok := data["user"].(string); ok {
+ srv.Username = user
+ }
+
+ return srv, nil
+}
+
+// SaveServer saves a server to Vault
+func (c *Client) SaveServer(ctx context.Context, srv *Server) error {
+ if srv.Group == "" {
+ srv.Group = "default"
+ }
+ data := map[string]interface{}{
+ "ip_address": srv.IPAddress,
+ "port": srv.Port,
+ "username": srv.Username,
+ }
+ return c.WriteSecret(ctx, "servers", srv.Group, srv.Name, data)
+}
+
+// DeleteServer deletes a server from Vault
+func (c *Client) DeleteServer(ctx context.Context, group, name string) error {
+ if group == "" {
+ group = "default"
+ }
+ return c.DeleteSecret(ctx, "servers", group, name)
+}
+
+// ListServerGroups returns all groups for servers
+func (c *Client) ListServerGroups(ctx context.Context) ([]string, error) {
+ return c.ListGroups(ctx, "servers")
+}
+
+// EnvVariable represents an environment variable stored in Vault
+type EnvVariable struct {
+ Name string `json:"name"`
+ Value string `json:"value"`
+ Description string `json:"description"`
+ Group string `json:"group"`
+}
+
+// ListEnvVariables returns all environment variables from Vault (across all groups)
+func (c *Client) ListEnvVariables(ctx context.Context) ([]EnvVariable, error) {
+ groups, err := c.ListGroups(ctx, "env")
+ if err != nil {
+ return nil, err
+ }
+
+ // If no groups exist, check default group
+ if len(groups) == 0 {
+ groups = []string{"default"}
+ }
+
+ var vars []EnvVariable
+ for _, group := range groups {
+ groupVars, err := c.ListEnvVariablesByGroup(ctx, group)
+ if err != nil {
+ continue
+ }
+ vars = append(vars, groupVars...)
+ }
+
+ return vars, nil
+}
+
+// ListEnvVariablesByGroup returns all environment variables in a specific group
+func (c *Client) ListEnvVariablesByGroup(ctx context.Context, group string) ([]EnvVariable, error) {
+ names, err := c.ListSecrets(ctx, "env", group)
+ if err != nil {
+ return nil, err
+ }
+
+ vars := make([]EnvVariable, 0, len(names))
+ for _, name := range names {
+ v, err := c.GetEnvVariable(ctx, group, name)
+ if err != nil {
+ continue
+ }
+ if v != nil {
+ vars = append(vars, *v)
+ }
+ }
+
+ return vars, nil
+}
+
+// GetEnvVariable retrieves a specific environment variable from Vault
+func (c *Client) GetEnvVariable(ctx context.Context, group, name string) (*EnvVariable, error) {
+ if group == "" {
+ group = "default"
+ }
+ data, err := c.ReadSecret(ctx, "env", group, name)
+ if err != nil {
+ return nil, err
+ }
+ if data == nil {
+ return nil, nil
+ }
+
+ v := &EnvVariable{Name: name, Group: group}
+
+ if val, ok := data["value"].(string); ok {
+ v.Value = val
+ }
+
+ if desc, ok := data["description"].(string); ok {
+ v.Description = desc
+ }
+
+ return v, nil
+}
+
+// SaveEnvVariable saves an environment variable to Vault
+func (c *Client) SaveEnvVariable(ctx context.Context, v *EnvVariable) error {
+ if v.Group == "" {
+ v.Group = "default"
+ }
+ data := map[string]interface{}{
+ "value": v.Value,
+ "description": v.Description,
+ }
+ return c.WriteSecret(ctx, "env", v.Group, v.Name, data)
+}
+
+// DeleteEnvVariable deletes an environment variable from Vault
+func (c *Client) DeleteEnvVariable(ctx context.Context, group, name string) error {
+ if group == "" {
+ group = "default"
+ }
+ return c.DeleteSecret(ctx, "env", group, name)
+}
+
+// ListEnvVariableGroups returns all groups for environment variables
+func (c *Client) ListEnvVariableGroups(ctx context.Context) ([]string, error) {
+ return c.ListGroups(ctx, "env")
+}
+
+// BashScript represents a bash script stored in Vault
+type BashScript struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Content string `json:"content"`
+ Filename string `json:"filename"`
+ Group string `json:"group"`
+}
+
+// ListBashScripts returns all bash scripts from Vault (across all groups)
+func (c *Client) ListBashScripts(ctx context.Context) ([]BashScript, error) {
+ groups, err := c.ListGroups(ctx, "scripts")
+ if err != nil {
+ return nil, err
+ }
+
+ // If no groups exist, check default group
+ if len(groups) == 0 {
+ groups = []string{"default"}
+ }
+
+ var scripts []BashScript
+ for _, group := range groups {
+ groupScripts, err := c.ListBashScriptsByGroup(ctx, group)
+ if err != nil {
+ continue
+ }
+ scripts = append(scripts, groupScripts...)
+ }
+
+ return scripts, nil
+}
+
+// ListBashScriptsByGroup returns all bash scripts in a specific group
+func (c *Client) ListBashScriptsByGroup(ctx context.Context, group string) ([]BashScript, error) {
+ names, err := c.ListSecrets(ctx, "scripts", group)
+ if err != nil {
+ return nil, err
+ }
+
+ scripts := make([]BashScript, 0, len(names))
+ for _, name := range names {
+ s, err := c.GetBashScript(ctx, group, name)
+ if err != nil {
+ continue
+ }
+ if s != nil {
+ scripts = append(scripts, *s)
+ }
+ }
+
+ return scripts, nil
+}
+
+// GetBashScript retrieves a specific bash script from Vault
+func (c *Client) GetBashScript(ctx context.Context, group, name string) (*BashScript, error) {
+ if group == "" {
+ group = "default"
+ }
+ data, err := c.ReadSecret(ctx, "scripts", group, name)
+ if err != nil {
+ return nil, err
+ }
+ if data == nil {
+ return nil, nil
+ }
+
+ s := &BashScript{Name: name, Group: group}
+
+ if content, ok := data["content"].(string); ok {
+ s.Content = content
+ }
+
+ if desc, ok := data["description"].(string); ok {
+ s.Description = desc
+ }
+
+ if fn, ok := data["filename"].(string); ok {
+ s.Filename = fn
+ }
+
+ return s, nil
+}
+
+// SaveBashScript saves a bash script to Vault
+func (c *Client) SaveBashScript(ctx context.Context, s *BashScript) error {
+ if s.Group == "" {
+ s.Group = "default"
+ }
+ data := map[string]interface{}{
+ "content": s.Content,
+ "description": s.Description,
+ "filename": s.Filename,
+ }
+ return c.WriteSecret(ctx, "scripts", s.Group, s.Name, data)
+}
+
+// DeleteBashScript deletes a bash script from Vault
+func (c *Client) DeleteBashScript(ctx context.Context, group, name string) error {
+ if group == "" {
+ group = "default"
+ }
+ return c.DeleteSecret(ctx, "scripts", group, name)
+}
+
+// ListBashScriptGroups returns all groups for bash scripts
+func (c *Client) ListBashScriptGroups(ctx context.Context) ([]string, error) {
+ return c.ListGroups(ctx, "scripts")
+}
diff --git a/manage.sh b/manage.sh
index e64b6a7..59cbf89 100755
--- a/manage.sh
+++ b/manage.sh
@@ -2,7 +2,8 @@
# Management script for web-cli
# Usage:
-# ./manage.sh start - Start the server
+# ./manage.sh start - Start the server (with auth)
+# ./manage.sh dev - Start the server WITHOUT auth (dev mode)
# ./manage.sh stop - Stop the server
# ./manage.sh restart - Restart the server
# ./manage.sh status - Check if server is running
@@ -35,8 +36,11 @@ is_running() {
return 1
}
-# Function to start the server
+# Function to start the server (with optional auth)
+# Usage: start [noauth]
start() {
+ local noauth="${1:-}"
+
if is_running; then
echo -e "${YELLOW}Server is already running (PID: $(cat $PID_FILE))${NC}"
exit 1
@@ -50,26 +54,48 @@ start() {
exit 1
fi
- # Generate random password for testing
- echo -e "${YELLOW}Generating test credentials...${NC}"
-
- # Check if openssl is available
- if command -v openssl &> /dev/null; then
- AUTH_PASSWORD=$(openssl rand -base64 16)
+ if [ "$noauth" = "noauth" ]; then
+ # Start without authentication (dev mode)
+ export AUTH_ENABLED=false
+ echo -e "${YELLOW}Starting in DEV MODE (no authentication)${NC}"
+
+ # Start the server in background without auth
+ ./$APP_NAME &
+ PID=$!
+ echo $PID > "$PID_FILE"
+
+ sleep 1
+
+ if is_running; then
+ echo -e "${GREEN}$APP_NAME started successfully (PID: $PID)${NC}"
+ echo -e "${GREEN}Access the application at http://localhost:$PORT${NC}"
+ echo -e "${RED}WARNING: Authentication is DISABLED${NC}"
+ else
+ echo -e "${RED}Failed to start $APP_NAME${NC}"
+ rm -f "$PID_FILE"
+ exit 1
+ fi
else
- # Fallback to simple random generation
- AUTH_PASSWORD="test-$(date +%s)-$(( RANDOM % 10000 ))"
- fi
-
- AUTH_USERNAME="admin"
-
- # Export environment variables for authentication
- export AUTH_ENABLED=true
- export AUTH_USERNAME="$AUTH_USERNAME"
- export AUTH_PASSWORD="$AUTH_PASSWORD"
-
- # Save credentials to file for reference
- cat > .web-cli-credentials << EOF
+ # Generate random password for testing
+ echo -e "${YELLOW}Generating test credentials...${NC}"
+
+ # Check if openssl is available
+ if command -v openssl &> /dev/null; then
+ AUTH_PASSWORD=$(openssl rand -base64 16)
+ else
+ # Fallback to simple random generation
+ AUTH_PASSWORD="test-$(date +%s)-$(( RANDOM % 10000 ))"
+ fi
+
+ AUTH_USERNAME="admin"
+
+ # Export environment variables for authentication
+ export AUTH_ENABLED=true
+ export AUTH_USERNAME="$AUTH_USERNAME"
+ export AUTH_PASSWORD="$AUTH_PASSWORD"
+
+ # Save credentials to file for reference
+ cat > .web-cli-credentials << EOF
# Web CLI Test Credentials
# Generated: $(date)
# ========================================
@@ -82,39 +108,45 @@ curl -u $AUTH_USERNAME:$AUTH_PASSWORD http://localhost:$PORT/api/health
# Web Browser:
Visit http://localhost:$PORT and login with the credentials above
EOF
-
- echo ""
- echo -e "${GREEN}========================================${NC}"
- echo -e "${GREEN} Test Credentials (saved to .web-cli-credentials)${NC}"
- echo -e "${GREEN}========================================${NC}"
- echo -e "${YELLOW} Username: ${NC}$AUTH_USERNAME"
- echo -e "${YELLOW} Password: ${NC}$AUTH_PASSWORD"
- echo -e "${GREEN}========================================${NC}"
- echo ""
-
- # Start the server in background with auth enabled
- ./$APP_NAME &
- PID=$!
- echo $PID > "$PID_FILE"
-
- sleep 1
- if is_running; then
- echo -e "${GREEN}$APP_NAME started successfully (PID: $PID)${NC}"
- echo -e "${GREEN}Access the application at http://localhost:$PORT${NC}"
- echo -e "${YELLOW}Authentication is ENABLED - use credentials above${NC}"
echo ""
- echo -e "${YELLOW}Quick test:${NC}"
- echo -e " curl -u $AUTH_USERNAME:$AUTH_PASSWORD http://localhost:$PORT/api/health"
+ echo -e "${GREEN}========================================${NC}"
+ echo -e "${GREEN} Test Credentials (saved to .web-cli-credentials)${NC}"
+ echo -e "${GREEN}========================================${NC}"
+ echo -e "${YELLOW} Username: ${NC}$AUTH_USERNAME"
+ echo -e "${YELLOW} Password: ${NC}$AUTH_PASSWORD"
+ echo -e "${GREEN}========================================${NC}"
echo ""
- else
- echo -e "${RED}Failed to start $APP_NAME${NC}"
- rm -f "$PID_FILE"
- rm -f .web-cli-credentials
- exit 1
+
+ # Start the server in background with auth enabled
+ ./$APP_NAME &
+ PID=$!
+ echo $PID > "$PID_FILE"
+
+ sleep 1
+
+ if is_running; then
+ echo -e "${GREEN}$APP_NAME started successfully (PID: $PID)${NC}"
+ echo -e "${GREEN}Access the application at http://localhost:$PORT${NC}"
+ echo -e "${YELLOW}Authentication is ENABLED - use credentials above${NC}"
+ echo ""
+ echo -e "${YELLOW}Quick test:${NC}"
+ echo -e " curl -u $AUTH_USERNAME:$AUTH_PASSWORD http://localhost:$PORT/api/health"
+ echo ""
+ else
+ echo -e "${RED}Failed to start $APP_NAME${NC}"
+ rm -f "$PID_FILE"
+ rm -f .web-cli-credentials
+ exit 1
+ fi
fi
}
+# Function to start in dev mode (no auth)
+dev() {
+ start noauth
+}
+
# Function to stop the server
stop() {
if ! is_running; then
@@ -218,6 +250,9 @@ case "${1:-}" in
start)
start
;;
+ dev)
+ dev
+ ;;
stop)
stop
;;
@@ -234,10 +269,11 @@ case "${1:-}" in
reset_data
;;
*)
- echo "Usage: $0 {start|stop|restart|status|fresh|reset}"
+ echo "Usage: $0 {start|stop|restart|status|fresh|reset|dev}"
echo ""
echo "Commands:"
- echo " start - Start the server"
+ echo " start - Start the server (with authentication)"
+ echo " dev - Start the server WITHOUT authentication (dev mode)"
echo " stop - Stop the server"
echo " restart - Restart the server"
echo " status - Check if server is running"
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..5cceb3f
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1221 @@
+{
+ "name": "web-cli",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "devDependencies": {
+ "puppeteer": "^24.35.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",
+ "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@puppeteer/browsers": {
+ "version": "2.11.1",
+ "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.11.1.tgz",
+ "integrity": "sha512-YmhAxs7XPuxN0j7LJloHpfD1ylhDuFmmwMvfy/+6nBSrETT2ycL53LrhgPtR+f+GcPSybQVuQ5inWWu5MrWCpA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "extract-zip": "^2.0.1",
+ "progress": "^2.0.3",
+ "proxy-agent": "^6.5.0",
+ "semver": "^7.7.3",
+ "tar-fs": "^3.1.1",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "browsers": "lib/cjs/main-cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@tootallnate/quickjs-emscripten": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
+ "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "25.0.9",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.9.tgz",
+ "integrity": "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/yauzl": {
+ "version": "2.10.3",
+ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
+ "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/ast-types": {
+ "version": "0.13.4",
+ "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
+ "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/b4a": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz",
+ "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "react-native-b4a": "*"
+ },
+ "peerDependenciesMeta": {
+ "react-native-b4a": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-events": {
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
+ "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "bare-abort-controller": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-abort-controller": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-fs": {
+ "version": "4.5.2",
+ "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.2.tgz",
+ "integrity": "sha512-veTnRzkb6aPHOvSKIOy60KzURfBdUflr5VReI+NSaPL6xf+XLdONQgZgpYvUuZLVQ8dCqxpBAudaOM1+KpAUxw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "bare-events": "^2.5.4",
+ "bare-path": "^3.0.0",
+ "bare-stream": "^2.6.4",
+ "bare-url": "^2.2.2",
+ "fast-fifo": "^1.3.2"
+ },
+ "engines": {
+ "bare": ">=1.16.0"
+ },
+ "peerDependencies": {
+ "bare-buffer": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-buffer": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-os": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
+ "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "engines": {
+ "bare": ">=1.14.0"
+ }
+ },
+ "node_modules/bare-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
+ "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "bare-os": "^3.0.1"
+ }
+ },
+ "node_modules/bare-stream": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
+ "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "streamx": "^2.21.0"
+ },
+ "peerDependencies": {
+ "bare-buffer": "*",
+ "bare-events": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-buffer": {
+ "optional": true
+ },
+ "bare-events": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-url": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
+ "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "bare-path": "^3.0.0"
+ }
+ },
+ "node_modules/basic-ftp": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.1.0.tgz",
+ "integrity": "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chromium-bidi": {
+ "version": "12.0.1",
+ "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-12.0.1.tgz",
+ "integrity": "sha512-fGg+6jr0xjQhzpy5N4ErZxQ4wF7KLEvhGZXD6EgvZKDhu7iOhZXnZhcDxPJDcwTcrD48NPzOCo84RP2lv3Z+Cg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "mitt": "^3.0.1",
+ "zod": "^3.24.1"
+ },
+ "peerDependencies": {
+ "devtools-protocol": "*"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cosmiconfig": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
+ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "env-paths": "^2.2.1",
+ "import-fresh": "^3.3.0",
+ "js-yaml": "^4.1.0",
+ "parse-json": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/d-fischer"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.9.5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/data-uri-to-buffer": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
+ "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/degenerator": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
+ "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ast-types": "^0.13.4",
+ "escodegen": "^2.1.0",
+ "esprima": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/devtools-protocol": {
+ "version": "0.0.1534754",
+ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1534754.tgz",
+ "integrity": "sha512-26T91cV5dbOYnXdJi5qQHoTtUoNEqwkHcAyu/IKtjIAxiEqPMrDiRkDOPWVsGfNZGmlQVHQbZRSjD8sxagWVsQ==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
+ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escodegen": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
+ "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/events-universal": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
+ "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-events": "^2.7.0"
+ }
+ },
+ "node_modules/extract-zip": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
+ "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "get-stream": "^5.1.0",
+ "yauzl": "^2.10.0"
+ },
+ "bin": {
+ "extract-zip": "cli.js"
+ },
+ "engines": {
+ "node": ">= 10.17.0"
+ },
+ "optionalDependencies": {
+ "@types/yauzl": "^2.9.1"
+ }
+ },
+ "node_modules/fast-fifo": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
+ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fd-slicer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+ "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pend": "~1.2.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-uri": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz",
+ "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "basic-ftp": "^5.0.2",
+ "data-uri-to-buffer": "^6.0.2",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ip-address": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
+ "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/mitt": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/netmask": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
+ "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/pac-proxy-agent": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz",
+ "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tootallnate/quickjs-emscripten": "^0.23.0",
+ "agent-base": "^7.1.2",
+ "debug": "^4.3.4",
+ "get-uri": "^6.0.1",
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.6",
+ "pac-resolver": "^7.0.1",
+ "socks-proxy-agent": "^8.0.5"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/pac-resolver": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
+ "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "degenerator": "^5.0.0",
+ "netmask": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/proxy-agent": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
+ "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "^4.3.4",
+ "http-proxy-agent": "^7.0.1",
+ "https-proxy-agent": "^7.0.6",
+ "lru-cache": "^7.14.1",
+ "pac-proxy-agent": "^7.1.0",
+ "proxy-from-env": "^1.1.0",
+ "socks-proxy-agent": "^8.0.5"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pump": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+ "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/puppeteer": {
+ "version": "24.35.0",
+ "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.35.0.tgz",
+ "integrity": "sha512-sbjB5JnJ+3nwgSdRM/bqkFXqLxRz/vsz0GRIeTlCk+j+fGpqaF2dId9Qp25rXz9zfhqnN9s0krek1M/C2GDKtA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@puppeteer/browsers": "2.11.1",
+ "chromium-bidi": "12.0.1",
+ "cosmiconfig": "^9.0.0",
+ "devtools-protocol": "0.0.1534754",
+ "puppeteer-core": "24.35.0",
+ "typed-query-selector": "^2.12.0"
+ },
+ "bin": {
+ "puppeteer": "lib/cjs/puppeteer/node/cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/puppeteer-core": {
+ "version": "24.35.0",
+ "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.35.0.tgz",
+ "integrity": "sha512-vt1zc2ME0kHBn7ZDOqLvgvrYD5bqNv5y2ZNXzYnCv8DEtZGw/zKhljlrGuImxptZ4rq+QI9dFGrUIYqG4/IQzA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@puppeteer/browsers": "2.11.1",
+ "chromium-bidi": "12.0.1",
+ "debug": "^4.4.3",
+ "devtools-protocol": "0.0.1534754",
+ "typed-query-selector": "^2.12.0",
+ "webdriver-bidi-protocol": "0.3.10",
+ "ws": "^8.19.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks": {
+ "version": "2.8.7",
+ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
+ "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ip-address": "^10.0.1",
+ "smart-buffer": "^4.2.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks-proxy-agent": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
+ "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "^4.3.4",
+ "socks": "^2.8.3"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/streamx": {
+ "version": "2.23.0",
+ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
+ "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "events-universal": "^1.0.0",
+ "fast-fifo": "^1.3.2",
+ "text-decoder": "^1.1.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tar-fs": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
+ "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0",
+ "tar-stream": "^3.1.5"
+ },
+ "optionalDependencies": {
+ "bare-fs": "^4.0.1",
+ "bare-path": "^3.0.0"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
+ "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "b4a": "^1.6.4",
+ "fast-fifo": "^1.2.0",
+ "streamx": "^2.15.0"
+ }
+ },
+ "node_modules/text-decoder": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
+ "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "b4a": "^1.6.4"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/typed-query-selector": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
+ "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/webdriver-bidi-protocol": {
+ "version": "0.3.10",
+ "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.10.tgz",
+ "integrity": "sha512-5LAE43jAVLOhB/QqX4bwSiv0Hg1HBfMmOuwBSXHdvg4GMGu9Y0lIq7p4R/yySu6w74WmaR4GM4H9t2IwLW7hgw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/ws": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
+ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yauzl": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+ "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-crc32": "~0.2.3",
+ "fd-slicer": "~1.1.0"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..8ae434d
--- /dev/null
+++ b/package.json
@@ -0,0 +1,5 @@
+{
+ "devDependencies": {
+ "puppeteer": "^24.35.0"
+ }
+}