api.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. import logging
  2. from typing import Optional
  3. import orjson
  4. from allauth.socialaccount.models import SocialApp
  5. from allauth.socialaccount.providers.openid_connect.views import (
  6. OpenIDConnectOAuth2Adapter,
  7. )
  8. from asgiref.sync import sync_to_async
  9. from django.conf import settings
  10. from django.contrib.auth import aget_user
  11. from django.http import HttpRequest
  12. from ninja import Field, ModelSchema, NinjaAPI, Schema
  13. from ninja.errors import ValidationError
  14. from sentry_sdk import capture_exception, set_context, set_level
  15. from apps.alerts.api import router as alerts_router
  16. from apps.api_tokens.api import router as api_tokens_router
  17. from apps.api_tokens.models import APIToken
  18. from apps.api_tokens.schema import APITokenSchema
  19. from apps.difs.api import router as difs_router
  20. from apps.environments.api import router as environments_router
  21. from apps.event_ingest.api import router as event_ingest_router
  22. from apps.event_ingest.embed_api import router as embed_router
  23. from apps.files.api import router as files_router
  24. from apps.importer.api import router as importer_router
  25. from apps.issue_events.api import router as issue_events_router
  26. from apps.observability.api import router as observability_router
  27. from apps.organizations_ext.api import router as organizations_ext_router
  28. from apps.performance.api import router as performance_router
  29. from apps.projects.api import router as projects_router
  30. from apps.releases.api import router as releases_router
  31. from apps.sourcecode.api import router as sourcecode_router
  32. from apps.stats.api import router as stats_router
  33. from apps.teams.api import router as teams_router
  34. from apps.uptime.api import router as uptime_router
  35. from apps.users.api import router as users_router
  36. from apps.users.models import User
  37. from apps.users.schema import UserSchema
  38. from apps.users.utils import ais_user_registration_open
  39. from apps.wizard.api import router as wizard_router
  40. from glitchtip.constants import SOCIAL_ADAPTER_MAP
  41. from ..schema import CamelSchema
  42. from .authentication import SessionAuth, TokenAuth
  43. from .exceptions import ThrottleException
  44. from .parsers import EnvelopeParser
  45. try:
  46. from djstripe.settings import djstripe_settings
  47. except ImportError:
  48. pass
  49. logger = logging.getLogger(__name__)
  50. api = NinjaAPI(
  51. parser=EnvelopeParser(),
  52. title="GlitchTip API",
  53. urls_namespace="api",
  54. auth=[TokenAuth(), SessionAuth()],
  55. )
  56. api.add_router("0", api_tokens_router)
  57. api.add_router("", event_ingest_router)
  58. api.add_router("0", alerts_router)
  59. api.add_router("0", difs_router)
  60. api.add_router("0", environments_router)
  61. api.add_router("0", files_router)
  62. api.add_router("0", importer_router)
  63. api.add_router("0", issue_events_router)
  64. api.add_router("0", observability_router)
  65. api.add_router("0", organizations_ext_router)
  66. api.add_router("0", performance_router)
  67. api.add_router("0", projects_router)
  68. api.add_router("0", stats_router)
  69. api.add_router("0", sourcecode_router)
  70. api.add_router("0", teams_router)
  71. api.add_router("0", uptime_router)
  72. api.add_router("0", users_router)
  73. api.add_router("0", wizard_router)
  74. api.add_router("0", releases_router)
  75. api.add_router("embed", embed_router)
  76. if settings.BILLING_ENABLED:
  77. from apps.djstripe_ext.api import router as djstripe_ext_router
  78. api.add_router("0", djstripe_ext_router)
  79. # Would be better at the router level
  80. # https://github.com/vitalik/django-ninja/issues/442
  81. @api.exception_handler(ValidationError)
  82. def log_validation(request, exc):
  83. if request.resolver_match.route == "api/<project_id>/envelope/":
  84. set_level("warning")
  85. set_context(
  86. "incoming event", [orjson.loads(line) for line in request.body.splitlines()]
  87. )
  88. capture_exception(exc)
  89. logger.warning(f"Validation error on {request.path}", exc_info=exc)
  90. return api.create_response(request, {"detail": exc.errors}, status=422)
  91. @api.exception_handler(ThrottleException)
  92. def throttled(request: HttpRequest, exc: ThrottleException):
  93. response = api.create_response(
  94. request,
  95. {"message": "Please retry later"},
  96. status=429,
  97. )
  98. if retry_after := exc.retry_after:
  99. if isinstance(retry_after, int):
  100. response["Retry-After"] = retry_after
  101. else:
  102. response["Retry-After"] = retry_after.strftime("%a, %d %b %Y %H:%M:%S GMT")
  103. return response
  104. class SocialAppSchema(ModelSchema):
  105. scopes: list[str]
  106. authorize_url: Optional[str]
  107. class Config:
  108. model = SocialApp
  109. model_fields = ["name", "client_id", "provider"]
  110. class SettingsOut(CamelSchema):
  111. social_apps: list[SocialAppSchema]
  112. billing_enabled: bool
  113. i_paid_for_glitchtip: bool = Field(alias="iPaidForGlitchTip")
  114. enable_user_registration: bool
  115. enable_organization_creation: bool
  116. stripe_public_key: Optional[str]
  117. plausible_url: Optional[str]
  118. plausible_domain: Optional[str]
  119. chatwoot_website_token: Optional[str]
  120. sentryDSN: Optional[str]
  121. sentry_traces_sample_rate: Optional[float]
  122. environment: Optional[str]
  123. version: str
  124. server_time_zone: str
  125. use_new_social_callbacks: bool
  126. @api.get("settings/", response=SettingsOut, by_alias=True, auth=None)
  127. async def get_settings(request: HttpRequest):
  128. social_apps: list[SocialApp] = []
  129. async for social_app in SocialApp.objects.order_by("name"):
  130. provider = social_app.get_provider(request)
  131. social_app.scopes = provider.get_scope()
  132. adapter_cls = SOCIAL_ADAPTER_MAP.get(social_app.provider)
  133. if adapter_cls == OpenIDConnectOAuth2Adapter:
  134. adapter = adapter_cls(request, social_app.provider_id)
  135. elif adapter_cls:
  136. adapter = adapter_cls(request)
  137. else:
  138. adapter = None
  139. if adapter:
  140. social_app.authorize_url = await sync_to_async(
  141. lambda: adapter.authorize_url
  142. )()
  143. social_app.provider = social_app.provider_id or social_app.provider
  144. social_apps.append(social_app)
  145. billing_enabled = settings.BILLING_ENABLED
  146. return {
  147. "social_apps": social_apps,
  148. "billing_enabled": billing_enabled,
  149. "i_paid_for_glitchtip": settings.I_PAID_FOR_GLITCHTIP,
  150. "enable_user_registration": await ais_user_registration_open(),
  151. "enable_organization_creation": settings.ENABLE_ORGANIZATION_CREATION,
  152. "stripe_public_key": djstripe_settings.STRIPE_PUBLIC_KEY
  153. if billing_enabled
  154. else None,
  155. "plausible_url": settings.PLAUSIBLE_URL,
  156. "plausible_domain": settings.PLAUSIBLE_DOMAIN,
  157. "chatwoot_website_token": settings.CHATWOOT_WEBSITE_TOKEN,
  158. "sentryDSN": settings.SENTRY_FRONTEND_DSN,
  159. "sentry_traces_sample_rate": settings.SENTRY_TRACES_SAMPLE_RATE,
  160. "environment": settings.ENVIRONMENT,
  161. "version": settings.GLITCHTIP_VERSION,
  162. "server_time_zone": settings.TIME_ZONE,
  163. "use_new_social_callbacks": settings.USE_NEW_SOCIAL_CALLBACKS,
  164. }
  165. class APIRootSchema(Schema):
  166. version: str
  167. user: Optional[UserSchema]
  168. auth: Optional[APITokenSchema]
  169. @api.get("0/", auth=None, response=APIRootSchema, by_alias=True)
  170. async def api_root(request: HttpRequest):
  171. """/api/0/ gives information about the server and current user"""
  172. user_data = None
  173. auth_data = None
  174. user = await aget_user(request)
  175. if user.is_authenticated:
  176. user_data = await User.objects.prefetch_related("socialaccount_set").aget(
  177. id=user.id
  178. )
  179. # Fetch api auth header to get api token
  180. openapi_scheme = "bearer"
  181. header = "Authorization"
  182. headers = request.headers
  183. auth_value = headers.get(header)
  184. if auth_value:
  185. parts = auth_value.split(" ")
  186. if len(parts) >= 2 and parts[0].lower() == openapi_scheme:
  187. token = " ".join(parts[1:])
  188. api_token = await APIToken.objects.filter(
  189. token=token, user__is_active=True
  190. ).afirst()
  191. if api_token:
  192. auth_data = api_token
  193. user_data = await User.objects.prefetch_related(
  194. "socialaccount_set"
  195. ).aget(id=api_token.user_id)
  196. return {
  197. "version": "0",
  198. "user": user_data,
  199. "auth": auth_data,
  200. }