Skip to content

Commit 1d172ad

Browse files
authored
Merge pull request #1511 from processing/chore/authentication-improvements
Authentication improvements - OAuth Login
2 parents bd788a3 + 3694e8b commit 1d172ad

File tree

16 files changed

+383
-109
lines changed

16 files changed

+383
-109
lines changed

client/modules/IDE/components/ErrorModal.jsx

+21-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ class ErrorModal extends React.Component {
1515
);
1616
}
1717

18+
oauthError() {
19+
const { t, service } = this.props;
20+
const serviceLabels = {
21+
github: 'GitHub',
22+
google: 'Google'
23+
};
24+
return (
25+
<p>
26+
{t('ErrorModal.LinkMessage', { serviceauth: serviceLabels[service] })}
27+
</p>
28+
);
29+
}
30+
1831
staleSession() {
1932
return (
2033
<p>
@@ -42,6 +55,8 @@ class ErrorModal extends React.Component {
4255
return this.staleSession();
4356
} else if (this.props.type === 'staleProject') {
4457
return this.staleProject();
58+
} else if (this.props.type === 'oauthError') {
59+
return this.oauthError();
4560
}
4661
})()}
4762
</div>
@@ -52,7 +67,12 @@ class ErrorModal extends React.Component {
5267
ErrorModal.propTypes = {
5368
type: PropTypes.string.isRequired,
5469
closeModal: PropTypes.func.isRequired,
55-
t: PropTypes.func.isRequired
70+
t: PropTypes.func.isRequired,
71+
service: PropTypes.string
72+
};
73+
74+
ErrorModal.defaultProps = {
75+
service: ''
5676
};
5777

5878
export default withTranslation()(ErrorModal);

client/modules/User/actions.js

+17
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,20 @@ export function removeApiKey(keyId) {
270270
Promise.reject(new Error(response.data.error));
271271
});
272272
}
273+
274+
export function unlinkService(service) {
275+
return (dispatch) => {
276+
if (!['github', 'google'].includes(service)) return;
277+
apiClient.delete(`/auth/${service}`)
278+
.then((response) => {
279+
dispatch({
280+
type: ActionTypes.AUTH_USER,
281+
user: response.data
282+
});
283+
}).catch((error) => {
284+
const { response } = error;
285+
const message = response.message || response.data.error;
286+
dispatch(authError(message));
287+
});
288+
};
289+
}

client/modules/User/components/AccountForm.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ AccountForm.propTypes = {
115115
newPassword: PropTypes.object.isRequired, // eslint-disable-line
116116
}).isRequired,
117117
user: PropTypes.shape({
118-
verified: PropTypes.number.isRequired,
118+
verified: PropTypes.string.isRequired,
119119
emailVerificationInitiate: PropTypes.bool.isRequired,
120120
}).isRequired,
121121
handleSubmit: PropTypes.func.isRequired,

client/modules/User/components/SocialAuthButton.jsx

+45-8
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import PropTypes from 'prop-types';
22
import React from 'react';
33
import styled from 'styled-components';
44
import { withTranslation } from 'react-i18next';
5+
import { useDispatch } from 'react-redux';
56

67
import { remSize } from '../../../theme';
7-
88
import { GithubIcon, GoogleIcon } from '../../../common/icons';
99
import Button from '../../../common/Button';
10+
import { unlinkService } from '../actions';
1011

