sforkowany z mirror/soapbox
Merge branch 'admin-permission-groups' into 'develop'
Allow promotion/demotion of users to staff roles Closes #687 See merge request soapbox-pub/soapbox-fe!611nank-fix
commit
e580084e9b
|
@ -53,6 +53,16 @@ export const ADMIN_USERS_UNTAG_REQUEST = 'ADMIN_USERS_UNTAG_REQUEST';
|
|||
export const ADMIN_USERS_UNTAG_SUCCESS = 'ADMIN_USERS_UNTAG_SUCCESS';
|
||||
export const ADMIN_USERS_UNTAG_FAIL = 'ADMIN_USERS_UNTAG_FAIL';
|
||||
|
||||
export const ADMIN_ADD_PERMISSION_GROUP_REQUEST = 'ADMIN_ADD_PERMISSION_GROUP_REQUEST';
|
||||
export const ADMIN_ADD_PERMISSION_GROUP_SUCCESS = 'ADMIN_ADD_PERMISSION_GROUP_SUCCESS';
|
||||
export const ADMIN_ADD_PERMISSION_GROUP_FAIL = 'ADMIN_ADD_PERMISSION_GROUP_FAIL';
|
||||
|
||||
export const ADMIN_REMOVE_PERMISSION_GROUP_REQUEST = 'ADMIN_REMOVE_PERMISSION_GROUP_REQUEST';
|
||||
export const ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS = 'ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS';
|
||||
export const ADMIN_REMOVE_PERMISSION_GROUP_FAIL = 'ADMIN_REMOVE_PERMISSION_GROUP_FAIL';
|
||||
|
||||
const nicknamesFromIds = (getState, ids) => ids.map(id => getState().getIn(['accounts', id, 'acct']));
|
||||
|
||||
export function fetchConfig() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: ADMIN_CONFIG_FETCH_REQUEST });
|
||||
|
@ -208,7 +218,7 @@ export function fetchModerationLog(params) {
|
|||
|
||||
export function tagUsers(accountIds, tags) {
|
||||
return (dispatch, getState) => {
|
||||
const nicknames = accountIds.map(id => getState().getIn(['accounts', id, 'acct']));
|
||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||
dispatch({ type: ADMIN_USERS_TAG_REQUEST, accountIds, tags });
|
||||
return api(getState)
|
||||
.put('/api/v1/pleroma/admin/users/tag', { nicknames, tags })
|
||||
|
@ -222,7 +232,7 @@ export function tagUsers(accountIds, tags) {
|
|||
|
||||
export function untagUsers(accountIds, tags) {
|
||||
return (dispatch, getState) => {
|
||||
const nicknames = accountIds.map(id => getState().getIn(['accounts', id, 'acct']));
|
||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||
dispatch({ type: ADMIN_USERS_UNTAG_REQUEST, accountIds, tags });
|
||||
return api(getState)
|
||||
.delete('/api/v1/pleroma/admin/users/tag', { data: { nicknames, tags } })
|
||||
|
@ -233,3 +243,70 @@ export function untagUsers(accountIds, tags) {
|
|||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function verifyUser(accountId) {
|
||||
return (dispatch, getState) => {
|
||||
return dispatch(tagUsers([accountId], ['verified']));
|
||||
};
|
||||
}
|
||||
|
||||
export function unverifyUser(accountId) {
|
||||
return (dispatch, getState) => {
|
||||
return dispatch(untagUsers([accountId], ['verified']));
|
||||
};
|
||||
}
|
||||
|
||||
export function addPermission(accountIds, permissionGroup) {
|
||||
return (dispatch, getState) => {
|
||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||
dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_REQUEST, accountIds, permissionGroup });
|
||||
return api(getState)
|
||||
.post(`/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { nicknames })
|
||||
.then(({ data }) => {
|
||||
dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_SUCCESS, accountIds, permissionGroup, data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_ADD_PERMISSION_GROUP_FAIL, error, accountIds, permissionGroup });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function removePermission(accountIds, permissionGroup) {
|
||||
return (dispatch, getState) => {
|
||||
const nicknames = nicknamesFromIds(getState, accountIds);
|
||||
dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_REQUEST, accountIds, permissionGroup });
|
||||
return api(getState)
|
||||
.delete(`/api/v1/pleroma/admin/users/permission_group/${permissionGroup}`, { data: { nicknames } })
|
||||
.then(({ data }) => {
|
||||
dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS, accountIds, permissionGroup, data });
|
||||
}).catch(error => {
|
||||
dispatch({ type: ADMIN_REMOVE_PERMISSION_GROUP_FAIL, error, accountIds, permissionGroup });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function promoteToAdmin(accountId) {
|
||||
return (dispatch, getState) => {
|
||||
return Promise.all([
|
||||
dispatch(addPermission([accountId], 'admin')),
|
||||
dispatch(removePermission([accountId], 'moderator')),
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
||||
export function promoteToModerator(accountId) {
|
||||
return (dispatch, getState) => {
|
||||
return Promise.all([
|
||||
dispatch(removePermission([accountId], 'admin')),
|
||||
dispatch(addPermission([accountId], 'moderator')),
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
||||
export function demoteToUser(accountId) {
|
||||
return (dispatch, getState) => {
|
||||
return Promise.all([
|
||||
dispatch(removePermission([accountId], 'admin')),
|
||||
dispatch(removePermission([accountId], 'moderator')),
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -8,7 +8,13 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|||
import Icon from 'soapbox/components/icon';
|
||||
import Button from 'soapbox/components/button';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { isStaff } from 'soapbox/utils/accounts';
|
||||
import {
|
||||
isStaff,
|
||||
isAdmin,
|
||||
isModerator,
|
||||
isVerified,
|
||||
isLocal,
|
||||
} from 'soapbox/utils/accounts';
|
||||
import { parseVersion } from 'soapbox/utils/features';
|
||||
import classNames from 'classnames';
|
||||
import Avatar from 'soapbox/components/avatar';
|
||||
|
@ -20,7 +26,6 @@ import { debounce } from 'lodash';
|
|||
import StillImage from 'soapbox/components/still_image';
|
||||
import ActionButton from 'soapbox/features/ui/components/action_button';
|
||||
import SubscriptionButton from 'soapbox/features/ui/components/subscription_button';
|
||||
import { isVerified } from 'soapbox/utils/accounts';
|
||||
import { openModal } from 'soapbox/actions/modal';
|
||||
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||
|
||||
|
@ -54,15 +59,21 @@ const messages = defineMessages({
|
|||
deleteUser: { id: 'admin.users.actions.delete_user', defaultMessage: 'Delete @{name}' },
|
||||
verifyUser: { id: 'admin.users.actions.verify_user', defaultMessage: 'Verify @{name}' },
|
||||
unverifyUser: { id: 'admin.users.actions.unverify_user', defaultMessage: 'Unverify @{name}' },
|
||||
promoteToAdmin: { id: 'admin.users.actions.promote_to_admin', defaultMessage: 'Promote @{name} to an admin' },
|
||||
promoteToModerator: { id: 'admin.users.actions.promote_to_moderator', defaultMessage: 'Promote @{name} to a moderator' },
|
||||
demoteToModerator: { id: 'admin.users.actions.demote_to_moderator', defaultMessage: 'Demote @{name} to a moderator' },
|
||||
demoteToUser: { id: 'admin.users.actions.demote_to_user', defaultMessage: 'Demote @{name} to a regular user' },
|
||||
subscribe: { id: 'account.subscribe', defaultMessage: 'Subscribe to notifications from @{name}' },
|
||||
unsubscribe: { id: 'account.unsubscribe', defaultMessage: 'Unsubscribe to notifications from @{name}' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const me = state.get('me');
|
||||
const account = state.getIn(['accounts', me]);
|
||||
|
||||
return {
|
||||
me,
|
||||
isStaff: isStaff(state.getIn(['accounts', me])),
|
||||
meAccount: account,
|
||||
version: parseVersion(state.getIn(['instance', 'version'])),
|
||||
};
|
||||
};
|
||||
|
@ -73,17 +84,13 @@ class Header extends ImmutablePureComponent {
|
|||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map,
|
||||
meAccount: ImmutablePropTypes.map,
|
||||
identity_props: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired,
|
||||
username: PropTypes.string,
|
||||
isStaff: PropTypes.bool.isRequired,
|
||||
version: PropTypes.object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isStaff: false,
|
||||
}
|
||||
|
||||
state = {
|
||||
isSmallScreen: (window.innerWidth <= 895),
|
||||
}
|
||||
|
@ -129,7 +136,7 @@ class Header extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
makeMenu() {
|
||||
const { account, intl, me, isStaff, version } = this.props;
|
||||
const { account, intl, me, meAccount, version } = this.props;
|
||||
|
||||
let menu = [];
|
||||
|
||||
|
@ -200,9 +207,25 @@ class Header extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
if (isStaff) {
|
||||
if (isStaff(meAccount)) {
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/pleroma/admin/#/users/${account.get('id')}/`, newTab: true });
|
||||
|
||||
if (isAdmin(meAccount)) {
|
||||
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/pleroma/admin/#/users/${account.get('id')}/`, newTab: true });
|
||||
}
|
||||
|
||||
if (account.get('id') !== me && isLocal(account)) {
|
||||
if (isAdmin(account)) {
|
||||
menu.push({ text: intl.formatMessage(messages.demoteToModerator, { name: account.get('username') }), action: this.props.onPromoteToModerator });
|
||||
menu.push({ text: intl.formatMessage(messages.demoteToUser, { name: account.get('username') }), action: this.props.onDemoteToUser });
|
||||
} else if (isModerator(account)) {
|
||||
menu.push({ text: intl.formatMessage(messages.promoteToAdmin, { name: account.get('username') }), action: this.props.onPromoteToAdmin });
|
||||
menu.push({ text: intl.formatMessage(messages.demoteToUser, { name: account.get('username') }), action: this.props.onDemoteToUser });
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.promoteToAdmin, { name: account.get('username') }), action: this.props.onPromoteToAdmin });
|
||||
menu.push({ text: intl.formatMessage(messages.promoteToModerator, { name: account.get('username') }), action: this.props.onPromoteToModerator });
|
||||
}
|
||||
}
|
||||
|
||||
if (isVerified(account)) {
|
||||
menu.push({ text: intl.formatMessage(messages.unverifyUser, { name: account.get('username') }), action: this.props.onUnverifyUser });
|
||||
|
|
|
@ -104,6 +104,18 @@ export default class Header extends ImmutablePureComponent {
|
|||
this.props.onUnverifyUser(this.props.account);
|
||||
}
|
||||
|
||||
handlePromoteToAdmin = () => {
|
||||
this.props.onPromoteToAdmin(this.props.account);
|
||||
}
|
||||
|
||||
handlePromoteToModerator = () => {
|
||||
this.props.onPromoteToModerator(this.props.account);
|
||||
}
|
||||
|
||||
handleDemoteToUser = () => {
|
||||
this.props.onDemoteToUser(this.props.account);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { account, identity_proofs } = this.props;
|
||||
const moved = (account) ? account.get('moved') : false;
|
||||
|
@ -132,6 +144,9 @@ export default class Header extends ImmutablePureComponent {
|
|||
onDeleteUser={this.handleDeleteUser}
|
||||
onVerifyUser={this.handleVerifyUser}
|
||||
onUnverifyUser={this.handleUnverifyUser}
|
||||
onPromoteToAdmin={this.handlePromoteToAdmin}
|
||||
onPromoteToModerator={this.handlePromoteToModerator}
|
||||
onDemoteToUser={this.handleDemoteToUser}
|
||||
username={this.props.username}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
// unpinAccount,
|
||||
subscribeAccount,
|
||||
unsubscribeAccount,
|
||||
|
||||
} from '../../../actions/accounts';
|
||||
import {
|
||||
mentionCompose,
|
||||
|
@ -27,7 +26,14 @@ import { List as ImmutableList } from 'immutable';
|
|||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { startChat, openChat } from 'soapbox/actions/chats';
|
||||
import { deactivateUserModal, deleteUserModal } from 'soapbox/actions/moderation';
|
||||
import { tagUsers, untagUsers } from 'soapbox/actions/admin';
|
||||
import {
|
||||
verifyUser,
|
||||
unverifyUser,
|
||||
promoteToAdmin,
|
||||
promoteToModerator,
|
||||
demoteToUser,
|
||||
} from 'soapbox/actions/admin';
|
||||
import { isAdmin } from 'soapbox/utils/accounts';
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -37,6 +43,11 @@ const messages = defineMessages({
|
|||
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
|
||||
userVerified: { id: 'admin.users.user_verified_message', defaultMessage: '@{acct} was verified' },
|
||||
userUnverified: { id: 'admin.users.user_unverified_message', defaultMessage: '@{acct} was unverified' },
|
||||
promotedToAdmin: { id: 'admin.users.actions.promote_to_admin_message', defaultMessage: '@{acct} was promoted to an admin' },
|
||||
promotedToModerator: { id: 'admin.users.actions.promote_to_moderator_message', defaultMessage: '@{acct} was promoted to a moderator' },
|
||||
demotedToModerator: { id: 'admin.users.actions.demote_to_moderator_message', defaultMessage: '@{acct} was demoted to a moderator' },
|
||||
demotedToUser: { id: 'admin.users.actions.demote_to_user_message', defaultMessage: '@{acct} was demoted to a regular user' },
|
||||
|
||||
});
|
||||
|
||||
const isMobile = width => width <= 1190;
|
||||
|
@ -173,16 +184,43 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
|
||||
onVerifyUser(account) {
|
||||
const message = intl.formatMessage(messages.userVerified, { acct: account.get('acct') });
|
||||
dispatch(tagUsers([account.get('id')], ['verified'])).then(() => {
|
||||
dispatch(snackbar.success(message));
|
||||
}).catch(() => {});
|
||||
|
||||
dispatch(verifyUser(account.get('id')))
|
||||
.then(() => dispatch(snackbar.success(message)))
|
||||
.catch(() => {});
|
||||
},
|
||||
|
||||
onUnverifyUser(account) {
|
||||
const message = intl.formatMessage(messages.userUnverified, { acct: account.get('acct') });
|
||||
dispatch(untagUsers([account.get('id')], ['verified'])).then(() => {
|
||||
dispatch(snackbar.info(message));
|
||||
}).catch(() => {});
|
||||
|
||||
dispatch(unverifyUser(account.get('id')))
|
||||
.then(() => dispatch(snackbar.success(message)))
|
||||
.catch(() => {});
|
||||
},
|
||||
|
||||
onPromoteToAdmin(account) {
|
||||
const message = intl.formatMessage(messages.promotedToAdmin, { acct: account.get('acct') });
|
||||
|
||||
dispatch(promoteToAdmin(account.get('id')))
|
||||
.then(() => dispatch(snackbar.success(message)))
|
||||
.catch(() => {});
|
||||
},
|
||||
|
||||
onPromoteToModerator(account) {
|
||||
const messageType = isAdmin(account) ? messages.demotedToModerator : messages.promotedToModerator;
|
||||
const message = intl.formatMessage(messageType, { acct: account.get('acct') });
|
||||
|
||||
dispatch(promoteToModerator(account.get('id')))
|
||||
.then(() => dispatch(snackbar.success(message)))
|
||||
.catch(() => {});
|
||||
},
|
||||
|
||||
onDemoteToUser(account) {
|
||||
const message = intl.formatMessage(messages.demotedToUser, { acct: account.get('acct') });
|
||||
|
||||
dispatch(demoteToUser(account.get('id')))
|
||||
.then(() => dispatch(snackbar.success(message)))
|
||||
.catch(() => {});
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -46,6 +46,18 @@ class ProfileInfoPanel extends ImmutablePureComponent {
|
|||
displayFqn: PropTypes.bool,
|
||||
};
|
||||
|
||||
getStaffBadge = () => {
|
||||
const { account } = this.props;
|
||||
|
||||
if (isAdmin(account)) {
|
||||
return <Badge slug='admin' title='Admin' />;
|
||||
} else if (isModerator(account)) {
|
||||
return <Badge slug='moderator' title='Moderator' />;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { account, displayFqn, intl, identity_proofs, username } = this.props;
|
||||
|
||||
|
@ -86,8 +98,7 @@ class ProfileInfoPanel extends ImmutablePureComponent {
|
|||
</div>
|
||||
|
||||
<div className='profile-info-panel-content__badges'>
|
||||
{isAdmin(account) && <Badge slug='admin' title='Admin' />}
|
||||
{isModerator(account) && <Badge slug='moderator' title='Moderator' />}
|
||||
{this.getStaffBadge()}
|
||||
{account.getIn(['patron', 'is_patron']) && <Badge slug='patron' title='Patron' />}
|
||||
{account.get('acct').includes('@') || <div className='profile-info-panel-content__badges__join-date'>
|
||||
<Icon id='calendar' />
|
||||
|
|
|
@ -17,6 +17,10 @@ import {
|
|||
ADMIN_USERS_TAG_FAIL,
|
||||
ADMIN_USERS_UNTAG_REQUEST,
|
||||
ADMIN_USERS_UNTAG_FAIL,
|
||||
ADMIN_ADD_PERMISSION_GROUP_REQUEST,
|
||||
ADMIN_ADD_PERMISSION_GROUP_FAIL,
|
||||
ADMIN_REMOVE_PERMISSION_GROUP_REQUEST,
|
||||
ADMIN_REMOVE_PERMISSION_GROUP_FAIL,
|
||||
} from 'soapbox/actions/admin';
|
||||
import { ADMIN_USERS_DELETE_REQUEST } from 'soapbox/actions/admin';
|
||||
|
||||
|
@ -90,6 +94,33 @@ const setDeactivated = (state, nicknames) => {
|
|||
});
|
||||
};
|
||||
|
||||
const permissionGroupFields = {
|
||||
admin: 'is_admin',
|
||||
moderator: 'is_moderator',
|
||||
};
|
||||
|
||||
const addPermission = (state, accountIds, permissionGroup) => {
|
||||
const field = permissionGroupFields[permissionGroup];
|
||||
if (!field) return state;
|
||||
|
||||
return state.withMutations(state => {
|
||||
accountIds.forEach(id => {
|
||||
state.setIn([id, 'pleroma', field], true);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const removePermission = (state, accountIds, permissionGroup) => {
|
||||
const field = permissionGroupFields[permissionGroup];
|
||||
if (!field) return state;
|
||||
|
||||
return state.withMutations(state => {
|
||||
accountIds.forEach(id => {
|
||||
state.setIn([id, 'pleroma', field], false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default function accounts(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case ACCOUNT_IMPORT:
|
||||
|
@ -111,6 +142,12 @@ export default function accounts(state = initialState, action) {
|
|||
case ADMIN_USERS_UNTAG_REQUEST:
|
||||
case ADMIN_USERS_TAG_FAIL:
|
||||
return removeTags(state, action.accountIds, action.tags);
|
||||
case ADMIN_ADD_PERMISSION_GROUP_REQUEST:
|
||||
case ADMIN_REMOVE_PERMISSION_GROUP_FAIL:
|
||||
return addPermission(state, action.accountIds, action.permissionGroup);
|
||||
case ADMIN_REMOVE_PERMISSION_GROUP_REQUEST:
|
||||
case ADMIN_ADD_PERMISSION_GROUP_FAIL:
|
||||
return removePermission(state, action.accountIds, action.permissionGroup);
|
||||
case ADMIN_USERS_DELETE_REQUEST:
|
||||
return setDeactivated(state, action.nicknames);
|
||||
default:
|
||||
|
|
Ładowanie…
Reference in New Issue