From e0e4e457da2fd9d517107d60db17ddeec164f1e5 Mon Sep 17 00:00:00 2001 From: Gamosoft Date: Tue, 25 Nov 2025 16:41:08 +0100 Subject: [PATCH] use environment variables (breaking change). added CORS. safe exception handler --- Dockerfile | 13 +- README.md | 1 + backend/main.py | 128 ++++++-- config.yaml | 10 +- documentation/AUTHENTICATION.md | 6 +- documentation/ENVIRONMENT_VARIABLES.md | 422 +++++++++++++++++++++++++ frontend/index.html | 2 +- generate_password.py | 2 +- render.yaml | 8 +- run.py | 9 +- 10 files changed, 556 insertions(+), 45 deletions(-) create mode 100644 documentation/ENVIRONMENT_VARIABLES.md diff --git a/Dockerfile b/Dockerfile index 0c49b53..6a81c1f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,13 +33,16 @@ COPY generate_password.py . # Create data directory RUN mkdir -p data -# Expose port +# Expose port (default, can be overridden) EXPOSE 8000 -# Health check +# Set default port (can be overridden via environment variable) +ENV PORT=8000 + +# Health check (uses PORT env var) HEALTHCHECK --interval=60s --timeout=3s --start-period=5s --retries=3 \ - CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" + CMD python -c "import os, urllib.request; urllib.request.urlopen(f'http://localhost:{os.getenv(\"PORT\", \"8000\")}/health')" -# Run the application -CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"] +# Run the application (shell form to allow environment variable expansion) +CMD uvicorn backend.main:app --host 0.0.0.0 --port $PORT diff --git a/README.md b/README.md index ea09441..79c5913 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,7 @@ Want to learn more? - šŸ”Œ **[PLUGINS.md](documentation/PLUGINS.md)** - Plugin system and available plugins - 🌐 **[API.md](documentation/API.md)** - REST API documentation and examples - šŸ” **[AUTHENTICATION.md](documentation/AUTHENTICATION.md)** - Enable password protection for your instance +- šŸ”§ **[ENVIRONMENT_VARIABLES.md](documentation/ENVIRONMENT_VARIABLES.md)** - Configure settings via environment variables šŸ’” **Pro Tip:** If you clone this repository, you can mount the `documentation/` folder to view these docs inside the app: diff --git a/backend/main.py b/backend/main.py index b542ed2..6233117 100644 --- a/backend/main.py +++ b/backend/main.py @@ -55,6 +55,25 @@ with open(version_path, 'r', encoding='utf-8') as f: version = f.read().strip() config['app']['version'] = version +# Environment variable overrides for authentication settings +# Allows different configs for local vs production deployments +if 'AUTHENTICATION_ENABLED' in os.environ: + auth_enabled = os.getenv('AUTHENTICATION_ENABLED', 'false').lower() in ('true', '1', 'yes') + config['authentication']['enabled'] = auth_enabled + print(f"šŸ” Authentication {'ENABLED' if auth_enabled else 'DISABLED'} (from AUTHENTICATION_ENABLED env var)") +else: + print(f"šŸ” Authentication {'ENABLED' if config.get('authentication', {}).get('enabled', False) else 'DISABLED'} (from config.yaml)") + +# Allow password hash to be set via environment variable (useful for demos) +if 'AUTHENTICATION_PASSWORD_HASH' in os.environ: + config['authentication']['password_hash'] = os.getenv('AUTHENTICATION_PASSWORD_HASH') + print("šŸ”‘ Password hash loaded from AUTHENTICATION_PASSWORD_HASH env var") + +# Allow secret key to be set via environment variable (for session security) +if 'AUTHENTICATION_SECRET_KEY' in os.environ: + config['authentication']['secret_key'] = os.getenv('AUTHENTICATION_SECRET_KEY') + print("šŸ” Secret key loaded from AUTHENTICATION_SECRET_KEY env var") + # Initialize app app = FastAPI( title=config['app']['name'], @@ -62,33 +81,69 @@ app = FastAPI( version=config['app']['version'] ) -# CORS middleware for development +# CORS middleware configuration +# Use config.yaml to control allowed origins (default: ["*"] for self-hosted simplicity) +allowed_origins = config.get('server', {}).get('allowed_origins', ["*"]) app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=allowed_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) +print(f"🌐 CORS allowed origins: {allowed_origins}") + +# ============================================================================ +# Security Helpers +# ============================================================================ + +def safe_error_message(error: Exception, user_message: str = "An error occurred") -> str: + """ + Return safe error message for API responses. + In debug mode, returns full error details. + In production, returns generic message and logs full details server-side. + + Args: + error: The caught exception + user_message: User-friendly message to show in production + + Returns: + Safe error message string + """ + error_details = f"{type(error).__name__}: {str(error)}" + + # Always log the full error server-side + print(f"āš ļø [ERROR] {error_details}") + + # In debug mode, return detailed error to help with development + if config.get('server', {}).get('debug', False): + return error_details + + # In production, return generic message (full details already logged) + return user_message # Session middleware for authentication app.add_middleware( SessionMiddleware, - secret_key=config.get('security', {}).get('secret_key', 'insecure_default_key_change_this'), - max_age=config.get('security', {}).get('session_max_age', 604800), # 7 days default + secret_key=config.get('authentication', {}).get('secret_key', 'insecure_default_key_change_this'), + max_age=config.get('authentication', {}).get('session_max_age', 604800), # 7 days default same_site='lax', https_only=False # Set to True if using HTTPS ) -# Rate limiting (enabled via environment variable for demo deployments) -ENABLE_RATE_LIMITING = os.getenv('ENABLE_RATE_LIMITING', 'false').lower() == 'true' +# Demo mode - Centralizes all demo-specific restrictions +# When DEMO_MODE=true, enables rate limiting and other demo protections +# Add additional demo restrictions here as needed (e.g., disable certain features) +DEMO_MODE = os.getenv('DEMO_MODE', 'false').lower() in ('true', '1', 'yes') -if ENABLE_RATE_LIMITING: +if DEMO_MODE: + # Enable rate limiting for demo deployments limiter = Limiter(key_func=get_remote_address, default_limits=["200/hour"]) app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) - print("āš ļø Rate limiting ENABLED (ENABLE_RATE_LIMITING=true)") + print("šŸŽ­ DEMO MODE enabled - Rate limiting active") else: + # Production/self-hosted mode - no restrictions # Create a dummy limiter that doesn't actually limit class DummyLimiter: def limit(self, *args, **kwargs): @@ -96,7 +151,6 @@ else: return func return decorator limiter = DummyLimiter() - print("āœ… Rate limiting DISABLED (set ENABLE_RATE_LIMITING=true to enable)") # Ensure required directories exist ensure_directories(config) @@ -149,7 +203,7 @@ async def http_exception_handler(request: Request, exc: HTTPException): def auth_enabled() -> bool: """Check if authentication is enabled in config""" - return config.get('security', {}).get('enabled', False) + return config.get('authentication', {}).get('enabled', False) async def require_auth(request: Request): @@ -164,7 +218,7 @@ async def require_auth(request: Request): def verify_password(password: str) -> bool: """Verify password against stored hash""" - password_hash = config.get('security', {}).get('password_hash', '') + password_hash = config.get('authentication', {}).get('password_hash', '') if not password_hash: return False @@ -430,8 +484,8 @@ async def get_config(): "tagline": config['app']['tagline'], "version": config['app']['version'], "searchEnabled": config['search']['enabled'], - "security": { - "enabled": config.get('security', {}).get('enabled', False) + "authentication": { + "enabled": config.get('authentication', {}).get('enabled', False) } } @@ -475,8 +529,10 @@ async def create_new_folder(request: Request, data: dict): "path": folder_path, "message": "Folder created successfully" } + except HTTPException: + raise except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to create folder")) @api_router.get("/images/{image_path:path}") @@ -506,7 +562,7 @@ async def get_image(image_path: str): except HTTPException: raise except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to load image")) @api_router.post("/upload-image") @@ -562,7 +618,7 @@ async def upload_image(request: Request, file: UploadFile = File(...), note_path except HTTPException: raise except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to upload image")) @api_router.post("/notes/move") @@ -590,8 +646,10 @@ async def move_note_endpoint(request: Request, data: dict): "newPath": new_path, "message": "Note moved successfully" } + except HTTPException: + raise except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to move note")) @api_router.post("/folders/move") @@ -616,8 +674,10 @@ async def move_folder_endpoint(request: Request, data: dict): "newPath": new_path, "message": "Folder moved successfully" } + except HTTPException: + raise except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to move folder")) @api_router.post("/folders/rename") @@ -642,8 +702,10 @@ async def rename_folder_endpoint(request: Request, data: dict): "newPath": new_path, "message": "Folder renamed successfully" } + except HTTPException: + raise except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to rename folder")) @api_router.delete("/folders/{folder_path:path}") @@ -664,8 +726,10 @@ async def delete_folder_endpoint(request: Request, folder_path: str): "path": folder_path, "message": "Folder deleted successfully" } + except HTTPException: + raise except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to delete folder")) # --- Tags Endpoints --- @@ -682,7 +746,7 @@ async def list_tags(): tags = get_all_tags(config['storage']['notes_dir']) return {"tags": tags} except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to load tags")) @api_router.get("/tags/{tag_name}") @@ -704,7 +768,7 @@ async def get_notes_by_tag_endpoint(tag_name: str): "notes": notes } except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to get notes by tag")) # --- Notes Endpoints --- @@ -717,7 +781,7 @@ async def list_notes(): folders = get_all_folders(config['storage']['notes_dir']) return {"notes": notes, "folders": folders} except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to list notes")) @api_router.get("/notes/{note_path:path}") @@ -741,7 +805,7 @@ async def get_note(note_path: str): except HTTPException: raise except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to load note")) @api_router.post("/notes/{note_path:path}") @@ -779,8 +843,10 @@ async def create_or_update_note(request: Request, note_path: str, content: dict) "message": "Note created successfully" if is_new_note else "Note saved successfully", "content": note_content # Return the (potentially modified) content } + except HTTPException: + raise except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to save note")) @api_router.delete("/notes/{note_path:path}") @@ -800,8 +866,10 @@ async def remove_note(request: Request, note_path: str): "success": True, "message": "Note deleted successfully" } + except HTTPException: + raise except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to delete note")) @api_router.get("/search") @@ -820,7 +888,7 @@ async def search(q: str): except HTTPException: raise except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=safe_error_message(e, "Search failed")) @api_router.get("/graph") @@ -844,7 +912,7 @@ async def get_graph(): return {"nodes": nodes, "edges": edges} except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to generate graph data")) @api_router.get("/plugins") @@ -864,7 +932,7 @@ async def calculate_note_stats(content: str): stats = plugin.calculate_stats(content) return {"enabled": True, "stats": stats} except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to calculate note statistics")) @api_router.post("/plugins/{plugin_name}/toggle") @@ -884,7 +952,7 @@ async def toggle_plugin(request: Request, plugin_name: str, enabled: dict): "enabled": is_enabled } except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + raise HTTPException(status_code=500, detail=safe_error_message(e, "Failed to toggle plugin")) @app.get("/health") diff --git a/config.yaml b/config.yaml index 6d0f215..6d57cd6 100644 --- a/config.yaml +++ b/config.yaml @@ -10,6 +10,14 @@ server: port: 8000 reload: false # Set to true for development + # CORS (Cross-Origin Resource Sharing) configuration + # For self-hosted use, "*" is fine. For production, specify allowed domains. + # Examples: ["http://localhost:8000", "https://yourdomain.com"] + allowed_origins: ["*"] + + # Debug mode - shows detailed error messages (DISABLE in production!) + debug: false + storage: notes_dir: "./data" plugins_dir: "./plugins" @@ -17,7 +25,7 @@ storage: search: enabled: true -security: +authentication: # Authentication settings # Set enabled to true to require login enabled: false diff --git a/documentation/AUTHENTICATION.md b/documentation/AUTHENTICATION.md index fc3375a..dd09be4 100644 --- a/documentation/AUTHENTICATION.md +++ b/documentation/AUTHENTICATION.md @@ -102,10 +102,10 @@ python -c "import secrets; print(secrets.token_hex(32))" ### Step 3: Update `config.yaml` -Edit your `config.yaml` and update the security section: +Edit your `config.yaml` and update the authentication section: ```yaml -security: +authentication: # Enable authentication enabled: true @@ -172,7 +172,7 @@ This is a **simple authentication system** designed for **self-hosted, single-us To disable authentication and allow open access: ```yaml -security: +authentication: enabled: false ``` diff --git a/documentation/ENVIRONMENT_VARIABLES.md b/documentation/ENVIRONMENT_VARIABLES.md new file mode 100644 index 0000000..8001206 --- /dev/null +++ b/documentation/ENVIRONMENT_VARIABLES.md @@ -0,0 +1,422 @@ +# šŸ”§ Environment Variables + +NoteDiscovery supports environment variables to override configuration settings, allowing different behavior in different deployment environments (local, staging, production). + +## šŸ“‹ Available Environment Variables + +### Core Settings + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `PORT` | integer | `8000` | HTTP port for the application (Docker, run.py) | + +> **Note**: Advanced server settings (CORS origins, debug mode) are configured via `config.yaml` only, not via environment variables. See [config.yaml](#advanced-server-configuration) for details. + +### Authentication + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `AUTHENTICATION_ENABLED` | boolean | `config.yaml` | Enable/disable authentication | +| `AUTHENTICATION_PASSWORD_HASH` | string | `config.yaml` | Bcrypt password hash | +| `AUTHENTICATION_SECRET_KEY` | string | `config.yaml` | Session secret key (for session security) | + +### Demo Mode + +| Variable | Type | Default | Description | +|----------|------|---------|-------------| +| `DEMO_MODE` | boolean | `false` | Enable demo mode (enables rate limiting and other demo restrictions) | + +## šŸŽÆ Configuration Priority + +Configuration is loaded in this order (later overrides earlier): + +1. **`config.yaml`** - Default configuration file +2. **Environment Variables** - Runtime overrides +3. **Command Line** - Highest priority (if applicable) + +## šŸ’” Common Use Cases + +### Use Case 1: Local Development (No Auth) + +**Local setup:** +```bash +# No environment variables needed +# Uses config.yaml defaults (auth disabled) +python run.py +``` + +**`config.yaml`:** +```yaml +authentication: + enabled: false +``` + +**Result:** āœ… No authentication, quick local testing + +--- + +### Use Case 2: Public Demo (Auth + Rate Limiting) + +**Render deployment:** +```yaml +# render.yaml +envVars: + - key: DEMO_MODE + value: "true" + - key: AUTHENTICATION_ENABLED + value: "true" +``` + +**`config.yaml`:** +```yaml +authentication: + enabled: false # Overridden by AUTHENTICATION_ENABLED=true +``` + +**Result:** āœ… Demo mode enabled (rate limiting + restrictions), authentication enabled + +--- + +### Use Case 3: Private Demo (Custom Password & Secret Key) + +**Generate password hash and secret key:** +```bash +# Generate password hash +python generate_password.py +# Enter password: mysecurepassword +# Generated hash: $2b$12$... + +# Generate secret key +python -c "import secrets; print(secrets.token_hex(32))" +# Generated key: a1b2c3d4e5f6... +``` + +**Render deployment:** +```yaml +envVars: + - key: AUTHENTICATION_ENABLED + value: "true" + - key: AUTHENTICATION_PASSWORD_HASH + value: "$2b$12$..." # Your generated hash + - key: AUTHENTICATION_SECRET_KEY + value: "a1b2c3d4e5f6..." # Your generated key +``` + +**Result:** āœ… Custom password and secure session key for production demo + +--- + +### Use Case 4: Docker Compose (Local Development) + +**`docker-compose.yml`:** +```yaml +services: + notediscovery: + image: ghcr.io/gamosoft/notediscovery:latest + ports: + - "8000:8000" + environment: + - AUTHENTICATION_ENABLED=false + - DEMO_MODE=false + volumes: + - ./data:/app/data +``` + +**Result:** āœ… Local deployment without restrictions (no auth, no rate limiting) + +--- + +### Use Case 5: Custom Port + +**Run on port 3000 instead of 8000:** + +**Local (run.py):** +```bash +PORT=3000 python run.py +# → Runs on http://localhost:3000 +``` + +**Docker:** +```bash +docker run -p 3000:3000 -e PORT=3000 ghcr.io/gamosoft/notediscovery:latest +``` + +**Docker Compose:** +```yaml +services: + notediscovery: + image: ghcr.io/gamosoft/notediscovery:latest + ports: + - "3000:3000" # Host:Container + environment: + - PORT=3000 +``` + +**Result:** āœ… App runs on custom port + +--- + +## šŸ” Authentication Examples + +### Example 1: Enable Auth via Environment + +**Without environment variables:** +```bash +# Uses config.yaml defaults +šŸ” Authentication DISABLED (from config.yaml) +āœ… Production mode - Rate limiting disabled +``` + +**With environment variables (demo mode):** +```bash +export DEMO_MODE=true +export AUTHENTICATION_ENABLED=true +python run.py + +# Output: +šŸŽ­ DEMO MODE enabled - Rate limiting active +šŸ” Authentication ENABLED (from AUTHENTICATION_ENABLED env var) +``` + +### Example 2: Demo Deployment + +**Public demo with restrictions:** +```bash +export DEMO_MODE=true +export AUTHENTICATION_ENABLED=true +export AUTHENTICATION_PASSWORD_HASH='$2b$12$custom_hash' +export AUTHENTICATION_SECRET_KEY='your_random_secret_key' +python run.py + +# Output: +šŸŽ­ DEMO MODE enabled - Rate limiting active +šŸ” Authentication ENABLED (from AUTHENTICATION_ENABLED env var) +šŸ”‘ Password hash loaded from AUTHENTICATION_PASSWORD_HASH env var +šŸ” Secret key loaded from AUTHENTICATION_SECRET_KEY env var +``` + +**Private instance (no restrictions):** +```bash +# No environment variables needed +python run.py + +# Output: +āœ… Production mode - Rate limiting disabled +šŸ” Authentication DISABLED (from config.yaml) +``` + +## šŸ“ Boolean Values + +Environment variables accept multiple formats for boolean values: + +**True values:** +- `true`, `True`, `TRUE` +- `1` +- `yes`, `Yes`, `YES` + +**False values:** +- `false`, `False`, `FALSE` +- `0` +- `no`, `No`, `NO` +- Not set (uses config.yaml default) + +**Examples:** +```bash +AUTHENTICATION_ENABLED=true # āœ… Enabled +AUTHENTICATION_ENABLED=1 # āœ… Enabled +AUTHENTICATION_ENABLED=yes # āœ… Enabled +AUTHENTICATION_ENABLED=false # āŒ Disabled +AUTHENTICATION_ENABLED=0 # āŒ Disabled +# Not set # Uses config.yaml +``` + +## šŸš€ Deployment Scenarios + +### Scenario 1: Same Codebase, Different Environments + +**Repository structure:** +``` +config.yaml # Default: auth disabled (for local dev) +render.yaml # Render: AUTHENTICATION_ENABLED=true +docker-compose.yml # Docker: AUTHENTICATION_ENABLED=false (or not set) +``` + +**Benefits:** +- āœ… Single codebase +- āœ… No code changes between environments +- āœ… Environment-specific security + +### Scenario 2: Team Collaboration + +**Team workflow:** +```bash +# Developer 1 (local, no auth) +git clone repo +python run.py +# → No auth needed for local testing + +# Developer 2 (local, testing auth) +AUTHENTICATION_ENABLED=true python run.py +# → Tests authentication locally + +# Production (Render) +# → AUTHENTICATION_ENABLED=true in render.yaml +# → Always requires authentication +``` + +### Scenario 3: CI/CD Pipeline + +**GitHub Actions:** +```yaml +- name: Test with auth disabled + run: | + export AUTHENTICATION_ENABLED=false + pytest tests/ + +- name: Test with auth enabled + run: | + export AUTHENTICATION_ENABLED=true + pytest tests/auth/ +``` + +## šŸ” Checking Current Configuration + +The application prints its configuration on startup: + +```bash +python run.py + +# Output: +šŸŽ­ DEMO MODE enabled - Rate limiting active +šŸ” Authentication ENABLED (from AUTHENTICATION_ENABLED env var) +šŸ”‘ Password hash loaded from AUTHENTICATION_PASSWORD_HASH env var +šŸ” Secret key loaded from AUTHENTICATION_SECRET_KEY env var +``` + +## āš™ļø Advanced: All Environment Variables + +**Full demo deployment example:** +```bash +export PORT=8080 +export DEMO_MODE=true +export AUTHENTICATION_ENABLED=true +export AUTHENTICATION_PASSWORD_HASH='$2b$12$...' +export AUTHENTICATION_SECRET_KEY='your_random_secret_key_here' + +python run.py +``` + +**Docker example:** +```dockerfile +ENV PORT=8000 +ENV DEMO_MODE=true +ENV AUTHENTICATION_ENABLED=true +ENV AUTHENTICATION_PASSWORD_HASH='$2b$12$...' +ENV AUTHENTICATION_SECRET_KEY='your_random_secret_key_here' +``` + +## šŸ“Š Quick Reference Table + +| Scenario | `DEMO_MODE` | `AUTHENTICATION_ENABLED` | Behavior | +|----------|-------------|---------------|----------| +| **Local Dev** | Not set | Not set | No restrictions, quick testing | +| **Local Auth Testing** | Not set | `true` | Auth only, no rate limits | +| **Public Demo** | `true` | `true` | Full restrictions + auth | +| **Private Instance** | Not set | `true` | Auth only, self-hosted | +| **Read-Only Demo** | `true` | Not set | Rate limited, no auth | + +## šŸ†˜ Troubleshooting + +### "Authentication not working" + +**Check startup logs:** +``` +šŸ” Authentication DISABLED (from config.yaml) +``` + +**Fix:** Set environment variable +```bash +export AUTHENTICATION_ENABLED=true +``` + +### "Wrong password accepted" + +**Check if password hash is loaded:** +``` +šŸ”‘ Password hash loaded from AUTHENTICATION_PASSWORD_HASH env var +``` + +**If not shown:** Environment variable not set, using `config.yaml` + +### "Can't disable auth in Render" + +**Option 1:** Remove from `render.yaml` +```yaml +# Remove or comment out: +# - key: AUTHENTICATION_ENABLED +# value: "true" +``` + +**Option 2:** Explicitly disable +```yaml +- key: AUTHENTICATION_ENABLED + value: "false" +``` + +## šŸ”§ Advanced Server Configuration + +The following settings are available in `config.yaml` only (not via environment variables): + +### CORS (Cross-Origin Resource Sharing) + +```yaml +server: + # List of allowed origins for CORS + # Default: ["*"] allows all origins (fine for self-hosted) + # Production: specify your domains + allowed_origins: ["*"] + + # Examples for production: + # allowed_origins: ["http://localhost:8000", "https://yourdomain.com"] + # allowed_origins: ["https://*.yourdomain.com"] # Wildcard subdomain +``` + +**Security Note:** +- `["*"]` is **safe for self-hosted** deployments on private networks +- For **public deployments**, specify exact origins to prevent unauthorized API access +- This prevents CSRF attacks when authentication is enabled + +### Debug Mode + +```yaml +server: + # Enable detailed error messages in API responses + # Default: false (production-safe) + # Set to true for development/troubleshooting + debug: false +``` + +**āš ļø CRITICAL**: Never enable `debug: true` in production! + +When `debug: true`: +- Full error stack traces are returned to users +- Internal paths and system details are exposed +- Security vulnerabilities may be revealed + +When `debug: false` (recommended): +- Generic error messages are returned +- Full error details are logged server-side only +- Production-safe error handling + +--- + +## šŸ“š Related Documentation + +- **Authentication**: [AUTHENTICATION.md](AUTHENTICATION.md) +- **Render Deployment**: [DEPLOYMENT_RENDER.md](DEPLOYMENT_RENDER.md) +- **API Rate Limiting**: [API.md](API.md#rate-limiting) + +--- + +**Pro Tip:** Use environment variables for **deployment-specific** settings, and `config.yaml` for **application defaults**. This keeps your configuration flexible and maintainable! šŸŽÆ + diff --git a/frontend/index.html b/frontend/index.html index 685404a..c53670d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -754,7 +754,7 @@

-
+