Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1427f43
feat(api): add is_admin field to User model
KyleTryon Nov 9, 2025
89181a4
feat(api): implement signup restrictions and first-user-admin
KyleTryon Nov 9, 2025
f105014
feat(api): add admin user management endpoints
KyleTryon Nov 9, 2025
a3547e7
test(api): add comprehensive tests for signup and admin features
KyleTryon Nov 9, 2025
96a5ec5
feat(app): implement admin user management UI
KyleTryon Nov 9, 2025
3e8db30
docs: add self-hosting security documentation
KyleTryon Nov 9, 2025
2b26f00
fix(api): standardize API responses to use camelCase serialization
KyleTryon Nov 9, 2025
0436689
fix(api): add missing is_active field to UserResponse model
KyleTryon Nov 9, 2025
955dd29
fix(auth): add email uniqueness validation to signup endpoint
KyleTryon Nov 9, 2025
3ebb1fd
test: update admin user tests for camelCase API responses
KyleTryon Nov 9, 2025
c4d5389
test: add comprehensive signup validation tests
KyleTryon Nov 9, 2025
dad0dfc
refactor(api): use Pydantic alias_generator for automatic camelCase c…
KyleTryon Nov 9, 2025
3054c3b
refactor(api): apply CamelCaseModel to all API models
KyleTryon Nov 9, 2025
34a3bca
refactor(auth): improve signup error handling and database session ma…
KyleTryon Nov 9, 2025
1806713
docs: update CONFIGURATION.md to include user enumeration protection
KyleTryon Nov 9, 2025
fc41610
feat(admin): enhance AdminUsersPage with confirmation dialogs for use…
KyleTryon Nov 10, 2025
79a0c38
chore: fix formatting
KyleTryon Nov 10, 2025
f6e0279
refactor: clean up volume import defaults
KyleTryon Nov 10, 2025
12ed53d
docs: update deployment dir doc
KyleTryon Nov 10, 2025
5b1bee9
ci: better dev script
KyleTryon Nov 10, 2025
5318d82
fix: create data dir
KyleTryon Nov 10, 2025
121c800
feat(auth): enhance user information retrieval with detailed logging
KyleTryon Nov 10, 2025
d4e0b89
feat(animate-ui): filter out non-DOM props in Slot component
KyleTryon Nov 10, 2025
bb1da2a
refactor(auth): improve logging and remove unused JSON serialization …
KyleTryon Nov 10, 2025
5fc458c
chore: fix import ordering in migration files
KyleTryon Nov 11, 2025
1357205
feat: add system settings infrastructure
KyleTryon Nov 11, 2025
534b33e
feat: add admin endpoints for system management
KyleTryon Nov 11, 2025
b5b8eaa
feat: integrate system settings into auth and config
KyleTryon Nov 11, 2025
1702627
test: add tests for system settings and admin endpoints
KyleTryon Nov 11, 2025
747b73e
feat: add admin settings UI for system configuration
KyleTryon Nov 11, 2025
65b6835
refactor: update frontend to use camelCase API types
KyleTryon Nov 11, 2025
0badae1
chore: add route generation script and update dependencies
KyleTryon Nov 11, 2025
fd435bb
chore: update yt-dlp dependency to version 2025.11.12 and add new gre…
KyleTryon Nov 30, 2025
2f8fe7d
feat: admin + user signup management (#47)
KyleTryon Nov 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,27 @@ HERMES_REFRESH_TOKEN_EXPIRE_DAYS=30
# JWT signing algorithm
HERMES_ALGORITHM=HS256

# =============================================================================
# SIGNUP CONTROL & INITIAL ADMIN
# =============================================================================

# Allow public user registration (default: true for backward compatibility)
# When false, only admins can create user accounts
# For self-hosted deployments, set to false for security
HERMES_ALLOW_PUBLIC_SIGNUP=true

# Initial admin account (optional - created on first startup if no users exist)
# IMPORTANT: Remove or change these credentials after first login
# Leave empty to skip initial admin creation and use first-user-admin pattern
HERMES_INITIAL_ADMIN_USERNAME=
HERMES_INITIAL_ADMIN_EMAIL=
HERMES_INITIAL_ADMIN_PASSWORD=

# Example for Docker/production deployments:
# HERMES_INITIAL_ADMIN_USERNAME=admin
# HERMES_INITIAL_ADMIN_EMAIL=admin@example.com
# HERMES_INITIAL_ADMIN_PASSWORD=changeme123

# =============================================================================
# FILE STORAGE
# =============================================================================
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
node_modules/
.pnpm-debug.log*
.venv/
.pnpm-store

# Build outputs
dist/
Expand Down Expand Up @@ -80,4 +81,4 @@ tmp/
# Non-PNPM Package Manager Files
yarn.lock
package-lock.json
bun.lockb
bun.lockb
56 changes: 55 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ https://github.com/user-attachments/assets/7d9c6b96-d966-4989-a8b3-d5d49af463ae
- 🎥 **Universal Support** - Download from YouTube, Vimeo, TikTok, and 1000+ sites
- ⚡ **Background Processing** - Queue downloads and process them asynchronously
- ▶️ **Playlists** - Download entire playlists asynchronously
- 🔒 **Secure Authentication** - JWT tokens with API key management
- 🔒 **Secure Authentication** - JWT tokens with API key management and admin user control
- 👥 **User Management** - Admin controls for self-hosted deployments with configurable signup

## Quick Start

Expand Down Expand Up @@ -147,6 +148,59 @@ VITE_API_BASE_URL=https://hermes-api.example.com/api/v1

See our [Proxy & Deployment Guide](docs/PROXY_DEPLOYMENT.md) for integrating with existing setups (Traefik, nginx, etc.) and deploying with separate subdomains.

## 🔒 Self-Hosting Security

Hermes includes security features designed specifically for self-hosted deployments:

### Signup Control

By default, anyone with access to your Hermes instance can create an account. For self-hosted deployments, you can restrict signup:

**Option 1: First User Becomes Admin** (Default)
- The first user to sign up automatically gets admin privileges
- Subsequent users can only sign up if public signup is enabled
- Simple for personal deployments

**Option 2: Disable Public Signup**
```bash
# In your .env file:
HERMES_ALLOW_PUBLIC_SIGNUP=false
```
- Only administrators can create new accounts via admin panel
- Prevents unauthorized account creation
- Recommended for shared/exposed deployments

**Option 3: Pre-seed Initial Admin** (Docker-friendly)
```bash
# In your .env file:
HERMES_INITIAL_ADMIN_USERNAME=admin
HERMES_INITIAL_ADMIN_EMAIL=admin@example.com
HERMES_INITIAL_ADMIN_PASSWORD=changeme123
```
- Admin account created automatically on first startup
- Perfect for Docker deployments and automation
- **Important**: Change the password after first login!

### Admin User Management

Administrators have access to user management features:
- **Create users** - Add accounts when public signup is disabled
- **Promote/demote admins** - Grant admin privileges to trusted users
- **Activate/deactivate accounts** - Disable access without deleting users
- **Delete users** - Permanently remove accounts
- **Safety checks** - Cannot delete yourself or the last admin

Access admin features via: **API endpoints at `/api/v1/users`** (see [API docs](http://localhost:8000/docs))

### Security Best Practices

1. **Set a strong secret key**: Generate with `python3 -c "import secrets; print(secrets.token_urlsafe(32))"`
2. **Disable public signup** for non-personal deployments: `HERMES_ALLOW_PUBLIC_SIGNUP=false`
3. **Change default credentials** if using pre-seeded admin
4. **Use HTTPS** in production (Caddy provides automatic HTTPS)
5. **Restrict CORS origins** to your actual domains in `.env`
6. **Keep Hermes updated** to get the latest security patches

### 🐛 Having Issues?

If you run into problems, please use our [issue templates](https://github.com/techsquidtv/hermes/issues/new/choose) instead of generic issues:
Expand Down
12 changes: 6 additions & 6 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@ services:
- HERMES_REDIS_URL=${HERMES_REDIS_URL:-redis://redis:6379}
- HERMES_ALLOWED_ORIGINS=["http://localhost:5173","http://localhost:3000","http://localhost:8000"]
volumes:
- ./packages/hermes-api/downloads:/app/downloads
- ./packages/hermes-api/temp:/app/temp
- ./packages/hermes-api/data:/app/data
- ./data/downloads:/app/downloads
- ./data/temp:/app/temp
- ./data/data:/app/data
# Mount source code for hot reload
- ./packages/hermes-api/app:/app/app
networks:
Expand Down Expand Up @@ -90,9 +90,9 @@ services:
- HERMES_DEBUG=true
- HERMES_REDIS_URL=${HERMES_REDIS_URL:-redis://redis:6379}
volumes:
- ./packages/hermes-api/downloads:/app/downloads
- ./packages/hermes-api/temp:/app/temp
- ./packages/hermes-api/data:/app/data
- ./data/downloads:/app/downloads
- ./data/temp:/app/temp
- ./data/data:/app/data
# Mount source code for hot reload
- ./packages/hermes-api/app:/app/app
networks:
Expand Down
12 changes: 6 additions & 6 deletions docker-compose.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ services:
- HERMES_SECRET_KEY=${HERMES_SECRET_KEY}
- HERMES_REDIS_URL=${HERMES_REDIS_URL:-redis://redis:6379}
volumes:
- ./packages/hermes-api/downloads:/app/downloads
- ./packages/hermes-api/temp:/app/temp
- ./packages/hermes-api/data:/app/data
- ./data/downloads:/app/downloads
- ./data/temp:/app/temp
- ./data/data:/app/data
networks:
- hermes-network
depends_on:
Expand All @@ -104,9 +104,9 @@ services:
- HERMES_SECRET_KEY=${HERMES_SECRET_KEY}
- HERMES_REDIS_URL=${HERMES_REDIS_URL:-redis://redis:6379}
volumes:
- ./packages/hermes-api/downloads:/app/downloads
- ./packages/hermes-api/temp:/app/temp
- ./packages/hermes-api/data:/app/data
- ./data/downloads:/app/downloads
- ./data/temp:/app/temp
- ./data/data:/app/data
networks:
- hermes-network
depends_on:
Expand Down
12 changes: 6 additions & 6 deletions docker-compose.test-separate-hosts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ services:
# CORS must allow requests from the app's origin
- HERMES_ALLOWED_ORIGINS=http://localhost:3001,http://127.0.0.1:3001
volumes:
- ./packages/hermes-api/downloads:/app/downloads
- ./packages/hermes-api/temp:/app/temp
- ./packages/hermes-api/data:/app/data
- ./data/downloads:/app/downloads
- ./data/temp:/app/temp
- ./data/data:/app/data
networks:
- hermes-test-network
depends_on:
Expand All @@ -60,9 +60,9 @@ services:
- HERMES_REDIS_URL=redis://redis:6379
- HERMES_DEBUG=true
volumes:
- ./packages/hermes-api/downloads:/app/downloads
- ./packages/hermes-api/temp:/app/temp
- ./packages/hermes-api/data:/app/data
- ./data/downloads:/app/downloads
- ./data/temp:/app/temp
- ./data/data:/app/data
networks:
- hermes-test-network
depends_on:
Expand Down
12 changes: 6 additions & 6 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,9 @@ services:
- HERMES_SECRET_KEY=${HERMES_SECRET_KEY}
- HERMES_REDIS_URL=${HERMES_REDIS_URL:-redis://redis:6379}
volumes:
- ./packages/hermes-api/downloads:/app/downloads
- ./packages/hermes-api/temp:/app/temp
- ./packages/hermes-api/data:/app/data
- ./data/downloads:/app/downloads
- ./data/temp:/app/temp
- ./data/data:/app/data
networks:
- hermes-network
depends_on:
Expand Down Expand Up @@ -119,9 +119,9 @@ services:
- HERMES_SECRET_KEY=${HERMES_SECRET_KEY}
- HERMES_REDIS_URL=${HERMES_REDIS_URL:-redis://redis:6379}
volumes:
- ./packages/hermes-api/downloads:/app/downloads
- ./packages/hermes-api/temp:/app/temp
- ./packages/hermes-api/data:/app/data
- ./data/downloads:/app/downloads
- ./data/temp:/app/temp
- ./data/data:/app/data
networks:
- hermes-network
depends_on:
Expand Down
1 change: 1 addition & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,5 +232,6 @@ openssl rand -hex 32
5. **Monitor authentication attempts**
6. **Keep dependencies updated**
7. **Use proper database backups**
8. **User enumeration protection** - Signup endpoints use generic error messages to prevent attackers from discovering registered usernames/emails

See the [Docker setup guide](DOCKER_OPTIMIZATION_README.md) for production deployment recommendations.
24 changes: 12 additions & 12 deletions docs/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ The Docker setup uses volume mounts to persist data and provide access to downlo

| Host Path | Container Path | Purpose | Contents |
|-----------|----------------|---------|----------|
| `./packages/hermes-api/data/` | `/app/data/` | **Database Storage** | SQLite database file (`hermes.db`) containing user accounts, download history, queue state, and application settings |
| `./packages/hermes-api/downloads/` | `/app/downloads/` | **Completed Downloads** | Successfully downloaded video files, organized by format and quality |
| `./packages/hermes-api/temp/` | `/app/temp/` | **Temporary Files** | Intermediate files during download process, partial downloads, and temporary processing files |
| `./data/data/` | `/app/data/` | **Database Storage** | SQLite database file (`hermes.db`) containing user accounts, download history, queue state, and application settings |
| `./data/downloads/` | `/app/downloads/` | **Completed Downloads** | Successfully downloaded video files, organized by format and quality |
| `./data/temp/` | `/app/temp/` | **Temporary Files** | Intermediate files during download process, partial downloads, and temporary processing files |

### Database Persistence (`data/`)

The SQLite database is persisted to the host machine at `packages/hermes-api/data/hermes.db`. This ensures:
The SQLite database is persisted to the host machine at `data/data/hermes.db`. This ensures:

- **User accounts** and authentication data persist across container restarts
- **Download history** and queue state are preserved
Expand Down Expand Up @@ -179,18 +179,18 @@ The version status appears in the bottom-left of the sidebar and provides:

```bash
# SQLite backup
cp packages/hermes-api/data/hermes.db packages/hermes-api/data/hermes.db.backup
cp data/data/hermes.db data/data/hermes.db.backup

# Create timestamped backup
cp packages/hermes-api/data/hermes.db \
packages/hermes-api/data/hermes.db.backup.$(date +%Y%m%d_%H%M%S)
cp data/data/hermes.db \
data/data/hermes.db.backup.$(date +%Y%m%d_%H%M%S)
```

### File Backups

```bash
# Backup downloads directory
tar -czf downloads_backup_$(date +%Y%m%d).tar.gz packages/hermes-api/downloads/
tar -czf downloads_backup_$(date +%Y%m%d).tar.gz data/downloads/

# Backup configuration
cp .env .env.backup
Expand All @@ -207,10 +207,10 @@ BACKUP_DIR="/data/backups"
DATE=$(date +%Y%m%d_%H%M%S)

# Database backup
cp packages/hermes-api/data/hermes.db $BACKUP_DIR/db_backup_$DATE.sqlite
cp data/data/hermes.db $BACKUP_DIR/db_backup_$DATE.sqlite

# Downloads backup (incremental)
rsync -a --delete packages/hermes-api/downloads/ $BACKUP_DIR/downloads_backup_$DATE/
rsync -a --delete data/downloads/ $BACKUP_DIR/downloads_backup_$DATE/

# Configuration backup
cp .env $BACKUP_DIR/
Expand Down Expand Up @@ -437,8 +437,8 @@ celery_worker:
df -h

# Check directory sizes
du -sh packages/hermes-api/downloads/
du -sh packages/hermes-api/temp/
du -sh data/downloads/
du -sh data/temp/
```

- **Consider external storage** for large download volumes:
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"packageManager": "pnpm@10.19.0+sha512.c9fc7236e92adf5c8af42fd5bf1612df99c2ceb62f27047032f4720b33f8eacdde311865e91c411f2774f618d82f320808ecb51718bfa82c060c4ba7c76a32b8",
"scripts": {
"dev": "docker compose up -d",
"dev": "docker compose down && docker compose -f docker-compose.dev.yml build && docker compose -f docker-compose.dev.yml up",
"dev:down": "docker compose down",
"dev:logs": "docker compose logs -f",
"dev:frontend": "pnpm --filter hermes-app dev",
Expand All @@ -21,15 +21,16 @@
"test:all": "pnpm test && pnpm test:api",
"test:coverage": "pnpm --filter hermes-app test:coverage && cd packages/hermes-api && uv run pytest --cov=app --cov-report=html --cov-report=term",
"clean": "pnpm --recursive clean",
"clean:all": "pnpm clean && docker compose down -v && rm -rf packages/hermes-api/data/* packages/hermes-api/downloads/* packages/hermes-api/temp/*",
"clean:all": "pnpm clean && docker compose down -v && rm -rf data/data/* data/downloads/* data/temp/*",
"setup": "pnpm install && echo 'Create .env file from .env.example and update values'",
"security:generate-key": "uv run python3 -c \"import secrets; print('HERMES_SECRET_KEY=' + secrets.token_urlsafe(32))\"",
"security:check": "echo 'Checking for potential security issues...' && echo '✓ .env file should not be committed' && echo '✓ Database files are gitignored' && echo '✓ Consider using .dockerignore for production'",
"pre-check": "pnpm pre-check:api && pnpm pre-check:app && docker compose build",
"format:check": "cd packages/hermes-api && uv run black --check --diff app/ tests/ && uv run isort --check-only --diff app/ tests/",
"pre-check:api": "cd packages/hermes-api && uv run black --check app/ tests/ && uv run isort --check-only app/ tests/ && uv run ruff check app/ tests/ && uv run python -m mypy --explicit-package-bases app/ tests/ && uv run pytest",
"pre-check:app": "pnpm --filter hermes-app lint &&pnpm --filter hermes-app build && pnpm --filter hermes-app type-check && pnpm --filter hermes-app test",
"update:all": "pnpm update --recursive && cd packages/hermes-api && uv lock --upgrade && uv sync"
"update:all": "pnpm update --recursive && cd packages/hermes-api && uv lock --upgrade && uv sync",
"generate:routes": "pnpm --filter hermes-app generate:routes"
},
"devDependencies": {
"shadcn": "^3.5.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = '96e3543c1899'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""add_user_is_admin_field

Revision ID: c4584c156bdd
Revises: 96e3543c1899
Create Date: 2025-11-09 13:04:00.567491

"""
from typing import Sequence, Union

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = 'c4584c156bdd'
down_revision: Union[str, Sequence[str], None] = '96e3543c1899'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# Add is_admin column to users table
op.add_column(
'users',
sa.Column('is_admin', sa.Boolean(), nullable=False, server_default='0')
)
# Create index on is_admin for query performance
op.create_index(op.f('ix_users_is_admin'), 'users', ['is_admin'], unique=False)


def downgrade() -> None:
"""Downgrade schema."""
# Drop index
op.drop_index(op.f('ix_users_is_admin'), table_name='users')
# Drop is_admin column
op.drop_column('users', 'is_admin')
Loading
Loading