Refactor auth to support multiple accounts
This commit is contained in:
parent
8e386ddfd4
commit
0162eac662
|
@ -1,11 +1,17 @@
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
import snackbar from 'soapbox/actions/snackbar';
|
import snackbar from 'soapbox/actions/snackbar';
|
||||||
|
|
||||||
|
export const SWITCH_ACCOUNT = 'SWITCH_ACCOUNT';
|
||||||
|
|
||||||
export const AUTH_APP_CREATED = 'AUTH_APP_CREATED';
|
export const AUTH_APP_CREATED = 'AUTH_APP_CREATED';
|
||||||
export const AUTH_APP_AUTHORIZED = 'AUTH_APP_AUTHORIZED';
|
export const AUTH_APP_AUTHORIZED = 'AUTH_APP_AUTHORIZED';
|
||||||
export const AUTH_LOGGED_IN = 'AUTH_LOGGED_IN';
|
export const AUTH_LOGGED_IN = 'AUTH_LOGGED_IN';
|
||||||
export const AUTH_LOGGED_OUT = 'AUTH_LOGGED_OUT';
|
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_REQUEST = 'AUTH_REGISTER_REQUEST';
|
||||||
export const AUTH_REGISTER_SUCCESS = 'AUTH_REGISTER_SUCCESS';
|
export const AUTH_REGISTER_SUCCESS = 'AUTH_REGISTER_SUCCESS';
|
||||||
export const AUTH_REGISTER_FAIL = 'AUTH_REGISTER_FAIL';
|
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) {
|
export function logIn(username, password) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
return dispatch(createAppAndToken()).then(() => {
|
return dispatch(createAppAndToken()).then(() => {
|
||||||
|
@ -161,6 +188,10 @@ export function logOut() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function switchAccount(accountId) {
|
||||||
|
return { type: SWITCH_ACCOUNT, accountId };
|
||||||
|
}
|
||||||
|
|
||||||
export function register(params) {
|
export function register(params) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
params.fullname = params.username;
|
params.fullname = params.username;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
import { importFetchedAccount } from './importer';
|
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_REQUEST = 'ME_FETCH_REQUEST';
|
||||||
export const ME_FETCH_SUCCESS = 'ME_FETCH_SUCCESS';
|
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_SUCCESS = 'ME_PATCH_SUCCESS';
|
||||||
export const ME_PATCH_FAIL = 'ME_PATCH_FAIL';
|
export const ME_PATCH_FAIL = 'ME_PATCH_FAIL';
|
||||||
|
|
||||||
const hasToken = getState => getState().hasIn(['auth', 'user', 'access_token']);
|
|
||||||
const noOp = () => new Promise(f => f());
|
const noOp = () => new Promise(f => f());
|
||||||
|
|
||||||
export function fetchMe() {
|
export function fetchMe() {
|
||||||
return (dispatch, getState) => {
|
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({ type: ME_FETCH_SKIP }); return noOp();
|
||||||
};
|
};
|
||||||
|
|
||||||
dispatch(fetchMeRequest());
|
dispatch(fetchMeRequest());
|
||||||
|
return dispatch(verifyCredentials(token)).then(account => {
|
||||||
return api(getState).get('/api/v1/accounts/verify_credentials').then(response => {
|
dispatch(fetchMeSuccess(account));
|
||||||
dispatch(fetchMeSuccess(response.data));
|
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchMeFail(error));
|
dispatch(fetchMeFail(error));
|
||||||
});
|
});;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,15 @@ export const getLinks = response => {
|
||||||
return LinkHeader.parse(value);
|
return LinkHeader.parse(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getToken = (getState, authType) =>
|
const getToken = (getState, authType) => {
|
||||||
getState().getIn(['auth', authType, 'access_token']);
|
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') => {
|
export default (getState, authType = 'user') => {
|
||||||
const accessToken = getToken(getState, authType);
|
const accessToken = getToken(getState, authType);
|
||||||
|
|
|
@ -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 (
|
||||||
|
<div className='compose__action-bar' style={{ 'marginTop':'-6px' }}>
|
||||||
|
<div className='compose__action-bar-dropdown'>
|
||||||
|
<DropdownMenuContainer items={menu} icon='chevron-down' size={size} direction='right' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(connect(mapStateToProps)(ProfileDropdown));
|
|
@ -8,7 +8,7 @@ import classNames from 'classnames';
|
||||||
import IconWithCounter from 'soapbox/components/icon_with_counter';
|
import IconWithCounter from 'soapbox/components/icon_with_counter';
|
||||||
import SearchContainer from 'soapbox/features/compose/containers/search_container';
|
import SearchContainer from 'soapbox/features/compose/containers/search_container';
|
||||||
import Avatar from '../../../components/avatar';
|
import Avatar from '../../../components/avatar';
|
||||||
import ActionBar from 'soapbox/features/compose/components/action_bar';
|
import ProfileDropdown from './profile_dropdown';
|
||||||
import { openModal } from '../../../actions/modal';
|
import { openModal } from '../../../actions/modal';
|
||||||
import { openSidebar } from '../../../actions/sidebar';
|
import { openSidebar } from '../../../actions/sidebar';
|
||||||
import Icon from '../../../components/icon';
|
import Icon from '../../../components/icon';
|
||||||
|
@ -126,7 +126,7 @@ class TabsBar extends React.PureComponent {
|
||||||
<div className='tabs-bar__profile'>
|
<div className='tabs-bar__profile'>
|
||||||
<Avatar account={account} />
|
<Avatar account={account} />
|
||||||
<button className='tabs-bar__sidebar-btn' onClick={onOpenSidebar} />
|
<button className='tabs-bar__sidebar-btn' onClick={onOpenSidebar} />
|
||||||
<ActionBar account={account} size={34} />
|
<ProfileDropdown account={account} size={34} />
|
||||||
</div>
|
</div>
|
||||||
<button className='tabs-bar__button-compose button' onClick={onOpenCompose} aria-label={intl.formatMessage(messages.post)}>
|
<button className='tabs-bar__button-compose button' onClick={onOpenCompose} aria-label={intl.formatMessage(messages.post)}>
|
||||||
<span>{intl.formatMessage(messages.post)}</span>
|
<span>{intl.formatMessage(messages.post)}</span>
|
||||||
|
|
|
@ -5,12 +5,14 @@ import {
|
||||||
AUTH_LOGGED_OUT,
|
AUTH_LOGGED_OUT,
|
||||||
FETCH_TOKENS_SUCCESS,
|
FETCH_TOKENS_SUCCESS,
|
||||||
REVOKE_TOKEN_SUCCESS,
|
REVOKE_TOKEN_SUCCESS,
|
||||||
|
SWITCH_ACCOUNT,
|
||||||
} from '../actions/auth';
|
} from '../actions/auth';
|
||||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
app: ImmutableMap(JSON.parse(localStorage.getItem('soapbox:auth:app'))),
|
app: ImmutableMap(JSON.parse(localStorage.getItem('soapbox:auth:app'))),
|
||||||
user: ImmutableMap(JSON.parse(localStorage.getItem('soapbox:auth:user'))),
|
users: fromJS(JSON.parse(localStorage.getItem('soapbox:auth:users'))),
|
||||||
|
me: localStorage.getItem('soapbox:auth:me'),
|
||||||
tokens: ImmutableList(),
|
tokens: ImmutableList(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -24,7 +26,6 @@ export default function auth(state = initialState, action) {
|
||||||
localStorage.setItem('soapbox:auth:app', JSON.stringify(merged)); // TODO: Better persistence
|
localStorage.setItem('soapbox:auth:app', JSON.stringify(merged)); // TODO: Better persistence
|
||||||
return state.set('app', merged);
|
return state.set('app', merged);
|
||||||
case AUTH_LOGGED_IN:
|
case AUTH_LOGGED_IN:
|
||||||
localStorage.setItem('soapbox:auth:user', JSON.stringify(action.user)); // TODO: Better persistence
|
|
||||||
return state.set('user', ImmutableMap(action.user));
|
return state.set('user', ImmutableMap(action.user));
|
||||||
case AUTH_LOGGED_OUT:
|
case AUTH_LOGGED_OUT:
|
||||||
localStorage.removeItem('soapbox:auth:user');
|
localStorage.removeItem('soapbox:auth:user');
|
||||||
|
@ -34,6 +35,10 @@ export default function auth(state = initialState, action) {
|
||||||
case REVOKE_TOKEN_SUCCESS:
|
case REVOKE_TOKEN_SUCCESS:
|
||||||
const idx = state.get('tokens').findIndex(t => t.get('id') === action.id);
|
const idx = state.get('tokens').findIndex(t => t.get('id') === action.id);
|
||||||
return state.deleteIn(['tokens', idx]);
|
return state.deleteIn(['tokens', idx]);
|
||||||
|
case SWITCH_ACCOUNT:
|
||||||
|
localStorage.setItem('soapbox:auth:me', action.accountId);
|
||||||
|
location.reload();
|
||||||
|
return state;
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,6 @@ export default function me(state = initialState, action) {
|
||||||
case ME_FETCH_FAIL:
|
case ME_FETCH_FAIL:
|
||||||
case ME_FETCH_SKIP:
|
case ME_FETCH_SKIP:
|
||||||
case AUTH_LOGGED_OUT:
|
case AUTH_LOGGED_OUT:
|
||||||
localStorage.removeItem('soapbox:auth:user');
|
|
||||||
return false;
|
return false;
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'soapbox/actions/me';
|
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'soapbox/actions/me';
|
||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
import { VERIFY_CREDENTIALS_SUCCESS } from 'soapbox/actions/auth';
|
||||||
|
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
const initialState = ImmutableMap();
|
||||||
|
|
||||||
|
@ -10,11 +11,16 @@ export default function meta(state = initialState, action) {
|
||||||
case ME_FETCH_SUCCESS:
|
case ME_FETCH_SUCCESS:
|
||||||
case ME_PATCH_SUCCESS:
|
case ME_PATCH_SUCCESS:
|
||||||
const me = fromJS(action.me);
|
const me = fromJS(action.me);
|
||||||
if (me.has('pleroma')) {
|
return state.withMutations(state => {
|
||||||
const pleroPrefs = me.get('pleroma').delete('settings_store');
|
state.set('me', me.get('id'));
|
||||||
return state.mergeIn(['pleroma'], pleroPrefs);
|
state.update('users', ImmutableOrderedSet(), v => v.add(me.get('id')));
|
||||||
}
|
if (me.has('pleroma')) {
|
||||||
return state;
|
const pleroPrefs = me.get('pleroma').delete('settings_store');
|
||||||
|
state.mergeIn(['pleroma'], pleroPrefs);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
case VERIFY_CREDENTIALS_SUCCESS:
|
||||||
|
return state.update('users', ImmutableOrderedSet(), v => v.add(action.account.id));
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,6 @@
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-transform: capitalize;
|
|
||||||
color: var(--primary-text-color);
|
color: var(--primary-text-color);
|
||||||
|
|
||||||
&:focus,
|
&:focus,
|
||||||
|
|
Loading…
Reference in New Issue