api.py 6.6 KB

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