feat: add public profiles list endpoint with search and pagination

- Add new /profiles/ endpoint for listing user profiles
- Support search by username/display name with ordering options
- Include pagination with configurable page size (max 100)
- Add comprehensive OpenAPI schema documentation
- Refactor passkey authentication state management in MFA flow
- Update URL routing and imports for new list_profiles view

This enables user discovery, leaderboards, and friend-finding features
with a publicly accessible, well-documented API endpoint.
This commit is contained in:
pacnpal
2026-01-10 13:00:02 -05:00
parent 22ff0d1c49
commit 692c0bbbbf
9 changed files with 424 additions and 45 deletions

View File

@@ -96,10 +96,10 @@ def get_registration_options(request):
from allauth.mfa.webauthn.internal import auth as webauthn_auth
# Use the correct allauth API: begin_registration
creation_options, state = webauthn_auth.begin_registration(request)
# The function takes (user, passwordless) - passwordless=False for standard passkeys
creation_options = webauthn_auth.begin_registration(request.user, passwordless=False)
# Store state in session for verification
webauthn_auth.set_state(request, state)
# State is stored internally by begin_registration via set_state()
return Response({
"options": creation_options,
@@ -154,8 +154,8 @@ def register_passkey(request):
status=status.HTTP_400_BAD_REQUEST,
)
# Get stored state from session
state = webauthn_auth.get_state(request)
# Get stored state from session (no request needed, uses context)
state = webauthn_auth.get_state()
if not state:
return Response(
{"detail": "No pending registration. Please start registration again."},
@@ -164,19 +164,24 @@ def register_passkey(request):
# Use the correct allauth API: complete_registration
try:
from allauth.mfa.models import Authenticator
# Parse the credential response
credential_data = webauthn_auth.parse_registration_response(credential)
# Complete registration - this creates the Authenticator
authenticator = webauthn_auth.complete_registration(
request,
credential_data,
state,
name=name,
)
# Complete registration - returns AuthenticatorData (binding)
authenticator_data = webauthn_auth.complete_registration(credential_data)
# Clear session state
webauthn_auth.clear_state(request)
# 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,
},
)
# State is cleared internally by complete_registration
return Response({
"detail": "Passkey registered successfully",
@@ -225,10 +230,8 @@ def get_authentication_options(request):
from allauth.mfa.webauthn.internal import auth as webauthn_auth
# Use the correct allauth API: begin_authentication
request_options, state = webauthn_auth.begin_authentication(request)
# Store state in session for verification
webauthn_auth.set_state(request, state)
# Takes optional user, returns just options (state is stored internally)
request_options = webauthn_auth.begin_authentication(request.user)
return Response({
"options": request_options,
@@ -281,8 +284,8 @@ def authenticate_passkey(request):
status=status.HTTP_400_BAD_REQUEST,
)
# Get stored state from session
state = webauthn_auth.get_state(request)
# Get stored state from session (no request needed, uses context)
state = webauthn_auth.get_state()
if not state:
return Response(
{"detail": "No pending authentication. Please start authentication again."},
@@ -291,14 +294,9 @@ def authenticate_passkey(request):
# Use the correct allauth API: complete_authentication
try:
# Parse the credential response
credential_data = webauthn_auth.parse_authentication_response(credential)
# Complete authentication
webauthn_auth.complete_authentication(request, credential_data, state)
# Clear session state
webauthn_auth.clear_state(request)
# Complete authentication - takes user and credential response
# State is handled internally
webauthn_auth.complete_authentication(request.user, credential)
return Response({"success": True})
except Exception as e:
@@ -514,9 +512,13 @@ def get_login_passkey_options(request):
request.user = user
try:
request_options, state = webauthn_auth.begin_authentication(request)
# begin_authentication takes just user, returns options (state stored internally)
request_options = webauthn_auth.begin_authentication(user)
# Note: State is managed by allauth's session context, but for MFA login flow
# we need to track user separately since they're not authenticated yet
passkey_state_key = f"mfa_passkey_state:{mfa_token}"
cache.set(passkey_state_key, state, timeout=300)
# Store a reference that this user has a pending passkey auth
cache.set(passkey_state_key, {"user_id": user_id}, timeout=300)
return Response({"options": request_options})
finally:
if original_user is not None:

View File

@@ -417,23 +417,23 @@ class MFALoginVerifyAPIView(APIView):
return {"success": False, "error": "No passkey registered for this user"}
try:
# Parse the authentication response
credential_data = webauthn_auth.parse_authentication_response(credential)
# Get or create authentication state
# For login flow, we need to set up the state first
state = webauthn_auth.get_state(request)
# For MFA login flow, we need to set up state first if not present
# Note: allauth's begin_authentication stores state internally
state = webauthn_auth.get_state()
if not state:
# If no state, generate one for this user
_, state = webauthn_auth.begin_authentication(request)
webauthn_auth.set_state(request, state)
# Need to temporarily set request.user for allauth context
original_user = getattr(request, "user", None)
request.user = user
try:
webauthn_auth.begin_authentication(user)
finally:
if original_user is not None:
request.user = original_user
# Complete authentication
webauthn_auth.complete_authentication(request, credential_data, state)
# Clear the state
webauthn_auth.clear_state(request)
# Complete authentication - takes user and credential dict
# State is managed internally by allauth
webauthn_auth.complete_authentication(user, credential)
return {"success": True}