Based on the git diff provided, here's a concise and descriptive commit message:

feat: add passkey authentication and enhance user preferences

- Add passkey login security event type with fingerprint icon
- Include request and site context in email confirmation for backend
- Add user_id exact match filter to prevent incorrect user lookups
- Enable PATCH method for updating user preferences via API
- Add moderation_preferences support to user settings
- Optimize ticket queries with select_related and prefetch_related

This commit introduces passkey authentication tracking, improves user
profile filtering accuracy, and extends the preferences API to support
updates. Query optimizations reduce database hits for ticket listings.
This commit is contained in:
pacnpal
2026-01-12 19:13:05 -05:00
parent 2b66814d82
commit d631f3183c
56 changed files with 5860 additions and 264 deletions

View File

@@ -511,6 +511,99 @@ class MFALoginVerifyAPIView(APIView):
return {"success": False, "error": "Passkey verification failed"}
@extend_schema_view(
post=extend_schema(
summary="Exchange session for JWT tokens",
description="Exchange allauth session_token (from passkey login) for JWT tokens.",
responses={
200: LoginOutputSerializer,
401: "Not authenticated",
},
tags=["Authentication"],
),
)
class SessionToTokenAPIView(APIView):
"""
API endpoint to exchange allauth session_token for JWT tokens.
Used after allauth headless passkey login to get JWT tokens for the frontend.
The allauth passkey login returns a session_token, and this endpoint
validates it and exchanges it for JWT tokens.
"""
# Allow unauthenticated - we validate the allauth session_token ourselves
permission_classes = [AllowAny]
authentication_classes = []
def post(self, request: Request) -> Response:
# Get the allauth session_token from header or body
session_token = request.headers.get('X-Session-Token') or request.data.get('session_token')
if not session_token:
return Response(
{"detail": "Session token required. Provide X-Session-Token header or session_token in body."},
status=status.HTTP_400_BAD_REQUEST,
)
# Validate the session_token with allauth's session store
try:
from allauth.headless.tokens.strategies.sessions import SessionTokenStrategy
strategy = SessionTokenStrategy()
session_data = strategy.lookup_session(session_token)
if not session_data:
return Response(
{"detail": "Invalid or expired session token."},
status=status.HTTP_401_UNAUTHORIZED,
)
# Get user from the session
user_id = session_data.get('_auth_user_id')
if not user_id:
return Response(
{"detail": "No user found in session."},
status=status.HTTP_401_UNAUTHORIZED,
)
user = UserModel.objects.get(pk=user_id)
except (ImportError, Exception) as e:
logger.error(f"Failed to validate allauth session token: {e}")
return Response(
{"detail": "Failed to validate session token."},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
# Generate JWT tokens with passkey auth method
from .jwt import create_tokens_for_user
tokens = create_tokens_for_user(
user,
auth_method="passkey",
mfa_verified=True, # Passkey is considered MFA
provider_mfa=False,
)
# Log successful session-to-token exchange
from apps.accounts.services.security_service import log_security_event
log_security_event(
"session_to_token",
request,
user=user,
metadata={"auth_method": "passkey"},
)
response_serializer = LoginOutputSerializer(
{
"access": tokens["access"],
"refresh": tokens["refresh"],
"user": user,
"message": "Token exchange successful",
}
)
return Response(response_serializer.data)
@extend_schema_view(
post=extend_schema(
summary="User registration",