Merge pull request #15 from gamosoft/features/authentication
Features/authentication - added authentication - possible breaking change. simplified data folder - creation of releases on tag - removed search_index for now
This commit is contained in:
commit
7afa65e802
|
|
@ -1,12 +1,6 @@
|
|||
name: Build and Push Docker Image to GHCR
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
|
@ -18,19 +12,16 @@ env:
|
|||
|
||||
jobs:
|
||||
build-and-push:
|
||||
# Only run if PR was merged (not just closed) or if triggered by tag/manual
|
||||
if: |
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true) ||
|
||||
github.event_name == 'push' ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for release notes
|
||||
|
||||
- name: Set up QEMU for multi-architecture builds
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
|
@ -73,3 +64,70 @@ jobs:
|
|||
- name: Image digest
|
||||
run: echo "Image pushed with digest ${{ steps.build-push.outputs.digest }}"
|
||||
|
||||
- name: Generate release notes
|
||||
id: release_notes
|
||||
run: |
|
||||
# Get the previous tag
|
||||
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
||||
|
||||
# Generate commit log - extract PR descriptions from merge commits
|
||||
if [ -z "$PREVIOUS_TAG" ]; then
|
||||
# First release, get only merge commits (PR merges)
|
||||
RAW_COMMITS=$(git log --merges --pretty=format:"%s|||%b|||%h" --first-parent)
|
||||
else
|
||||
# Get merge commits since last tag
|
||||
RAW_COMMITS=$(git log ${PREVIOUS_TAG}..HEAD --merges --pretty=format:"%s|||%b|||%h" --first-parent)
|
||||
fi
|
||||
|
||||
# Process commits to extract meaningful messages
|
||||
COMMITS=""
|
||||
while IFS= read -r line; do
|
||||
if [ -n "$line" ]; then
|
||||
SUBJECT=$(echo "$line" | cut -d'|' -f1)
|
||||
BODY=$(echo "$line" | cut -d'|' -f4 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
||||
HASH=$(echo "$line" | cut -d'|' -f7)
|
||||
|
||||
# If body exists and is not empty, use it; otherwise use subject
|
||||
if [ -n "$BODY" ] && [ "$BODY" != "" ]; then
|
||||
# Capitalize first letter of body
|
||||
MESSAGE=$(echo "$BODY" | sed 's/^./\U&/')
|
||||
COMMITS="${COMMITS}- ${MESSAGE} (${HASH})"$'\n'
|
||||
else
|
||||
COMMITS="${COMMITS}- ${SUBJECT} (${HASH})"$'\n'
|
||||
fi
|
||||
fi
|
||||
done <<< "$RAW_COMMITS"
|
||||
|
||||
# Create release notes
|
||||
echo "## What's Changed" > release_notes.md
|
||||
echo "" >> release_notes.md
|
||||
if [ -z "$COMMITS" ]; then
|
||||
echo "- Minor updates and improvements" >> release_notes.md
|
||||
else
|
||||
echo "$COMMITS" >> release_notes.md
|
||||
fi
|
||||
echo "" >> release_notes.md
|
||||
echo "## Docker Images" >> release_notes.md
|
||||
echo "" >> release_notes.md
|
||||
echo "This release is available as a Docker image:" >> release_notes.md
|
||||
echo '```bash' >> release_notes.md
|
||||
echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${GITHUB_REF_NAME#v}" >> release_notes.md
|
||||
echo '```' >> release_notes.md
|
||||
echo "" >> release_notes.md
|
||||
if [ -z "$PREVIOUS_TAG" ]; then
|
||||
echo "**Full Changelog**: https://github.com/${{ github.repository }}/commits/${GITHUB_REF_NAME}" >> release_notes.md
|
||||
else
|
||||
echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREVIOUS_TAG}...${GITHUB_REF_NAME}" >> release_notes.md
|
||||
fi
|
||||
|
||||
cat release_notes.md
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body_path: release_notes.md
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ env/
|
|||
.venv
|
||||
|
||||
# Data directories
|
||||
search_index/
|
||||
data/
|
||||
*.db
|
||||
*.sqlite
|
||||
|
||||
|
|
|
|||
|
|
@ -27,9 +27,10 @@ COPY frontend ./frontend
|
|||
COPY config.yaml .
|
||||
COPY plugins ./plugins
|
||||
COPY themes ./themes
|
||||
COPY generate_password.py .
|
||||
|
||||
# Create data directories
|
||||
RUN mkdir -p data/notes data/search_index
|
||||
# Create data directory
|
||||
RUN mkdir -p data
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
|
|
|||
63
README.md
63
README.md
|
|
@ -32,6 +32,7 @@ NoteDiscovery is a **lightweight, self-hosted note-taking application** that put
|
|||
### Key Benefits
|
||||
|
||||
- 🔒 **Total Privacy** - Your notes never leave your server
|
||||
- 🔐 **Optional Authentication** - Simple password protection for self-hosted deployments
|
||||
- 💰 **Zero Cost** - No subscriptions, no hidden fees
|
||||
- 🚀 **Fast & Lightweight** - Instant search and navigation
|
||||
- 🎨 **Beautiful Themes** - Multiple themes, easy to customize
|
||||
|
|
@ -47,20 +48,21 @@ NoteDiscovery is a **lightweight, self-hosted note-taking application** that put
|
|||
|
||||
Use the pre-built image directly from GHCR - no building required!
|
||||
|
||||
> **💡 Tip**: Always use `ghcr.io/gamosoft/notediscovery:latest` to get the newest features and fixes. Images are automatically built when PRs are merged to main.
|
||||
> **💡 Tip**: Always use `ghcr.io/gamosoft/notediscovery:latest` to get the newest features and fixes.
|
||||
|
||||
> **📁 Important - Volume Mapping**: The container needs local folders/files to work:
|
||||
> - **Required**: `data` folder (your notes will be stored here)
|
||||
> - **Required**: `themes` folder with theme `.css` files (at least light.css and dark.css)
|
||||
> - **Required**: `data` folder - **Your personal notes** will be stored here (create an empty folder)
|
||||
> - **Required**: `themes` folder with theme `.css` files (at least a single theme must exist)
|
||||
> - **Required**: `plugins` folder (can be empty for basic functionality)
|
||||
> - **Required**: `config.yaml` file (needed for the app to run)
|
||||
> - **Optional**: `documentation` folder - If you cloned the repo, mount this to view app docs inside NoteDiscovery
|
||||
>
|
||||
> **Setup Options:**
|
||||
>
|
||||
> 1. **Minimal** (quick test - download just the essentials):
|
||||
> ```bash
|
||||
> # Linux/macOS
|
||||
> mkdir -p data plugins themes
|
||||
> mkdir -p data plugins themes # data/ is for YOUR notes
|
||||
> curl -O https://raw.githubusercontent.com/gamosoft/notediscovery/main/config.yaml
|
||||
> # Download at least light and dark themes
|
||||
> curl -o themes/light.css https://raw.githubusercontent.com/gamosoft/notediscovery/main/themes/light.css
|
||||
|
|
@ -69,20 +71,23 @@ Use the pre-built image directly from GHCR - no building required!
|
|||
>
|
||||
> ```powershell
|
||||
> # Windows PowerShell
|
||||
> mkdir data, plugins, themes -Force
|
||||
> mkdir data, plugins, themes -Force # data\ is for YOUR notes
|
||||
> Invoke-WebRequest -Uri https://raw.githubusercontent.com/gamosoft/notediscovery/main/config.yaml -OutFile config.yaml
|
||||
> # Download at least light and dark themes
|
||||
> Invoke-WebRequest -Uri https://raw.githubusercontent.com/gamosoft/notediscovery/main/themes/light.css -OutFile themes/light.css
|
||||
> Invoke-WebRequest -Uri https://raw.githubusercontent.com/gamosoft/notediscovery/main/themes/dark.css -OutFile themes/dark.css
|
||||
> ```
|
||||
>
|
||||
> 2. **Full Setup** (recommended - includes all themes, plugins, sample notes):
|
||||
> 2. **Full Setup** (recommended - includes all themes, plugins, and documentation):
|
||||
> ```bash
|
||||
> git clone https://github.com/gamosoft/notediscovery.git
|
||||
> cd notediscovery
|
||||
> # Now you have everything - run docker-compose below
|
||||
> # The data/ folder is empty - for your personal notes
|
||||
> # The documentation/ folder has app docs you can optionally mount
|
||||
> ```
|
||||
|
||||
> **🔐 Security Note**: Authentication is **disabled by default** with password `admin`. For testing/local use, this is fine. If exposing to a network, **change the password immediately** - see [AUTHENTICATION.md](documentation/AUTHENTICATION.md) for instructions on how to enable it.
|
||||
|
||||
**Option 1: Docker Compose (Recommended)**
|
||||
|
||||
> 💡 **Multi-Architecture Support**: Docker images are available for both `x86_64` and `ARM64` (Raspberry Pi, Apple Silicon, etc.)
|
||||
|
|
@ -95,6 +100,7 @@ curl -O https://raw.githubusercontent.com/gamosoft/notediscovery/main/docker-com
|
|||
docker-compose -f docker-compose.ghcr.yml up -d
|
||||
|
||||
# Access at http://localhost:8000
|
||||
# Login with default password: admin
|
||||
|
||||
# View logs
|
||||
docker-compose -f docker-compose.ghcr.yml logs -f
|
||||
|
|
@ -197,15 +203,25 @@ python run.py
|
|||
Want to learn more? **The full documentation lives inside the app as interactive notes!**
|
||||
|
||||
Once you've started NoteDiscovery, you'll find comprehensive guides on:
|
||||
- 🎨 **THEMES.md** - Theme customization and creating custom themes
|
||||
- ✨ **FEATURES.md** - Complete feature list and keyboard shortcuts
|
||||
- 🧮 **MATHJAX.md** - LaTeX/Math notation examples and syntax reference
|
||||
- 🔌 **PLUGINS.md** - Plugin system and available plugins
|
||||
- 🌐 **API.md** - REST API documentation and examples
|
||||
- 🎨 **[THEMES.md](documentation/THEMES.md)** - Theme customization and creating custom themes
|
||||
- ✨ **[FEATURES.md](documentation/FEATURES.md)** - Complete feature list and keyboard shortcuts
|
||||
- 🧮 **[MATHJAX.md](documentation/MATHJAX.md)** - LaTeX/Math notation examples and syntax reference
|
||||
- 🔌 **[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
|
||||
|
||||
**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 [`documentation/`](documentation/) folder!
|
||||
|
||||
💡 **Tip:** These documentation files are regular markdown notes—edit them, add your own notes, or use them as templates. It's your knowledge base!
|
||||
💡 **Pro Tip:** If you clone this repository, you can mount the `documentation/` folder to view these docs inside the app:
|
||||
|
||||
```yaml
|
||||
# In your docker-compose.yml
|
||||
volumes:
|
||||
- ./data:/app/data # Your personal notes
|
||||
- ./documentation:/app/data/docs:ro # Mount docs subfolder inside the data folder (read-only)
|
||||
```
|
||||
|
||||
Then access them at `http://localhost:8000` - the docs will appear as a `docs/` folder in the file browser!
|
||||
|
||||
## 💖 Support Development
|
||||
|
||||
|
|
@ -217,18 +233,21 @@ NoteDiscovery is designed for **self-hosted, private use**. Please keep these se
|
|||
|
||||
### Network Security
|
||||
- ⚠️ **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
|
||||
- By default, the app listens on `0.0.0.0:8000` (all network interfaces)
|
||||
|
||||
### No Built-in Authentication
|
||||
- The app has **no authentication by design** (single-user, self-hosted)
|
||||
- Anyone with network access can read and modify your notes
|
||||
- Use network-level security (firewall, VPN) for access control
|
||||
- Consider adding authentication via reverse proxy if needed
|
||||
### Authentication
|
||||
- **Password protection is ENABLED by default** with password: `admin`
|
||||
- ⚠️ **CHANGE THE DEFAULT PASSWORD IMMEDIATELY** if exposing to a network!
|
||||
- See **[AUTHENTICATION.md](documentation/AUTHENTICATION.md)** for complete setup instructions
|
||||
- To disable auth, set `security.enabled: false` in `config.yaml`
|
||||
- Change password with Docker: `docker-compose exec notediscovery python generate_password.py`
|
||||
- Perfect for single-user or small team deployments
|
||||
- For multi-user setups, consider a reverse proxy with OAuth/SSO
|
||||
|
||||
### Data Privacy
|
||||
- Your notes are stored as **plain text markdown files** in `data/notes/`
|
||||
- Your notes are stored as **plain text markdown files** in the `data/` folder
|
||||
- No data is sent to external services
|
||||
- Regular backups are recommended
|
||||
|
||||
|
|
@ -239,7 +258,7 @@ NoteDiscovery is designed for **self-hosted, private use**. Please keep these se
|
|||
- Review and audit any plugins you install
|
||||
- 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
|
||||
|
||||
|
|
|
|||
210
backend/main.py
210
backend/main.py
|
|
@ -3,10 +3,11 @@ NoteDiscovery - Self-Hosted Markdown Knowledge Base
|
|||
Main FastAPI application
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI, HTTPException, UploadFile, File
|
||||
from fastapi import FastAPI, HTTPException, UploadFile, File, Request, Form, Depends, APIRouter
|
||||
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 starlette.middleware.sessions import SessionMiddleware
|
||||
import os
|
||||
import yaml
|
||||
import json
|
||||
|
|
@ -14,6 +15,7 @@ from pathlib import Path
|
|||
from typing import List, Optional
|
||||
import aiofiles
|
||||
from datetime import datetime
|
||||
import bcrypt
|
||||
|
||||
from .utils import (
|
||||
get_all_notes,
|
||||
|
|
@ -55,6 +57,15 @@ app.add_middleware(
|
|||
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_directories(config)
|
||||
|
||||
|
|
@ -69,8 +80,144 @@ static_path = Path(__file__).parent.parent / "frontend"
|
|||
app.mount("/static", StaticFiles(directory=static_path), name="static")
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def root():
|
||||
# ============================================================================
|
||||
# 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_CLASS_PLACEHOLDER -->', 'class="error"')
|
||||
content = content.replace('<!-- ERROR_MESSAGE_PLACEHOLDER -->',
|
||||
f'<div class="error-message">{error}</div>')
|
||||
else:
|
||||
content = content.replace('<!-- ERROR_CLASS_PLACEHOLDER -->', '')
|
||||
content = content.replace('<!-- ERROR_MESSAGE_PLACEHOLDER -->', '')
|
||||
|
||||
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=Incorrect+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)
|
||||
|
||||
# ============================================================================
|
||||
# Routers with Authentication
|
||||
# ============================================================================
|
||||
|
||||
# Create API router with authentication dependency applied globally
|
||||
api_router = APIRouter(
|
||||
prefix="/api",
|
||||
dependencies=[Depends(require_auth)] # Apply auth to ALL routes in this router
|
||||
)
|
||||
|
||||
# Create pages router with authentication dependency applied globally
|
||||
pages_router = APIRouter(
|
||||
dependencies=[Depends(require_auth)] # Apply auth to ALL routes in this router
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Application Routes (with auth via router dependencies)
|
||||
# ============================================================================
|
||||
|
||||
@pages_router.get("/", response_class=HTMLResponse)
|
||||
async def root(request: Request):
|
||||
"""Serve the main application page"""
|
||||
index_path = static_path / "index.html"
|
||||
async with aiofiles.open(index_path, 'r', encoding='utf-8') as f:
|
||||
|
|
@ -78,7 +225,7 @@ async def root():
|
|||
return content
|
||||
|
||||
|
||||
@app.get("/api")
|
||||
@api_router.get("")
|
||||
async def api_documentation():
|
||||
"""API Documentation - List all available endpoints"""
|
||||
return {
|
||||
|
|
@ -230,18 +377,21 @@ async def api_documentation():
|
|||
}
|
||||
|
||||
|
||||
@app.get("/api/config")
|
||||
@api_router.get("/config")
|
||||
async def get_config():
|
||||
"""Get app configuration for frontend"""
|
||||
return {
|
||||
"name": config['app']['name'],
|
||||
"tagline": config['app']['tagline'],
|
||||
"version": config['app']['version'],
|
||||
"searchEnabled": config['search']['enabled']
|
||||
"searchEnabled": config['search']['enabled'],
|
||||
"security": {
|
||||
"enabled": config.get('security', {}).get('enabled', False)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/themes")
|
||||
@api_router.get("/themes")
|
||||
async def list_themes():
|
||||
"""Get all available themes"""
|
||||
themes_dir = Path(__file__).parent.parent / "themes"
|
||||
|
|
@ -249,7 +399,7 @@ async def list_themes():
|
|||
return {"themes": themes}
|
||||
|
||||
|
||||
@app.get("/api/themes/{theme_id}")
|
||||
@app.get("/api/themes/{theme_id}") # Don't use the router here, as we want this route unsecured
|
||||
async def get_theme(theme_id: str):
|
||||
"""Get CSS for a specific theme"""
|
||||
themes_dir = Path(__file__).parent.parent / "themes"
|
||||
|
|
@ -261,7 +411,7 @@ async def get_theme(theme_id: str):
|
|||
return {"css": css, "theme_id": theme_id}
|
||||
|
||||
|
||||
@app.post("/api/folders")
|
||||
@api_router.post("/folders")
|
||||
async def create_new_folder(data: dict):
|
||||
"""Create a new folder"""
|
||||
try:
|
||||
|
|
@ -283,7 +433,7 @@ async def create_new_folder(data: dict):
|
|||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/api/notes/move")
|
||||
@api_router.post("/notes/move")
|
||||
async def move_note_endpoint(data: dict):
|
||||
"""Move a note to a different folder"""
|
||||
try:
|
||||
|
|
@ -311,7 +461,7 @@ async def move_note_endpoint(data: dict):
|
|||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/api/folders/move")
|
||||
@api_router.post("/folders/move")
|
||||
async def move_folder_endpoint(data: dict):
|
||||
"""Move a folder to a different location"""
|
||||
try:
|
||||
|
|
@ -336,7 +486,7 @@ async def move_folder_endpoint(data: dict):
|
|||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/api/folders/rename")
|
||||
@api_router.post("/folders/rename")
|
||||
async def rename_folder_endpoint(data: dict):
|
||||
"""Rename a folder"""
|
||||
try:
|
||||
|
|
@ -361,7 +511,7 @@ async def rename_folder_endpoint(data: dict):
|
|||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.delete("/api/folders/{folder_path:path}")
|
||||
@api_router.delete("/folders/{folder_path:path}")
|
||||
async def delete_folder_endpoint(folder_path: str):
|
||||
"""Delete a folder and all its contents"""
|
||||
try:
|
||||
|
|
@ -382,7 +532,7 @@ async def delete_folder_endpoint(folder_path: str):
|
|||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/api/notes")
|
||||
@api_router.get("/notes")
|
||||
async def list_notes():
|
||||
"""List all notes with metadata"""
|
||||
try:
|
||||
|
|
@ -393,7 +543,7 @@ async def list_notes():
|
|||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/api/notes/{note_path:path}")
|
||||
@api_router.get("/notes/{note_path:path}")
|
||||
async def get_note(note_path: str):
|
||||
"""Get a specific note's content"""
|
||||
try:
|
||||
|
|
@ -421,7 +571,7 @@ async def get_note(note_path: str):
|
|||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/api/notes/{note_path:path}")
|
||||
@api_router.post("/notes/{note_path:path}")
|
||||
async def create_or_update_note(note_path: str, content: dict):
|
||||
"""Create or update a note"""
|
||||
try:
|
||||
|
|
@ -459,7 +609,7 @@ async def create_or_update_note(note_path: str, content: dict):
|
|||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.delete("/api/notes/{note_path:path}")
|
||||
@api_router.delete("/notes/{note_path:path}")
|
||||
async def remove_note(note_path: str):
|
||||
"""Delete a note"""
|
||||
try:
|
||||
|
|
@ -479,7 +629,7 @@ async def remove_note(note_path: str):
|
|||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/api/search")
|
||||
@api_router.get("/search")
|
||||
async def search(q: str):
|
||||
"""Search notes by content"""
|
||||
try:
|
||||
|
|
@ -498,7 +648,7 @@ async def search(q: str):
|
|||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/api/graph")
|
||||
@api_router.get("/graph")
|
||||
async def get_graph():
|
||||
"""Get graph data for visualization"""
|
||||
try:
|
||||
|
|
@ -528,13 +678,13 @@ async def get_graph():
|
|||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.get("/api/plugins")
|
||||
@api_router.get("/plugins")
|
||||
async def list_plugins():
|
||||
"""List all available plugins"""
|
||||
return {"plugins": plugin_manager.list_plugins()}
|
||||
|
||||
|
||||
@app.get("/api/plugins/note_stats/calculate")
|
||||
@api_router.get("/plugins/note_stats/calculate")
|
||||
async def calculate_note_stats(content: str):
|
||||
"""Calculate statistics for note content (if plugin enabled)"""
|
||||
try:
|
||||
|
|
@ -548,7 +698,7 @@ async def calculate_note_stats(content: str):
|
|||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@app.post("/api/plugins/{plugin_name}/toggle")
|
||||
@api_router.post("/plugins/{plugin_name}/toggle")
|
||||
async def toggle_plugin(plugin_name: str, enabled: dict):
|
||||
"""Enable or disable a plugin"""
|
||||
try:
|
||||
|
|
@ -579,8 +729,8 @@ async def health_check():
|
|||
|
||||
# Catch-all route for SPA (Single Page Application) routing
|
||||
# This allows URLs like /folder/note to work for direct navigation
|
||||
@app.get("/{full_path:path}", response_class=HTMLResponse)
|
||||
async def catch_all(full_path: str):
|
||||
@pages_router.get("/{full_path:path}", response_class=HTMLResponse)
|
||||
async def catch_all(full_path: str, request: Request):
|
||||
"""
|
||||
Serve index.html for all non-API routes.
|
||||
This enables client-side routing (e.g., /folder/note)
|
||||
|
|
@ -596,6 +746,16 @@ async def catch_all(full_path: str):
|
|||
return content
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Register Routers
|
||||
# ============================================================================
|
||||
|
||||
# Register routers with the main app
|
||||
# Authentication is applied via router dependencies
|
||||
app.include_router(api_router)
|
||||
app.include_router(pages_router)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
|
|
|
|||
|
|
@ -35,8 +35,6 @@ def ensure_directories(config: dict):
|
|||
config['storage']['notes_dir'],
|
||||
config['storage']['plugins_dir'],
|
||||
]
|
||||
if config['search']['enabled']:
|
||||
dirs.append(config['search']['index_dir'])
|
||||
|
||||
for dir_path in dirs:
|
||||
Path(dir_path).mkdir(parents=True, exist_ok=True)
|
||||
|
|
|
|||
18
config.yaml
18
config.yaml
|
|
@ -12,14 +12,24 @@ server:
|
|||
reload: false # Set to true for development
|
||||
|
||||
storage:
|
||||
notes_dir: "./data/notes"
|
||||
notes_dir: "./data"
|
||||
plugins_dir: "./plugins"
|
||||
|
||||
search:
|
||||
enabled: true
|
||||
index_dir: "./data/search_index"
|
||||
|
||||
security:
|
||||
# Add authentication later if needed
|
||||
require_auth: false
|
||||
# Authentication settings
|
||||
# Set enabled to true to require login
|
||||
enabled: false
|
||||
|
||||
# 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
|
||||
password_hash: "$2b$12$t/6PGExFzdpU2PUta0iVY.eDQwvu63kH.c/d4bEnnHaQ5CspH1yrG" # Default: "admin"
|
||||
|
||||
# Session expiry in seconds (default: 7 days)
|
||||
session_max_age: 604800
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ services:
|
|||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
# Persist notes and data (inside 'notes' folder)
|
||||
# Persist notes and data
|
||||
- ./data:/app/data
|
||||
# Custom plugins
|
||||
- ./plugins:/app/plugins
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ services:
|
|||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
# Persist notes and data (inside 'notes' folder)
|
||||
# Persist notes and data
|
||||
- ./data:/app/data
|
||||
# Custom plugins
|
||||
- ./plugins:/app/plugins
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 363 KiB After Width: | Height: | Size: 338 KiB |
|
|
@ -46,7 +46,7 @@ Content-Type: application/json
|
|||
}
|
||||
```
|
||||
|
||||
**Note:** When creating a new note, the `on_note_create` hook is triggered, allowing plugins (like Template Generator) to modify the initial content. The response includes the potentially modified content.
|
||||
**Note:** When creating a new note, the `on_note_create` hook is triggered, allowing plugins to modify the initial content. The response includes the potentially modified content.
|
||||
|
||||
**Linux/Mac:**
|
||||
```bash
|
||||
|
|
@ -241,11 +241,6 @@ All endpoints return JSON responses:
|
|||
"detail": "Error message"
|
||||
}
|
||||
```
|
||||
|
||||
## 🔑 Authentication
|
||||
|
||||
Currently, no authentication is required. Suitable for self-hosted local environments.
|
||||
|
||||
---
|
||||
|
||||
💡 **Tip:** Use the `/api` endpoint to get a live, self-documented list of all available endpoints!
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
# 🔒 NoteDiscovery Authentication Guide
|
||||
|
||||
## ⚠️ **IMPORTANT: Default Password Warning**
|
||||
|
||||
> **The default `config.yaml` includes authentication disabled by default, with password: `admin`**
|
||||
>
|
||||
> 🔴 **CHANGE THIS if you're exposing NoteDiscovery to a network!**
|
||||
>
|
||||
> The default configuration is provided for **quick testing only**. Follow the setup guide below to set your own secure password and secret key.
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Setup
|
||||
|
||||
**Default Configuration:**
|
||||
- Authentication is **enabled by default**
|
||||
- Default password is `admin`
|
||||
- Default secret key is insecure
|
||||
|
||||
**⚠️ IMPORTANT:** For production or network-exposed deployments, **change both the password and secret key immediately**.
|
||||
|
||||
---
|
||||
|
||||
### 🧪 **Quick Test (Use Default Password)**
|
||||
|
||||
For **local testing only**, you can use the default configuration:
|
||||
|
||||
1. Start NoteDiscovery (Docker or locally)
|
||||
2. Navigate to `http://localhost:8000`
|
||||
3. Log in with password: `admin`
|
||||
|
||||
**⚠️ Only use this for local testing on your own machine!**
|
||||
|
||||
---
|
||||
|
||||
### 🔒 **Production Setup (Change Password & Secret Key)**
|
||||
|
||||
For any deployment exposed to a network, follow these steps:
|
||||
|
||||
### Step 1: Generate a Password Hash
|
||||
|
||||
Choose your environment:
|
||||
|
||||
**Docker Users:**
|
||||
|
||||
```bash
|
||||
# Docker Compose
|
||||
docker-compose exec notediscovery python generate_password.py
|
||||
|
||||
# Or with docker run
|
||||
docker exec -it notediscovery python generate_password.py
|
||||
```
|
||||
|
||||
**Local Users:**
|
||||
|
||||
```bash
|
||||
# Install bcrypt if not already installed
|
||||
pip install bcrypt
|
||||
|
||||
# Run the password generator
|
||||
python generate_password.py
|
||||
```
|
||||
|
||||
The script will:
|
||||
1. Prompt you for your password (input is hidden)
|
||||
2. Ask you to confirm it
|
||||
3. Generate a bcrypt hash
|
||||
4. Display the hash with instructions
|
||||
|
||||
**Copy the hash** - you'll need it for Step 3.
|
||||
|
||||
### Step 2: Generate a Secret Key
|
||||
|
||||
Generate a random secret key for session encryption:
|
||||
|
||||
**Docker Users:**
|
||||
```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))"
|
||||
```
|
||||
|
||||
**Local Users:**
|
||||
```bash
|
||||
python -c "import secrets; print(secrets.token_hex(32))"
|
||||
```
|
||||
|
||||
**Copy the key** - you'll need it for Step 3.
|
||||
|
||||
### 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 2)
|
||||
secret_key: "your_generated_secret_key_here"
|
||||
|
||||
# Password hash (paste the output from Step 1)
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 🔒 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 (Traefik, 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.
|
||||
|
|
@ -67,14 +67,6 @@
|
|||
- **Event hooks** - React to note saves, deletes, searches
|
||||
- **API access** - Full access to backend functionality
|
||||
|
||||
### Example Use Cases
|
||||
- Git auto-sync
|
||||
- Note encryption
|
||||
- Export to PDF
|
||||
- Daily note creation
|
||||
- Backlinks tracking
|
||||
- Tag management
|
||||
|
||||
## 🔍 Search
|
||||
|
||||
- **Full-text search** - Find notes by content
|
||||
|
|
@ -702,7 +702,20 @@
|
|||
</template>
|
||||
</select>
|
||||
</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>
|
||||
|
||||
<!-- Search -->
|
||||
|
|
|
|||
|
|
@ -0,0 +1,218 @@
|
|||
<!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;
|
||||
}
|
||||
|
||||
form > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
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 {
|
||||
color: var(--error, #c53030);
|
||||
font-size: 0.813rem;
|
||||
margin-top: 0.5rem;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
animation: fadeInDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
.error-message::before {
|
||||
content: "⚠️";
|
||||
flex-shrink: 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Shake animation for password input on error */
|
||||
input.error {
|
||||
animation: shake 0.4s ease-in-out;
|
||||
border-color: var(--error, #c53030);
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
10%, 30%, 50%, 70%, 90% { transform: translateX(-8px); }
|
||||
20%, 40%, 60%, 80% { transform: translateX(8px); }
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
<form method="post" action="/login">
|
||||
<div>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
id="password"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
autofocus
|
||||
<!-- ERROR_CLASS_PLACEHOLDER -->
|
||||
>
|
||||
<!-- ERROR_MESSAGE_PLACEHOLDER -->
|
||||
</div>
|
||||
<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}")
|
||||
|
||||
|
|
@ -5,4 +5,6 @@ markdown==3.5.1
|
|||
pyyaml==6.0.1
|
||||
aiofiles==23.2.1
|
||||
cryptography==41.0.7
|
||||
bcrypt==4.1.2
|
||||
itsdangerous==2.1.2
|
||||
|
||||
|
|
|
|||
5
run.py
5
run.py
|
|
@ -20,8 +20,7 @@ def main():
|
|||
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"])
|
||||
|
||||
# Create data directories
|
||||
Path("data/notes").mkdir(parents=True, exist_ok=True)
|
||||
Path("data/search_index").mkdir(parents=True, exist_ok=True)
|
||||
Path("data").mkdir(parents=True, exist_ok=True)
|
||||
Path("plugins").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print("✓ Dependencies installed")
|
||||
|
|
@ -32,7 +31,7 @@ def main():
|
|||
print("\n📝 Open your browser to: http://localhost:8000")
|
||||
print("\n💡 Tips:")
|
||||
print(" - Press Ctrl+C to stop the server")
|
||||
print(" - Your notes are in ./data/notes/")
|
||||
print(" - Your notes are in ./data/")
|
||||
print(" - Plugins go in ./plugins/")
|
||||
print("\n" + "="*50 + "\n")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue