From 860b2d18f4d6e83d8fddf6b3a945e5514051b68d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 5 Jun 2020 14:25:26 -0500 Subject: [PATCH 01/14] Security: Rudimentary email change --- app/soapbox/actions/security.js | 38 ++++++++++ app/soapbox/features/security/index.js | 73 +++++++++++++++++++ app/soapbox/features/ui/index.js | 2 + .../features/ui/util/async-components.js | 4 + 4 files changed, 117 insertions(+) create mode 100644 app/soapbox/actions/security.js create mode 100644 app/soapbox/features/security/index.js diff --git a/app/soapbox/actions/security.js b/app/soapbox/actions/security.js new file mode 100644 index 000000000..5005c2017 --- /dev/null +++ b/app/soapbox/actions/security.js @@ -0,0 +1,38 @@ +import api from '../api'; + +export const CHANGE_EMAIL_REQUEST = 'CHANGE_EMAIL_REQUEST'; +export const CHANGE_EMAIL_SUCCESS = 'CHANGE_EMAIL_SUCCESS'; +export const CHANGE_EMAIL_FAIL = 'CHANGE_EMAIL_FAIL'; + +export const CHANGE_PASSWORD_REQUEST = 'CHANGE_PASSWORD_REQUEST'; +export const CHANGE_PASSWORD_SUCCESS = 'CHANGE_PASSWORD_SUCCESS'; +export const CHANGE_PASSWORD_FAIL = 'CHANGE_PASSWORD_FAIL'; + +export function changeEmail(email, password) { + return (dispatch, getState) => { + dispatch({ type: CHANGE_EMAIL_REQUEST, email }); + api(getState).post('/api/pleroma/change_email', { + email, + password, + }).then(response => { + dispatch({ type: CHANGE_EMAIL_SUCCESS, email, response }); + }).catch(error => { + dispatch({ type: CHANGE_EMAIL_FAIL, email, error }); + }); + }; +} + +export function changePassword(oldPassword, newPassword, confirmation) { + return (dispatch, getState) => { + dispatch({ type: CHANGE_PASSWORD_REQUEST }); + api(getState).post('/api/pleroma/change_password', { + password: oldPassword, + new_password: newPassword, + new_password_confirmation: confirmation, + }).then(response => { + dispatch({ type: CHANGE_PASSWORD_SUCCESS, response }); + }).catch(error => { + dispatch({ type: CHANGE_PASSWORD_FAIL, error }); + }); + }; +} diff --git a/app/soapbox/features/security/index.js b/app/soapbox/features/security/index.js new file mode 100644 index 000000000..df3224b58 --- /dev/null +++ b/app/soapbox/features/security/index.js @@ -0,0 +1,73 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { defineMessages, injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import PropTypes from 'prop-types'; +import Column from '../ui/components/column'; +import { + SimpleForm, + SimpleInput, + FieldsGroup, + TextInput, +} from 'soapbox/features/forms'; +import { changeEmail } from 'soapbox/actions/security'; + +const messages = defineMessages({ + heading: { id: 'column.security', defaultMessage: 'Security' }, + submit: { id: 'security.submit', defaultMessage: 'Save changes' }, +}); + +export default @connect() +@injectIntl +class Security extends ImmutablePureComponent { + + static propTypes = { + email: PropTypes.string, + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + state = {} + + handleInputChange = e => { + this.setState({ [e.target.name]: e.target.value }); + } + + handleSubmit = e => { + const { email, password } = this.state; + this.props.dispatch(changeEmail(email, password)); + } + + render() { + const { intl } = this.props; + + return ( + + + + + +
+ +
+
+
+
+ ); + } + +} diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index 887d24fce..98af4dc8c 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -72,6 +72,7 @@ import { Preferences, EditProfile, PasswordReset, + Security, } from './util/async-components'; // Dummy import, to make sure that ends up in the application bundle. @@ -194,6 +195,7 @@ class SwitchingColumnsArea extends React.PureComponent { + diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js index 823a91eb2..f50401dbc 100644 --- a/app/soapbox/features/ui/util/async-components.js +++ b/app/soapbox/features/ui/util/async-components.js @@ -177,3 +177,7 @@ export function EditProfile() { export function PasswordReset() { return import(/* webpackChunkName: "features/auth_login" */'../../auth_login/components/password_reset'); } + +export function Security() { + return import(/* webpackChunkName: "features/security" */'../../security'); +} From 1076788addcce57399de9bc3340820ca26aadc26 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 5 Jun 2020 14:39:27 -0500 Subject: [PATCH 02/14] Security: Form confirmations --- app/soapbox/actions/security.js | 7 ++++--- app/soapbox/features/security/index.js | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/soapbox/actions/security.js b/app/soapbox/actions/security.js index 5005c2017..0df6b9fe6 100644 --- a/app/soapbox/actions/security.js +++ b/app/soapbox/actions/security.js @@ -11,13 +11,14 @@ export const CHANGE_PASSWORD_FAIL = 'CHANGE_PASSWORD_FAIL'; export function changeEmail(email, password) { return (dispatch, getState) => { dispatch({ type: CHANGE_EMAIL_REQUEST, email }); - api(getState).post('/api/pleroma/change_email', { + return api(getState).post('/api/pleroma/change_email', { email, password, }).then(response => { dispatch({ type: CHANGE_EMAIL_SUCCESS, email, response }); }).catch(error => { - dispatch({ type: CHANGE_EMAIL_FAIL, email, error }); + dispatch({ type: CHANGE_EMAIL_FAIL, email, error, skipAlert: true }); + throw error; }); }; } @@ -25,7 +26,7 @@ export function changeEmail(email, password) { export function changePassword(oldPassword, newPassword, confirmation) { return (dispatch, getState) => { dispatch({ type: CHANGE_PASSWORD_REQUEST }); - api(getState).post('/api/pleroma/change_password', { + return api(getState).post('/api/pleroma/change_password', { password: oldPassword, new_password: newPassword, new_password_confirmation: confirmation, diff --git a/app/soapbox/features/security/index.js b/app/soapbox/features/security/index.js index df3224b58..8c36a3e45 100644 --- a/app/soapbox/features/security/index.js +++ b/app/soapbox/features/security/index.js @@ -11,10 +11,13 @@ import { TextInput, } from 'soapbox/features/forms'; import { changeEmail } from 'soapbox/actions/security'; +import { showAlert } from 'soapbox/actions/alerts'; const messages = defineMessages({ heading: { id: 'column.security', defaultMessage: 'Security' }, submit: { id: 'security.submit', defaultMessage: 'Save changes' }, + updateEmailSuccess: { id: 'security.update_email.success', defaultMessage: 'Email successfully updated.' }, + updateEmailFail: { id: 'security.update_email.fail', defaultMessage: 'Update email failed.' }, }); export default @connect() @@ -27,7 +30,10 @@ class Security extends ImmutablePureComponent { intl: PropTypes.object.isRequired, }; - state = {} + state = { + email: '', + password: '', + } handleInputChange = e => { this.setState({ [e.target.name]: e.target.value }); @@ -35,7 +41,12 @@ class Security extends ImmutablePureComponent { handleSubmit = e => { const { email, password } = this.state; - this.props.dispatch(changeEmail(email, password)); + const { dispatch, intl } = this.props; + dispatch(changeEmail(email, password)).then(() => { + dispatch(showAlert('', intl.formatMessage(messages.updateEmailSuccess))); + }).catch(error => { + dispatch(showAlert('', intl.formatMessage(messages.updateEmailFail))); + }); } render() { From 91d511c4b9b19d42288d6f23dd62a10922a1b0b8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 5 Jun 2020 14:45:00 -0500 Subject: [PATCH 03/14] SecurityForm: Respond to submit --- app/soapbox/actions/security.js | 1 + app/soapbox/features/security/index.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/soapbox/actions/security.js b/app/soapbox/actions/security.js index 0df6b9fe6..504cfc8f0 100644 --- a/app/soapbox/actions/security.js +++ b/app/soapbox/actions/security.js @@ -15,6 +15,7 @@ export function changeEmail(email, password) { email, password, }).then(response => { + if (response.data.error) throw response.data.error; // This endpoint returns HTTP 200 even on failure dispatch({ type: CHANGE_EMAIL_SUCCESS, email, response }); }).catch(error => { dispatch({ type: CHANGE_EMAIL_FAIL, email, error, skipAlert: true }); diff --git a/app/soapbox/features/security/index.js b/app/soapbox/features/security/index.js index 8c36a3e45..20e24b240 100644 --- a/app/soapbox/features/security/index.js +++ b/app/soapbox/features/security/index.js @@ -43,8 +43,10 @@ class Security extends ImmutablePureComponent { const { email, password } = this.state; const { dispatch, intl } = this.props; dispatch(changeEmail(email, password)).then(() => { + this.setState({ email: '', password: '' }); // TODO: Maybe redirect user dispatch(showAlert('', intl.formatMessage(messages.updateEmailSuccess))); }).catch(error => { + this.setState({ password: '' }); dispatch(showAlert('', intl.formatMessage(messages.updateEmailFail))); }); } From 683950b73c864717069504f3ee2859b19edc8710 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 5 Jun 2020 14:50:24 -0500 Subject: [PATCH 04/14] SecurityForm: Disable on submit --- app/soapbox/features/security/index.js | 50 ++++++++++++++------------ 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/app/soapbox/features/security/index.js b/app/soapbox/features/security/index.js index 20e24b240..b533ec0b2 100644 --- a/app/soapbox/features/security/index.js +++ b/app/soapbox/features/security/index.js @@ -33,6 +33,7 @@ class Security extends ImmutablePureComponent { state = { email: '', password: '', + isLoading: false, } handleInputChange = e => { @@ -42,12 +43,15 @@ class Security extends ImmutablePureComponent { handleSubmit = e => { const { email, password } = this.state; const { dispatch, intl } = this.props; - dispatch(changeEmail(email, password)).then(() => { + this.setState({ isLoading: true }); + return dispatch(changeEmail(email, password)).then(() => { this.setState({ email: '', password: '' }); // TODO: Maybe redirect user dispatch(showAlert('', intl.formatMessage(messages.updateEmailSuccess))); }).catch(error => { this.setState({ password: '' }); dispatch(showAlert('', intl.formatMessage(messages.updateEmailFail))); + }).then(() => { + this.setState({ isLoading: false }); }); } @@ -57,27 +61,29 @@ class Security extends ImmutablePureComponent { return ( - - - -
- -
-
+
+ + + +
+ +
+
+
); From ab280b80e14ac243af72c1a2474c7e14155f184f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 5 Jun 2020 14:58:59 -0500 Subject: [PATCH 05/14] SecurityForm: Add navigation links --- app/soapbox/components/sidebar_menu.js | 5 +++++ app/soapbox/features/compose/components/action_bar.js | 2 ++ 2 files changed, 7 insertions(+) diff --git a/app/soapbox/components/sidebar_menu.js b/app/soapbox/components/sidebar_menu.js index e660ddb19..844a1bc4d 100644 --- a/app/soapbox/components/sidebar_menu.js +++ b/app/soapbox/components/sidebar_menu.js @@ -28,6 +28,7 @@ const messages = defineMessages({ mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' }, admin_settings: { id: 'navigation_bar.admin_settings', defaultMessage: 'Admin settings' }, + security: { id: 'navigation_bar.security', defaultMessage: 'Security' }, logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, lists: { id: 'column.lists', defaultMessage: 'Lists' }, apps: { id: 'tabs_bar.apps', defaultMessage: 'Apps' }, @@ -167,6 +168,10 @@ class SidebarMenu extends ImmutablePureComponent { {intl.formatMessage(messages.preferences)} + + + {intl.formatMessage(messages.security)} +
diff --git a/app/soapbox/features/compose/components/action_bar.js b/app/soapbox/features/compose/components/action_bar.js index 47b774850..db1774903 100644 --- a/app/soapbox/features/compose/components/action_bar.js +++ b/app/soapbox/features/compose/components/action_bar.js @@ -18,6 +18,7 @@ const messages = defineMessages({ mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' }, admin_settings: { id: 'navigation_bar.admin_settings', defaultMessage: 'Admin settings' }, + security: { id: 'navigation_bar.security', defaultMessage: 'Security' }, logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Hotkeys' }, }); @@ -80,6 +81,7 @@ class ActionBar extends React.PureComponent { menu.push({ text: intl.formatMessage(messages.admin_settings), href: '/pleroma/admin/' }); } menu.push({ text: intl.formatMessage(messages.preferences), to: '/settings/preferences' }); + menu.push({ text: intl.formatMessage(messages.security), to: '/auth/edit' }); menu.push({ text: intl.formatMessage(messages.logout), to: '/auth/sign_out', action: onClickLogOut }); return ( From f99d1300c2ef9dcb161ee9631a8175ac0b4e6bf8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 5 Jun 2020 15:00:24 -0500 Subject: [PATCH 06/14] Security --> SecurityForm --- app/soapbox/features/security/index.js | 2 +- app/soapbox/features/ui/index.js | 4 ++-- app/soapbox/features/ui/util/async-components.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/soapbox/features/security/index.js b/app/soapbox/features/security/index.js index b533ec0b2..24c65aefa 100644 --- a/app/soapbox/features/security/index.js +++ b/app/soapbox/features/security/index.js @@ -22,7 +22,7 @@ const messages = defineMessages({ export default @connect() @injectIntl -class Security extends ImmutablePureComponent { +class SecurityForm extends ImmutablePureComponent { static propTypes = { email: PropTypes.string, diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index 98af4dc8c..a78f57b6e 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -72,7 +72,7 @@ import { Preferences, EditProfile, PasswordReset, - Security, + SecurityForm, } from './util/async-components'; // Dummy import, to make sure that ends up in the application bundle. @@ -195,7 +195,7 @@ class SwitchingColumnsArea extends React.PureComponent { - + diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js index f50401dbc..749b72cc2 100644 --- a/app/soapbox/features/ui/util/async-components.js +++ b/app/soapbox/features/ui/util/async-components.js @@ -178,6 +178,6 @@ export function PasswordReset() { return import(/* webpackChunkName: "features/auth_login" */'../../auth_login/components/password_reset'); } -export function Security() { +export function SecurityForm() { return import(/* webpackChunkName: "features/security" */'../../security'); } From e972cfc19163cd8edb386e2921282a51160bbe6a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 5 Jun 2020 15:03:21 -0500 Subject: [PATCH 07/14] SecurityForm: Break ChangeEmailForm into separate component --- app/soapbox/features/security/index.js | 71 +++++++++++++++----------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/app/soapbox/features/security/index.js b/app/soapbox/features/security/index.js index 24c65aefa..51064852e 100644 --- a/app/soapbox/features/security/index.js +++ b/app/soapbox/features/security/index.js @@ -20,10 +20,25 @@ const messages = defineMessages({ updateEmailFail: { id: 'security.update_email.fail', defaultMessage: 'Update email failed.' }, }); -export default @connect() -@injectIntl +export default @injectIntl class SecurityForm extends ImmutablePureComponent { + render() { + const { intl } = this.props; + + return ( + + + + ); + } + +} + +@connect() +@injectIntl +class ChangeEmailForm extends ImmutablePureComponent { + static propTypes = { email: PropTypes.string, dispatch: PropTypes.func.isRequired, @@ -59,33 +74,31 @@ class SecurityForm extends ImmutablePureComponent { const { intl } = this.props; return ( - - -
- - - -
- -
-
-
-
-
+ +
+ + + +
+ +
+
+
+
); } From da44a769d602b6e27a8eeae6374b1d1a2b959f49 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 5 Jun 2020 15:14:27 -0500 Subject: [PATCH 08/14] Get ChangePasswordForm working --- app/soapbox/actions/security.js | 4 +- app/soapbox/features/security/index.js | 85 +++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/app/soapbox/actions/security.js b/app/soapbox/actions/security.js index 504cfc8f0..3779a58a8 100644 --- a/app/soapbox/actions/security.js +++ b/app/soapbox/actions/security.js @@ -32,9 +32,11 @@ export function changePassword(oldPassword, newPassword, confirmation) { new_password: newPassword, new_password_confirmation: confirmation, }).then(response => { + if (response.data.error) throw response.data.error; // This endpoint returns HTTP 200 even on failure dispatch({ type: CHANGE_PASSWORD_SUCCESS, response }); }).catch(error => { - dispatch({ type: CHANGE_PASSWORD_FAIL, error }); + dispatch({ type: CHANGE_PASSWORD_FAIL, error, skipAlert: true }); + throw error; }); }; } diff --git a/app/soapbox/features/security/index.js b/app/soapbox/features/security/index.js index 51064852e..77cd7224c 100644 --- a/app/soapbox/features/security/index.js +++ b/app/soapbox/features/security/index.js @@ -10,7 +10,7 @@ import { FieldsGroup, TextInput, } from 'soapbox/features/forms'; -import { changeEmail } from 'soapbox/actions/security'; +import { changeEmail, changePassword } from 'soapbox/actions/security'; import { showAlert } from 'soapbox/actions/alerts'; const messages = defineMessages({ @@ -18,6 +18,8 @@ const messages = defineMessages({ submit: { id: 'security.submit', defaultMessage: 'Save changes' }, updateEmailSuccess: { id: 'security.update_email.success', defaultMessage: 'Email successfully updated.' }, updateEmailFail: { id: 'security.update_email.fail', defaultMessage: 'Update email failed.' }, + updatePasswordSuccess: { id: 'security.update_password.success', defaultMessage: 'Password successfully updated.' }, + updatePasswordFail: { id: 'security.update_password.fail', defaultMessage: 'Update password failed.' }, }); export default @injectIntl @@ -29,6 +31,7 @@ class SecurityForm extends ImmutablePureComponent { return ( + ); } @@ -103,3 +106,83 @@ class ChangeEmailForm extends ImmutablePureComponent { } } + +@connect() +@injectIntl +class ChangePasswordForm extends ImmutablePureComponent { + + static propTypes = { + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + state = { + oldPassword: '', + newPassword: '', + confirmation: '', + isLoading: false, + } + + handleInputChange = e => { + this.setState({ [e.target.name]: e.target.value }); + } + + clearForm = () => { + this.setState({ oldPassword: '', newPassword: '', confirmation: '' }); + } + + handleSubmit = e => { + const { oldPassword, newPassword, confirmation } = this.state; + const { dispatch, intl } = this.props; + this.setState({ isLoading: true }); + return dispatch(changePassword(oldPassword, newPassword, confirmation)).then(() => { + this.clearForm(); // TODO: Maybe redirect user + dispatch(showAlert('', intl.formatMessage(messages.updatePasswordSuccess))); + }).catch(error => { + this.clearForm(); + dispatch(showAlert('', intl.formatMessage(messages.updatePasswordFail))); + }).then(() => { + this.setState({ isLoading: false }); + }); + } + + render() { + const { intl } = this.props; + + return ( + +
+ + + + +
+ +
+
+
+
+ ); + } + +} From 75a0062dd4cfae71dae1b6a25ec27267b2906ce0 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 5 Jun 2020 15:21:19 -0500 Subject: [PATCH 09/14] Make /auth/edit a private route --- app/soapbox/features/ui/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index a78f57b6e..001f55cfc 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -195,7 +195,7 @@ class SwitchingColumnsArea extends React.PureComponent { - + From c1f3dbd22d63bb94edfd7e6f5386ed1c09d111a7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 5 Jun 2020 15:21:29 -0500 Subject: [PATCH 10/14] SecurityForm: i18n --- app/soapbox/features/security/index.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/soapbox/features/security/index.js b/app/soapbox/features/security/index.js index 77cd7224c..477d0f520 100644 --- a/app/soapbox/features/security/index.js +++ b/app/soapbox/features/security/index.js @@ -20,6 +20,11 @@ const messages = defineMessages({ updateEmailFail: { id: 'security.update_email.fail', defaultMessage: 'Update email failed.' }, updatePasswordSuccess: { id: 'security.update_password.success', defaultMessage: 'Password successfully updated.' }, updatePasswordFail: { id: 'security.update_password.fail', defaultMessage: 'Update password failed.' }, + emailFieldLabel: { id: 'security.fields.email.label', defaultMessage: 'Email address' }, + passwordFieldLabel: { id: 'security.fields.password.label', defaultMessage: 'Password' }, + oldPasswordFieldLabel: { id: 'security.fields.old_password.label', defaultMessage: 'Current password' }, + newPasswordFieldLabel: { id: 'security.fields.new_password.label', defaultMessage: 'New password' }, + confirmationFieldLabel: { id: 'security.fields.password_confirmation.label', defaultMessage: 'New password (again)' }, }); export default @injectIntl @@ -81,7 +86,7 @@ class ChangeEmailForm extends ImmutablePureComponent {
Date: Fri, 5 Jun 2020 15:27:31 -0500 Subject: [PATCH 11/14] Consolidate actions/security.js --> actions/auth.js --- app/soapbox/actions/auth.js | 41 +++++++++++++++++++++++++ app/soapbox/actions/security.js | 42 -------------------------- app/soapbox/features/security/index.js | 2 +- 3 files changed, 42 insertions(+), 43 deletions(-) delete mode 100644 app/soapbox/actions/security.js diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index 46e7ea97e..23252cbcd 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -15,6 +15,14 @@ export const RESET_PASSWORD_REQUEST = 'RESET_PASSWORD_REQUEST'; export const RESET_PASSWORD_SUCCESS = 'RESET_PASSWORD_SUCCESS'; export const RESET_PASSWORD_FAIL = 'RESET_PASSWORD_FAIL'; +export const CHANGE_EMAIL_REQUEST = 'CHANGE_EMAIL_REQUEST'; +export const CHANGE_EMAIL_SUCCESS = 'CHANGE_EMAIL_SUCCESS'; +export const CHANGE_EMAIL_FAIL = 'CHANGE_EMAIL_FAIL'; + +export const CHANGE_PASSWORD_REQUEST = 'CHANGE_PASSWORD_REQUEST'; +export const CHANGE_PASSWORD_SUCCESS = 'CHANGE_PASSWORD_SUCCESS'; +export const CHANGE_PASSWORD_FAIL = 'CHANGE_PASSWORD_FAIL'; + const noOp = () => () => new Promise(f => f()); function createAppAndToken() { @@ -148,6 +156,39 @@ export function resetPassword(nickNameOrEmail) { }; } +export function changeEmail(email, password) { + return (dispatch, getState) => { + dispatch({ type: CHANGE_EMAIL_REQUEST, email }); + return api(getState).post('/api/pleroma/change_email', { + email, + password, + }).then(response => { + if (response.data.error) throw response.data.error; // This endpoint returns HTTP 200 even on failure + dispatch({ type: CHANGE_EMAIL_SUCCESS, email, response }); + }).catch(error => { + dispatch({ type: CHANGE_EMAIL_FAIL, email, error, skipAlert: true }); + throw error; + }); + }; +} + +export function changePassword(oldPassword, newPassword, confirmation) { + return (dispatch, getState) => { + dispatch({ type: CHANGE_PASSWORD_REQUEST }); + return api(getState).post('/api/pleroma/change_password', { + password: oldPassword, + new_password: newPassword, + new_password_confirmation: confirmation, + }).then(response => { + if (response.data.error) throw response.data.error; // This endpoint returns HTTP 200 even on failure + dispatch({ type: CHANGE_PASSWORD_SUCCESS, response }); + }).catch(error => { + dispatch({ type: CHANGE_PASSWORD_FAIL, error, skipAlert: true }); + throw error; + }); + }; +} + export function authAppCreated(app) { return { type: AUTH_APP_CREATED, diff --git a/app/soapbox/actions/security.js b/app/soapbox/actions/security.js deleted file mode 100644 index 3779a58a8..000000000 --- a/app/soapbox/actions/security.js +++ /dev/null @@ -1,42 +0,0 @@ -import api from '../api'; - -export const CHANGE_EMAIL_REQUEST = 'CHANGE_EMAIL_REQUEST'; -export const CHANGE_EMAIL_SUCCESS = 'CHANGE_EMAIL_SUCCESS'; -export const CHANGE_EMAIL_FAIL = 'CHANGE_EMAIL_FAIL'; - -export const CHANGE_PASSWORD_REQUEST = 'CHANGE_PASSWORD_REQUEST'; -export const CHANGE_PASSWORD_SUCCESS = 'CHANGE_PASSWORD_SUCCESS'; -export const CHANGE_PASSWORD_FAIL = 'CHANGE_PASSWORD_FAIL'; - -export function changeEmail(email, password) { - return (dispatch, getState) => { - dispatch({ type: CHANGE_EMAIL_REQUEST, email }); - return api(getState).post('/api/pleroma/change_email', { - email, - password, - }).then(response => { - if (response.data.error) throw response.data.error; // This endpoint returns HTTP 200 even on failure - dispatch({ type: CHANGE_EMAIL_SUCCESS, email, response }); - }).catch(error => { - dispatch({ type: CHANGE_EMAIL_FAIL, email, error, skipAlert: true }); - throw error; - }); - }; -} - -export function changePassword(oldPassword, newPassword, confirmation) { - return (dispatch, getState) => { - dispatch({ type: CHANGE_PASSWORD_REQUEST }); - return api(getState).post('/api/pleroma/change_password', { - password: oldPassword, - new_password: newPassword, - new_password_confirmation: confirmation, - }).then(response => { - if (response.data.error) throw response.data.error; // This endpoint returns HTTP 200 even on failure - dispatch({ type: CHANGE_PASSWORD_SUCCESS, response }); - }).catch(error => { - dispatch({ type: CHANGE_PASSWORD_FAIL, error, skipAlert: true }); - throw error; - }); - }; -} diff --git a/app/soapbox/features/security/index.js b/app/soapbox/features/security/index.js index 477d0f520..91d907ce7 100644 --- a/app/soapbox/features/security/index.js +++ b/app/soapbox/features/security/index.js @@ -10,7 +10,7 @@ import { FieldsGroup, TextInput, } from 'soapbox/features/forms'; -import { changeEmail, changePassword } from 'soapbox/actions/security'; +import { changeEmail, changePassword } from 'soapbox/actions/auth'; import { showAlert } from 'soapbox/actions/alerts'; const messages = defineMessages({ From db1ad3e16fd6a83e85ba4081fd514120057ea572 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 5 Jun 2020 15:43:03 -0500 Subject: [PATCH 12/14] SecurityForm: Display OAuth tokens --- app/soapbox/actions/auth.js | 15 +++++++++ app/soapbox/features/security/index.js | 42 +++++++++++++++++++++++++- app/soapbox/reducers/auth.js | 8 +++-- 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index 23252cbcd..b6bcd888c 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -23,6 +23,10 @@ export const CHANGE_PASSWORD_REQUEST = 'CHANGE_PASSWORD_REQUEST'; export const CHANGE_PASSWORD_SUCCESS = 'CHANGE_PASSWORD_SUCCESS'; export const CHANGE_PASSWORD_FAIL = 'CHANGE_PASSWORD_FAIL'; +export const FETCH_TOKENS_REQUEST = 'FETCH_TOKENS_REQUEST'; +export const FETCH_TOKENS_SUCCESS = 'FETCH_TOKENS_SUCCESS'; +export const FETCH_TOKENS_FAIL = 'FETCH_TOKENS_FAIL'; + const noOp = () => () => new Promise(f => f()); function createAppAndToken() { @@ -189,6 +193,17 @@ export function changePassword(oldPassword, newPassword, confirmation) { }; } +export function fetchOAuthTokens() { + return (dispatch, getState) => { + dispatch({ type: FETCH_TOKENS_REQUEST }); + return api(getState).get('/api/oauth_tokens.json').then(response => { + dispatch({ type: FETCH_TOKENS_SUCCESS, tokens: response.data }); + }).catch(error => { + dispatch({ type: FETCH_TOKENS_FAIL }); + }); + }; +} + export function authAppCreated(app) { return { type: AUTH_APP_CREATED, diff --git a/app/soapbox/features/security/index.js b/app/soapbox/features/security/index.js index 91d907ce7..2dc378ff6 100644 --- a/app/soapbox/features/security/index.js +++ b/app/soapbox/features/security/index.js @@ -3,6 +3,7 @@ import { connect } from 'react-redux'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; import Column from '../ui/components/column'; import { SimpleForm, @@ -10,7 +11,11 @@ import { FieldsGroup, TextInput, } from 'soapbox/features/forms'; -import { changeEmail, changePassword } from 'soapbox/actions/auth'; +import { + changeEmail, + changePassword, + fetchOAuthTokens, +} from 'soapbox/actions/auth'; import { showAlert } from 'soapbox/actions/alerts'; const messages = defineMessages({ @@ -37,6 +42,7 @@ class SecurityForm extends ImmutablePureComponent { + ); } @@ -191,3 +197,37 @@ class ChangePasswordForm extends ImmutablePureComponent { } } + +const mapStateToProps = state => ({ + tokens: state.getIn(['auth', 'tokens']), +}); + +@connect(mapStateToProps) +@injectIntl +class AuthTokenList extends ImmutablePureComponent { + + static propTypes = { + tokens: ImmutablePropTypes.list, + } + + componentDidMount() { + this.props.dispatch(fetchOAuthTokens()); + } + + render() { + if (this.props.tokens.isEmpty()) return null; + return ( + + {this.props.tokens.map((token, i) => ( +
+
{token.get('app_name')}
+
{token.get('id')}
+
{token.get('valid_until')}
+
+
+ ))} +
+ ); + } + +} diff --git a/app/soapbox/reducers/auth.js b/app/soapbox/reducers/auth.js index b25330518..35bf85559 100644 --- a/app/soapbox/reducers/auth.js +++ b/app/soapbox/reducers/auth.js @@ -3,12 +3,14 @@ import { AUTH_LOGGED_IN, AUTH_APP_AUTHORIZED, AUTH_LOGGED_OUT, + FETCH_TOKENS_SUCCESS, } from '../actions/auth'; -import { Map as ImmutableMap } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; const initialState = ImmutableMap({ app: ImmutableMap(JSON.parse(localStorage.getItem('soapbox:auth:app'))), user: ImmutableMap(JSON.parse(localStorage.getItem('soapbox:auth:user'))), + tokens: ImmutableList(), }); export default function auth(state = initialState, action) { @@ -25,7 +27,9 @@ export default function auth(state = initialState, action) { return state.set('user', ImmutableMap(action.user)); case AUTH_LOGGED_OUT: localStorage.removeItem('soapbox:auth:user'); - return state.setIn(['user'], ImmutableMap()); + return state.set('user', ImmutableMap()); + case FETCH_TOKENS_SUCCESS: + return state.set('tokens', fromJS(action.tokens)); default: return state; } From 35d5e7d649bdaecf57aefde44463fd0225c660d4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 5 Jun 2020 15:54:09 -0500 Subject: [PATCH 13/14] SecurityForm: Revoke OAuth token --- app/soapbox/actions/auth.js | 15 +++++++++++++++ app/soapbox/features/security/index.js | 14 ++++++++++++-- app/soapbox/reducers/auth.js | 4 ++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index b6bcd888c..5c884dfd1 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -27,6 +27,10 @@ export const FETCH_TOKENS_REQUEST = 'FETCH_TOKENS_REQUEST'; export const FETCH_TOKENS_SUCCESS = 'FETCH_TOKENS_SUCCESS'; export const FETCH_TOKENS_FAIL = 'FETCH_TOKENS_FAIL'; +export const REVOKE_TOKEN_REQUEST = 'REVOKE_TOKEN_REQUEST'; +export const REVOKE_TOKEN_SUCCESS = 'REVOKE_TOKEN_SUCCESS'; +export const REVOKE_TOKEN_FAIL = 'REVOKE_TOKEN_FAIL'; + const noOp = () => () => new Promise(f => f()); function createAppAndToken() { @@ -204,6 +208,17 @@ export function fetchOAuthTokens() { }; } +export function revokeOAuthToken(id) { + return (dispatch, getState) => { + dispatch({ type: REVOKE_TOKEN_REQUEST, id }); + return api(getState).delete(`/api/oauth_tokens/${id}`).then(response => { + dispatch({ type: REVOKE_TOKEN_SUCCESS, id }); + }).catch(error => { + dispatch({ type: REVOKE_TOKEN_FAIL, id }); + }); + }; +} + export function authAppCreated(app) { return { type: AUTH_APP_CREATED, diff --git a/app/soapbox/features/security/index.js b/app/soapbox/features/security/index.js index 2dc378ff6..bfbd98946 100644 --- a/app/soapbox/features/security/index.js +++ b/app/soapbox/features/security/index.js @@ -15,6 +15,7 @@ import { changeEmail, changePassword, fetchOAuthTokens, + revokeOAuthToken, } from 'soapbox/actions/auth'; import { showAlert } from 'soapbox/actions/alerts'; @@ -210,6 +211,12 @@ class AuthTokenList extends ImmutablePureComponent { tokens: ImmutablePropTypes.list, } + handleRevoke = id => { + return e => { + this.props.dispatch(revokeOAuthToken(id)); + }; + } + componentDidMount() { this.props.dispatch(fetchOAuthTokens()); } @@ -221,9 +228,12 @@ class AuthTokenList extends ImmutablePureComponent { {this.props.tokens.map((token, i) => (
{token.get('app_name')}
-
{token.get('id')}
{token.get('valid_until')}
-
+
+ +
))} diff --git a/app/soapbox/reducers/auth.js b/app/soapbox/reducers/auth.js index 35bf85559..5bd996f34 100644 --- a/app/soapbox/reducers/auth.js +++ b/app/soapbox/reducers/auth.js @@ -4,6 +4,7 @@ import { AUTH_APP_AUTHORIZED, AUTH_LOGGED_OUT, FETCH_TOKENS_SUCCESS, + REVOKE_TOKEN_SUCCESS, } from '../actions/auth'; import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; @@ -30,6 +31,9 @@ export default function auth(state = initialState, action) { return state.set('user', ImmutableMap()); case FETCH_TOKENS_SUCCESS: return state.set('tokens', fromJS(action.tokens)); + case REVOKE_TOKEN_SUCCESS: + const idx = state.get('tokens').findIndex(t => t.get('id') === action.id); + return state.deleteIn(['tokens', idx]); default: return state; } From 2178c0d5955428de264442c91ed6e7aca280f75d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 5 Jun 2020 16:24:07 -0500 Subject: [PATCH 14/14] SecurityForm: Style OAuth tokens --- app/soapbox/features/security/index.js | 53 +++++++++++++++++++------- app/styles/forms.scss | 32 ++++++++++++++++ 2 files changed, 72 insertions(+), 13 deletions(-) diff --git a/app/soapbox/features/security/index.js b/app/soapbox/features/security/index.js index bfbd98946..c3d5fe41f 100644 --- a/app/soapbox/features/security/index.js +++ b/app/soapbox/features/security/index.js @@ -1,6 +1,6 @@ import React from 'react'; import { connect } from 'react-redux'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedDate } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; @@ -31,11 +31,20 @@ const messages = defineMessages({ oldPasswordFieldLabel: { id: 'security.fields.old_password.label', defaultMessage: 'Current password' }, newPasswordFieldLabel: { id: 'security.fields.new_password.label', defaultMessage: 'New password' }, confirmationFieldLabel: { id: 'security.fields.password_confirmation.label', defaultMessage: 'New password (again)' }, + revoke: { id: 'security.tokens.revoke', defaultMessage: 'Revoke' }, + emailHeader: { id: 'security.headers.update_email', defaultMessage: 'Change Email' }, + passwordHeader: { id: 'security.headers.update_password', defaultMessage: 'Change Password' }, + tokenHeader: { id: 'security.headers.tokens', defaultMessage: 'Sessions' }, }); export default @injectIntl class SecurityForm extends ImmutablePureComponent { + static propTypes = { + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + render() { const { intl } = this.props; @@ -90,6 +99,7 @@ class ChangeEmailForm extends ImmutablePureComponent { return ( +

{intl.formatMessage(messages.emailHeader)}

+

{intl.formatMessage(messages.passwordHeader)}

({ class AuthTokenList extends ImmutablePureComponent { static propTypes = { + dispatch: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, tokens: ImmutablePropTypes.list, - } + }; handleRevoke = id => { return e => { @@ -222,20 +235,34 @@ class AuthTokenList extends ImmutablePureComponent { } render() { - if (this.props.tokens.isEmpty()) return null; + const { tokens, intl } = this.props; + if (tokens.isEmpty()) return null; return ( - {this.props.tokens.map((token, i) => ( -
-
{token.get('app_name')}
-
{token.get('valid_until')}
-
- +

{intl.formatMessage(messages.tokenHeader)}

+
+ {tokens.reverse().map((token, i) => ( +
+
{token.get('app_name')}
+
+ +
+
+ +
-
- ))} + ))} +
); } diff --git a/app/styles/forms.scss b/app/styles/forms.scss index d954d4e74..8510fc59e 100644 --- a/app/styles/forms.scss +++ b/app/styles/forms.scss @@ -504,6 +504,13 @@ code { } } } + + h2 { + font-size: 20px; + line-height: normal; + margin-bottom: 14px; + font-weight: bold; + } } .block-icon { @@ -930,3 +937,28 @@ code { border-radius: 0 0 4px 4px; } } + +.authtokens { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-gap: 20px; +} + +.authtoken { + &__app-name { + font-size: 16px; + font-weight: bold; + overflow: hidden; + text-overflow: ellipsis; + } + + &__valid-until { + font-size: 14px; + overflow: hidden; + text-overflow: ellipsis; + } + + &__revoke { + margin-top: 10px; + } +}