soundwave/backend/user/README_MULTI_TENANT.md
Iulian 51679d1943 Initial commit - SoundWave v1.0
- Full PWA support with offline capabilities
- Comprehensive search across songs, playlists, and channels
- Offline playlist manager with download tracking
- Pre-built frontend for zero-build deployment
- Docker-based deployment with docker compose
- Material-UI dark theme interface
- YouTube audio download and management
- Multi-user authentication support
2025-12-16 23:43:07 +00:00

12 KiB

Multi-Tenant Admin System - Implementation Guide

Overview

This system transforms SoundWave into a multi-tenant platform where:

  • Admins can manage all users and their content
  • Users have isolated YouTube accounts, channels, playlists, and audio files
  • Each user operates as if they have their own Docker container
  • Resource limits (storage, channels, playlists) are enforced per user

Architecture

User Isolation Model

Admin User (is_admin=True)
├── Can create/manage all users
├── Access all content across users
└── Set resource quotas

Regular User
├── Own YouTube accounts
├── Own channels (subscriptions)
├── Own playlists
├── Own audio files
└── Cannot see other users' data

Database Schema Changes

Account Model (user/models.py):

- storage_quota_gb: int (default 50 GB)
- storage_used_gb: float (tracked automatically)
- max_channels: int (default 50)
- max_playlists: int (default 100)
- user_notes: text (admin notes)
- created_by: ForeignKey to admin who created user

UserYouTubeAccount Model (NEW):

- user: ForeignKey to Account
- account_name: str (friendly name)
- youtube_channel_id: str
- youtube_channel_name: str
- cookies_file: text (for authentication)
- auto_download: bool
- download_quality: choices

Channel Model (UPDATED):

+ owner: ForeignKey to Account
+ youtube_account: ForeignKey to UserYouTubeAccount
+ auto_download: bool per channel
+ download_quality: choices per channel

Audio Model (UPDATED):

+ owner: ForeignKey to Account

Playlist Model (UPDATED):

+ owner: ForeignKey to Account
+ auto_download: bool per playlist

Unique Constraints

  • Channel: (owner, channel_id) - Each user can subscribe once per channel
  • Audio: (owner, youtube_id) - Each user can have one copy of each video
  • Playlist: (owner, playlist_id) - Each user can subscribe once per playlist

Backend Implementation

Middleware (config/middleware.py)

UserIsolationMiddleware:

  • Adds request.filter_by_user() helper
  • Automatically filters querysets by owner
  • Admins bypass filtering

StorageQuotaMiddleware:

  • Tracks storage usage
  • Prevents uploads when quota exceeded

Permissions (common/permissions.py)

IsOwnerOrAdmin:

  • Users can only access their own objects
  • Admins can access everything

CanManageUsers:

  • Only admins can manage users

WithinQuotaLimits:

  • Checks storage/channel/playlist quotas
  • Admins bypass quota checks

Admin API (user/views_admin.py)

UserManagementViewSet:

GET    /api/user/admin/users/              # List users
POST   /api/user/admin/users/              # Create user
GET    /api/user/admin/users/{id}/         # User details
PATCH  /api/user/admin/users/{id}/         # Update user
GET    /api/user/admin/users/{id}/stats/   # User statistics
POST   /api/user/admin/users/{id}/reset_storage/
POST   /api/user/admin/users/{id}/reset_2fa/
POST   /api/user/admin/users/{id}/toggle_active/
GET    /api/user/admin/users/{id}/channels/
GET    /api/user/admin/users/{id}/playlists/
GET    /api/user/admin/users/system_stats/ # System-wide stats

UserYouTubeAccountViewSet:

GET    /api/user/admin/youtube-accounts/              # List accounts
POST   /api/user/admin/youtube-accounts/              # Add account
GET    /api/user/admin/youtube-accounts/{id}/         # Account details
PATCH  /api/user/admin/youtube-accounts/{id}/         # Update account
DELETE /api/user/admin/youtube-accounts/{id}/         # Delete account
POST   /api/user/admin/youtube-accounts/{id}/verify/  # Verify credentials
POST   /api/user/admin/youtube-accounts/{id}/toggle_active/

