405 error

This commit is contained in:
alorig
2025-11-21 20:59:50 +05:00
parent 1227df4a41
commit a82be89d21
3 changed files with 116 additions and 3 deletions

View File

@@ -83,3 +83,60 @@ class JWTAuthentication(BaseAuthentication):
# This allows session authentication to work if JWT fails # This allows session authentication to work if JWT fails
return None return None
class APIKeyAuthentication(BaseAuthentication):
"""
API Key authentication for WordPress integration.
Validates API keys stored in Site.wp_api_key field.
"""
def authenticate(self, request):
"""
Authenticate using WordPress API key.
Returns (user, api_key) tuple if valid.
"""
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
if not auth_header.startswith('Bearer '):
return None # Not an API key request
api_key = auth_header.split(' ')[1] if len(auth_header.split(' ')) > 1 else None
if not api_key or len(api_key) < 20: # API keys should be at least 20 chars
return None
# Don't try to authenticate JWT tokens (they start with 'ey')
if api_key.startswith('ey'):
return None # Let JWTAuthentication handle it
try:
from igny8_core.auth.models import Site, User
# Find site by API key
site = Site.objects.select_related('account', 'account__owner').filter(
wp_api_key=api_key,
is_active=True
).first()
if not site:
return None # API key not found or site inactive
# Get account and user
account = site.account
user = account.owner # Use account owner as the authenticated user
if not user.is_active:
raise AuthenticationFailed('User account is disabled.')
# Set account on request for tenant isolation
request.account = account
# Set site on request for WordPress integration context
request.site = site
return (user, api_key)
except Exception as e:
# Log the error but return None to allow other auth classes to try
import logging
logger = logging.getLogger(__name__)
logger.debug(f'APIKeyAuthentication error: {str(e)}')
return None

View File

@@ -105,11 +105,66 @@ class SectorInline(admin.TabularInline):
@admin.register(Site) @admin.register(Site)
class SiteAdmin(AccountAdminMixin, admin.ModelAdmin): class SiteAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = ['name', 'slug', 'account', 'industry', 'domain', 'status', 'is_active', 'get_sectors_count'] list_display = ['name', 'slug', 'account', 'industry', 'domain', 'status', 'is_active', 'get_api_key_status', 'get_sectors_count']
list_filter = ['status', 'is_active', 'account', 'industry'] list_filter = ['status', 'is_active', 'account', 'industry', 'hosting_type']
search_fields = ['name', 'slug', 'domain', 'industry__name'] search_fields = ['name', 'slug', 'domain', 'industry__name']
readonly_fields = ['created_at', 'updated_at'] readonly_fields = ['created_at', 'updated_at', 'get_api_key_display']
inlines = [SectorInline] inlines = [SectorInline]
actions = ['generate_api_keys']
fieldsets = (
('Site Info', {
'fields': ('name', 'slug', 'account', 'domain', 'description', 'industry', 'site_type', 'hosting_type', 'status', 'is_active')
}),
('WordPress Integration', {
'fields': ('wp_url', 'wp_username', 'wp_app_password', 'get_api_key_display'),
'description': 'Legacy WordPress integration fields. For WordPress sites using the IGNY8 WP Bridge plugin.'
}),
('SEO Metadata', {
'fields': ('seo_metadata',),
'classes': ('collapse',)
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
def get_api_key_display(self, obj):
"""Display API key with copy button"""
if obj.wp_api_key:
from django.utils.html import format_html
return format_html(
'<div style="display:flex; align-items:center; gap:10px;">'
'<code style="background:#f0f0f0; padding:5px 10px; border-radius:3px;">{}</code>'
'<button type="button" onclick="navigator.clipboard.writeText(\'{}\'); alert(\'API Key copied to clipboard!\');" '
'style="padding:5px 10px; cursor:pointer;">Copy</button>'
'</div>',
obj.wp_api_key,
obj.wp_api_key
)
return format_html('<em>No API key generated</em>')
get_api_key_display.short_description = 'WordPress API Key'
def get_api_key_status(self, obj):
"""Show API key status in list view"""
if obj.wp_api_key:
from django.utils.html import format_html
return format_html('<span style="color:green;">●</span> Active')
return format_html('<span style="color:gray;">○</span> None')
get_api_key_status.short_description = 'API Key'
def generate_api_keys(self, request, queryset):
"""Generate API keys for selected sites"""
import secrets
updated_count = 0
for site in queryset:
if not site.wp_api_key:
site.wp_api_key = f"igny8_{''.join(secrets.choice('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') for _ in range(40))}"
site.save()
updated_count += 1
self.message_user(request, f'Generated API keys for {updated_count} site(s). Sites with existing keys were skipped.')
generate_api_keys.short_description = 'Generate WordPress API Keys'
def get_sectors_count(self, obj): def get_sectors_count(self, obj):
try: try:

View File

@@ -219,6 +219,7 @@ REST_FRAMEWORK = {
'rest_framework.permissions.AllowAny', # Allow unauthenticated access for now 'rest_framework.permissions.AllowAny', # Allow unauthenticated access for now
], ],
'DEFAULT_AUTHENTICATION_CLASSES': [ 'DEFAULT_AUTHENTICATION_CLASSES': [
'igny8_core.api.authentication.APIKeyAuthentication', # WordPress API key authentication (check first)
'igny8_core.api.authentication.JWTAuthentication', # JWT token authentication 'igny8_core.api.authentication.JWTAuthentication', # JWT token authentication
'igny8_core.api.authentication.CSRFExemptSessionAuthentication', # Session auth without CSRF for API 'igny8_core.api.authentication.CSRFExemptSessionAuthentication', # Session auth without CSRF for API
'rest_framework.authentication.BasicAuthentication', # Enable basic auth as fallback 'rest_framework.authentication.BasicAuthentication', # Enable basic auth as fallback