fix: Use request.tenant for staff filtering in multi-tenant context

- UserTenantFilteredMixin now uses request.tenant (from django-tenants
  middleware) instead of request.user.tenant for filtering
- ResourceSerializer._get_valid_user uses request.tenant for validation
- Frontend useResources sends user_id instead of user field

This fixes 400 errors when creating staff resources because the tenant
context is now correctly derived from the subdomain being accessed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-08 10:31:18 -05:00
parent 156ad09232
commit df45a6f5d7
3 changed files with 33 additions and 7 deletions

View File

@@ -70,8 +70,7 @@ export const useCreateResource = () => {
const backendData: any = {
name: resourceData.name,
type: resourceData.type,
user: resourceData.userId ? parseInt(resourceData.userId) : null,
timezone: 'UTC', // Default timezone
user_id: resourceData.userId ? parseInt(resourceData.userId) : null,
};
if (resourceData.maxConcurrentEvents !== undefined) {

View File

@@ -310,14 +310,21 @@ class UserTenantFilteredMixin(SandboxFilteredQuerySetMixin):
"""
def filter_queryset_for_tenant(self, queryset):
"""Filter users by tenant foreign key."""
"""Filter users by tenant foreign key.
Uses request.tenant (from django-tenants middleware) rather than
request.user.tenant because platform-level users (owners) may have
tenant=None on their user record but still access tenant subdomains.
"""
queryset = super().filter_queryset_for_tenant(queryset)
user = self.request.user
if user.tenant:
queryset = queryset.filter(tenant=user.tenant)
# Use request.tenant (from middleware) - this is set based on the
# subdomain being accessed, not the user's tenant FK
request_tenant = getattr(self.request, 'tenant', None)
if request_tenant:
queryset = queryset.filter(tenant=request_tenant)
else:
# User has no tenant - return empty for safety
# No tenant on request - return empty for safety
return queryset.none()
return queryset

View File

@@ -232,26 +232,42 @@ class ResourceSerializer(serializers.ModelSerializer):
because platform-level users (owners) may have tenant=None on their user record
but still access tenant subdomains.
"""
import logging
logger = logging.getLogger(__name__)
logger.warning(f"_get_valid_user called with user_id={user_id}")
if not user_id:
logger.warning("_get_valid_user: user_id is empty, returning None")
return None
request = self.context.get('request')
if not request or not request.user.is_authenticated:
logger.warning(f"_get_valid_user: no request or not authenticated, returning None")
return None
# Use request.tenant (from django-tenants middleware) - this is set based on
# the subdomain being accessed, not the user's tenant FK
tenant = getattr(request, 'tenant', None)
logger.warning(f"_get_valid_user: request.tenant={tenant}, request.user={request.user}, request.user.tenant={getattr(request.user, 'tenant', 'N/A')}")
if not tenant:
logger.warning("_get_valid_user: no tenant on request, returning None")
return None
try:
user = User.objects.get(id=user_id)
logger.warning(f"_get_valid_user: found user {user.email}, user.tenant={user.tenant}, comparing to request.tenant={tenant}")
# Verify user belongs to the same tenant as the request
if user.tenant == tenant:
logger.warning(f"_get_valid_user: tenant match! Returning user")
return user
logger.warning(f"_get_valid_user: tenant mismatch, returning None")
return None
except User.DoesNotExist:
logger.warning(f"_get_valid_user: user {user_id} not found")
return None
def _is_staff_type(self, resource_type=None, legacy_type=None):
@@ -274,6 +290,10 @@ class ResourceSerializer(serializers.ModelSerializer):
Validate that staff-type resources have a user assigned.
Staff resources MUST be linked to a staff member.
"""
import logging
logger = logging.getLogger(__name__)
logger.warning(f"ResourceSerializer.validate called with attrs={attrs}")
user_id = attrs.get('user_id')
resource_type = attrs.get('resource_type')
legacy_type = attrs.get('type')