Django Admin (user/admin_users.py)

Enhanced admin interface with:

  • User list with storage/channel/playlist counts
  • Visual storage progress bars
  • Bulk actions (reset storage, disable users, reset 2FA)
  • YouTube account management
  • Per-user notes

Frontend Implementation

AdminUsersPage Component

Features:

  • System statistics dashboard (users, content, storage)
  • Users table with status, storage, content counts
  • Create user dialog with full settings
  • Edit user dialog with quota management
  • User details modal with comprehensive info
  • Quick actions (activate/deactivate, reset storage, reset 2FA)

UI Components:

- System stats cards (users, content, storage)
- Users table (sortable, filterable)
- Create user form (username, email, password, quotas)
- Edit user form (quotas, status, permissions)
- User details modal (all stats and metadata)
- Actions menu (edit, toggle, reset)

Migration Strategy

Step 1: Run Migrations

# Create migrations
python manage.py makemigrations user channel audio playlist

# Apply migrations
python manage.py migrate

# Create superuser
python manage.py createsuperuser

Step 2: Data Migration

For existing data, create a data migration to set owner fields:

# Create empty migration
python manage.py makemigrations --empty user --name set_default_owner

# Edit migration file
def set_default_owner(apps, schema_editor):
    Account = apps.get_model('user', 'Account')
    Channel = apps.get_model('channel', 'Channel')
    Audio = apps.get_model('audio', 'Audio')
    Playlist = apps.get_model('playlist', 'Playlist')
    
    # Get or create default admin user
    admin = Account.objects.filter(is_superuser=True).first()
    if not admin:
        admin = Account.objects.create_superuser(
            username='admin',
            email='admin@example.com',
            password='changeme'
        )
    
    # Assign owner to existing records
    Channel.objects.filter(owner__isnull=True).update(owner=admin)
    Audio.objects.filter(owner__isnull=True).update(owner=admin)
    Playlist.objects.filter(owner__isnull=True).update(owner=admin)

Step 3: Update Views

Update existing views to use owner filtering:

# Before
Audio.objects.all()

# After
Audio.objects.filter(owner=request.user)
# or use middleware
request.filter_by_user(Audio.objects.all())

Step 4: Update Serializers

Ensure owner is set on create:

def perform_create(self, serializer):
    serializer.save(owner=self.request.user)

Usage Examples

Admin Creating User

POST /api/user/admin/users/
{
  "username": "john_doe",
  "email": "john@example.com",
  "password": "secure123",
  "password_confirm": "secure123",
  "storage_quota_gb": 100,
  "max_channels": 75,
  "max_playlists": 150,
  "is_admin": false,
  "is_active": true,
  "user_notes": "Premium user - increased quotas"
}

User Adding YouTube Account

POST /api/user/admin/youtube-accounts/
{
  "account_name": "Personal YouTube",
  "youtube_channel_id": "UCxxxxxxxx",
  "youtube_channel_name": "John's Channel",
  "cookies_file": "# Netscape HTTP Cookie File...",
  "auto_download": true,
  "download_quality": "high"
}

User Subscribing to Channel

POST /api/channels/
{
  "channel_id": "UCxxxxxxxx",
  "channel_name": "Tech Channel",
  "youtube_account": 1,  # User's YouTube account ID
  "subscribed": true,
  "auto_download": true,
  "download_quality": "auto"
}

Resource Quota Enforcement

Storage Quota

# Checked before download
if user.storage_used_gb >= user.storage_quota_gb:
    raise PermissionDenied("Storage quota exceeded")

# Updated after download
file_size_gb = file_size_bytes / (1024**3)
user.storage_used_gb += file_size_gb
user.save()

# Updated after deletion
user.storage_used_gb -= file_size_gb
user.save()