1112
const authUrls = {
1213
github: '/auth/github',
@@ -23,22 +24,51 @@ const services = {
2324
google: 'google'
2425
};
2526

27+
const servicesLabels = {
28+
github: 'GitHub',
29+
google: 'Google'
30+
};
31+
2632
const StyledButton = styled(Button)`
2733
width: ${remSize(300)};
2834
`;
2935

30-
function SocialAuthButton({ service, t }) {
36+
function SocialAuthButton({
37+
service, linkStyle, isConnected, t
38+
}) {
3139
const ServiceIcon = icons[service];
32-
const labels = {
33-
github: t('SocialAuthButton.Github'),
34-
google: t('SocialAuthButton.Google')
35-
};
40+
const serviceLabel = servicesLabels[service];
41+
const loginLabel = t('SocialAuthButton.Login', { serviceauth: serviceLabel });
42+
const connectLabel = t('SocialAuthButton.Connect', { serviceauth: serviceLabel });
43+
const unlinkLabel = t('SocialAuthButton.Unlink', { serviceauth: serviceLabel });
44+
const ariaLabel = t('SocialAuthButton.LogoARIA', { serviceauth: service });
45+
const dispatch = useDispatch();
46+
if (linkStyle) {
47+
if (isConnected) {
48+
return (
49+
<StyledButton
50+
iconBefore={<ServiceIcon aria-label={ariaLabel} />}
51+
onClick={() => { dispatch(unlinkService(service)); }}
52+
>
53+
{unlinkLabel}
54+
</StyledButton>
55+
);
56+
}
57+
return (
58+
<StyledButton
59+
iconBefore={<ServiceIcon aria-label={ariaLabel} />}
60+
href={authUrls[service]}
61+
>
62+
{connectLabel}
63+
</StyledButton>
64+
);
65+
}
3666
return (
3767
<StyledButton
38-
iconBefore={<ServiceIcon aria-label={t('SocialAuthButton.LogoARIA', { serviceauth: service })} />}
68+
iconBefore={<ServiceIcon aria-label={ariaLabel} />}
3969
href={authUrls[service]}
4070
>
41-
{labels[service]}
71+
{loginLabel}
4272
</StyledButton>
4373
);
4474
}
@@ -47,9 +77,16 @@ SocialAuthButton.services = services;
4777

4878
SocialAuthButton.propTypes = {
4979
service: PropTypes.oneOf(['github', 'google']).isRequired,
80+
linkStyle: PropTypes.bool,
81+
isConnected: PropTypes.bool,
5082
t: PropTypes.func.isRequired
5183
};
5284

85+
SocialAuthButton.defaultProps = {
86+
linkStyle: false,
87+
isConnected: false
88+
};
89+
5390
const SocialAuthButtonPublic = withTranslation()(SocialAuthButton);
5491
SocialAuthButtonPublic.services = services;
5592
export default SocialAuthButtonPublic;

client/modules/User/pages/AccountView.jsx

+47-5
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,20 @@ import { bindActionCreators } from 'redux';
55
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
66
import { Helmet } from 'react-helmet';
77
import { withTranslation } from 'react-i18next';
8+
import { withRouter, browserHistory } from 'react-router';
9+
import { parse } from 'query-string';
810
import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions';
911
import AccountForm from '../components/AccountForm';
1012
import apiClient from '../../../utils/apiClient';
1113
import { validateSettings } from '../../../utils/reduxFormUtils';
1214
import SocialAuthButton from '../components/SocialAuthButton';
1315
import APIKeyForm from '../components/APIKeyForm';
1416
import Nav from '../../../components/Nav';
17+
import ErrorModal from '../../IDE/components/ErrorModal';
18+
import Overlay from '../../App/components/Overlay';
1519

1620
function SocialLoginPanel(props) {
21+
const { user } = props;
1722
return (
1823
<React.Fragment>
1924
<AccountForm {...props} />
@@ -24,19 +29,37 @@ function SocialLoginPanel(props) {
2429
{props.t('AccountView.SocialLoginDescription')}
2530
</p>
2631
<div className="account__social-stack">
27-
<SocialAuthButton service={SocialAuthButton.services.github} />
28-
<SocialAuthButton service={SocialAuthButton.services.google} />
32+
<SocialAuthButton
33+
service={SocialAuthButton.services.github}
34+
linkStyle
35+
isConnected={!!user.github}
36+
/>
37+
<SocialAuthButton
38+
service={SocialAuthButton.services.google}
39+
linkStyle
40+
isConnected={!!user.google}
41+
/>
2942
</div>
3043
</React.Fragment>
3144
);
3245
}
3346

47+
SocialLoginPanel.propTypes = {
48+
user: PropTypes.shape({
49+
github: PropTypes.string,
50+
google: PropTypes.string
51+
}).isRequired
52+
};
53+
3454
class AccountView extends React.Component {
3555
componentDidMount() {
3656
document.body.className = this.props.theme;
3757
}
3858

3959
render() {
60+
const queryParams = parse(this.props.location.search);
61+
const showError = !!queryParams.error;
62+
const errorType = queryParams.error;
4063
const accessTokensUIEnabled = window.process.env.UI_ACCESS_TOKEN_ENABLED;
4164

4265
return (
@@ -47,6 +70,21 @@ class AccountView extends React.Component {
4770

4871
<Nav layout="dashboard" />
4972

73+
{showError &&
74+
<Overlay
75+
title={this.props.t('ErrorModal.LinkTitle')}
76+
ariaLabel={this.props.t('ErrorModal.LinkTitle')}
77+
closeOverlay={() => {
78+
browserHistory.push(this.props.location.pathname);
79+
}}
80+
>
81+
<ErrorModal
82+
type="oauthError"
83+
service={errorType}
84+
/>
85+
</Overlay>
86+
}
87+
5088
<main className="account-settings">
5189
<header className="account-settings__header">
5290
<h1 className="account-settings__title">{this.props.t('AccountView.Settings')}</h1>
@@ -111,13 +149,17 @@ function asyncValidate(formProps, dispatch, props) {
111149
AccountView.propTypes = {
112150
previousPath: PropTypes.string.isRequired,
113151
theme: PropTypes.string.isRequired,
114-
t: PropTypes.func.isRequired
152+
t: PropTypes.func.isRequired,
153+
location: PropTypes.shape({
154+
search: PropTypes.string.isRequired,
155+
pathname: PropTypes.string.isRequired
156+
}).isRequired
115157
};
116158

117-
export default withTranslation()(reduxForm({
159+
export default withTranslation()(withRouter(reduxForm({
118160
form: 'updateAllSettings',
119161
fields: ['username', 'email', 'currentPassword', 'newPassword'],
120162
validate: validateSettings,
121163
asyncValidate,
122164
asyncBlurFields: ['username', 'email', 'currentPassword']
123-
}, mapStateToProps, mapDispatchToProps)(AccountView));
165+
}, mapStateToProps, mapDispatchToProps)(AccountView)));

client/styles/components/_error-modal.scss

+5-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717

1818
.error-modal__content {
1919
padding: #{20 / $base-font-size}rem;
20-
padding-top: 0;
20+
padding-top: #{40 / $base-font-size}rem;
2121
padding-bottom: #{60 / $base-font-size}rem;
22+
max-width: #{500 / $base-font-size}rem;
23+
& p {
24+
font-size: #{16 / $base-font-size}rem;
25+
}
2226
}

package-lock.json

+41-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@
195195
"primer-tooltips": "^1.5.11",
196196
"prop-types": "^15.6.2",
197197
"q": "^1.4.1",
198+
"query-string": "^6.13.2",
198199
"react": "^16.12.0",
199200
"react-dom": "^16.12.0",
200201
"react-helmet": "^5.1.3",

0 commit comments

Comments
 (0)