405 error
This commit is contained in:
@@ -83,3 +83,60 @@ class JWTAuthentication(BaseAuthentication):
|
||||
# This allows session authentication to work if JWT fails
|
||||
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
|
||||
|
||||
@@ -105,11 +105,66 @@ class SectorInline(admin.TabularInline):
|
||||
|
||||
@admin.register(Site)
|
||||
class SiteAdmin(AccountAdminMixin, admin.ModelAdmin):
|
||||
list_display = ['name', 'slug', 'account', 'industry', 'domain', 'status', 'is_active', 'get_sectors_count']
|
||||
list_filter = ['status', 'is_active', 'account', 'industry']
|
||||
list_display = ['name', 'slug', 'account', 'industry', 'domain', 'status', 'is_active', 'get_api_key_status', 'get_sectors_count']
|
||||
list_filter = ['status', 'is_active', 'account', 'industry', 'hosting_type']
|
||||
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]
|
||||
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):
|
||||
try:
|
||||
|
||||
@@ -219,6 +219,7 @@ REST_FRAMEWORK = {
|
||||
'rest_framework.permissions.AllowAny', # Allow unauthenticated access for now
|
||||
],
|
||||
'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.CSRFExemptSessionAuthentication', # Session auth without CSRF for API
|
||||
'rest_framework.authentication.BasicAuthentication', # Enable basic auth as fallback
|
||||
|
||||
Reference in New Issue
Block a user