Channel Limit

# Checked before subscribing
if not user.can_add_channel:
    raise PermissionDenied(f"Channel limit reached ({user.max_channels})")

# Property in Account model
@property
def can_add_channel(self):
    current_count = self.channels.count()
    return current_count < self.max_channels

Playlist Limit

# Checked before creating
if not user.can_add_playlist:
    raise PermissionDenied(f"Playlist limit reached ({user.max_playlists})")

# Property in Account model
@property
def can_add_playlist(self):
    current_count = self.playlists.count()
    return current_count < self.max_playlists

Security Considerations

Data Isolation

  1. Queryset Filtering: All queries automatically filtered by owner
  2. Middleware: UserIsolationMiddleware enforces filtering
  3. Permissions: IsOwnerOrAdmin checks object-level permissions
  4. Admin Bypass: Admins can access all data for management

Authentication

  1. User Authentication: Standard Django auth with 2FA support
  2. YouTube Authentication: Cookie-based (stored per user)
  3. API Authentication: Token-based with user context

File Storage

User files should be stored in isolated directories:

# File path structure
/media/
  └── users/
      ├── user_1/
         ├── audio/
         ├── thumbnails/
         └── cookies/
      ├── user_2/
         ├── audio/
         ├── thumbnails/
         └── cookies/
      └── ...

Celery Tasks

Update tasks to respect user isolation:

@shared_task
def download_audio(audio_id, user_id):
    audio = Audio.objects.get(id=audio_id, owner_id=user_id)
    user = audio.owner
    
    # Use user's YouTube account
    youtube_account = audio.channel.youtube_account
    cookies_file = youtube_account.cookies_file if youtube_account else None
    
    # Download to user's directory
    output_path = f'/media/users/user_{user_id}/audio/'
    
    # Check quota before download
    if user.storage_used_gb >= user.storage_quota_gb:
        raise Exception("Storage quota exceeded")
    
    # Download...
    
    # Update storage
    user.storage_used_gb += file_size_gb
    user.save()

Testing

Test User Isolation

def test_user_cannot_access_other_user_data():
    user1 = Account.objects.create_user('user1', 'user1@test.com', 'pass')
    user2 = Account.objects.create_user('user2', 'user2@test.com', 'pass')
    
    audio1 = Audio.objects.create(owner=user1, youtube_id='xxx')
    audio2 = Audio.objects.create(owner=user2, youtube_id='yyy')
    
    # User1 should only see their audio
    assert Audio.objects.filter(owner=user1).count() == 1
    assert Audio.objects.filter(owner=user2).count() == 1

Test Quota Enforcement

def test_storage_quota_enforced():
    user = Account.objects.create_user(
        'user', 'user@test.com', 'pass',
        storage_quota_gb=10,
        storage_used_gb=10
    )
    
    # Should fail when quota exceeded
    with pytest.raises(PermissionDenied):
        download_audio(audio_id, user.id)

Performance Optimization

Database Indexes

class Meta:
    indexes = [
        models.Index(fields=['owner', 'youtube_id']),
        models.Index(fields=['owner', 'channel_id']),
        models.Index(fields=['owner', '-published_date']),
    ]

Query Optimization

# Use select_related for foreign keys
Audio.objects.filter(owner=user).select_related('owner')

# Use prefetch_related for reverse relations
User.objects.prefetch_related('channels', 'playlists', 'audio_files')

Caching

# Cache user stats
cache_key = f'user_stats_{user.id}'
stats = cache.get(cache_key)
if not stats:
    stats = calculate_user_stats(user)
    cache.set(cache_key, stats, 300)  # 5 minutes

Future Enhancements

  • User groups and team accounts
  • Shared playlists between users
  • Storage pooling for organizations
  • Usage analytics per user
  • API rate limiting per user
  • Custom branding per user
  • Billing and subscription management
  • OAuth integration for YouTube
  • Automated quota adjustment based on usage
  • User data export/import