api.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. from allauth.account.models import EmailAddress
  2. from allauth.mfa.models import Authenticator
  3. from allauth.mfa.recovery_codes.internal.auth import RecoveryCodes
  4. from asgiref.sync import sync_to_async
  5. from django.core.cache import cache
  6. from django.db.utils import IntegrityError
  7. from django.http import Http404, HttpResponse
  8. from django.shortcuts import aget_object_or_404
  9. from ninja import Router
  10. from ninja.errors import HttpError
  11. from ninja.pagination import paginate
  12. from apps.shared.types import MeID
  13. from glitchtip.api.authentication import AuthHttpRequest
  14. from .models import User
  15. from .schema import (
  16. EmailAddressIn,
  17. EmailAddressSchema,
  18. RecoveryCodeSchema,
  19. RecoveryCodesSchema,
  20. UserDetailSchema,
  21. UserIn,
  22. UserNotificationsSchema,
  23. UserSchema,
  24. )
  25. router = Router()
  26. """
  27. Sentry OSS does not document any of these, but they exist
  28. GET /users/
  29. GET /users/<me_id>/
  30. DELETE /users/<me_id>/
  31. PUT /users/<me_id>/
  32. GET /organizations/burke-software/users/ (Not implemented)
  33. GET /users/<me_id>/emails/
  34. POST /users/<me_id>/emails/
  35. PUT /users/<me_id>/emails/ (Set as primary)
  36. DELETE /users/<me_id>/emails/
  37. GET /users/<me_id>/notifications/
  38. PUT /users/<me_id>/notifications/
  39. """
  40. def generate_user_seed_key(user_id: int):
  41. return f"seed{user_id}"
  42. def get_user_queryset(user_id: int, add_details=False):
  43. qs = User.objects.filter(id=user_id)
  44. if add_details:
  45. qs = qs.prefetch_related("socialaccount_set")
  46. return qs
  47. def get_email_queryset(user_id: int, verified: bool = False):
  48. qs = EmailAddress.objects.filter(user_id=user_id)
  49. if verified:
  50. qs = qs.filter(verified=verified)
  51. return qs
  52. @router.get("/users/", response=list[UserSchema], by_alias=True)
  53. @paginate
  54. async def list_users(request: AuthHttpRequest, response: HttpResponse):
  55. """
  56. Exists in Sentry OSS, unsure what the use case is
  57. We make it only list the current user
  58. """
  59. return get_user_queryset(user_id=request.auth.user_id, add_details=True)
  60. @router.get("/users/{slug:user_id}/", response=UserDetailSchema, by_alias=True)
  61. async def get_user(request: AuthHttpRequest, user_id: MeID):
  62. user_id = request.auth.user_id
  63. return await aget_object_or_404(get_user_queryset(user_id, add_details=True))
  64. @router.delete("/users/{slug:user_id}/", response={204: None})
  65. async def delete_user(request: AuthHttpRequest, user_id: MeID):
  66. # Can only delete self
  67. if user_id != request.auth.user_id and user_id != "me":
  68. raise Http404
  69. user_id = request.auth.user_id
  70. queryset = get_user_queryset(user_id=user_id)
  71. result, _ = await queryset.filter(
  72. organizations_ext_organizationuser__organizationowner__isnull=True
  73. ).adelete()
  74. if result:
  75. return 204, None
  76. if await queryset.aexists():
  77. raise HttpError(
  78. 400,
  79. "User is organization owner. Delete organization or transfer ownership first.",
  80. )
  81. raise Http404
  82. @router.put(
  83. "/users/{slug:user_id}/",
  84. response=UserSchema,
  85. by_alias=True,
  86. )
  87. async def update_user(request: AuthHttpRequest, user_id: MeID, payload: UserIn):
  88. if user_id != request.auth.user_id and user_id != "me":
  89. raise Http404
  90. user_id = request.auth.user_id
  91. user = await aget_object_or_404(get_user_queryset(user_id, add_details=True))
  92. for attr, value in payload.dict().items():
  93. setattr(user, attr, value)
  94. await user.asave()
  95. return user
  96. @router.get(
  97. "/users/{slug:user_id}/emails/", response=list[EmailAddressSchema], by_alias=True
  98. )
  99. async def list_emails(request: AuthHttpRequest, user_id: MeID):
  100. if user_id != request.auth.user_id and user_id != "me":
  101. raise Http404
  102. user_id = request.auth.user_id
  103. # No pagination, thus sanity check limit
  104. return [email async for email in get_email_queryset(user_id=user_id)[:200]]
  105. @router.post(
  106. "/users/{slug:user_id}/emails/", response={201: EmailAddressSchema}, by_alias=True
  107. )
  108. async def create_email(
  109. request: AuthHttpRequest, user_id: MeID, payload: EmailAddressIn
  110. ):
  111. """
  112. Create a new unverified email address. Will return 400 if the email already exists
  113. and is verified.
  114. """
  115. if user_id != request.auth.user_id and user_id != "me":
  116. raise Http404
  117. user_id = request.auth.user_id
  118. if await EmailAddress.objects.filter(email=payload.email, verified=True).aexists():
  119. raise HttpError(
  120. 400,
  121. "Email already exists",
  122. )
  123. try:
  124. email_address = await EmailAddress.objects.acreate(
  125. email=payload.email, user_id=user_id
  126. )
  127. except IntegrityError:
  128. raise HttpError(
  129. 400,
  130. "Email already exists",
  131. )
  132. await sync_to_async(email_address.send_confirmation)(request, signup=False)
  133. return 201, email_address
  134. @router.put("/users/{slug:user_id}/emails/", response=EmailAddressSchema, by_alias=True)
  135. async def set_email_as_primary(
  136. request: AuthHttpRequest, user_id: MeID, payload: EmailAddressIn
  137. ):
  138. if user_id != request.auth.user_id and user_id != "me":
  139. raise Http404
  140. user_id = request.auth.user_id
  141. queryset = get_email_queryset(user_id)
  142. email_address = await aget_object_or_404(
  143. queryset, verified=True, email=payload.email
  144. )
  145. await queryset.aupdate(primary=False)
  146. email_address.primary = True
  147. await email_address.asave(update_fields=["primary"])
  148. return email_address
  149. @router.delete("/users/{slug:user_id}/emails/", response={204: None})
  150. async def delete_email(
  151. request: AuthHttpRequest, user_id: MeID, payload: EmailAddressIn
  152. ):
  153. if user_id != request.auth.user_id and user_id != "me":
  154. raise Http404
  155. user_id = request.auth.user_id
  156. queryset = get_email_queryset(user_id)
  157. result, _ = await queryset.filter(email=payload.email, primary=False).adelete()
  158. if result:
  159. return 204, None
  160. raise Http404
  161. @router.post("/users/{slug:user_id}/emails/confirm/", response={204: None})
  162. async def send_confirm_email(
  163. request: AuthHttpRequest, user_id: MeID, payload: EmailAddressIn
  164. ):
  165. user_id = request.auth.user_id
  166. email_address = await aget_object_or_404(
  167. get_email_queryset(user_id, verified=False), email=payload.email
  168. )
  169. await sync_to_async(email_address.send_confirmation)(request)
  170. return 204, None
  171. @router.get(
  172. "/users/{slug:user_id}/notifications/",
  173. response=UserNotificationsSchema,
  174. by_alias=True,
  175. )
  176. async def get_notifications(request: AuthHttpRequest, user_id: MeID):
  177. user_id = request.auth.user_id
  178. return await aget_object_or_404(get_user_queryset(user_id))
  179. @router.put(
  180. "/users/{slug:user_id}/notifications/",
  181. response=UserNotificationsSchema,
  182. by_alias=True,
  183. )
  184. async def update_notifications(
  185. request: AuthHttpRequest, user_id: MeID, payload: UserNotificationsSchema
  186. ):
  187. user_id = request.auth.user_id
  188. user = await aget_object_or_404(get_user_queryset(user_id))
  189. for attr, value in payload.dict().items():
  190. setattr(user, attr, value)
  191. await user.asave()
  192. return user
  193. @router.get("/generate-recovery-codes/", response=RecoveryCodesSchema)
  194. async def generate_recovery_codes(request: AuthHttpRequest):
  195. """
  196. Extension of django-allauth headless API to pre-generate recovery codes before saving
  197. """
  198. authenticator = Authenticator(data={"seed": RecoveryCodes.generate_seed()})
  199. codes = RecoveryCodes(authenticator).generate_codes()
  200. cache.set(generate_user_seed_key(request.auth.user_id), authenticator.data["seed"])
  201. return {
  202. "codes": codes,
  203. }
  204. @router.post("/generate-recovery-codes/", response={204: None})
  205. async def set_recovery_codes(request: AuthHttpRequest, payload: RecoveryCodeSchema):
  206. """
  207. Extension of django-allauth headless API to set recovery codes
  208. """
  209. user_id = request.auth.user_id
  210. seed = cache.get(generate_user_seed_key(user_id))
  211. if not seed:
  212. raise HttpError(400, "No recovery codes set, use GET first")
  213. authenticator = Authenticator(
  214. type=Authenticator.Type.RECOVERY_CODES,
  215. user_id=user_id,
  216. data={"seed": seed, "used_mask": 0},
  217. )
  218. for code in RecoveryCodes(authenticator).generate_codes():
  219. if code == payload.code:
  220. await Authenticator.objects.filter(
  221. type=Authenticator.Type.RECOVERY_CODES, user_id=user_id
  222. ).adelete()
  223. await authenticator.asave()
  224. return 204, None
  225. raise HttpError(400, "Invalid code")