diff --git a/app/soapbox/actions/auth.js b/app/soapbox/actions/auth.js index a0bb9b9a2..26662803c 100644 --- a/app/soapbox/actions/auth.js +++ b/app/soapbox/actions/auth.js @@ -1,11 +1,17 @@ import api from '../api'; import snackbar from 'soapbox/actions/snackbar'; +export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT'; + export const AUTH_APP_CREATED = 'AUTH_APP_CREATED'; export const AUTH_APP_AUTHORIZED = 'AUTH_APP_AUTHORIZED'; export const AUTH_LOGGED_IN = 'AUTH_LOGGED_IN'; export const AUTH_LOGGED_OUT = 'AUTH_LOGGED_OUT'; +export const VERIFY_CREDENTIALS_REQUEST = 'VERIFY_CREDENTIALS_REQUEST'; +export const VERIFY_CREDENTIALS_SUCCESS = 'VERIFY_CREDENTIALS_SUCCESS'; +export const VERIFY_CREDENTIALS_FAIL = 'VERIFY_CREDENTIALS_FAIL'; + export const AUTH_REGISTER_REQUEST = 'AUTH_REGISTER_REQUEST'; export const AUTH_REGISTER_SUCCESS = 'AUTH_REGISTER_SUCCESS'; export const AUTH_REGISTER_FAIL = 'AUTH_REGISTER_FAIL'; @@ -127,6 +133,27 @@ export function otpVerify(code, mfa_token) { }; } +export function verifyCredentials(token) { + return (dispatch, getState) => { + dispatch({ type: VERIFY_CREDENTIALS_REQUEST }); + + const request = { + method: 'get', + url: '/api/v1/accounts/verify_credentials', + headers: { + 'Authorization': `Bearer ${token.get('access_token')}`, + }, + }; + + return api(getState).request(request).then(({ data: account }) => { + dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account }); + return account; + }).catch(error => { + dispatch({ type: VERIFY_CREDENTIALS_FAIL, token, error }); + }); + }; +} + export function logIn(username, password) { return (dispatch, getState) => { return dispatch(createAppAndToken()).then(() => { @@ -161,6 +188,10 @@ export function logOut() { }; } +export function switchAccount(accountId) { + return { type: SWITCH_ACCOUNT, accountId }; +} + export function register(params) { return (dispatch, getState) => { params.fullname = params.username; diff --git a/app/soapbox/actions/me.js b/app/soapbox/actions/me.js index a05f2b516..c0571b27d 100644 --- a/app/soapbox/actions/me.js +++ b/app/soapbox/actions/me.js @@ -1,5 +1,7 @@ import api from '../api'; import { importFetchedAccount } from './importer'; +import { List as ImmutableList } from 'immutable'; +import { verifyCredentials } from './auth'; export const ME_FETCH_REQUEST = 'ME_FETCH_REQUEST'; export const ME_FETCH_SUCCESS = 'ME_FETCH_SUCCESS'; @@ -10,23 +12,25 @@ export const ME_PATCH_REQUEST = 'ME_PATCH_REQUEST'; export const ME_PATCH_SUCCESS = 'ME_PATCH_SUCCESS'; export const ME_PATCH_FAIL = 'ME_PATCH_FAIL'; -const hasToken = getState => getState().hasIn(['auth', 'user', 'access_token']); const noOp = () => new Promise(f => f()); export function fetchMe() { return (dispatch, getState) => { + const state = getState(); - if (!hasToken(getState)) { + const me = state.getIn(['auth', 'me']); + const token = state.getIn(['auth', 'users', me]); + + if (!token) { dispatch({ type: ME_FETCH_SKIP }); return noOp(); }; dispatch(fetchMeRequest()); - - return api(getState).get('/api/v1/accounts/verify_credentials').then(response => { - dispatch(fetchMeSuccess(response.data)); + return dispatch(verifyCredentials(token)).then(account => { + dispatch(fetchMeSuccess(account)); }).catch(error => { dispatch(fetchMeFail(error)); - }); + });; }; } diff --git a/app/soapbox/api.js b/app/soapbox/api.js index f65971e64..dc9be3add 100644 --- a/app/soapbox/api.js +++ b/app/soapbox/api.js @@ -9,8 +9,15 @@ export const getLinks = response => { return LinkHeader.parse(value); }; -const getToken = (getState, authType) => - getState().getIn(['auth', authType, 'access_token']); +const getToken = (getState, authType) => { + const state = getState(); + if (authType === 'app') { + return state.getIn(['auth', 'app', 'access_token']); + } else { + const me = state.get('me'); + return state.getIn(['auth', 'users', me, 'access_token']); + } +}; export default (getState, authType = 'user') => { const accessToken = getToken(getState, authType); diff --git a/app/soapbox/features/ui/components/profile_dropdown.js b/app/soapbox/features/ui/components/profile_dropdown.js new file mode 100644 index 000000000..57114f275 --- /dev/null +++ b/app/soapbox/features/ui/components/profile_dropdown.js @@ -0,0 +1,91 @@ +import React from 'react'; +import { connect } from 'react-redux'; +// import { openModal } from '../../../actions/modal'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; +import { isStaff } from 'soapbox/utils/accounts'; +import { defineMessages, injectIntl } from 'react-intl'; +import { logOut, switchAccount } from 'soapbox/actions/auth'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; + +const messages = defineMessages({ + switch: { id: 'profile_dropdown.switch_account', defaultMessage: 'Switch to @{acct}' }, + logout: { id: 'profile_dropdown.logout', defaultMessage: 'Log out @{acct}' }, +}); + +const mapStateToProps = state => { + const me = state.get('me'); + + const otherAccounts = + state + .getIn(['auth', 'users']) + .keySeq() + .reduce((list, id) => { + if (id === me) return list; + const account = state.getIn(['accounts', id]) || ImmutableMap({ id: id, acct: id }); + return list.push(account); + }, ImmutableList()); + + return { + account: state.getIn(['accounts', me]), + otherAccounts, + isStaff: isStaff(state.getIn(['accounts', me])), + }; +}; + +class ProfileDropdown extends React.PureComponent { + + static propTypes = { + intl: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + size: PropTypes.number, + account: ImmutablePropTypes.map, + otherAccounts: ImmutablePropTypes.list, + isStaff: PropTypes.bool.isRequired, + }; + + static defaultProps = { + isStaff: false, + } + + handleLogOut = e => { + this.props.dispatch(logOut()); + e.preventDefault(); + }; + + handleSwitchAccount = account => { + return e => { + this.props.dispatch(switchAccount(account.get('id'))); + e.preventDefault(); + }; + } + + render() { + const { intl, account, otherAccounts } = this.props; + const size = this.props.size || 16; + + let menu = []; + + otherAccounts.forEach(account => { + menu.push({ text: intl.formatMessage(messages.switch, { acct: account.get('acct') }), action: this.handleSwitchAccount(account) }); + }); + + if (otherAccounts.size > 0) { + menu.push(null); + } + + menu.push({ text: intl.formatMessage(messages.logout, { acct: account.get('acct') }), to: '/auth/sign_out', action: this.handleLogOut }); + + return ( +