loginForm.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import {Component} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import {ClassNames} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import {Client} from 'sentry/api';
  6. import Button from 'sentry/components/button';
  7. import Form from 'sentry/components/deprecatedforms/form';
  8. import PasswordField from 'sentry/components/deprecatedforms/passwordField';
  9. import TextField from 'sentry/components/deprecatedforms/textField';
  10. import Link from 'sentry/components/links/link';
  11. import {IconGithub, IconGoogle, IconVsts} from 'sentry/icons';
  12. import {t} from 'sentry/locale';
  13. import ConfigStore from 'sentry/stores/configStore';
  14. import space from 'sentry/styles/space';
  15. import {AuthConfig} from 'sentry/types';
  16. import {formFooterClass} from 'sentry/views/auth/login';
  17. type LoginProvidersProps = Partial<
  18. Pick<AuthConfig, 'vstsLoginLink' | 'githubLoginLink' | 'googleLoginLink'>
  19. >;
  20. // TODO(epurkhiser): The abstraction here would be much nicer if we just
  21. // exposed a configuration object telling us what auth providers there are.
  22. const LoginProviders = ({
  23. vstsLoginLink,
  24. githubLoginLink,
  25. googleLoginLink,
  26. }: LoginProvidersProps) => (
  27. <ProviderWrapper>
  28. <ProviderHeading>{t('External Account Login')}</ProviderHeading>
  29. {googleLoginLink && (
  30. <Button
  31. align="left"
  32. size="sm"
  33. icon={<IconGoogle size="xs" />}
  34. href={googleLoginLink}
  35. >
  36. {t('Sign in with Google')}
  37. </Button>
  38. )}
  39. {githubLoginLink && (
  40. <Button
  41. align="left"
  42. size="sm"
  43. icon={<IconGithub size="xs" />}
  44. href={githubLoginLink}
  45. >
  46. {t('Sign in with GitHub')}
  47. </Button>
  48. )}
  49. {vstsLoginLink && (
  50. <Button align="left" size="sm" icon={<IconVsts size="xs" />} href={vstsLoginLink}>
  51. {t('Sign in with Azure DevOps')}
  52. </Button>
  53. )}
  54. </ProviderWrapper>
  55. );
  56. type Props = {
  57. api: Client;
  58. authConfig: AuthConfig;
  59. };
  60. type State = {
  61. errorMessage: null | string;
  62. errors: Record<string, string>;
  63. };
  64. class LoginForm extends Component<Props, State> {
  65. state: State = {
  66. errorMessage: null,
  67. errors: {},
  68. };
  69. handleSubmit: Form['props']['onSubmit'] = async (data, onSuccess, onError) => {
  70. try {
  71. const response = await this.props.api.requestPromise('/auth/login/', {
  72. method: 'POST',
  73. data,
  74. });
  75. onSuccess(data);
  76. // TODO(epurkhiser): There is likely more that needs to happen to update
  77. // the application state after user login.
  78. ConfigStore.set('user', response.user);
  79. // TODO(epurkhiser): Reconfigure sentry SDK identity
  80. browserHistory.push({pathname: response.nextUri});
  81. } catch (e) {
  82. if (!e.responseJSON || !e.responseJSON.errors) {
  83. onError(e);
  84. return;
  85. }
  86. let message = e.responseJSON.detail;
  87. if (e.responseJSON.errors.__all__) {
  88. message = e.responseJSON.errors.__all__;
  89. }
  90. this.setState({
  91. errorMessage: message,
  92. errors: e.responseJSON.errors || {},
  93. });
  94. onError(e);
  95. }
  96. };
  97. render() {
  98. const {errorMessage, errors} = this.state;
  99. const {githubLoginLink, vstsLoginLink} = this.props.authConfig;
  100. const hasLoginProvider = !!(githubLoginLink || vstsLoginLink);
  101. return (
  102. <ClassNames>
  103. {({css}) => (
  104. <FormWrapper hasLoginProvider={hasLoginProvider}>
  105. <Form
  106. submitLabel={t('Continue')}
  107. onSubmit={this.handleSubmit}
  108. footerClass={css`
  109. ${formFooterClass}
  110. `}
  111. errorMessage={errorMessage}
  112. extraButton={
  113. <LostPasswordLink to="/account/recover/">
  114. {t('Lost your password?')}
  115. </LostPasswordLink>
  116. }
  117. >
  118. <TextField
  119. name="username"
  120. placeholder={t('username or email')}
  121. label={t('Account')}
  122. error={errors.username}
  123. required
  124. />
  125. <PasswordField
  126. name="password"
  127. placeholder={t('password')}
  128. label={t('Password')}
  129. error={errors.password}
  130. required
  131. />
  132. </Form>
  133. {hasLoginProvider && <LoginProviders {...{vstsLoginLink, githubLoginLink}} />}
  134. </FormWrapper>
  135. )}
  136. </ClassNames>
  137. );
  138. }
  139. }
  140. const FormWrapper = styled('div')<{hasLoginProvider: boolean}>`
  141. display: grid;
  142. gap: 60px;
  143. grid-template-columns: ${p => (p.hasLoginProvider ? '1fr 0.8fr' : '1fr')};
  144. `;
  145. const ProviderHeading = styled('div')`
  146. margin: 0;
  147. font-size: 15px;
  148. font-weight: bold;
  149. line-height: 24px;
  150. `;
  151. const ProviderWrapper = styled('div')`
  152. position: relative;
  153. display: grid;
  154. grid-auto-rows: max-content;
  155. gap: ${space(1.5)};
  156. &:before {
  157. position: absolute;
  158. display: block;
  159. content: '';
  160. top: 0;
  161. bottom: 0;
  162. left: -30px;
  163. border-left: 1px solid ${p => p.theme.border};
  164. }
  165. `;
  166. const LostPasswordLink = styled(Link)`
  167. color: ${p => p.theme.gray300};
  168. font-size: ${p => p.theme.fontSizeMedium};
  169. &:hover {
  170. color: ${p => p.theme.textColor};
  171. }
  172. `;
  173. export default LoginForm;