withDomainRequired.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import {RouteComponent, RouteComponentProps} from 'react-router';
  2. import {Location, LocationDescriptor} from 'history';
  3. import trimEnd from 'lodash/trimEnd';
  4. import trimStart from 'lodash/trimStart';
  5. // If you change this also update the patterns in sentry.api.utils
  6. const NORMALIZE_PATTERNS: Array<[pattern: RegExp, replacement: string]> = [
  7. // /organizations/slug/section, but not /organizations/new
  8. [/\/organizations\/(?!new)[^\/]+\/(.*)/, '/$1'],
  9. // For /settings/:orgId/ -> /settings/organization/
  10. [/\/settings\/(?!account)(?!projects)(?!teams)[^\/]+\/?$/, '/settings/organization/'],
  11. // Move /settings/:orgId/:section -> /settings/:section
  12. // but not /settings/organization or /settings/projects which is a new URL
  13. [/^\/?settings\/(?!account)(?!projects)(?!teams)[^\/]+\/(.*)/, '/settings/$1'],
  14. [/^\/?join-request\/[^\/]+\/?.*/, '/join-request/'],
  15. [/^\/?onboarding\/[^\/]+\/(.*)/, '/onboarding/$1'],
  16. // Handles /org-slug/project-slug/getting-started/platform/ -> /getting-started/project-slug/platform/
  17. [/^\/?(?!settings)[^\/]+\/([^\/]+)\/getting-started\/(.*)/, '/getting-started/$1/$2'],
  18. ];
  19. type LocationTarget = ((location: Location) => LocationDescriptor) | LocationDescriptor;
  20. type NormalizeUrlOptions = {
  21. forceCustomerDomain: boolean;
  22. };
  23. /**
  24. * Normalize a URL for customer domains based on the organization that was
  25. * present in the initial page load.
  26. */
  27. export function normalizeUrl(path: string, options?: NormalizeUrlOptions): string;
  28. export function normalizeUrl(
  29. path: LocationDescriptor,
  30. options?: NormalizeUrlOptions
  31. ): LocationDescriptor;
  32. export function normalizeUrl(
  33. path: LocationTarget,
  34. location?: Location,
  35. options?: NormalizeUrlOptions
  36. ): LocationTarget;
  37. export function normalizeUrl(
  38. path: LocationTarget,
  39. location?: Location | NormalizeUrlOptions,
  40. options?: NormalizeUrlOptions
  41. ): LocationTarget {
  42. if (location && 'forceCustomerDomain' in location) {
  43. options = location;
  44. location = undefined;
  45. }
  46. if (!options?.forceCustomerDomain && !window.__initialData?.customerDomain) {
  47. return path;
  48. }
  49. let resolved: LocationDescriptor;
  50. if (typeof path === 'function') {
  51. if (!location) {
  52. throw new Error('Cannot resolve function URL without a location');
  53. }
  54. resolved = path(location);
  55. } else {
  56. resolved = path;
  57. }
  58. if (typeof resolved === 'string') {
  59. for (const patternData of NORMALIZE_PATTERNS) {
  60. resolved = resolved.replace(patternData[0], patternData[1]);
  61. if (resolved !== path) {
  62. return resolved;
  63. }
  64. }
  65. return resolved;
  66. }
  67. if (!resolved.pathname) {
  68. return resolved;
  69. }
  70. for (const patternData of NORMALIZE_PATTERNS) {
  71. const replacement = resolved.pathname.replace(patternData[0], patternData[1]);
  72. if (replacement !== resolved.pathname) {
  73. return {...resolved, pathname: replacement};
  74. }
  75. }
  76. return resolved;
  77. }
  78. /**
  79. * withDomainRequired is a higher-order component (HOC) meant to be used with <Route /> components within
  80. * static/app/routes.tsx whose route paths do not contain the :orgId parameter.
  81. * For example:
  82. * <Route
  83. * path="/issues/(searches/:searchId/)"
  84. * component={withDomainRequired(errorHandler(IssueListContainer))}
  85. * / >
  86. *
  87. * withDomainRequired ensures that the route path is only accessed whenever a customer domain is used.
  88. * For example: orgslug.sentry.io
  89. *
  90. * The side-effect that this HOC provides is that it'll redirect the browser to sentryUrl (from window.__initialData.links)
  91. * whenever one of the following conditions are not satisfied:
  92. * - window.__initialData.customerDomain is present.
  93. * - window.__initialData.features contains organizations:customer-domains feature.
  94. *
  95. * If both conditions above are satisfied, then WrappedComponent will be rendered with orgId included in the route
  96. * params prop.
  97. *
  98. * Whenever https://orgslug.sentry.io/ is accessed in the browser, then both conditions above will be satisfied.
  99. */
  100. function withDomainRequired<P extends RouteComponentProps<{}, {}>>(
  101. WrappedComponent: RouteComponent
  102. ) {
  103. return function withDomainRequiredWrapper(props: P) {
  104. const {params} = props;
  105. const {features, customerDomain} = window.__initialData;
  106. const {sentryUrl} = window.__initialData.links;
  107. const hasCustomerDomain = (features as unknown as string[]).includes(
  108. 'organizations:customer-domains'
  109. );
  110. if (!customerDomain || !hasCustomerDomain) {
  111. // This route should only be accessed if a customer domain is used.
  112. // We redirect the user to the sentryUrl.
  113. const redirectPath = `${window.location.pathname}${window.location.search}${window.location.hash}`;
  114. const redirectURL = `${trimEnd(sentryUrl, '/')}/${trimStart(redirectPath, '/')}`;
  115. window.location.replace(redirectURL);
  116. return null;
  117. }
  118. const newParams = {
  119. ...params,
  120. orgId: customerDomain.subdomain,
  121. };
  122. return <WrappedComponent {...props} params={newParams} />;
  123. };
  124. }
  125. export default withDomainRequired;