Merge pull request #146 from gamosoft/features/plain_password

Features/plain password
This commit is contained in:
Guillermo Villar 2026-01-12 12:22:45 +01:00 committed by GitHub
commit 2fd403b04a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 234 additions and 179 deletions

View File

@ -146,6 +146,33 @@ python run.py
- Python 3.8 or higher - Python 3.8 or higher
- pip (Python package manager) - pip (Python package manager)
#### Using Virtual Environments (Recommended for Arch/Fedora/Ubuntu 23.04+)
Modern Linux distributions enforce [PEP 668](https://peps.python.org/pep-0668/), which prevents system-wide pip installs. Use a virtual environment instead:
```bash
# Clone the repository
git clone https://github.com/gamosoft/notediscovery.git
cd notediscovery
# Create a virtual environment
python -m venv venv
# Activate it (choose your shell):
source venv/bin/activate # Bash/Zsh (most Linux distros)
source venv/bin/activate.fish # Fish (CachyOS, etc.)
source venv/bin/activate.csh # Csh/Tcsh
.\venv\Scripts\activate # Windows PowerShell
# Install dependencies and run
pip install -r requirements.txt
python run.py
```
> ⚠️ **Warning**
>
> *You'll need to activate the virtual environment (source venv/bin/activate) each time you open a new terminal before running the app*
### Advanced Docker Setup ### Advanced Docker Setup
The image includes bundled config, themes, plugins, and locales. To customize, you must: The image includes bundled config, themes, plugins, and locales. To customize, you must:

View File

@ -67,10 +67,34 @@ if 'AUTHENTICATION_ENABLED' in os.environ:
else: else:
print(f"🔐 Authentication {'ENABLED' if config.get('authentication', {}).get('enabled', False) else 'DISABLED'} (from config.yaml)") print(f"🔐 Authentication {'ENABLED' if config.get('authentication', {}).get('enabled', False) else 'DISABLED'} (from config.yaml)")
# Allow password hash to be set via environment variable (useful for demos) # Password configuration priority:
if 'AUTHENTICATION_PASSWORD_HASH' in os.environ: # 1. AUTHENTICATION_PASSWORD env var (plain text, hashed at startup)
# 2. AUTHENTICATION_PASSWORD_HASH env var (pre-hashed)
# 3. authentication.password in config.yaml (plain text, hashed at startup)
# 4. authentication.password_hash in config.yaml (pre-hashed)
# Default password is "admin" if nothing is configured
if 'AUTHENTICATION_PASSWORD' in os.environ:
# Plain text password from env var - hash it
plain_password = os.getenv('AUTHENTICATION_PASSWORD')
config['authentication']['password_hash'] = bcrypt.hashpw(
plain_password.encode('utf-8'),
bcrypt.gensalt()
).decode('utf-8')
print("🔑 Password loaded from AUTHENTICATION_PASSWORD env var (hashed at startup)")
elif 'AUTHENTICATION_PASSWORD_HASH' in os.environ:
# Pre-hashed password from env var
config['authentication']['password_hash'] = os.getenv('AUTHENTICATION_PASSWORD_HASH') config['authentication']['password_hash'] = os.getenv('AUTHENTICATION_PASSWORD_HASH')
print("🔑 Password hash loaded from AUTHENTICATION_PASSWORD_HASH env var") print("🔑 Password hash loaded from AUTHENTICATION_PASSWORD_HASH env var")
elif config.get('authentication', {}).get('password'):
# Plain text password from config.yaml - hash it
plain_password = config['authentication']['password']
config['authentication']['password_hash'] = bcrypt.hashpw(
plain_password.encode('utf-8'),
bcrypt.gensalt()
).decode('utf-8')
# Clear the plain text password from config for security
del config['authentication']['password']
print("🔑 Password loaded from config.yaml (hashed at startup)")
# Allow secret key to be set via environment variable (for session security) # Allow secret key to be set via environment variable (for session security)
if 'AUTHENTICATION_SECRET_KEY' in os.environ: if 'AUTHENTICATION_SECRET_KEY' in os.environ:

View File

@ -35,9 +35,16 @@ authentication:
# Generate with: python -c "import secrets; print(secrets.token_hex(32))" # Generate with: python -c "import secrets; print(secrets.token_hex(32))"
secret_key: "change_this_to_a_random_secret_key_in_production" secret_key: "change_this_to_a_random_secret_key_in_production"
# Password hash - Generate with: python generate_password.py # Password configuration - choose ONE of the following options:
#
# Option 1: Plain text password (recommended for ease of use)
# The password is hashed automatically at startup
# ⚠️ Default password is "admin" - CHANGE THIS for production! # ⚠️ Default password is "admin" - CHANGE THIS for production!
password_hash: "$2b$12$t/6PGExFzdpU2PUta0iVY.eDQwvu63kH.c/d4bEnnHaQ5CspH1yrG" # Default: "admin" password: "admin"
# Option 2: Pre-hashed password (for advanced users)
# Generate with: python generate_password.py
# password_hash: "$2b$12$..."
# Session expiry in seconds (default: 7 days) # Session expiry in seconds (default: 7 days)
session_max_age: 604800 session_max_age: 604800

View File

@ -353,7 +353,7 @@
animation: fadeInScale 0.6s ease-out forwards; animation: fadeInScale 0.6s ease-out forwards;
} }
/* Stagger animation delays for feature cards */ /* Stagger animation delays for feature cards (5 rows × 3 cols = 15 cards) */
.feature.animated:nth-child(1) { animation-delay: 0s; } .feature.animated:nth-child(1) { animation-delay: 0s; }
.feature.animated:nth-child(2) { animation-delay: 0.1s; } .feature.animated:nth-child(2) { animation-delay: 0.1s; }
.feature.animated:nth-child(3) { animation-delay: 0.2s; } .feature.animated:nth-child(3) { animation-delay: 0.2s; }
@ -367,6 +367,8 @@
.feature.animated:nth-child(11) { animation-delay: 0.25s; } .feature.animated:nth-child(11) { animation-delay: 0.25s; }
.feature.animated:nth-child(12) { animation-delay: 0.35s; } .feature.animated:nth-child(12) { animation-delay: 0.35s; }
.feature.animated:nth-child(13) { animation-delay: 0.2s; } .feature.animated:nth-child(13) { animation-delay: 0.2s; }
.feature.animated:nth-child(14) { animation-delay: 0.3s; }
.feature.animated:nth-child(15) { animation-delay: 0.4s; }
.screenshot-section { .screenshot-section {
margin: 4rem 0 3rem 0; margin: 4rem 0 3rem 0;
@ -832,12 +834,6 @@
<p>No subscriptions, no hidden fees. Free and open source forever.</p> <p>No subscriptions, no hidden fees. Free and open source forever.</p>
</div> </div>
<div class="feature animate-on-scroll">
<div class="feature-icon">🚀</div>
<h3>Lightning Fast</h3>
<p>Instant search and navigation. Works offline, always responsive.</p>
</div>
<div class="feature animate-on-scroll"> <div class="feature animate-on-scroll">
<div class="feature-icon">🧮</div> <div class="feature-icon">🧮</div>
<h3>LaTeX Math</h3> <h3>LaTeX Math</h3>
@ -868,12 +864,6 @@
<p>Create from custom templates with dynamic placeholders. Meeting notes, journals, and more.</p> <p>Create from custom templates with dynamic placeholders. Meeting notes, journals, and more.</p>
</div> </div>
<div class="feature animate-on-scroll">
<div class="feature-icon">🔗</div>
<h3>Wikilinks</h3>
<p>Link notes with [[double brackets]] Obsidian-style. Jump to sections with [[note#heading]] anchors.</p>
</div>
<div class="feature animate-on-scroll"> <div class="feature animate-on-scroll">
<div class="feature-icon">🕸️</div> <div class="feature-icon">🕸️</div>
<h3>Graph View</h3> <h3>Graph View</h3>
@ -909,6 +899,18 @@
<h3>Extensible</h3> <h3>Extensible</h3>
<p>Plugin system lets you add custom features and integrations.</p> <p>Plugin system lets you add custom features and integrations.</p>
</div> </div>
<div class="feature animate-on-scroll">
<div class="feature-icon">📲</div>
<h3>Install as App</h3>
<p>Progressive Web App (PWA) - install on desktop or mobile for a native experience.</p>
</div>
<div class="feature animate-on-scroll">
<div class="feature-icon">🌍</div>
<h3>Multi-Language</h3>
<p>Built-in translations for English, Spanish, German, and French. Easy to add more.</p>
</div>
</div> </div>
<div class="benefits animate-on-scroll"> <div class="benefits animate-on-scroll">
@ -946,38 +948,6 @@
</div> </div>
</div> </div>
<div class="benefit-item">
<span class="emoji">🌙</span>
<div class="text">
<strong>Dark Mode</strong>
Easy on the eyes
</div>
</div>
<div class="benefit-item">
<span class="emoji">💻</span>
<div class="text">
<strong>Self-Hosted</strong>
You own your data
</div>
</div>
<div class="benefit-item">
<span class="emoji">🔗</span>
<div class="text">
<strong>Wikilinks</strong>
[[Link notes]] Obsidian-style
</div>
</div>
<div class="benefit-item">
<span class="emoji">🕸️</span>
<div class="text">
<strong>Graph View</strong>
Visualize note connections
</div>
</div>
<div class="benefit-item"> <div class="benefit-item">
<span class="emoji">🖼️</span> <span class="emoji">🖼️</span>
<div class="text"> <div class="text">
@ -1011,10 +981,10 @@
</div> </div>
<div class="benefit-item"> <div class="benefit-item">
<span class="emoji">⚙️</span> <span class="emoji">🔗</span>
<div class="text"> <div class="text">
<strong>Properties Panel</strong> <strong>Wikilinks</strong>
View frontmatter metadata at a glance [[Link notes]] Obsidian-style
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,179 +1,165 @@
# 🔒 NoteDiscovery Authentication Guide # 🔒 NoteDiscovery Authentication Guide
## ⚠️ **IMPORTANT: Default Password Warning** ## ⚠️ Default Password Warning
> **The default `config.yaml` includes authentication disabled by default, with password: `admin`** > **Default password is `admin`** — CHANGE THIS before exposing to any network!
>
> 🔴 **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 ## 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. NoteDiscovery includes simple password protection for single-user deployments. When enabled, users must log in before accessing notes.
## ✨ Features - ✅ Single user / self-hosted use
- ✅ Passwords hashed with bcrypt
- ✅ **Single User** - Perfect for personal/self-hosted use - ✅ Session-based (7 days default, configurable)
- ✅ **Secure** - Passwords hashed with bcrypt
- ✅ **Session-based** - Stay logged in for 7 days (configurable)
--- ---
## 🚀 Quick Setup ## Quick Test (Local Only)
**Default Configuration:** For local testing, authentication is **disabled by default**. To test with auth:
- Authentication is **disabled 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**. 1. Set `authentication.enabled: true` in `config.yaml`
2. Restart the app
---
### 🧪 **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` 3. Log in with password: `admin`
**⚠️ Only use this for local testing on your own machine!** ⚠️ **Don't use the default password on any network!**
--- ---
### 🔒 **Production Setup (Change Password & Secret Key)** ## Production Setup
For any deployment exposed to a network, follow these steps: For any deployment exposed to a network, follow these steps:
### Step 1: Generate a Password Hash ### Step 1: Generate a Secret Key
Choose your environment: The secret key encrypts session cookies. Generate a random one:
**Docker Users:**
```bash ```bash
# Docker Compose with the GHCR compose file # Docker
docker-compose -f docker-compose.ghcr.yml 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))" docker exec -it notediscovery python -c "import secrets; print(secrets.token_hex(32))"
```
**Local Users:** # Local
```bash
python -c "import secrets; print(secrets.token_hex(32))" python -c "import secrets; print(secrets.token_hex(32))"
``` ```
**Copy the key** - you'll need it for Step 3. **Save this key** — you'll need it in Step 2.
### Step 3: Update `config.yaml`
Edit your `config.yaml` and update the authentication section:
```yaml
authentication:
# 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 ### Step 2: Configure Authentication
Choose **one** of these options:
#### Option A: Plain Text Password (Recommended)
The easiest approach. Your password is automatically hashed at startup.
**Via Environment Variables (Docker):**
```bash
docker run -d \
-e AUTHENTICATION_ENABLED=true \
-e AUTHENTICATION_PASSWORD=your_secure_password \
-e AUTHENTICATION_SECRET_KEY=your_generated_secret_key \
...
```
**Via config.yaml:**
```yaml
authentication:
enabled: true
password: "your_secure_password"
secret_key: "your_generated_secret_key"
```
---
#### Option B: Pre-Hashed Password (Advanced)
For users who prefer to hash passwords themselves.
**Generate a hash:**
```bash
# Docker
docker exec -it notediscovery python generate_password.py
# Local
python generate_password.py
```
**Then configure:**
```yaml
authentication:
enabled: true
password_hash: "$2b$12$..." # paste your hash here
secret_key: "your_generated_secret_key"
```
---
### Step 3: Restart & Test
```bash
# Docker Compose
docker-compose restart
# Docker run
docker restart notediscovery
# Local
python run.py
```
Navigate to `http://localhost:8000` — you'll be redirected to the login page.
---
## Configuration Priority
If multiple sources are configured, this priority applies (first wins):
| Priority | Source | Type |
|----------|--------|------|
| 1st | `AUTHENTICATION_PASSWORD` env var | Plain text |
| 2nd | `AUTHENTICATION_PASSWORD_HASH` env var | Pre-hashed |
| 3rd | `password` in config.yaml | Plain text |
| 4th | `password_hash` in config.yaml | Pre-hashed |
**Example:** If you set `AUTHENTICATION_PASSWORD` as an env var, it overrides anything in config.yaml.
---
## Security Considerations
### ✅ What This Protects ### ✅ What This Protects
- Unauthorized access to your notes - Unauthorized access to your notes
- Viewing, creating, editing, and deleting notes
- All API endpoints - All API endpoints
- Viewing, creating, editing, deleting notes
### ⚠️ What This Doesn't Protect ### ⚠️ What This Doesn't Protect
This is a **simple authentication system** designed for **self-hosted, single-user** deployments. It is **NOT** suitable for: This is a **simple single-user** system. NOT suitable for:
- ❌ Multi-user environments - ❌ Multi-user environments
- ❌ Public internet exposure without HTTPS - ❌ Public internet without HTTPS
- ❌ Production SaaS applications
- ❌ Compliance requirements (HIPAA, GDPR, etc.) - ❌ Compliance requirements (HIPAA, GDPR, etc.)
### 🛡️ Best Practices ### 🛡️ Best Practices
1. **Use HTTPS** - Always run behind a reverse proxy (Traefik, nginx, Caddy) with SSL/TLS 1. **Use HTTPS** — Run behind a reverse proxy (Traefik, nginx, Caddy)
2. **Strong Password** - Use at least 12 characters with mixed case, numbers, and symbols 2. **Strong password** — At least 12 characters, mixed case, numbers, symbols
3. **Unique Secret Key** - Never reuse secret keys across applications 3. **Unique secret key** — Never reuse across applications
4. **Keep Config Secure** - Don't commit `config.yaml` with real credentials to version control 4. **Keep config secure** — Don't commit credentials to version control
5. **VPN/Private Network** - Keep NoteDiscovery on a private network or behind a VPN
--- ---
## 🚫 Disabling Authentication ## Disabling Authentication
To disable authentication and allow open access:
```yaml ```yaml
authentication: authentication:
enabled: false enabled: false
``` ```
Restart the application, and authentication will be bypassed. Restart the app to apply.

View File

@ -17,9 +17,22 @@ NoteDiscovery supports environment variables to override configuration settings,
| Variable | Type | Default | Description | | Variable | Type | Default | Description |
|----------|------|---------|-------------| |----------|------|---------|-------------|
| `AUTHENTICATION_ENABLED` | boolean | `config.yaml` | Enable/disable authentication | | `AUTHENTICATION_ENABLED` | boolean | `config.yaml` | Enable/disable authentication |
| `AUTHENTICATION_PASSWORD_HASH` | string | `config.yaml` | Bcrypt password hash | | `AUTHENTICATION_PASSWORD` | string | `admin` | Plain text password (hashed automatically at startup) |
| `AUTHENTICATION_PASSWORD_HASH` | string | - | Pre-hashed bcrypt password (for advanced users) |
| `AUTHENTICATION_SECRET_KEY` | string | `config.yaml` | Session secret key (for session security) | | `AUTHENTICATION_SECRET_KEY` | string | `config.yaml` | Session secret key (for session security) |
> **Password Priority:** `AUTHENTICATION_PASSWORD` takes precedence over `AUTHENTICATION_PASSWORD_HASH`. If both are set, the plain text password is used.
#### Example: Setting password via environment variable
```bash
# Docker
docker run -e AUTHENTICATION_ENABLED=true -e AUTHENTICATION_PASSWORD=mysecretpassword ...
# Docker Compose (in .env file or docker-compose.yml)
AUTHENTICATION_PASSWORD=mysecretpassword
```
### Demo Mode ### Demo Mode
| Variable | Type | Default | Description | | Variable | Type | Default | Description |

View File

@ -318,12 +318,32 @@ Full immersive distraction-free writing experience:
- **Easy exit** - Press `Esc`, click exit button, or use shortcut again - **Easy exit** - Press `Esc`, click exit button, or use shortcut again
- **State preserved** - Returns to your previous view mode on exit - **State preserved** - Returns to your previous view mode on exit
## 📱 Progressive Web App (PWA)
NoteDiscovery can be installed as a standalone app on your device:
- **Install as app** - Add to home screen on mobile, or install via browser on desktop
- **Standalone mode** - Runs without browser chrome for a native app feel
### How to Install
- **Desktop (Chrome/Edge)**: Click the install icon in the address bar, or Menu → "Install NoteDiscovery"
- **Android**: Chrome Menu → "Add to Home Screen"
- **iOS**: Safari Share → "Add to Home Screen"
## 🌍 Internationalization
- **Multiple languages** - English, Spanish, German, French built-in
- **Easy to add** - Drop JSON files in `locales/` folder
- **Instant switch** - Change language in Settings without reload
- **Community translations** - Contributions welcome!
## 🚀 Performance ## 🚀 Performance
- **Instant loading** - No lag, no loading spinners - **Instant loading** - No lag, no loading spinners
- **Efficient caching** - Smart local storage - **Efficient caching** - Smart local storage
- **Minimal resources** - Runs on modest hardware - **Minimal resources** - Runs on modest hardware
- **No bloat** - Focused on what matters - **No bloat** - Focused on what matters
- **Lightweight** - No heavy frameworks
--- ---

View File

@ -800,9 +800,17 @@ function noteApp() {
// Links [text](url) // Links [text](url)
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<span class="md-link">[$1]</span><span class="md-link-url">($2)</span>'); html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<span class="md-link">[$1]</span><span class="md-link-url">($2)</span>');
// Lists // Lists - use [ \t] instead of \s to avoid matching newlines
html = html.replace(/^(\s*)([-*+])\s/gm, '$1<span class="md-list">$2</span> '); // Capture rest of line to handle empty bullets properly (issue #142)
html = html.replace(/^(\s*)(\d+\.)\s/gm, '$1<span class="md-list">$2</span> '); html = html.replace(/^(\s*)([-*+])[ \t](.*)$/gm, (match, indent, bullet, rest) => {
// If rest is empty/whitespace-only, add zero-width space to prevent line collapse
const content = rest.trim() === '' ? '\u200B' : rest;
return `${indent}<span class="md-list">${bullet}</span> ${content}`;
});
html = html.replace(/^(\s*)(\d+\.)[ \t](.*)$/gm, (match, indent, bullet, rest) => {
const content = rest.trim() === '' ? '\u200B' : rest;
return `${indent}<span class="md-list">${bullet}</span> ${content}`;
});
// Blockquotes // Blockquotes
html = html.replace(/^(&gt;.*)$/gm, '<span class="md-blockquote">$1</span>'); html = html.replace(/^(&gt;.*)$/gm, '<span class="md-blockquote">$1</span>');