# Ginxsom Web Admin Interface - Technical Specification ## Overview A minimal single-page admin interface for ginxsom server management, built with vanilla JavaScript and C-based API endpoints. Uses Nostr-compliant authentication with admin pubkey verification. ## Architecture ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ Web Browser │───▶│ nginx │───▶│ ginxsom FastCGI │ │ (admin.html) │ │ (static files │ │ + admin_api │ │ │ │ + API proxy) │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ ▼ ┌─────────────────┐ │ SQLite DB │ │ (ginxsom.db) │ └─────────────────┘ ``` ## File Structure ``` src/ ├── main.c # Existing main FastCGI application ├── admin_api.c # NEW: Admin API endpoint handlers ├── admin_api.h # NEW: Admin API function declarations └── ginxsom.h # Existing shared headers admin/ ├── admin.html # Single-page admin interface (with inline CSS/JS) └── README.md # Setup and usage guide config/ └── local-nginx.conf # Updated with admin interface routes ``` ## Backend Implementation Details ### 1. admin_api.h Header File ```c #ifndef ADMIN_API_H #define ADMIN_API_H #include "ginxsom.h" // Main API request handler void handle_admin_api_request(const char* method, const char* uri); // Individual endpoint handlers void handle_stats_api(void); void handle_config_get_api(void); void handle_config_put_api(void); void handle_files_api(void); void handle_health_api(void); // Admin authentication functions int authenticate_admin_request(const char* auth_header); int is_admin_enabled(void); int verify_admin_pubkey(const char* event_pubkey); // Utility functions void send_json_response(int status, const char* json_content); void send_json_error(int status, const char* error, const char* message); int parse_query_params(const char* query_string, char params[][256], int max_params); #endif ``` ### 2. admin_api.c Implementation Structure #### API Router Function ```c void handle_admin_api_request(const char* method, const char* uri) { const char* path = uri + 4; // Skip "/api" // Check if admin interface is enabled if (!is_admin_enabled()) { send_json_error(503, "admin_disabled", "Admin interface is disabled"); return; } // Authentication required for all admin operations except health check if (strcmp(path, "/health") != 0) { const char* auth_header = getenv("HTTP_AUTHORIZATION"); if (!authenticate_admin_request(auth_header)) { send_json_error(401, "admin_auth_required", "Valid admin authentication required"); return; } } if (strcmp(method, "GET") == 0) { if (strcmp(path, "/stats") == 0) { handle_stats_api(); } else if (strcmp(path, "/config") == 0) { handle_config_get_api(); } else if (strncmp(path, "/files", 6) == 0) { handle_files_api(); } else if (strcmp(path, "/health") == 0) { handle_health_api(); } else { send_json_error(404, "not_found", "API endpoint not found"); } } else if (strcmp(method, "PUT") == 0) { if (strcmp(path, "/config") == 0) { handle_config_put_api(); } else { send_json_error(405, "method_not_allowed", "Method not allowed"); } } else { send_json_error(405, "method_not_allowed", "Method not allowed"); } } ``` #### Admin Authentication Functions ```c int authenticate_admin_request(const char* auth_header) { if (!auth_header) { return 0; // No auth header } // Use existing authentication system with "admin" method int auth_result = authenticate_request(auth_header, "admin", NULL); if (auth_result != NOSTR_SUCCESS) { return 0; // Invalid Nostr event } // Extract pubkey from validated event using existing parser char event_json[4096]; int parse_result = parse_authorization_header(auth_header, event_json, sizeof(event_json)); if (parse_result != NOSTR_SUCCESS) { return 0; } cJSON* event = cJSON_Parse(event_json); if (!event) { return 0; } cJSON* pubkey_json = cJSON_GetObjectItem(event, "pubkey"); if (!pubkey_json || !cJSON_IsString(pubkey_json)) { cJSON_Delete(event); return 0; } const char* event_pubkey = cJSON_GetStringValue(pubkey_json); int is_admin = verify_admin_pubkey(event_pubkey); cJSON_Delete(event); return is_admin; } int verify_admin_pubkey(const char* event_pubkey) { if (!event_pubkey) { return 0; } sqlite3* db; sqlite3_stmt* stmt; int rc, is_admin = 0; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { return 0; } const char* sql = "SELECT value FROM server_config WHERE key = 'admin_pubkey'"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { const char* admin_pubkey = (const char*)sqlite3_column_text(stmt, 0); if (admin_pubkey && strcmp(event_pubkey, admin_pubkey) == 0) { is_admin = 1; } } sqlite3_finalize(stmt); } sqlite3_close(db); return is_admin; } int is_admin_enabled(void) { sqlite3* db; sqlite3_stmt* stmt; int rc, enabled = 0; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { return 0; // Default disabled if can't access DB } const char* sql = "SELECT value FROM server_config WHERE key = 'admin_enabled'"; rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (rc == SQLITE_OK) { rc = sqlite3_step(stmt); if (rc == SQLITE_ROW) { const char* value = (const char*)sqlite3_column_text(stmt, 0); enabled = (value && strcmp(value, "true") == 0) ? 1 : 0; } sqlite3_finalize(stmt); } sqlite3_close(db); return enabled; } ``` #### Statistics API Handler ```c void handle_stats_api(void) { sqlite3* db; sqlite3_stmt* stmt; int rc; rc = sqlite3_open_v2(DB_PATH, &db, SQLITE_OPEN_READONLY, NULL); if (rc) { send_json_error(500, "database_error", "Failed to open database"); return; } // Query storage_stats view const char* sql = "SELECT total_blobs, total_bytes, avg_blob_size, " "unique_uploaders, first_upload, last_upload FROM storage_stats"; // ... SQLite query implementation // ... JSON response generation sqlite3_close(db); } ``` ### 3. main.c Integration Points #### Minimal changes to main.c: ```c #include "admin_api.h" // Add at top // In main() request routing section, add this condition: } else if (strncmp(request_uri, "/api/", 5) == 0) { // Route API calls to admin handlers handle_admin_api_request(request_method, request_uri); ``` ## API Endpoint Specifications ### GET /api/stats **Purpose**: Retrieve server statistics and metrics **Response**: ```json { "status": "success", "data": { "total_files": 1234, "total_bytes": 104857600, "total_size_mb": 100.0, "unique_uploaders": 56, "first_upload": 1693929600, "last_upload": 1704067200, "avg_file_size": 85049, "file_types": { "image/png": 45, "image/jpeg": 32, "application/pdf": 12, "other": 8 } } } ``` ### GET /api/config **Purpose**: Retrieve current server configuration **Response**: ```json { "status": "success", "data": { "max_file_size": "104857600", "require_auth": "false", "server_name": "ginxsom", "nip94_enabled": "true", "cdn_origin": "http://localhost:9001", "auth_rules_enabled": "false", "auth_cache_ttl": "300" } } ``` ### PUT /api/config **Purpose**: Update server configuration **Request Body**: ```json { "max_file_size": "209715200", "require_auth": "true", "nip94_enabled": "true", "cdn_origin": "https://cdn.example.com" } ``` **Response**: ```json { "status": "success", "message": "Configuration updated successfully", "updated_keys": ["max_file_size", "require_auth"] } ``` ### GET /api/files?limit=50&offset=0 **Purpose**: Retrieve recent files with pagination **Parameters**: - `limit` (default: 50): Number of files to return - `offset` (default: 0): Pagination offset **Response**: ```json { "status": "success", "data": { "files": [ { "sha256": "b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553", "size": 184292, "type": "application/pdf", "uploaded_at": 1725105921, "uploader_pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", "filename": "document.pdf", "url": "http://localhost:9001/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf" } ], "total": 1234, "limit": 50, "offset": 0 } } ``` ### GET /api/health **Purpose**: System health check **Response**: ```json { "status": "success", "data": { "database": "connected", "blob_directory": "accessible", "disk_usage": { "total_bytes": 1073741824, "used_bytes": 536870912, "available_bytes": 536870912, "usage_percent": 50.0 }, "server_time": 1704067200, "uptime": 3600 } } ``` ## Frontend Implementation Details ### Single File Structure (admin.html) **Complete self-contained HTML file with inline CSS and JavaScript** ```html Ginxsom Admin

Ginxsom Admin Dashboard

Loading...

Server Statistics

Total Files

-

Storage Used

-

Unique Users

-

Server Configuration

Recent Files

``` ## nginx Configuration ### Updated local-nginx.conf additions: ```nginx # Admin interface (single file) location /admin { alias admin/; try_files $uri /admin.html; } # Admin API endpoints location /api/ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root/ginxsom.fcgi; fastcgi_pass fastcgi_backend; # CORS headers for admin interface add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods "GET, PUT, OPTIONS"; add_header Access-Control-Allow-Headers "Content-Type"; } ``` ## Database Schema Requirements ### Existing tables used: - `blobs` - File metadata and statistics - `server_config` - Configuration key-value pairs (ENHANCED) - `storage_stats` - Aggregated statistics view ### Required server_config additions: ```sql INSERT OR IGNORE INTO server_config (key, value, description) VALUES ('admin_pubkey', '', 'Nostr public key authorized for admin operations'), ('admin_enabled', 'false', 'Enable admin interface (requires admin_pubkey)'); ``` ## Security Considerations 1. **Nostr Authentication**: All admin operations require valid Nostr event signatures 2. **Admin Pubkey Verification**: Only events signed by configured admin pubkey are accepted 3. **Event Validation**: Full Nostr event structure and signature verification 4. **Expiration Enforcement**: Admin events must include expiration timestamps 5. **Input Validation**: All config updates validated before DB storage 6. **SQL Injection Prevention**: Prepared statements only 7. **CORS**: Controlled CORS headers for API endpoints 8. **Rate Limiting**: Consider nginx rate limiting for API endpoints ## Testing Strategy 1. **Unit Tests**: Test each API endpoint individually 2. **Integration Tests**: Test frontend-backend communication 3. **Manual Testing**: Browser-based UI testing 4. **Performance Tests**: API response times under load ## Implementation Steps - API First Approach 1. Create [`admin_api.h`](src/admin_api.h) header file 2. Implement [`admin_api.c`](src/admin_api.c) with all endpoint handlers 3. Add minimal routing code to [`main.c`](src/main.c) 4. Create [`admin_test.sh`](tests/admin_test.sh) script with nak/curl testing 5. Update [`local-nginx.conf`](config/local-nginx.conf) for API routing 6. Test each API endpoint with command-line tools 7. Document admin API usage with practical examples ## Testing Strategy - Command Line First ### Admin Test Script (admin_test.sh) **Complete test script for admin API using nak and curl:** ```bash #!/bin/bash # Ginxsom Admin API Test Script # Tests admin API endpoints using nak (for Nostr events) and curl # # Prerequisites: # - nak: https://github.com/fiatjaf/nak # - curl, jq # - Admin pubkey configured in ginxsom server_config set -e # Configuration GINXSOM_URL="http://localhost:9001" ADMIN_PRIVKEY="${ADMIN_PRIVKEY:-}" ADMIN_PUBKEY="" # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Helper functions log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; } check_dependencies() { log_info "Checking dependencies..." for cmd in nak curl jq; do if ! command -v $cmd &> /dev/null; then log_error "$cmd is not installed" exit 1 fi done log_success "All dependencies found" } generate_admin_keys() { if [[ -z "$ADMIN_PRIVKEY" ]]; then log_info "Generating new admin key pair..." ADMIN_PRIVKEY=$(nak key generate) log_warning "Generated admin private key: $ADMIN_PRIVKEY" log_warning "Save this key: export ADMIN_PRIVKEY='$ADMIN_PRIVKEY'" fi ADMIN_PUBKEY=$(echo "$ADMIN_PRIVKEY" | nak key public) log_info "Admin public key: $ADMIN_PUBKEY" } create_admin_event() { local method="$1" local content="admin_request" local expiration=$(($(date +%s) + 3600)) # 1 hour from now # Create Nostr event with nak local event=$(nak event -k 24242 -c "$content" \ --tag t="$method" \ --tag expiration="$expiration" \ --sec "$ADMIN_PRIVKEY") echo "$event" } send_admin_request() { local method="$1" local endpoint="$2" local data="$3" log_info "Testing $method $endpoint" # Create authenticated Nostr event local event=$(create_admin_event "$method") local auth_header="Nostr $(echo "$event" | base64 -w 0)" # Send request with curl local curl_args=(-s -w "%{http_code}" -H "Authorization: $auth_header") if [[ "$method" == "PUT" && -n "$data" ]]; then curl_args+=(-H "Content-Type: application/json" -d "$data") fi local response=$(curl "${curl_args[@]}" -X "$method" "$GINXSOM_URL$endpoint") local http_code="${response: -3}" local body="${response%???}" if [[ "$http_code" =~ ^2 ]]; then log_success "$method $endpoint - HTTP $http_code" if [[ -n "$body" ]]; then echo "$body" | jq . 2>/dev/null || echo "$body" fi else log_error "$method $endpoint - HTTP $http_code" echo "$body" | jq . 2>/dev/null || echo "$body" fi return $([[ "$http_code" =~ ^2 ]]) } test_health_endpoint() { log_info "=== Testing Health Endpoint (no auth required) ===" local response=$(curl -s -w "%{http_code}" "$GINXSOM_URL/api/health") local http_code="${response: -3}" local body="${response%???}" if [[ "$http_code" =~ ^2 ]]; then log_success "GET /api/health - HTTP $http_code" echo "$body" | jq . else log_error "GET /api/health - HTTP $http_code" echo "$body" fi } test_stats_endpoint() { log_info "=== Testing Statistics Endpoint ===" send_admin_request "GET" "/api/stats" } test_config_endpoints() { log_info "=== Testing Configuration Endpoints ===" # Get current config send_admin_request "GET" "/api/config" # Update config local config_update='{ "max_file_size": "209715200", "require_auth": "true", "nip94_enabled": "true" }' send_admin_request "PUT" "/api/config" "$config_update" # Get config again to verify send_admin_request "GET" "/api/config" } test_files_endpoint() { log_info "=== Testing Files Endpoint ===" send_admin_request "GET" "/api/files?limit=10&offset=0" } configure_server_admin() { log_warning "=== Server Configuration Required ===" log_warning "Add the following to your ginxsom database:" log_warning "" log_warning "sqlite3 db/ginxsom.db << EOF" log_warning "INSERT OR REPLACE INTO server_config (key, value, description) VALUES" log_warning " ('admin_pubkey', '$ADMIN_PUBKEY', 'Admin authorized pubkey')," log_warning " ('admin_enabled', 'true', 'Enable admin interface');" log_warning "EOF" log_warning "" log_warning "Then restart ginxsom server." } main() { echo "=== Ginxsom Admin API Test Suite ===" echo "" check_dependencies generate_admin_keys configure_server_admin echo "" read -p "Press Enter after configuring the server..." echo "" # Test endpoints test_health_endpoint echo "" test_stats_endpoint echo "" test_config_endpoints echo "" test_files_endpoint echo "" log_success "Admin API testing complete!" log_info "Admin pubkey for server config: $ADMIN_PUBKEY" } # Allow sourcing for individual function testing if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main "$@" fi ``` ### Usage Examples **1. Run complete test suite:** ```bash chmod +x tests/admin_test.sh ./tests/admin_test.sh ``` **2. Use existing admin key:** ```bash export ADMIN_PRIVKEY="your_existing_admin_private_key_here" ./tests/admin_test.sh ``` **3. Manual testing with nak and curl:** ```bash # Generate admin event ADMIN_PRIVKEY="your_private_key" EVENT=$(nak event -k 24242 -c "admin_request" \ --tag t="GET" \ --tag expiration="$(date -d '+1 hour' +%s)" \ --sec "$ADMIN_PRIVKEY") # Send authenticated request AUTH_HEADER="Nostr $(echo "$EVENT" | base64 -w 0)" curl -H "Authorization: $AUTH_HEADER" http://localhost:9001/api/stats ``` **4. Configure server for admin access:** ```bash # Add admin pubkey to database ADMIN_PUBKEY="your_admin_public_key" sqlite3 db/ginxsom.db << EOF INSERT OR REPLACE INTO server_config (key, value, description) VALUES ('admin_pubkey', '$ADMIN_PUBKEY', 'Admin authorized pubkey'), ('admin_enabled', 'true', 'Enable admin interface'); EOF ``` ### Benefits of API-First Approach - **Immediate testing capability** with command-line tools - **Foundation for Nostr relay integration** in the future - **Proper authentication testing** using real Nostr events - **Easy debugging** with verbose curl output - **Reusable components** for future web interface - **Command-line administration** without web dependencies This specification provides complete implementation details focused on API-first development with robust command-line testing tools.