use environment variables (breaking change). added CORS. safe exception handler
This commit is contained in:
parent
dfa3710dc6
commit
e0e4e457da
13
Dockerfile
13
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
128
backend/main.py
128
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")
|
||||
|
|
|
|||
10
config.yaml
10
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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! 🎯
|
||||
|
||||
|
|
@ -754,7 +754,7 @@
|
|||
</div>
|
||||
<p class="text-xs mb-3" style="color: var(--text-secondary);" x-text="appTagline"></p>
|
||||
<!-- Logout Button (only show if auth is enabled) -->
|
||||
<div x-data="{ authEnabled: false }" x-init="fetch('/api/config').then(r => r.json()).then(d => authEnabled = d.security?.enabled || false)">
|
||||
<div x-data="{ authEnabled: false }" x-init="fetch('/api/config').then(r => r.json()).then(d => authEnabled = d.authentication?.enabled || false)">
|
||||
<a x-show="authEnabled"
|
||||
href="/logout"
|
||||
class="flex items-center justify-center gap-2 px-3 py-2 text-sm rounded transition-colors"
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ def generate_password_hash():
|
|||
print("\nExample config.yaml:")
|
||||
print("="*60)
|
||||
print("""
|
||||
security:
|
||||
authentication:
|
||||
enabled: true
|
||||
secret_key: "your_secret_key_here"
|
||||
password_hash: "{}"
|
||||
|
|
|
|||
|
|
@ -20,10 +20,14 @@ services:
|
|||
envVars:
|
||||
- key: PORT
|
||||
value: 8000
|
||||
- key: ENABLE_RATE_LIMITING
|
||||
value: "true"
|
||||
- key: DEMO_MODE
|
||||
value: "true"
|
||||
- key: AUTHENTICATION_ENABLED
|
||||
value: "true"
|
||||
- key: AUTHENTICATION_PASSWORD_HASH
|
||||
value: "$2b$12$t/6PGExFzdpU2PUta0iVY.eDQwvu63kH.c/d4bEnnHaQ5CspH1yrG" # admin
|
||||
- key: AUTHENTICATION_SECRET_KEY
|
||||
value: "4f36da5af76627301dcdc0347c4b111bdc6c86ae830444af852de935198c3210"
|
||||
|
||||
# Manual deployment only
|
||||
autoDeploy: false
|
||||
9
run.py
9
run.py
|
|
@ -5,6 +5,7 @@ Run this to start the application without Docker
|
|||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -23,16 +24,20 @@ def main():
|
|||
Path("data").mkdir(parents=True, exist_ok=True)
|
||||
Path("plugins").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Get port from environment variable or use default
|
||||
port = os.getenv("PORT", "8000")
|
||||
|
||||
print("✓ Dependencies installed")
|
||||
print("✓ Directories created")
|
||||
print("\n" + "="*50)
|
||||
print("🎉 NoteDiscovery is running!")
|
||||
print("="*50)
|
||||
print("\n📝 Open your browser to: http://localhost:8000")
|
||||
print(f"\n📝 Open your browser to: http://localhost:{port}")
|
||||
print("\n💡 Tips:")
|
||||
print(" - Press Ctrl+C to stop the server")
|
||||
print(" - Your notes are in ./data/")
|
||||
print(" - Plugins go in ./plugins/")
|
||||
print(f" - Change port with: PORT={port} python run.py")
|
||||
print("\n" + "="*50 + "\n")
|
||||
|
||||
# Run the application
|
||||
|
|
@ -41,7 +46,7 @@ def main():
|
|||
"backend.main:app",
|
||||
"--reload",
|
||||
"--host", "0.0.0.0",
|
||||
"--port", "8000"
|
||||
"--port", port
|
||||
])
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
Loading…
Reference in New Issue