Coverage for apps / core / auth_api.py: 85%

62 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-02 13:22 +0000

1"""Authentication API endpoints — shared endpoints for passkey mode.""" 

2 

3import logging 

4 

5from django.conf import settings 

6from django.contrib.auth import logout 

7from django.http import Http404 

8from ninja import Router, Status 

9 

10from apps.core.auth import SessionAuth 

11from apps.core.auth_helpers import passkey_user_profile_response 

12from apps.profiles.deletion import ( 

13 collect_remix_image_paths, 

14 get_deletion_preview, 

15 remove_remix_image_files, 

16) 

17from apps.profiles.models import Profile 

18 

19security_logger = logging.getLogger("security") 

20 

21router = Router(tags=["auth"]) 

22 

23 

24def _require_auth_mode(request): 

25 """Raise 404 if not in passkey mode (the only mode with user accounts).""" 

26 if settings.AUTH_MODE != "passkey": 

27 raise Http404 

28 

29 

30# --- Endpoints --- 

31 

32 

33@router.post("/logout/", response={200: dict}, auth=SessionAuth()) 

34def logout_view(request): 

35 _require_auth_mode(request) 

36 username = getattr(request, "user", None) 

37 username = username.username if username and hasattr(username, "username") else "unknown" 

38 logout(request) 

39 request.session.flush() 

40 security_logger.info("Logout: user=%s", username) 

41 return {"message": "Logged out successfully"} 

42 

43 

44@router.get("/me/", response={200: dict, 401: dict}, auth=SessionAuth()) 

45def get_me(request): 

46 _require_auth_mode(request) 

47 user = request.user 

48 if not user or not getattr(user, "is_authenticated", False): 

49 return Status(401, {"error": "Authentication required"}) 

50 

51 try: 

52 profile = user.profile 

53 except Profile.DoesNotExist: 

54 return Status(401, {"error": "Authentication required"}) 

55 

56 return Status(200, passkey_user_profile_response(user, profile)) 

57 

58 

59@router.get( 

60 "/me/deletion-preview/", 

61 response={200: dict, 401: dict}, 

62 auth=SessionAuth(), 

63) 

64def get_me_deletion_preview(request): 

65 """Preview the data that will be removed if the caller deletes their 

66 own account. Passkey-mode counterpart of 

67 `GET /api/profiles/{id}/deletion-preview/` (which is HomeOnly).""" 

68 _require_auth_mode(request) 

69 user = request.user 

70 if not user or not getattr(user, "is_authenticated", False): 

71 return Status(401, {"error": "Authentication required"}) 

72 try: 

73 profile = user.profile 

74 except Profile.DoesNotExist: 

75 return Status(401, {"error": "Authentication required"}) 

76 return Status(200, get_deletion_preview(profile)) 

77 

78 

79@router.delete( 

80 "/me/", 

81 response={204: None, 401: dict}, 

82 auth=SessionAuth(), 

83) 

84def delete_me(request): 

85 """Delete the caller's own account and all associated data. Passkey-mode 

86 counterpart of `DELETE /api/profiles/{id}/` (which is HomeOnly). 

87 

88 Deletes, in order: 

89 1. Snapshots remix image paths from the DB. 

90 2. Deletes the User, which CASCADE-drops the Profile and all its 

91 associated Recipe/Collection/Favorite/etc. rows. 

92 3. Best-effort removes remix image files from media storage. 

93 4. Flushes the session so the now-invalid cookie is 401 on replay. 

94 

95 Step ordering is deliberate: DB first (atomic), media cleanup after 

96 (best-effort). A mid-delete crash leaves orphan image files — fine — 

97 rather than orphan DB rows. 

98 """ 

99 _require_auth_mode(request) 

100 user = request.user 

101 if not user or not getattr(user, "is_authenticated", False): 

102 return Status(401, {"error": "Authentication required"}) 

103 try: 

104 profile = user.profile 

105 except Profile.DoesNotExist: 

106 return Status(401, {"error": "Authentication required"}) 

107 

108 username = user.username 

109 image_paths = collect_remix_image_paths(profile) 

110 # Deleting the User CASCADEs the Profile via its OneToOne FK. 

111 user.delete() 

112 remove_remix_image_files(image_paths) 

113 request.session.flush() 

114 security_logger.info("Self-delete: user=%s", username) 

115 return Status(204, None) 

← Back to Dashboard