withDomainRequired.tsx 4.3 KB

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