added basic authentication
This commit is contained in:
parent
c184b41a5b
commit
e5e34152a0
|
|
@ -0,0 +1,311 @@
|
||||||
|
# 🔒 NoteDiscovery Authentication Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
NoteDiscovery includes a simple, secure authentication system for single-user deployments. When enabled, users must log in with a password before accessing the application.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
- ✅ **Single User** - Perfect for personal/self-hosted use
|
||||||
|
- ✅ **Secure** - Passwords hashed with bcrypt
|
||||||
|
- ✅ **Session-based** - Stay logged in for 7 days (configurable)
|
||||||
|
- ✅ **Theme-aware** - Login page matches your selected theme
|
||||||
|
- ✅ **Simple Setup** - Just 3 steps to enable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Setup
|
||||||
|
|
||||||
|
**⚠️ IMPORTANT:** Authentication is **disabled by default**. Follow these steps to enable it:
|
||||||
|
|
||||||
|
### Step 1: Generate a Password Hash
|
||||||
|
|
||||||
|
Choose one of these methods:
|
||||||
|
|
||||||
|
**Option A: Using Docker (Recommended - No local Python needed)**
|
||||||
|
|
||||||
|
If you're running NoteDiscovery in Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Docker Compose
|
||||||
|
docker-compose exec notediscovery /app/generate_password_hash.sh
|
||||||
|
|
||||||
|
# Or with docker run
|
||||||
|
docker exec -it notediscovery /app/generate_password_hash.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The script will prompt you for your password and output the hash.
|
||||||
|
|
||||||
|
**Option B: Using Local Python**
|
||||||
|
|
||||||
|
If you're running NoteDiscovery locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install bcrypt if not already installed
|
||||||
|
pip install bcrypt
|
||||||
|
|
||||||
|
# Run the password generator
|
||||||
|
python generate_password.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Enter your desired password when prompted. The script will output a bcrypt hash.
|
||||||
|
|
||||||
|
**Copy the hash** - you'll need it for the next step.
|
||||||
|
|
||||||
|
### Step 2: Generate a Secret Key
|
||||||
|
|
||||||
|
Generate a random secret key for session encryption:
|
||||||
|
|
||||||
|
**Using Docker:**
|
||||||
|
```bash
|
||||||
|
docker-compose exec notediscovery python -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
|
||||||
|
# Or with docker run
|
||||||
|
docker exec -it notediscovery python -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Using Local Python:**
|
||||||
|
```bash
|
||||||
|
python -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Copy the key** - you'll need it for the next step.
|
||||||
|
|
||||||
|
### Step 3: Update `config.yaml`
|
||||||
|
|
||||||
|
Edit your `config.yaml` and update the security section:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
security:
|
||||||
|
# Enable authentication
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Session secret key (paste the output from Step 3)
|
||||||
|
secret_key: "your_generated_secret_key_here"
|
||||||
|
|
||||||
|
# Password hash (paste the output from Step 2)
|
||||||
|
password_hash: "$2b$12$..."
|
||||||
|
|
||||||
|
# Session expiry in seconds (7 days by default)
|
||||||
|
session_max_age: 604800
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Restart the Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# If running locally
|
||||||
|
uvicorn backend.main:app --reload
|
||||||
|
|
||||||
|
# If using Docker Compose
|
||||||
|
docker-compose restart
|
||||||
|
|
||||||
|
# Or with docker run
|
||||||
|
docker restart notediscovery
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Test Login
|
||||||
|
|
||||||
|
Navigate to `http://localhost:8000` and you'll be redirected to the login page.
|
||||||
|
|
||||||
|
Enter the password you chose in Step 2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Usage
|
||||||
|
|
||||||
|
### Logging In
|
||||||
|
|
||||||
|
1. Navigate to `http://localhost:8000`
|
||||||
|
2. You'll be **automatically redirected** to the login page if not authenticated
|
||||||
|
3. Enter your password (the one you generated in setup)
|
||||||
|
4. Click "🔓 Unlock"
|
||||||
|
5. You'll be redirected back to the main app
|
||||||
|
|
||||||
|
**Note:** The login page **automatically inherits the theme** you selected in the main app. It uses the same theme loading system as the main app (fetches CSS from `/api/themes` and stores preference in localStorage as `noteDiscoveryTheme`).
|
||||||
|
|
||||||
|
**What happens if I'm not logged in?**
|
||||||
|
- 🌐 **Page requests** (like `/`, `/MATHJAX`, etc.) → **Automatically redirected to login page**
|
||||||
|
- 🔌 **API requests** (like `/api/notes`) → **Return JSON error** `{"detail": "Not authenticated"}`
|
||||||
|
|
||||||
|
This smart routing ensures:
|
||||||
|
- ✅ Users always see the login page (never a JSON error)
|
||||||
|
- ✅ API calls get proper JSON errors (for programmatic access)
|
||||||
|
- ✅ No broken page loads or error messages
|
||||||
|
|
||||||
|
### Logging Out
|
||||||
|
|
||||||
|
**Option 1: Use the built-in logout button**
|
||||||
|
|
||||||
|
A logout button (🔒 Logout) automatically appears in the sidebar header when authentication is enabled. Simply click it to log out.
|
||||||
|
|
||||||
|
**Option 2: Navigate directly**
|
||||||
|
|
||||||
|
Visit `http://localhost:8000/logout` in your browser.
|
||||||
|
|
||||||
|
### Changing Password
|
||||||
|
|
||||||
|
1. Generate a new password hash:
|
||||||
|
- **Docker**: `docker-compose exec notediscovery /app/generate_password_hash.sh`
|
||||||
|
- **Local**: `python generate_password.py`
|
||||||
|
2. Update `password_hash` in `config.yaml` with the new hash
|
||||||
|
3. Restart the application:
|
||||||
|
- **Docker**: `docker-compose restart`
|
||||||
|
- **Local**: Restart uvicorn
|
||||||
|
4. All existing sessions will remain valid until they expire
|
||||||
|
|
||||||
|
### Session Expiry
|
||||||
|
|
||||||
|
- **Default**: 7 days (604800 seconds)
|
||||||
|
- **Custom**: Change `session_max_age` in `config.yaml`
|
||||||
|
- Sessions are cleared when you log out
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Security Considerations
|
||||||
|
|
||||||
|
### ✅ What This Protects
|
||||||
|
|
||||||
|
- Unauthorized access to your notes
|
||||||
|
- Viewing, creating, editing, and deleting notes
|
||||||
|
- All API endpoints
|
||||||
|
|
||||||
|
### ⚠️ What This Doesn't Protect
|
||||||
|
|
||||||
|
This is a **simple authentication system** designed for **self-hosted, single-user** deployments. It is **NOT** suitable for:
|
||||||
|
|
||||||
|
- ❌ Multi-user environments
|
||||||
|
- ❌ Public internet exposure without HTTPS
|
||||||
|
- ❌ Production SaaS applications
|
||||||
|
- ❌ Compliance requirements (HIPAA, GDPR, etc.)
|
||||||
|
|
||||||
|
### 🛡️ Best Practices
|
||||||
|
|
||||||
|
1. **Use HTTPS** - Always run behind a reverse proxy (nginx, Caddy) with SSL/TLS
|
||||||
|
2. **Strong Password** - Use at least 12 characters with mixed case, numbers, and symbols
|
||||||
|
3. **Unique Secret Key** - Never reuse secret keys across applications
|
||||||
|
4. **Keep Config Secure** - Don't commit `config.yaml` with real credentials to version control
|
||||||
|
5. **VPN/Private Network** - Keep NoteDiscovery on a private network or behind a VPN
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚫 Disabling Authentication
|
||||||
|
|
||||||
|
To disable authentication and allow open access:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
security:
|
||||||
|
enabled: false
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart the application, and authentication will be bypassed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### "Invalid Password" Error
|
||||||
|
|
||||||
|
- **Check password hash**: Ensure the hash in `config.yaml` matches your password
|
||||||
|
- **Regenerate hash**: Run `python generate_password.py` again
|
||||||
|
- **Check encoding**: Password must be UTF-8 encoded
|
||||||
|
|
||||||
|
### "Not authenticated" Error
|
||||||
|
|
||||||
|
- **Check session**: Your session may have expired (default: 7 days)
|
||||||
|
- **Clear cookies**: Clear your browser cookies and log in again
|
||||||
|
- **Check config**: Ensure `enabled: true` in `config.yaml`
|
||||||
|
|
||||||
|
### Login Page Not Showing
|
||||||
|
|
||||||
|
- **Check config**: Verify `enabled: true` in `config.yaml`
|
||||||
|
- **Check routes**: Visit `/login` directly
|
||||||
|
- **Check logs**: Look for errors in the console
|
||||||
|
|
||||||
|
### Can't Access `/logout`
|
||||||
|
|
||||||
|
- You must be logged in to access `/logout`
|
||||||
|
- Clear your browser cookies manually if needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Customizing Login Page
|
||||||
|
|
||||||
|
The login page (`frontend/login.html`) can be customized:
|
||||||
|
|
||||||
|
- **Theme**: Automatically inherits the theme you selected in the main app (from localStorage)
|
||||||
|
- **Logo**: Uses `/static/logo.svg`
|
||||||
|
- **Styling**: Edit the CSS in the `<style>` section
|
||||||
|
- **Content**: Modify the HTML directly
|
||||||
|
- **Error Messages**: Displayed inline in red when login fails (no separate error page)
|
||||||
|
|
||||||
|
**Note:** The theme selector is intentionally hidden on the login page to keep it clean. Users will see the theme they selected in the main app.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Docker Deployment
|
||||||
|
|
||||||
|
When using Docker, mount your `config.yaml` with credentials:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
notediscovery:
|
||||||
|
image: ghcr.io/yourusername/notediscovery:latest
|
||||||
|
volumes:
|
||||||
|
- ./config.yaml:/app/config.yaml
|
||||||
|
- ./data:/app/data
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Security Note**: Don't build credentials into the Docker image. Always mount them as a volume.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 Multi-User Support
|
||||||
|
|
||||||
|
This authentication system is designed for **single-user** deployments. If you need multi-user support:
|
||||||
|
|
||||||
|
1. Run separate instances (one per user)
|
||||||
|
2. Use Docker containers with different ports
|
||||||
|
3. Use a reverse proxy for routing
|
||||||
|
|
||||||
|
For enterprise/multi-user needs, consider:
|
||||||
|
- OAuth 2.0 / OpenID Connect
|
||||||
|
- Database-backed user management
|
||||||
|
- Role-based access control (RBAC)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Technical Details
|
||||||
|
|
||||||
|
### Password Hashing
|
||||||
|
|
||||||
|
- **Algorithm**: bcrypt
|
||||||
|
- **Rounds**: 12 (default)
|
||||||
|
- **Salt**: Automatically generated per password
|
||||||
|
|
||||||
|
### Session Management
|
||||||
|
|
||||||
|
- **Storage**: Server-side session cookies
|
||||||
|
- **Signing**: HMAC with secret key
|
||||||
|
- **Security**: HttpOnly, SameSite=Lax
|
||||||
|
- **Expiry**: Configurable (default 7 days)
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- `bcrypt` - Password hashing
|
||||||
|
- `itsdangerous` - Session signing
|
||||||
|
- `starlette` - Session middleware
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Additional Resources
|
||||||
|
|
||||||
|
- [bcrypt Documentation](https://github.com/pyca/bcrypt/)
|
||||||
|
- [FastAPI Security](https://fastapi.tiangolo.com/tutorial/security/)
|
||||||
|
- [OWASP Authentication Cheatsheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Need Help?** Open an issue on [GitHub](https://github.com/yourusername/notediscovery/issues) or consult the [README.md](README.md).
|
||||||
|
|
||||||
|
|
@ -27,9 +27,11 @@ COPY frontend ./frontend
|
||||||
COPY config.yaml .
|
COPY config.yaml .
|
||||||
COPY plugins ./plugins
|
COPY plugins ./plugins
|
||||||
COPY themes ./themes
|
COPY themes ./themes
|
||||||
|
COPY generate_password_hash.sh .
|
||||||
|
|
||||||
# Create data directories
|
# Create data directories and make password script executable
|
||||||
RUN mkdir -p data/notes data/search_index
|
RUN mkdir -p data/notes data/search_index && \
|
||||||
|
chmod +x generate_password_hash.sh
|
||||||
|
|
||||||
# Expose port
|
# Expose port
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
|
||||||
17
README.md
17
README.md
|
|
@ -32,6 +32,7 @@ NoteDiscovery is a **lightweight, self-hosted note-taking application** that put
|
||||||
### Key Benefits
|
### Key Benefits
|
||||||
|
|
||||||
- 🔒 **Total Privacy** - Your notes never leave your server
|
- 🔒 **Total Privacy** - Your notes never leave your server
|
||||||
|
- 🔐 **Optional Authentication** - Simple password protection for self-hosted deployments
|
||||||
- 💰 **Zero Cost** - No subscriptions, no hidden fees
|
- 💰 **Zero Cost** - No subscriptions, no hidden fees
|
||||||
- 🚀 **Fast & Lightweight** - Instant search and navigation
|
- 🚀 **Fast & Lightweight** - Instant search and navigation
|
||||||
- 🎨 **Beautiful Themes** - Multiple themes, easy to customize
|
- 🎨 **Beautiful Themes** - Multiple themes, easy to customize
|
||||||
|
|
@ -202,6 +203,7 @@ Once you've started NoteDiscovery, you'll find comprehensive guides on:
|
||||||
- 🧮 **MATHJAX.md** - LaTeX/Math notation examples and syntax reference
|
- 🧮 **MATHJAX.md** - LaTeX/Math notation examples and syntax reference
|
||||||
- 🔌 **PLUGINS.md** - Plugin system and available plugins
|
- 🔌 **PLUGINS.md** - Plugin system and available plugins
|
||||||
- 🌐 **API.md** - REST API documentation and examples
|
- 🌐 **API.md** - REST API documentation and examples
|
||||||
|
- 🔐 **AUTHENTICATION.md** - Enable password protection for your instance
|
||||||
|
|
||||||
**Can't wait to start the app?** Browse the documentation notes directly on GitHub in the [`data/notes/`](data/notes/) folder!
|
**Can't wait to start the app?** Browse the documentation notes directly on GitHub in the [`data/notes/`](data/notes/) folder!
|
||||||
|
|
||||||
|
|
@ -217,15 +219,16 @@ NoteDiscovery is designed for **self-hosted, private use**. Please keep these se
|
||||||
|
|
||||||
### Network Security
|
### Network Security
|
||||||
- ⚠️ **Do NOT expose directly to the internet** without additional security measures
|
- ⚠️ **Do NOT expose directly to the internet** without additional security measures
|
||||||
- Run behind a reverse proxy (nginx, Caddy) with HTTPS and authentication if needed
|
- Run behind a reverse proxy (nginx, Caddy) with HTTPS for production use
|
||||||
- Keep it on your local network or use a VPN for remote access
|
- Keep it on your local network or use a VPN for remote access
|
||||||
- By default, the app listens on `0.0.0.0:8000` (all network interfaces)
|
- By default, the app listens on `0.0.0.0:8000` (all network interfaces)
|
||||||
|
|
||||||
### No Built-in Authentication
|
### Authentication
|
||||||
- The app has **no authentication by design** (single-user, self-hosted)
|
- **Optional password protection** is available (disabled by default)
|
||||||
- Anyone with network access can read and modify your notes
|
- See **[AUTHENTICATION.md](AUTHENTICATION.md)** for setup instructions
|
||||||
- Use network-level security (firewall, VPN) for access control
|
- Simple to enable with Docker: `docker-compose exec notediscovery /app/generate_password_hash.sh`
|
||||||
- Consider adding authentication via reverse proxy if needed
|
- Perfect for single-user or small team deployments
|
||||||
|
- For multi-user setups, consider a reverse proxy with OAuth/SSO
|
||||||
|
|
||||||
### Data Privacy
|
### Data Privacy
|
||||||
- Your notes are stored as **plain text markdown files** in `data/notes/`
|
- Your notes are stored as **plain text markdown files** in `data/notes/`
|
||||||
|
|
@ -239,7 +242,7 @@ NoteDiscovery is designed for **self-hosted, private use**. Please keep these se
|
||||||
- Review and audit any plugins you install
|
- Review and audit any plugins you install
|
||||||
- Set appropriate file permissions on the `data/` directory
|
- Set appropriate file permissions on the `data/` directory
|
||||||
|
|
||||||
**TL;DR**: Perfect for personal use on your local machine or home network. Add a reverse proxy with authentication if exposing to wider networks.
|
**TL;DR**: Perfect for personal use on your local machine or home network. Enable built-in password protection if needed, or use a reverse proxy with authentication if exposing to wider networks.
|
||||||
|
|
||||||
## 📄 License
|
## 📄 License
|
||||||
|
|
||||||
|
|
|
||||||
175
backend/main.py
175
backend/main.py
|
|
@ -3,10 +3,11 @@ NoteDiscovery - Self-Hosted Markdown Knowledge Base
|
||||||
Main FastAPI application
|
Main FastAPI application
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, UploadFile, File
|
from fastapi import FastAPI, HTTPException, UploadFile, File, Request, Form, Depends
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
|
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, RedirectResponse
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
import os
|
import os
|
||||||
import yaml
|
import yaml
|
||||||
import json
|
import json
|
||||||
|
|
@ -14,6 +15,7 @@ from pathlib import Path
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
import aiofiles
|
import aiofiles
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import bcrypt
|
||||||
|
|
||||||
from .utils import (
|
from .utils import (
|
||||||
get_all_notes,
|
get_all_notes,
|
||||||
|
|
@ -55,6 +57,15 @@ app.add_middleware(
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 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
|
||||||
|
same_site='lax',
|
||||||
|
https_only=False # Set to True if using HTTPS
|
||||||
|
)
|
||||||
|
|
||||||
# Ensure required directories exist
|
# Ensure required directories exist
|
||||||
ensure_directories(config)
|
ensure_directories(config)
|
||||||
|
|
||||||
|
|
@ -69,8 +80,131 @@ static_path = Path(__file__).parent.parent / "frontend"
|
||||||
app.mount("/static", StaticFiles(directory=static_path), name="static")
|
app.mount("/static", StaticFiles(directory=static_path), name="static")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Custom Exception Handlers
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@app.exception_handler(HTTPException)
|
||||||
|
async def http_exception_handler(request: Request, exc: HTTPException):
|
||||||
|
"""
|
||||||
|
Custom exception handler for HTTP exceptions.
|
||||||
|
Handles 401 errors specially:
|
||||||
|
- For API requests: return JSON error
|
||||||
|
- For page requests: redirect to login
|
||||||
|
"""
|
||||||
|
# Only handle 401 errors specially
|
||||||
|
if exc.status_code == 401:
|
||||||
|
# Check if this is an API request
|
||||||
|
if request.url.path.startswith('/api/'):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=401,
|
||||||
|
content={"detail": exc.detail}
|
||||||
|
)
|
||||||
|
|
||||||
|
# For page requests, redirect to login
|
||||||
|
return RedirectResponse(url='/login', status_code=303)
|
||||||
|
|
||||||
|
# For all other HTTP exceptions, return default JSON response
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content={"detail": exc.detail}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Authentication Helpers
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def auth_enabled() -> bool:
|
||||||
|
"""Check if authentication is enabled in config"""
|
||||||
|
return config.get('security', {}).get('enabled', False)
|
||||||
|
|
||||||
|
|
||||||
|
async def require_auth(request: Request):
|
||||||
|
"""Dependency to require authentication on protected routes"""
|
||||||
|
if not auth_enabled():
|
||||||
|
return # Auth disabled, allow all
|
||||||
|
|
||||||
|
if not request.session.get('authenticated'):
|
||||||
|
# Always raise exception - route handlers will catch and redirect as needed
|
||||||
|
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||||
|
|
||||||
|
|
||||||
|
def verify_password(password: str) -> bool:
|
||||||
|
"""Verify password against stored hash"""
|
||||||
|
password_hash = config.get('security', {}).get('password_hash', '')
|
||||||
|
if not password_hash:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
return bcrypt.checkpw(password.encode('utf-8'), password_hash.encode('utf-8'))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Password verification error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Authentication Routes
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
@app.get("/login", response_class=HTMLResponse)
|
||||||
|
async def login_page(request: Request, error: str = None):
|
||||||
|
"""Serve the login page"""
|
||||||
|
if not auth_enabled():
|
||||||
|
return RedirectResponse(url="/", status_code=303)
|
||||||
|
|
||||||
|
# If already authenticated, redirect to home
|
||||||
|
if request.session.get('authenticated'):
|
||||||
|
return RedirectResponse(url="/", status_code=303)
|
||||||
|
|
||||||
|
# Serve login page
|
||||||
|
login_path = static_path / "login.html"
|
||||||
|
async with aiofiles.open(login_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = await f.read()
|
||||||
|
|
||||||
|
# Inject error message if present
|
||||||
|
if error:
|
||||||
|
content = content.replace('<!-- ERROR_PLACEHOLDER -->',
|
||||||
|
f'<div class="error-message">❌ {error}</div>')
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/login")
|
||||||
|
async def login(request: Request, password: str = Form(...)):
|
||||||
|
"""Handle login form submission"""
|
||||||
|
if not auth_enabled():
|
||||||
|
return RedirectResponse(url="/", status_code=303)
|
||||||
|
|
||||||
|
# Verify password
|
||||||
|
if verify_password(password):
|
||||||
|
# Set session
|
||||||
|
request.session['authenticated'] = True
|
||||||
|
return RedirectResponse(url="/", status_code=303)
|
||||||
|
else:
|
||||||
|
# Redirect back to login with error message
|
||||||
|
return RedirectResponse(url="/login?error=Invalid+password.+Please+try+again.", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/logout")
|
||||||
|
async def logout(request: Request):
|
||||||
|
"""Log out the current user"""
|
||||||
|
request.session.clear()
|
||||||
|
return RedirectResponse(url="/login", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/favicon.ico")
|
||||||
|
async def favicon():
|
||||||
|
"""Serve favicon (redirect to SVG)"""
|
||||||
|
return RedirectResponse(url="/static/favicon.svg", status_code=301)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Application Routes
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def root():
|
async def root(request: Request, _auth: None = Depends(require_auth)):
|
||||||
"""Serve the main application page"""
|
"""Serve the main application page"""
|
||||||
index_path = static_path / "index.html"
|
index_path = static_path / "index.html"
|
||||||
async with aiofiles.open(index_path, 'r', encoding='utf-8') as f:
|
async with aiofiles.open(index_path, 'r', encoding='utf-8') as f:
|
||||||
|
|
@ -237,7 +371,10 @@ async def get_config():
|
||||||
"name": config['app']['name'],
|
"name": config['app']['name'],
|
||||||
"tagline": config['app']['tagline'],
|
"tagline": config['app']['tagline'],
|
||||||
"version": config['app']['version'],
|
"version": config['app']['version'],
|
||||||
"searchEnabled": config['search']['enabled']
|
"searchEnabled": config['search']['enabled'],
|
||||||
|
"security": {
|
||||||
|
"enabled": config.get('security', {}).get('enabled', False)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -262,7 +399,7 @@ async def get_theme(theme_id: str):
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/folders")
|
@app.post("/api/folders")
|
||||||
async def create_new_folder(data: dict):
|
async def create_new_folder(data: dict, _auth: None = Depends(require_auth)):
|
||||||
"""Create a new folder"""
|
"""Create a new folder"""
|
||||||
try:
|
try:
|
||||||
folder_path = data.get('path', '')
|
folder_path = data.get('path', '')
|
||||||
|
|
@ -284,7 +421,7 @@ async def create_new_folder(data: dict):
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/notes/move")
|
@app.post("/api/notes/move")
|
||||||
async def move_note_endpoint(data: dict):
|
async def move_note_endpoint(data: dict, _auth: None = Depends(require_auth)):
|
||||||
"""Move a note to a different folder"""
|
"""Move a note to a different folder"""
|
||||||
try:
|
try:
|
||||||
old_path = data.get('oldPath', '')
|
old_path = data.get('oldPath', '')
|
||||||
|
|
@ -312,7 +449,7 @@ async def move_note_endpoint(data: dict):
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/folders/move")
|
@app.post("/api/folders/move")
|
||||||
async def move_folder_endpoint(data: dict):
|
async def move_folder_endpoint(data: dict, _auth: None = Depends(require_auth)):
|
||||||
"""Move a folder to a different location"""
|
"""Move a folder to a different location"""
|
||||||
try:
|
try:
|
||||||
old_path = data.get('oldPath', '')
|
old_path = data.get('oldPath', '')
|
||||||
|
|
@ -337,7 +474,7 @@ async def move_folder_endpoint(data: dict):
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/folders/rename")
|
@app.post("/api/folders/rename")
|
||||||
async def rename_folder_endpoint(data: dict):
|
async def rename_folder_endpoint(data: dict, _auth: None = Depends(require_auth)):
|
||||||
"""Rename a folder"""
|
"""Rename a folder"""
|
||||||
try:
|
try:
|
||||||
old_path = data.get('oldPath', '')
|
old_path = data.get('oldPath', '')
|
||||||
|
|
@ -362,7 +499,7 @@ async def rename_folder_endpoint(data: dict):
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/api/folders/{folder_path:path}")
|
@app.delete("/api/folders/{folder_path:path}")
|
||||||
async def delete_folder_endpoint(folder_path: str):
|
async def delete_folder_endpoint(folder_path: str, _auth: None = Depends(require_auth)):
|
||||||
"""Delete a folder and all its contents"""
|
"""Delete a folder and all its contents"""
|
||||||
try:
|
try:
|
||||||
if not folder_path:
|
if not folder_path:
|
||||||
|
|
@ -383,7 +520,7 @@ async def delete_folder_endpoint(folder_path: str):
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/notes")
|
@app.get("/api/notes")
|
||||||
async def list_notes():
|
async def list_notes(_auth: None = Depends(require_auth)):
|
||||||
"""List all notes with metadata"""
|
"""List all notes with metadata"""
|
||||||
try:
|
try:
|
||||||
notes = get_all_notes(config['storage']['notes_dir'])
|
notes = get_all_notes(config['storage']['notes_dir'])
|
||||||
|
|
@ -394,7 +531,7 @@ async def list_notes():
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/notes/{note_path:path}")
|
@app.get("/api/notes/{note_path:path}")
|
||||||
async def get_note(note_path: str):
|
async def get_note(note_path: str, _auth: None = Depends(require_auth)):
|
||||||
"""Get a specific note's content"""
|
"""Get a specific note's content"""
|
||||||
try:
|
try:
|
||||||
content = get_note_content(config['storage']['notes_dir'], note_path)
|
content = get_note_content(config['storage']['notes_dir'], note_path)
|
||||||
|
|
@ -422,7 +559,7 @@ async def get_note(note_path: str):
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/notes/{note_path:path}")
|
@app.post("/api/notes/{note_path:path}")
|
||||||
async def create_or_update_note(note_path: str, content: dict):
|
async def create_or_update_note(note_path: str, content: dict, _auth: None = Depends(require_auth)):
|
||||||
"""Create or update a note"""
|
"""Create or update a note"""
|
||||||
try:
|
try:
|
||||||
note_content = content.get('content', '')
|
note_content = content.get('content', '')
|
||||||
|
|
@ -460,7 +597,7 @@ async def create_or_update_note(note_path: str, content: dict):
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/api/notes/{note_path:path}")
|
@app.delete("/api/notes/{note_path:path}")
|
||||||
async def remove_note(note_path: str):
|
async def remove_note(note_path: str, _auth: None = Depends(require_auth)):
|
||||||
"""Delete a note"""
|
"""Delete a note"""
|
||||||
try:
|
try:
|
||||||
success = delete_note(config['storage']['notes_dir'], note_path)
|
success = delete_note(config['storage']['notes_dir'], note_path)
|
||||||
|
|
@ -480,7 +617,7 @@ async def remove_note(note_path: str):
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/search")
|
@app.get("/api/search")
|
||||||
async def search(q: str):
|
async def search(q: str, _auth: None = Depends(require_auth)):
|
||||||
"""Search notes by content"""
|
"""Search notes by content"""
|
||||||
try:
|
try:
|
||||||
if not config['search']['enabled']:
|
if not config['search']['enabled']:
|
||||||
|
|
@ -499,7 +636,7 @@ async def search(q: str):
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/graph")
|
@app.get("/api/graph")
|
||||||
async def get_graph():
|
async def get_graph(_auth: None = Depends(require_auth)):
|
||||||
"""Get graph data for visualization"""
|
"""Get graph data for visualization"""
|
||||||
try:
|
try:
|
||||||
notes = get_all_notes(config['storage']['notes_dir'])
|
notes = get_all_notes(config['storage']['notes_dir'])
|
||||||
|
|
@ -529,13 +666,13 @@ async def get_graph():
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/plugins")
|
@app.get("/api/plugins")
|
||||||
async def list_plugins():
|
async def list_plugins(_auth: None = Depends(require_auth)):
|
||||||
"""List all available plugins"""
|
"""List all available plugins"""
|
||||||
return {"plugins": plugin_manager.list_plugins()}
|
return {"plugins": plugin_manager.list_plugins()}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/plugins/note_stats/calculate")
|
@app.get("/api/plugins/note_stats/calculate")
|
||||||
async def calculate_note_stats(content: str):
|
async def calculate_note_stats(content: str, _auth: None = Depends(require_auth)):
|
||||||
"""Calculate statistics for note content (if plugin enabled)"""
|
"""Calculate statistics for note content (if plugin enabled)"""
|
||||||
try:
|
try:
|
||||||
plugin = plugin_manager.plugins.get('note_stats')
|
plugin = plugin_manager.plugins.get('note_stats')
|
||||||
|
|
@ -549,7 +686,7 @@ async def calculate_note_stats(content: str):
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/plugins/{plugin_name}/toggle")
|
@app.post("/api/plugins/{plugin_name}/toggle")
|
||||||
async def toggle_plugin(plugin_name: str, enabled: dict):
|
async def toggle_plugin(plugin_name: str, enabled: dict, _auth: None = Depends(require_auth)):
|
||||||
"""Enable or disable a plugin"""
|
"""Enable or disable a plugin"""
|
||||||
try:
|
try:
|
||||||
is_enabled = enabled.get('enabled', False)
|
is_enabled = enabled.get('enabled', False)
|
||||||
|
|
@ -580,7 +717,7 @@ async def health_check():
|
||||||
# Catch-all route for SPA (Single Page Application) routing
|
# Catch-all route for SPA (Single Page Application) routing
|
||||||
# This allows URLs like /folder/note to work for direct navigation
|
# This allows URLs like /folder/note to work for direct navigation
|
||||||
@app.get("/{full_path:path}", response_class=HTMLResponse)
|
@app.get("/{full_path:path}", response_class=HTMLResponse)
|
||||||
async def catch_all(full_path: str):
|
async def catch_all(full_path: str, request: Request, _auth: None = Depends(require_auth)):
|
||||||
"""
|
"""
|
||||||
Serve index.html for all non-API routes.
|
Serve index.html for all non-API routes.
|
||||||
This enables client-side routing (e.g., /folder/note)
|
This enables client-side routing (e.g., /folder/note)
|
||||||
|
|
|
||||||
22
config.yaml
22
config.yaml
|
|
@ -20,6 +20,24 @@ search:
|
||||||
index_dir: "./data/search_index"
|
index_dir: "./data/search_index"
|
||||||
|
|
||||||
security:
|
security:
|
||||||
# Add authentication later if needed
|
# Authentication settings
|
||||||
require_auth: false
|
# Set enabled to true to require login
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Session secret key - CHANGE THIS TO A RANDOM STRING!
|
||||||
|
# Generate with: python -c "import secrets; print(secrets.token_hex(32))"
|
||||||
|
secret_key: "change_this_to_a_random_secret_key_in_production"
|
||||||
|
|
||||||
|
# Password hash - Generate with: python generate_password.py
|
||||||
|
# INSTRUCTIONS:
|
||||||
|
# 1. Run: pip install bcrypt
|
||||||
|
# 2. Run: python generate_password.py
|
||||||
|
# 3. Enter your desired password
|
||||||
|
# 4. Copy the generated hash below
|
||||||
|
# 5. Set enabled: true above
|
||||||
|
# 6. Restart the application
|
||||||
|
password_hash: "$2b$12$t/6PGExFzdpU2PUta0iVY.eDQwvu63kH.c/d4bEnnHaQ5CspH1yrG"
|
||||||
|
|
||||||
|
# Session expiry in seconds (default: 7 days)
|
||||||
|
session_max_age: 604800
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -702,7 +702,20 @@
|
||||||
</template>
|
</template>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xs" style="color: var(--text-secondary);" x-text="appTagline"></p>
|
<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)">
|
||||||
|
<a x-show="authEnabled"
|
||||||
|
href="/logout"
|
||||||
|
class="flex items-center justify-center gap-2 px-3 py-2 text-sm rounded transition-colors"
|
||||||
|
style="background-color: var(--bg-secondary); color: var(--text-secondary); border: 1px solid var(--border-primary);"
|
||||||
|
onmouseover="this.style.backgroundColor='var(--bg-hover)'; this.style.color='var(--text-primary)'"
|
||||||
|
onmouseout="this.style.backgroundColor='var(--bg-secondary)'; this.style.color='var(--text-secondary)'">
|
||||||
|
<span>🔒</span>
|
||||||
|
<span>Logout</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Search -->
|
<!-- Search -->
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login - NoteDiscovery</title>
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||||
|
|
||||||
|
<!-- Theme styles will be loaded dynamically -->
|
||||||
|
<script>
|
||||||
|
// Load theme immediately (same approach as index.html)
|
||||||
|
(async function() {
|
||||||
|
const savedTheme = localStorage.getItem('noteDiscoveryTheme') || 'light';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/themes/${savedTheme}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Create style element with theme CSS
|
||||||
|
const styleEl = document.createElement('style');
|
||||||
|
styleEl.id = 'dynamic-theme';
|
||||||
|
styleEl.textContent = data.css;
|
||||||
|
document.head.appendChild(styleEl);
|
||||||
|
|
||||||
|
// Set data attribute for theme-specific selectors
|
||||||
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load theme:', error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 1rem;
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
padding: 3rem 2rem;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
margin: 0 auto 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 1.75rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagline {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="password"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
|
border: 2px solid var(--border-primary);
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.3s;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="password"]:focus {
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
box-shadow: 0 0 0 3px var(--accent-light, rgba(102, 126, 234, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
|
background-color: var(--accent-primary);
|
||||||
|
color: var(--bg-primary);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s, background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: var(--accent-hover);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 20px var(--accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background-color: #fee;
|
||||||
|
color: #c53030;
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border: 1px solid #fcc;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: 2rem;
|
||||||
|
color: var(--text-tertiary, var(--text-secondary));
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsive */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.login-container {
|
||||||
|
padding: 2rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<img src="/static/logo.svg" alt="NoteDiscovery" class="logo">
|
||||||
|
<h1>NoteDiscovery</h1>
|
||||||
|
<p class="tagline">Your Self-Hosted Knowledge Base</p>
|
||||||
|
|
||||||
|
<!-- ERROR_PLACEHOLDER -->
|
||||||
|
|
||||||
|
<form method="post" action="/login">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
required
|
||||||
|
autofocus
|
||||||
|
>
|
||||||
|
<button type="submit">🔓 Unlock</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
🔒 Secure & Self-Hosted
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Helper script to generate a password hash for NoteDiscovery authentication.
|
||||||
|
Usage: python generate_password.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import getpass
|
||||||
|
import bcrypt
|
||||||
|
|
||||||
|
def generate_password_hash():
|
||||||
|
"""Generate a bcrypt password hash"""
|
||||||
|
print("=== NoteDiscovery Password Hash Generator ===\n")
|
||||||
|
|
||||||
|
# Get password from user (hidden input)
|
||||||
|
password = getpass.getpass("Enter your password: ")
|
||||||
|
password_confirm = getpass.getpass("Confirm your password: ")
|
||||||
|
|
||||||
|
# Check if passwords match
|
||||||
|
if password != password_confirm:
|
||||||
|
print("\n❌ Error: Passwords do not match!")
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(password) < 4:
|
||||||
|
print("\n⚠️ Warning: Password is too short! Use at least 8 characters for better security.")
|
||||||
|
proceed = input("Continue anyway? (y/N): ")
|
||||||
|
if proceed.lower() != 'y':
|
||||||
|
return
|
||||||
|
|
||||||
|
# Generate hash
|
||||||
|
print("\n🔐 Generating password hash...")
|
||||||
|
password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
||||||
|
|
||||||
|
print("\n✅ Password hash generated successfully!")
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("Copy this hash to your config.yaml:")
|
||||||
|
print("="*60)
|
||||||
|
print(f"\npassword_hash: \"{password_hash}\"")
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print("\nExample config.yaml:")
|
||||||
|
print("="*60)
|
||||||
|
print("""
|
||||||
|
security:
|
||||||
|
enabled: true
|
||||||
|
secret_key: "your_secret_key_here"
|
||||||
|
password_hash: "{}"
|
||||||
|
session_max_age: 604800
|
||||||
|
""".format(password_hash))
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
print("\n💡 Don't forget to also set a secret key!")
|
||||||
|
print(" Generate one with: python -c \"import secrets; print(secrets.token_hex(32))\"")
|
||||||
|
print("\n🔒 Keep your password and secret key secure!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
generate_password_hash()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\n❌ Cancelled.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ Error: {e}")
|
||||||
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Generate password hash inside Docker container
|
||||||
|
# Usage: docker exec -it notediscovery /app/generate_password_hash.sh
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo " NoteDiscovery Password Hash Generator"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
echo "This will generate a bcrypt hash for your password."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prompt for password
|
||||||
|
read -s -p "Enter your password: " PASSWORD
|
||||||
|
echo ""
|
||||||
|
read -s -p "Confirm password: " PASSWORD2
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if passwords match
|
||||||
|
if [ "$PASSWORD" != "$PASSWORD2" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "❌ Passwords do not match!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if password is empty
|
||||||
|
if [ -z "$PASSWORD" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "❌ Password cannot be empty!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Generating hash..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Generate hash using Python
|
||||||
|
HASH=$(python3 -c "import bcrypt; print(bcrypt.hashpw(b'$PASSWORD', bcrypt.gensalt()).decode())")
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "=========================================="
|
||||||
|
echo " ✅ Password hash generated!"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
echo "Copy this hash to your config.yaml:"
|
||||||
|
echo ""
|
||||||
|
echo "password_hash: \"$HASH\""
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo "1. Update config.yaml with the hash above"
|
||||||
|
echo "2. Set enabled: true in the security section"
|
||||||
|
echo "3. Restart the container: docker-compose restart"
|
||||||
|
echo "=========================================="
|
||||||
|
else
|
||||||
|
echo "❌ Failed to generate hash"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
@ -5,4 +5,6 @@ markdown==3.5.1
|
||||||
pyyaml==6.0.1
|
pyyaml==6.0.1
|
||||||
aiofiles==23.2.1
|
aiofiles==23.2.1
|
||||||
cryptography==41.0.7
|
cryptography==41.0.7
|
||||||
|
bcrypt==4.1.2
|
||||||
|
itsdangerous==2.1.2
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue