mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 11:25:19 -05:00
Based on the git diff provided, here's a concise and descriptive commit message:
feat: add security event taxonomy and optimize park queryset - Add comprehensive security_event_types ChoiceGroup with categories for authentication, MFA, password, account, session, and API key events - Include severity levels, icons, and CSS classes for each event type - Fix park queryset optimization by using select_related for OneToOne location relationship - Remove location property fields (latitude/longitude) from values() call as they are not actual DB columns - Add proper location fields (city, state, country) to values() for map display This change enhances security event tracking capabilities and resolves a queryset optimization issue where property decorators were incorrectly used in values() queries.
This commit is contained in:
@@ -93,6 +93,7 @@ def get_passkey_status(request):
|
||||
def get_registration_options(request):
|
||||
"""Get WebAuthn registration options for passkey setup."""
|
||||
try:
|
||||
from django.utils import timezone
|
||||
from allauth.mfa.webauthn.internal import auth as webauthn_auth
|
||||
|
||||
# Use the correct allauth API: begin_registration
|
||||
@@ -101,8 +102,17 @@ def get_registration_options(request):
|
||||
|
||||
# State is stored internally by begin_registration via set_state()
|
||||
|
||||
# Store registration timeout in session (5 minutes)
|
||||
request.session["pending_passkey_expires"] = timezone.now().timestamp() + 300 # 5 minutes
|
||||
|
||||
# Debug log the structure
|
||||
logger.debug(f"WebAuthn registration options type: {type(creation_options)}")
|
||||
logger.debug(f"WebAuthn registration options keys: {creation_options.keys() if isinstance(creation_options, dict) else 'not a dict'}")
|
||||
logger.info(f"WebAuthn registration options: {creation_options}")
|
||||
|
||||
return Response({
|
||||
"options": creation_options,
|
||||
"expires_in_seconds": 300,
|
||||
})
|
||||
except ImportError as e:
|
||||
logger.error(f"WebAuthn module import error: {e}")
|
||||
@@ -143,8 +153,14 @@ def get_registration_options(request):
|
||||
def register_passkey(request):
|
||||
"""Complete passkey registration with WebAuthn response."""
|
||||
try:
|
||||
from django.utils import timezone
|
||||
from allauth.mfa.webauthn.internal import auth as webauthn_auth
|
||||
|
||||
from apps.accounts.services.security_service import (
|
||||
log_security_event,
|
||||
send_security_notification,
|
||||
)
|
||||
|
||||
credential = request.data.get("credential")
|
||||
name = request.data.get("name", "Passkey")
|
||||
|
||||
@@ -154,6 +170,17 @@ def register_passkey(request):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if registration has expired (5 minute timeout)
|
||||
expires_at = request.session.get("pending_passkey_expires")
|
||||
if expires_at and timezone.now().timestamp() > expires_at:
|
||||
# Clear expired session data
|
||||
if "pending_passkey_expires" in request.session:
|
||||
del request.session["pending_passkey_expires"]
|
||||
return Response(
|
||||
{"detail": "Passkey registration session expired. Please start registration again."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Get stored state from session (no request needed, uses context)
|
||||
state = webauthn_auth.get_state()
|
||||
if not state:
|
||||
@@ -164,24 +191,33 @@ def register_passkey(request):
|
||||
|
||||
# Use the correct allauth API: complete_registration
|
||||
try:
|
||||
from allauth.mfa.models import Authenticator
|
||||
from allauth.mfa.webauthn.internal.auth import WebAuthn
|
||||
|
||||
# Parse the credential response
|
||||
# Parse the credential response to validate it
|
||||
credential_data = webauthn_auth.parse_registration_response(credential)
|
||||
|
||||
# Complete registration - returns AuthenticatorData (binding)
|
||||
authenticator_data = webauthn_auth.complete_registration(credential_data)
|
||||
# Complete registration to validate and clear state
|
||||
webauthn_auth.complete_registration(credential_data)
|
||||
|
||||
# Create the Authenticator record ourselves
|
||||
authenticator = Authenticator.objects.create(
|
||||
user=request.user,
|
||||
type=Authenticator.Type.WEBAUTHN,
|
||||
data={
|
||||
"name": name,
|
||||
"credential": authenticator_data.credential_data.aaguid.hex if authenticator_data.credential_data else None,
|
||||
},
|
||||
# Use allauth's WebAuthn.add() to create the Authenticator properly
|
||||
# It stores the raw credential dict and name in the data field
|
||||
webauthn_wrapper = WebAuthn.add(
|
||||
request.user,
|
||||
name,
|
||||
credential, # Pass raw credential dict, not parsed data
|
||||
)
|
||||
# State is cleared internally by complete_registration
|
||||
authenticator = webauthn_wrapper.instance
|
||||
|
||||
# Log security event
|
||||
log_security_event(
|
||||
"passkey_registered",
|
||||
request,
|
||||
user=request.user,
|
||||
metadata={"passkey_name": name, "passkey_id": str(authenticator.id) if authenticator else None},
|
||||
)
|
||||
|
||||
# Send security notification email
|
||||
send_security_notification(request.user, "passkey_registered", {"passkey_name": name})
|
||||
|
||||
return Response({
|
||||
"detail": "Passkey registered successfully",
|
||||
@@ -345,6 +381,12 @@ def delete_passkey(request, passkey_id):
|
||||
try:
|
||||
from allauth.mfa.models import Authenticator
|
||||
|
||||
from apps.accounts.services.security_service import (
|
||||
check_auth_method_availability,
|
||||
log_security_event,
|
||||
send_security_notification,
|
||||
)
|
||||
|
||||
user = request.user
|
||||
password = request.data.get("password", "")
|
||||
|
||||
@@ -355,6 +397,17 @@ def delete_passkey(request, passkey_id):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Check if user has other auth methods before removing passkey
|
||||
auth_methods = check_auth_method_availability(user)
|
||||
|
||||
# If this is the last passkey and user has no other auth method, block removal
|
||||
if auth_methods["passkey_count"] == 1:
|
||||
if not auth_methods["has_password"] and not auth_methods["has_social"] and not auth_methods["has_totp"]:
|
||||
return Response(
|
||||
{"detail": "Cannot remove last passkey: you must have at least one authentication method. Please set a password or connect a social account first."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Find and delete the passkey
|
||||
try:
|
||||
authenticator = Authenticator.objects.get(
|
||||
@@ -362,7 +415,20 @@ def delete_passkey(request, passkey_id):
|
||||
user=user,
|
||||
type=Authenticator.Type.WEBAUTHN,
|
||||
)
|
||||
passkey_name = authenticator.data.get("name", "Passkey") if authenticator.data else "Passkey"
|
||||
authenticator.delete()
|
||||
|
||||
# Log security event
|
||||
log_security_event(
|
||||
"passkey_removed",
|
||||
request,
|
||||
user=user,
|
||||
metadata={"passkey_name": passkey_name, "passkey_id": str(passkey_id)},
|
||||
)
|
||||
|
||||
# Send security notification email
|
||||
send_security_notification(user, "passkey_removed", {"passkey_name": passkey_name})
|
||||
|
||||
except Authenticator.DoesNotExist:
|
||||
return Response(
|
||||
{"detail": "Passkey not found"},
|
||||
|
||||
Reference in New Issue
Block a user