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
This commit is contained in:
Iulian 2025-12-16 23:43:07 +00:00
commit 51679d1943
254 changed files with 37281 additions and 0 deletions

View file

@ -0,0 +1,464 @@
# 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`):
```python
- 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):
```python
- 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):
```python
+ owner: ForeignKey to Account
+ youtube_account: ForeignKey to UserYouTubeAccount
+ auto_download: bool per channel
+ download_quality: choices per channel
```
**Audio Model** (UPDATED):
```python
+ owner: ForeignKey to Account
```
**Playlist Model** (UPDATED):
```python
+ 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**:
```python
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**:
```python
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**:
```tsx
- 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
```bash
# 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:
```python
# 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:
```python
# 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:
```python
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
```
## Usage Examples
### Admin Creating User
```bash
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
```bash
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
```bash
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
```python
# 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
```python
# 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
```python
# 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:
```python
# File path structure
/media/
└── users/
├── user_1/
│ ├── audio/
│ ├── thumbnails/
│ └── cookies/
├── user_2/
│ ├── audio/
│ ├── thumbnails/
│ └── cookies/
└── ...
```
## Celery Tasks
Update tasks to respect user isolation:
```python
@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
```python
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
```python
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
```python
class Meta:
indexes = [
models.Index(fields=['owner', 'youtube_id']),
models.Index(fields=['owner', 'channel_id']),
models.Index(fields=['owner', '-published_date']),
]
```
### Query Optimization
```python
# 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
```python
# 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