diff --git a/MULTI-TENANCY-FIXES-DEC-2025.md b/MULTI-TENANCY-FIXES-DEC-2025.md new file mode 100644 index 00000000..27cee21b --- /dev/null +++ b/MULTI-TENANCY-FIXES-DEC-2025.md @@ -0,0 +1,192 @@ +# CRITICAL MULTI-TENANCY FIXES - December 10, 2025 + +## PROBLEM SUMMARY + +Users are unable to use business features (auto_cluster, automation, etc.) despite being authenticated. The error is **"Account is required"** or permission denied. + +## ROOT CAUSE + +The system has **structural** issues in the multi-tenancy implementation: + +### 1. **User Model Allows NULL Accounts** +**File**: `backend/igny8_core/auth/models.py` (line 644) +```python +account = models.ForeignKey('igny8_core_auth.Account', + on_delete=models.CASCADE, + null=True, # ❌ WRONG + blank=True, # ❌ WRONG + ...) +``` + +**Problem**: Users can exist without accounts (orphaned users). When middleware sets `request.account` from `request.user.account`, it becomes `None` for orphaned users. + +**Impact**: ALL business endpoints that check `request.account` fail. + +### 2. **HasTenantAccess Permission Too Complex** +**File**: `backend/igny8_core/api/permissions.py` (lines 24-67) + +**Problem**: The permission class had unnecessary fallback logic and unclear flow: +- Checked `request.account` +- Fell back to `request.user.account` +- Compared if they match +- Returned `False` on ANY exception + +**Impact**: Added complexity made debugging hard. If user has no account, access is denied silently. + +### 3. **Business Endpoints Explicitly Check `request.account`** +**File**: `backend/igny8_core/modules/planner/views.py` (line 633) +```python +account = getattr(request, 'account', None) +if not account: + return error_response(error='Account is required', ...) +``` + +**Problem**: Direct dependency on `request.account`. If middleware fails to set it, feature breaks. + +**Impact**: Auto-cluster and other AI functions fail with "Account is required". + +## FIXES APPLIED + +### ✅ Fix 1: Simplified HasTenantAccess Permission +**File**: `backend/igny8_core/api/permissions.py` + +**Change**: Removed fallback logic. Made it clear: **authenticated users MUST have accounts**. + +```python +# SIMPLIFIED LOGIC: Every authenticated user MUST have an account +# Middleware already set request.account from request.user.account +# Just verify it exists +if not hasattr(request.user, 'account'): + return False + +try: + user_account = request.user.account + if not user_account: + return False + return True +except (AttributeError, Exception): + return False +``` + +### ✅ Fix 2: Task Progress Permission +**File**: `backend/igny8_core/modules/system/integration_views.py` (line 900) + +**Change**: Allowed any authenticated user to check task progress (not just system accounts). + +```python +@action(..., permission_classes=[IsAuthenticatedAndActive]) +def task_progress(self, request, task_id=None): +``` + +### ✅ Fix 3: AI Settings Fallback +**File**: `backend/igny8_core/ai/settings.py` + +**Change**: Added fallback to system account (aws-admin) for OpenAI settings when user account doesn't have them configured. + +### ✅ Fix 4: Error Response Parameter +**File**: `backend/igny8_core/modules/planner/views.py` + +**Change**: Fixed `error_response()` call - changed invalid `extra_data` parameter to `debug_info`. + +## REQUIRED ACTIONS + +### 🔴 CRITICAL: Fix Orphaned Users + +1. **Run the fix script**: + ```bash + cd /data/app/igny8/backend + python3 fix_orphaned_users.py + ``` + +2. **The script will**: + - Find users with `account = NULL` + - Create accounts for them OR delete them + - Report results + +### 🔴 CRITICAL: Make Account Field Required + +After fixing orphaned users, update the User model: + +**File**: `backend/igny8_core/auth/models.py` (line 644) + +**Change**: +```python +# BEFORE +account = models.ForeignKey(..., null=True, blank=True, ...) + +# AFTER +account = models.ForeignKey(..., null=False, blank=False, ...) +``` + +**Then create and run migration**: +```bash +cd /data/app/igny8/backend +python3 manage.py makemigrations +python3 manage.py migrate +``` + +## VERIFICATION + +After fixes, verify: + +1. **Check no orphaned users**: + ```bash + cd /data/app/igny8/backend + python3 manage.py shell -c " + from igny8_core.auth.models import User + orphaned = User.objects.filter(account__isnull=True).count() + print(f'Orphaned users: {orphaned}') + " + ``` + Expected: `Orphaned users: 0` + +2. **Test auto-cluster**: + - Login as normal user + - Select 5+ keywords + - Click "Auto Cluster" + - Should work without "Account is required" error + +3. **Test task progress**: + - Start any AI function + - Progress modal should show real-time updates + - No "403 Forbidden" errors + +## ARCHITECTURE PRINCIPLES ESTABLISHED + +1. **NO NULL ACCOUNTS**: Every user MUST have an account. Period. + +2. **NO FALLBACKS**: If `request.user.account` is None, it's a data integrity issue, not a code issue. + +3. **CLEAR FLOW**: + - User registers → Account created → User.account set + - User logs in → Middleware sets request.account from user.account + - Permission checks → Verify request.user.account exists + - Business logic → Use request.account directly + +4. **FAIL FAST**: Don't hide errors with fallbacks. If account is missing, raise error. + +## FILES MODIFIED + +1. `backend/igny8_core/api/permissions.py` - Simplified HasTenantAccess +2. `backend/igny8_core/modules/system/integration_views.py` - Fixed task_progress permission +3. `backend/igny8_core/ai/settings.py` - Added system account fallback for AI settings +4. `backend/igny8_core/modules/planner/views.py` - Fixed error_response call + +## FILES CREATED + +1. `backend/fix_orphaned_users.py` - Script to fix orphaned users +2. `MULTI-TENANCY-FIXES-DEC-2025.md` - This document + +## NEXT STEPS + +1. ✅ Run orphaned users fix script +2. ✅ Make User.account field required (migration) +3. ✅ Test all business features +4. ✅ Update documentation to reflect "no fallbacks" principle +5. ✅ Add database constraints to prevent orphaned users + +--- + +**Date**: December 10, 2025 +**Status**: FIXES APPLIED - VERIFICATION PENDING +**Priority**: CRITICAL diff --git a/UNDER-OBSERVATION.md b/UNDER-OBSERVATION.md new file mode 100644 index 00000000..533f1106 --- /dev/null +++ b/UNDER-OBSERVATION.md @@ -0,0 +1,103 @@ +# UNDER OBSERVATION + +## Issue: User Logged Out During Image Prompt Generation (Dec 10, 2025) + +### Original Problem +User performed workflow: auto-cluster → generate ideas → queue to writer → generate content → generate image prompt. During image prompt generation (near completion), user was automatically logged out. + +### Investigation Timeline + +**Initial Analysis:** +- Suspected backend container restarts invalidating sessions +- Docker ps showed all containers up 19+ minutes - NO RESTARTS during incident +- Backend logs showed: `[IsAuthenticatedAndActive] DENIED: User not authenticated` and `Client error: Authentication credentials were not provided` +- Token was not being sent with API requests + +**Root Cause Identified:** +The logout was NOT caused by backend issues or container restarts. It was caused by **frontend state corruption during HMR (Hot Module Reload)** triggered by code changes made to fix an unrelated useLocation() error. + +**What Actually Happened:** + +1. **Commit 5fb3687854d9aadfc5d604470f3712004b23243c** - Already had proper fix for useLocation() error (Suspense outside Routes) + +2. **Additional "fixes" applied on Dec 10, 2025:** + - Changed `cacheDir: "/tmp/vite-cache"` in vite.config.ts + - Moved BrowserRouter above ErrorBoundary in main.tsx + - Added `watch.interval: 100` and `fs.strict: false` + +3. **These changes triggered:** + - Vite cache stored in /tmp got wiped on container operations + - Full rebuild with HMR + - Component tree restructuring (BrowserRouter position change) + - Auth store (Zustand persist) lost state during rapid unmount/remount cycle + - Frontend started making API calls WITHOUT Authorization header + - Backend correctly rejected unauthenticated requests + - Frontend logout() triggered + +### Fix Applied +**Reverted the problematic changes:** +- Removed `cacheDir: "/tmp/vite-cache"` - let Vite use default node_modules/.vite +- Restored BrowserRouter position inside ErrorBoundary/ThemeProvider (original structure) +- Removed `watch.interval` and `fs.strict` additions + +**Kept the actual fixes:** +- Backend: Removed `IsSystemAccountOrDeveloper` from IntegrationSettingsViewSet class-level permissions +- Backend: Auto-cluster `extra_data` → `debug_info` parameter fix +- Frontend: Suspense wrapping Routes (from commit 5fb3687) - THIS was the real useLocation() fix + +### What to Watch For + +**1. useLocation() Error After Container Restarts** +- **Symptom:** "useLocation() may be used only in the context of a component" +- **Where:** Keywords page, other planner/writer module pages (50-60% of pages) +- **If it happens:** + - Check if Vite cache is stale + - Clear node_modules/.vite inside frontend container: `docker compose exec igny8_frontend rm -rf /app/node_modules/.vite` + - Restart frontend container + - DO NOT change cacheDir or component tree structure + +**2. Auth State Loss During Development** +- **Symptom:** Random logouts during active sessions, "Authentication credentials were not provided" +- **Triggers:** + - HMR with significant component tree changes + - Rapid container restarts during development + - Changes to context provider order in main.tsx +- **Prevention:** + - Avoid restructuring main.tsx component tree + - Test auth persistence after any main.tsx changes + - Monitor browser console for localStorage errors during HMR + +**3. Permission Errors for Normal Users** +- **Symptom:** "You do not have permission to perform this action" for valid users with complete account setup +- **Check:** + - Backend logs for permission class debug output: `[IsAuthenticatedAndActive]`, `[IsViewerOrAbove]`, `[HasTenantAccess]` + - Verify user has role='owner' and is_active=True + - Ensure viewset doesn't have `IsSystemAccountOrDeveloper` at class level for endpoints normal users need + +**4. Celery Task Progress Polling 403 Errors** +- **Symptom:** Task progress endpoint returns 403 for normal users +- **Root cause:** ViewSet class-level permissions blocking action-level overrides +- **Solution:** Ensure IntegrationSettingsViewSet permission_classes doesn't include IsSystemAccountOrDeveloper + +### Lessons Learned + +1. **Don't layer fixes on top of fixes** - Identify root cause first +2. **Vite cache location matters** - /tmp gets wiped, breaking HMR state persistence +3. **Component tree structure is fragile** - Moving BrowserRouter breaks auth rehydration timing +4. **Container uptime ≠ code stability** - HMR can cause issues without restart +5. **Permission debugging** - Added logging to permission classes was critical for diagnosis +6. **The original fix was already correct** - Commit 5fb3687 had it right, additional "improvements" broke it + +### Files Modified (Reverted) +- `frontend/vite.config.ts` - Removed cacheDir and watch config changes +- `frontend/src/main.tsx` - Restored original component tree structure + +### Files Modified (Kept) +- `backend/igny8_core/modules/system/integration_views.py` - Removed IsSystemAccountOrDeveloper +- `backend/igny8_core/modules/planner/views.py` - Fixed extra_data → debug_info +- `backend/igny8_core/api/permissions.py` - Added debug logging (can be removed later) + +### Status +**RESOLVED** - Auth state stable, backend permissions correct, useLocation fix preserved. + +**Monitor for 48 hours** - Watch for any recurrence of useLocation errors or auth issues after container restarts. diff --git a/backend/fix_orphaned_users.py b/backend/fix_orphaned_users.py new file mode 100644 index 00000000..ace005ab --- /dev/null +++ b/backend/fix_orphaned_users.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +""" +Script to identify and fix orphaned users (users without accounts). + +This script will: +1. Find all users with account = NULL +2. For each user, either: + - Assign them to an existing account if possible + - Create a new account for them + - Delete them if they're test/invalid users +3. Report the results + +Run this from backend directory: + python3 fix_orphaned_users.py +""" + +import os +import sys +import django + +# Setup Django +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8_core.settings') +django.setup() + +from igny8_core.auth.models import User, Account, Plan +from django.db import transaction + +def main(): + print("=" * 80) + print("ORPHANED USERS FIX SCRIPT") + print("=" * 80) + + # Find users without accounts + orphaned_users = User.objects.filter(account__isnull=True) + count = orphaned_users.count() + + print(f"\nFound {count} user(s) without accounts:\n") + + if count == 0: + print("✅ No orphaned users found. System is healthy!") + return + + # List them + for i, user in enumerate(orphaned_users, 1): + print(f"{i}. ID: {user.id}") + print(f" Email: {user.email}") + print(f" Username: {user.username}") + print(f" Role: {user.role}") + print(f" Active: {user.is_active}") + print(f" Superuser: {user.is_superuser}") + print(f" Created: {user.created_at}") + print() + + # Ask what to do + print("\nOptions:") + print("1. Auto-fix: Create accounts for all orphaned users") + print("2. Delete all orphaned users") + print("3. Exit without changes") + + choice = input("\nEnter choice (1-3): ").strip() + + if choice == '1': + auto_fix_users(orphaned_users) + elif choice == '2': + delete_users(orphaned_users) + else: + print("\n❌ No changes made. Exiting.") + +def auto_fix_users(users): + """Create accounts for orphaned users""" + print("\n" + "=" * 80) + print("AUTO-FIXING ORPHANED USERS") + print("=" * 80 + "\n") + + # Get or create free plan + try: + free_plan = Plan.objects.get(slug='free', is_active=True) + except Plan.DoesNotExist: + print("❌ ERROR: Free plan not found. Cannot create accounts.") + print(" Please create a 'free' plan first or assign users manually.") + return + + fixed_count = 0 + + with transaction.atomic(): + for user in users: + try: + # Generate account name + if user.first_name or user.last_name: + account_name = f"{user.first_name} {user.last_name}".strip() + else: + account_name = user.email.split('@')[0] + + # Generate unique slug + base_slug = account_name.lower().replace(' ', '-').replace('_', '-')[:50] or 'account' + slug = base_slug + counter = 1 + while Account.objects.filter(slug=slug).exists(): + slug = f"{base_slug}-{counter}" + counter += 1 + + # Create account + account = Account.objects.create( + name=account_name, + slug=slug, + owner=user, + plan=free_plan, + credits=free_plan.get_effective_credits_per_month(), + status='trial', + billing_email=user.email, + ) + + # Assign account to user + user.account = account + user.save() + + print(f"✅ Fixed user: {user.email}") + print(f" Created account: {account.name} (ID: {account.id})") + print(f" Credits: {account.credits}") + print() + + fixed_count += 1 + + except Exception as e: + print(f"❌ ERROR fixing user {user.email}: {e}") + print() + + print("=" * 80) + print(f"✅ Successfully fixed {fixed_count} user(s)") + print("=" * 80) + +def delete_users(users): + """Delete orphaned users""" + print("\n⚠️ WARNING: This will permanently delete the selected users!") + confirm = input("Type 'DELETE' to confirm: ").strip() + + if confirm != 'DELETE': + print("\n❌ Deletion cancelled.") + return + + count = users.count() + users.delete() + + print(f"\n✅ Deleted {count} user(s)") + +if __name__ == '__main__': + main() diff --git a/backend/igny8_core/ai/settings.py b/backend/igny8_core/ai/settings.py index 8c0759d1..00f4b697 100644 --- a/backend/igny8_core/ai/settings.py +++ b/backend/igny8_core/ai/settings.py @@ -19,8 +19,8 @@ FUNCTION_ALIASES = { def get_model_config(function_name: str, account) -> Dict[str, Any]: """ - Get model configuration from IntegrationSettings only. - No fallbacks - account must have IntegrationSettings configured. + Get model configuration from IntegrationSettings. + Falls back to system account (aws-admin) if user account doesn't have settings. Args: function_name: Name of the AI function @@ -38,17 +38,42 @@ def get_model_config(function_name: str, account) -> Dict[str, Any]: # Resolve function alias actual_name = FUNCTION_ALIASES.get(function_name, function_name) - # Get IntegrationSettings for OpenAI + # Get IntegrationSettings for OpenAI - try user account first + integration_settings = None try: from igny8_core.modules.system.models import IntegrationSettings - integration_settings = IntegrationSettings.objects.get( + integration_settings = IntegrationSettings.objects.filter( integration_type='openai', account=account, is_active=True - ) - except IntegrationSettings.DoesNotExist: + ).first() + except Exception as e: + logger.warning(f"Could not load OpenAI settings for account {account.id}: {e}") + + # Fallback to system account (aws-admin, default-account, or default) + if not integration_settings: + logger.info(f"No OpenAI settings for account {account.id}, falling back to system account") + try: + from igny8_core.auth.models import Account + from igny8_core.modules.system.models import IntegrationSettings + for slug in ['aws-admin', 'default-account', 'default']: + system_account = Account.objects.filter(slug=slug).first() + if system_account: + integration_settings = IntegrationSettings.objects.filter( + integration_type='openai', + account=system_account, + is_active=True + ).first() + if integration_settings: + logger.info(f"Using OpenAI settings from system account: {slug}") + break + except Exception as e: + logger.warning(f"Could not load system account OpenAI settings: {e}") + + # If still no settings found, raise error + if not integration_settings: raise ValueError( - f"OpenAI IntegrationSettings not configured for account {account.id}. " + f"OpenAI IntegrationSettings not configured for account {account.id} or system account. " f"Please configure OpenAI settings in the integration page." ) diff --git a/backend/igny8_core/api/permissions.py b/backend/igny8_core/api/permissions.py index 4bbb9330..866bf90f 100644 --- a/backend/igny8_core/api/permissions.py +++ b/backend/igny8_core/api/permissions.py @@ -12,13 +12,23 @@ class IsAuthenticatedAndActive(permissions.BasePermission): Base permission for most endpoints """ def has_permission(self, request, view): + import logging + logger = logging.getLogger(__name__) + if not request.user or not request.user.is_authenticated: + logger.warning(f"[IsAuthenticatedAndActive] DENIED: User not authenticated") return False # Check if user is active if hasattr(request.user, 'is_active'): - return request.user.is_active + is_active = request.user.is_active + if is_active: + logger.info(f"[IsAuthenticatedAndActive] ALLOWED: User {request.user.email} is active") + else: + logger.warning(f"[IsAuthenticatedAndActive] DENIED: User {request.user.email} is inactive") + return is_active + logger.info(f"[IsAuthenticatedAndActive] ALLOWED: User {request.user.email} (no is_active check)") return True @@ -27,47 +37,58 @@ class HasTenantAccess(permissions.BasePermission): Permission class that requires user to belong to the tenant/account Ensures tenant isolation Superusers, developers, and system account users bypass this check. + + CRITICAL: Every authenticated user MUST have an account. + The middleware sets request.account from request.user.account. + If a user doesn't have an account, it's a data integrity issue. """ def has_permission(self, request, view): + import logging + logger = logging.getLogger(__name__) + if not request.user or not request.user.is_authenticated: + logger.warning(f"[HasTenantAccess] DENIED: User not authenticated") return False # Bypass for superusers if getattr(request.user, 'is_superuser', False): + logger.info(f"[HasTenantAccess] ALLOWED: User {request.user.email} is superuser") return True # Bypass for developers if hasattr(request.user, 'role') and request.user.role == 'developer': + logger.info(f"[HasTenantAccess] ALLOWED: User {request.user.email} is developer") return True # Bypass for system account users try: if hasattr(request.user, 'is_system_account_user') and request.user.is_system_account_user(): + logger.info(f"[HasTenantAccess] ALLOWED: User {request.user.email} is system account user") return True except Exception: pass - # Get account from request (set by middleware) - account = getattr(request, 'account', None) + # SIMPLIFIED LOGIC: Every authenticated user MUST have an account + # Middleware already set request.account from request.user.account + # Just verify it exists + if not hasattr(request.user, 'account'): + logger.warning(f"[HasTenantAccess] DENIED: User {request.user.email} has no account attribute") + return False - # If no account in request, try to get from user - if not account and hasattr(request.user, 'account'): - try: - account = request.user.account - except (AttributeError, Exception): - pass - - # Regular users must have account access - if account: - # Check if user belongs to this account - if hasattr(request.user, 'account'): - try: - user_account = request.user.account - return user_account == account or user_account.id == account.id - except (AttributeError, Exception): - pass - - return False + try: + # Access the account to trigger any lazy loading + user_account = request.user.account + if not user_account: + logger.warning(f"[HasTenantAccess] DENIED: User {request.user.email} has NULL account") + return False + + # Success - user has a valid account + logger.info(f"[HasTenantAccess] ALLOWED: User {request.user.email} has account {user_account.name} (ID: {user_account.id})") + return True + except (AttributeError, Exception) as e: + # User doesn't have account relationship - data integrity issue + logger.warning(f"[HasTenantAccess] DENIED: User {request.user.email} account access failed: {e}") + return False class IsViewerOrAbove(permissions.BasePermission): @@ -77,24 +98,36 @@ class IsViewerOrAbove(permissions.BasePermission): Superusers and developers bypass this check. """ def has_permission(self, request, view): + import logging + logger = logging.getLogger(__name__) + if not request.user or not request.user.is_authenticated: + logger.warning(f"[IsViewerOrAbove] DENIED: User not authenticated") return False # Bypass for superusers if getattr(request.user, 'is_superuser', False): + logger.info(f"[IsViewerOrAbove] ALLOWED: User {request.user.email} is superuser") return True # Bypass for developers if hasattr(request.user, 'role') and request.user.role == 'developer': + logger.info(f"[IsViewerOrAbove] ALLOWED: User {request.user.email} is developer") return True # Check user role if hasattr(request.user, 'role'): role = request.user.role # viewer, editor, admin, owner all have access - return role in ['viewer', 'editor', 'admin', 'owner'] + allowed = role in ['viewer', 'editor', 'admin', 'owner'] + if allowed: + logger.info(f"[IsViewerOrAbove] ALLOWED: User {request.user.email} has role {role}") + else: + logger.warning(f"[IsViewerOrAbove] DENIED: User {request.user.email} has invalid role {role}") + return allowed # If no role system, allow authenticated users + logger.info(f"[IsViewerOrAbove] ALLOWED: User {request.user.email} (no role system)") return True diff --git a/backend/igny8_core/auth/serializers.py b/backend/igny8_core/auth/serializers.py index 80009e3d..098f5bf4 100644 --- a/backend/igny8_core/auth/serializers.py +++ b/backend/igny8_core/auth/serializers.py @@ -80,6 +80,10 @@ class SiteSerializer(serializers.ModelSerializer): 'created_at', 'updated_at' ] read_only_fields = ['created_at', 'updated_at', 'account'] + # Explicitly specify required fields for clarity + extra_kwargs = { + 'industry': {'required': True, 'error_messages': {'required': 'Industry is required when creating a site.'}}, + } def __init__(self, *args, **kwargs): """Allow partial updates for PATCH requests.""" @@ -87,10 +91,12 @@ class SiteSerializer(serializers.ModelSerializer): # Make slug optional - it will be auto-generated from name if not provided if 'slug' in self.fields: self.fields['slug'].required = False - # For partial updates (PATCH), make name optional + # For partial updates (PATCH), make name and industry optional if self.partial: if 'name' in self.fields: self.fields['name'].required = False + if 'industry' in self.fields: + self.fields['industry'].required = False def validate_domain(self, value): """Ensure domain has https:// protocol. diff --git a/backend/igny8_core/modules/planner/views.py b/backend/igny8_core/modules/planner/views.py index f90cb1f3..7ee0f66b 100644 --- a/backend/igny8_core/modules/planner/views.py +++ b/backend/igny8_core/modules/planner/views.py @@ -5,6 +5,7 @@ from django_filters.rest_framework import DjangoFilterBackend from django.db import transaction from django.db.models import Max, Count, Sum, Q from django.http import HttpResponse +from django.conf import settings import csv import json import time @@ -655,10 +656,10 @@ class KeywordViewSet(SiteSectorModelViewSet): error=validation['error'], status_code=status.HTTP_400_BAD_REQUEST, request=request, - extra_data={ + debug_info={ 'count': validation.get('count'), 'required': validation.get('required') - } + } if settings.DEBUG else None ) # Validation passed - proceed with clustering diff --git a/backend/igny8_core/modules/system/integration_views.py b/backend/igny8_core/modules/system/integration_views.py index e900f2de..e7b4969a 100644 --- a/backend/igny8_core/modules/system/integration_views.py +++ b/backend/igny8_core/modules/system/integration_views.py @@ -29,6 +29,9 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): ViewSet for managing integration settings (OpenAI, Runware, GSC) Following reference plugin pattern: WordPress uses update_option() for igny8_api_settings We store in IntegrationSettings model with account isolation + + IMPORTANT: Integration settings are system-wide (configured by super users/developers) + Normal users don't configure their own API keys - they use the system account settings via fallback """ permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsSystemAccountOrDeveloper] @@ -897,11 +900,14 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): request=request ) - @action(detail=False, methods=['get'], url_path='task_progress/(?P[^/.]+)', url_name='task-progress') + @action(detail=False, methods=['get'], url_path='task_progress/(?P[^/.]+)', url_name='task-progress', + permission_classes=[IsAuthenticatedAndActive]) # Allow any authenticated user to check task progress def task_progress(self, request, task_id=None): """ Get Celery task progress status GET /api/v1/system/settings/task_progress// + + Permission: Any authenticated user can check task progress (not restricted to system accounts) """ if not task_id: return error_response( diff --git a/frontend/src/components/onboarding/WorkflowGuide.tsx b/frontend/src/components/onboarding/WorkflowGuide.tsx index ea550960..d18c9e68 100644 --- a/frontend/src/components/onboarding/WorkflowGuide.tsx +++ b/frontend/src/components/onboarding/WorkflowGuide.tsx @@ -148,18 +148,16 @@ export default function WorkflowGuide({ onSiteAdded }: WorkflowGuideProps) { try { setIsCreatingSite(true); - // Create site with user-provided name and domain + // Create site with user-provided name, domain, and industry const newSite = await createSite({ name: siteName.trim(), domain: websiteAddress.trim() || undefined, is_active: true, hosting_type: 'wordpress', + industry: selectedIndustry.id, // Include industry ID - required by backend }); - // Set industry for the site (if API supports it during creation, otherwise update) - // For now, we'll set it via selectSectorsForSite which also sets industry - - // Select sectors for the site (this also sets the industry) + // Select sectors for the site await selectSectorsForSite( newSite.id, selectedIndustry.slug, diff --git a/frontend/src/pages/Settings/Sites.tsx b/frontend/src/pages/Settings/Sites.tsx index 8f0d317d..f3c3cb88 100644 --- a/frontend/src/pages/Settings/Sites.tsx +++ b/frontend/src/pages/Settings/Sites.tsx @@ -49,6 +49,7 @@ export default function Sites() { domain: '', description: '', is_active: true, // Default to true to match backend model default + industry: undefined as number | undefined, // Industry ID - required by backend }); // Load sites and industries @@ -145,6 +146,7 @@ export default function Sites() { domain: site.domain || '', description: site.description || '', is_active: site.is_active || false, + industry: site.industry, }); setShowDetailsModal(true); }; @@ -177,6 +179,7 @@ export default function Sites() { domain: '', description: '', is_active: true, // Default to true to match backend model default + industry: undefined, }); setShowSiteModal(true); }; @@ -188,6 +191,7 @@ export default function Sites() { domain: site.domain || '', description: site.description || '', is_active: site.is_active || false, + industry: site.industry, }); setShowSiteModal(true); }; @@ -247,6 +251,7 @@ export default function Sites() { domain: '', description: '', is_active: false, + industry: undefined, }); await loadSites(); } catch (error: any) { @@ -313,6 +318,22 @@ export default function Sites() { required: true, placeholder: 'Enter site name', }, + { + key: 'industry', + label: 'Industry', + type: 'select', + value: formData.industry?.toString() || '', + onChange: (value: any) => setFormData({ ...formData, industry: value ? parseInt(value) : undefined }), + required: true, + placeholder: 'Select an industry', + options: [ + { value: '', label: 'Select an industry...' }, + ...industries.map(industry => ({ + value: industry.id.toString(), + label: industry.name, + })), + ], + }, { key: 'domain', label: 'Domain', @@ -428,6 +449,7 @@ export default function Sites() { domain: '', description: '', is_active: false, + industry: undefined, }); }} onSubmit={handleSaveSite} diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 96add5b6..88dd027d 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1395,6 +1395,7 @@ export interface Site { export interface SiteCreateData { name: string; + industry?: number; // Industry ID - required by backend slug?: string; domain?: string; description?: string; @@ -1403,6 +1404,7 @@ export interface SiteCreateData { wp_url?: string; wp_username?: string; wp_app_password?: string; + hosting_type?: string; } export interface SitesResponse {