Merge branch 'admin-permission-groups' into 'develop'

Allow promotion/demotion of users to staff roles

Closes #687

See merge request soapbox-pub/soapbox-fe!611
nank-fix
Alex Gleason 2021-07-13 17:55:12 +00:00
commit e580084e9b
6 zmienionych plików z 224 dodań i 23 usunięć

Wyświetl plik

@ -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')),
]);
};
}

Wyświetl plik

@ -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 });

Wyświetl plik

@ -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>

Wyświetl plik

@ -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(() => {});
},
});

Wyświetl plik

@ -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' />

Wyświetl plik

@ -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: