acceptOrganizationInvite.tsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. import React, {MouseEvent} from 'react';
  2. import {browserHistory, RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {urlEncode} from '@sentry/utils';
  5. import {logout} from 'app/actionCreators/account';
  6. import Alert from 'app/components/alert';
  7. import Button from 'app/components/button';
  8. import ExternalLink from 'app/components/links/externalLink';
  9. import Link from 'app/components/links/link';
  10. import NarrowLayout from 'app/components/narrowLayout';
  11. import {t, tct} from 'app/locale';
  12. import ConfigStore from 'app/stores/configStore';
  13. import space from 'app/styles/space';
  14. import AsyncView from 'app/views/asyncView';
  15. import SettingsPageHeader from 'app/views/settings/components/settingsPageHeader';
  16. type InviteDetails = {
  17. orgSlug: string;
  18. needsAuthentication: boolean;
  19. needs2fa: boolean;
  20. needsSso: boolean;
  21. requireSso: boolean;
  22. existingMember: boolean;
  23. ssoProvider?: string;
  24. };
  25. type Props = RouteComponentProps<{memberId: string; token: string}, {}>;
  26. type State = AsyncView['state'] & {
  27. inviteDetails: InviteDetails;
  28. accepting: boolean | undefined;
  29. acceptError: boolean | undefined;
  30. };
  31. class AcceptOrganizationInvite extends AsyncView<Props, State> {
  32. getEndpoints(): ReturnType<AsyncView['getEndpoints']> {
  33. const {memberId, token} = this.props.params;
  34. return [['inviteDetails', `/accept-invite/${memberId}/${token}/`]];
  35. }
  36. getTitle() {
  37. return t('Accept Organization Invite');
  38. }
  39. makeNextUrl(path: string) {
  40. return `${path}?${urlEncode({next: window.location.pathname})}`;
  41. }
  42. handleLogout = async (e: MouseEvent) => {
  43. e.preventDefault();
  44. await logout(this.api);
  45. window.location.replace(this.makeNextUrl('/auth/login/'));
  46. };
  47. handleAcceptInvite = async () => {
  48. const {memberId, token} = this.props.params;
  49. this.setState({accepting: true});
  50. try {
  51. await this.api.requestPromise(`/accept-invite/${memberId}/${token}/`, {
  52. method: 'POST',
  53. });
  54. browserHistory.replace(`/${this.state.inviteDetails.orgSlug}/`);
  55. } catch {
  56. this.setState({acceptError: true});
  57. }
  58. this.setState({accepting: false});
  59. };
  60. get existingMemberAlert() {
  61. const user = ConfigStore.get('user');
  62. return (
  63. <Alert type="warning" data-test-id="existing-member">
  64. {tct(
  65. 'Your account ([email]) is already a member of this organization. [switchLink:Switch accounts]?',
  66. {
  67. email: user.email,
  68. switchLink: (
  69. <Link
  70. to=""
  71. data-test-id="existing-member-link"
  72. onClick={this.handleLogout}
  73. />
  74. ),
  75. }
  76. )}
  77. </Alert>
  78. );
  79. }
  80. get authenticationActions() {
  81. const {inviteDetails} = this.state;
  82. return (
  83. <React.Fragment>
  84. {!inviteDetails.requireSso && (
  85. <p data-test-id="action-info-general">
  86. {t(
  87. `To continue, you must either create a new account, or login to an
  88. existing Sentry account.`
  89. )}
  90. </p>
  91. )}
  92. {inviteDetails.needsSso && (
  93. <p data-test-id="action-info-sso">
  94. {inviteDetails.requireSso
  95. ? tct(
  96. `Note that [orgSlug] has required Single Sign-On (SSO) using
  97. [authProvider]. You may create an account by authenticating with
  98. the organization's SSO provider.`,
  99. {
  100. orgSlug: <strong>{inviteDetails.orgSlug}</strong>,
  101. authProvider: inviteDetails.ssoProvider,
  102. }
  103. )
  104. : tct(
  105. `Note that [orgSlug] has enabled Single Sign-On (SSO) using
  106. [authProvider]. You may create an account by authenticating with
  107. the organization's SSO provider.`,
  108. {
  109. orgSlug: <strong>{inviteDetails.orgSlug}</strong>,
  110. authProvider: inviteDetails.ssoProvider,
  111. }
  112. )}
  113. </p>
  114. )}
  115. <Actions>
  116. <ActionsLeft>
  117. {inviteDetails.needsSso && (
  118. <Button
  119. label="sso-login"
  120. priority="primary"
  121. href={this.makeNextUrl(`/auth/login/${inviteDetails.orgSlug}/`)}
  122. >
  123. {t('Join with %s', inviteDetails.ssoProvider)}
  124. </Button>
  125. )}
  126. {!inviteDetails.requireSso && (
  127. <Button
  128. label="create-account"
  129. priority="primary"
  130. href={this.makeNextUrl('/auth/register/')}
  131. >
  132. {t('Create a new account')}
  133. </Button>
  134. )}
  135. </ActionsLeft>
  136. {!inviteDetails.requireSso && (
  137. <ExternalLink
  138. href={this.makeNextUrl('/auth/login/')}
  139. openInNewTab={false}
  140. data-test-id="link-with-existing"
  141. >
  142. {t('Login using an existing account')}
  143. </ExternalLink>
  144. )}
  145. </Actions>
  146. </React.Fragment>
  147. );
  148. }
  149. get warning2fa() {
  150. const {inviteDetails} = this.state;
  151. return (
  152. <React.Fragment>
  153. <p data-test-id="2fa-warning">
  154. {tct(
  155. 'To continue, [orgSlug] requires all members to configure two-factor authentication.',
  156. {orgSlug: inviteDetails.orgSlug}
  157. )}
  158. </p>
  159. <Actions>
  160. <Button priority="primary" to="/settings/account/security/">
  161. {t('Configure Two-Factor Auth')}
  162. </Button>
  163. </Actions>
  164. </React.Fragment>
  165. );
  166. }
  167. get acceptActions() {
  168. const {inviteDetails, accepting} = this.state;
  169. return (
  170. <Actions>
  171. <Button
  172. label="join-organization"
  173. priority="primary"
  174. disabled={accepting}
  175. onClick={this.handleAcceptInvite}
  176. >
  177. {t('Join the %s organization', inviteDetails.orgSlug)}
  178. </Button>
  179. </Actions>
  180. );
  181. }
  182. renderError() {
  183. return (
  184. <NarrowLayout>
  185. <Alert type="warning">
  186. {t('This organization invite link is no longer valid.')}
  187. </Alert>
  188. </NarrowLayout>
  189. );
  190. }
  191. renderBody() {
  192. const {inviteDetails, acceptError} = this.state;
  193. return (
  194. <NarrowLayout>
  195. <SettingsPageHeader title={t('Accept organization invite')} />
  196. {acceptError && (
  197. <Alert type="error">
  198. {t('Failed to join this organization. Please try again')}
  199. </Alert>
  200. )}
  201. <InviteDescription data-test-id="accept-invite">
  202. {tct('[orgSlug] is using Sentry to track and debug errors.', {
  203. orgSlug: <strong>{inviteDetails.orgSlug}</strong>,
  204. })}
  205. </InviteDescription>
  206. {inviteDetails.needsAuthentication
  207. ? this.authenticationActions
  208. : inviteDetails.existingMember
  209. ? this.existingMemberAlert
  210. : inviteDetails.needs2fa
  211. ? this.warning2fa
  212. : this.acceptActions}
  213. </NarrowLayout>
  214. );
  215. }
  216. }
  217. const Actions = styled('div')`
  218. display: flex;
  219. align-items: center;
  220. justify-content: space-between;
  221. margin-bottom: ${space(3)};
  222. `;
  223. const ActionsLeft = styled('span')`
  224. > a {
  225. margin-right: ${space(1)};
  226. }
  227. `;
  228. const InviteDescription = styled('p')`
  229. font-size: 1.2em;
  230. `;
  231. export default AcceptOrganizationInvite;