diff --git a/frontend/src/components/single-selector.js b/frontend/src/components/single-selector.js index 3f6b72288f..73852227fb 100644 --- a/frontend/src/components/single-selector.js +++ b/frontend/src/components/single-selector.js @@ -8,6 +8,7 @@ const propTypes = { currentSelectedOption: PropTypes.object.isRequired, options: PropTypes.array.isRequired, selectOption: PropTypes.func.isRequired, + operationBeforeSelect: PropTypes.func, toggleItemFreezed: PropTypes.func }; @@ -52,7 +53,11 @@ class Selector extends Component { selectItem = (e, targetItem) => { e.stopPropagation(); - this.props.selectOption(targetItem); + if (this.props.operationBeforeSelect) { + this.props.operationBeforeSelect(); + } else { + this.props.selectOption(targetItem); + } this.togglePopover(); }; diff --git a/frontend/src/pages/org-admin/org-user-item.js b/frontend/src/pages/org-admin/org-user-item.js index 2c6f6f9081..e56bf10a73 100644 --- a/frontend/src/pages/org-admin/org-user-item.js +++ b/frontend/src/pages/org-admin/org-user-item.js @@ -6,6 +6,7 @@ import { seafileAPI } from '../../utils/seafile-api'; import { Utils } from '../../utils/utils'; import toaster from '../../components/toast'; import Selector from '../../components/single-selector'; +import CommonOperationConfirmationDialog from '../../components/dialog/common-operation-confirmation-dialog'; const propTypes = { user: PropTypes.object, @@ -84,6 +85,11 @@ class UserItem extends React.Component { this.props.changeStatus(this.props.user.email, isActive); }; + setUserInactive = () => { + const isActive = false; + this.props.changeStatus(this.props.user.email, isActive); + }; + onDropdownToggleClick = (e) => { e.preventDefault(); this.toggleOperationMenu(e); @@ -126,8 +132,12 @@ class UserItem extends React.Component { } }; + toggleConfirmInactiveDialog= () => { + this.setState({isConfirmInactiveDialogOpen: !this.state.isConfirmInactiveDialogOpen}); + }; + render() { - const { highlight } = this.state; + const { highlight, isConfirmInactiveDialogOpen } = this.state; let { user, currentTab } = this.props; let href = siteRoot + 'org/useradmin/info/' + encodeURIComponent(user.email) + '/'; let isOperationMenuShow = (user.email !== username) && this.state.showMenu; @@ -143,47 +153,62 @@ class UserItem extends React.Component { }); const currentSelectedStatusOption = this.statusOptions.filter(item => item.isSelected)[0]; + const itemName = '' + Utils.HTMLescape(user.name) + ''; + const confirmSetUserInactiveMsg = gettext('Are you sure you want to set {user_placeholder} inactive?').replace('{user_placeholder}', itemName); + return ( - - - {user.name} - - - - - {`${Utils.formatSize({bytes: user.quota_usage})} / ${this.getQuotaTotal(user.quota_total)}`} - - {user.ctime} / -
- {user.last_login ? user.last_login : '--'} - - - {isOperationMenuShow && ( - - - - {gettext('Delete')} - {gettext('ResetPwd')} - {currentTab == 'admins' && {gettext('Revoke Admin')}} - - - )} - - + <> + + + {user.name} + + + + + {`${Utils.formatSize({bytes: user.quota_usage})} / ${this.getQuotaTotal(user.quota_total)}`} + + {user.ctime} / +
+ {user.last_login ? user.last_login : '--'} + + + {isOperationMenuShow && ( + + + + {gettext('Delete')} + {gettext('ResetPwd')} + {currentTab == 'admins' && {gettext('Revoke Admin')}} + + + )} + + + {isConfirmInactiveDialogOpen && + + } + ); } } diff --git a/frontend/src/pages/sys-admin/orgs/org-users.js b/frontend/src/pages/sys-admin/orgs/org-users.js index b9ca94a92a..b4965de6b9 100644 --- a/frontend/src/pages/sys-admin/orgs/org-users.js +++ b/frontend/src/pages/sys-admin/orgs/org-users.js @@ -158,10 +158,18 @@ class Item extends Component { this.setState({isResetPasswordDialogOpen: !this.state.isResetPasswordDialogOpen}); }; + toggleConfirmInactiveDialog= () => { + this.setState({isConfirmInactiveDialogOpen: !this.state.isConfirmInactiveDialogOpen}); + }; + updateStatus= (statusOption) => { this.props.updateStatus(this.props.item.email, statusOption.value); }; + setUserInactive = () => { + this.props.updateStatus(this.props.item.email, 'inactive'); + }; + updateMembership= (membershipOption) => { this.props.updateMembership(this.props.item.email, membershipOption.value); }; @@ -214,11 +222,12 @@ class Item extends Component { render() { const { item } = this.props; - const { highlight, isOpIconShown, isDeleteDialogOpen, isResetPasswordDialogOpen } = this.state; + const { highlight, isOpIconShown, isDeleteDialogOpen, isResetPasswordDialogOpen, isConfirmInactiveDialogOpen } = this.state; const itemName = '' + Utils.HTMLescape(item.name) + ''; let deleteDialogMsg = gettext('Are you sure you want to delete {placeholder} ?').replace('{placeholder}', itemName); let resetPasswordDialogMsg = gettext('Are you sure you want to reset the password of {placeholder} ?').replace('{placeholder}', itemName); + const confirmSetUserInactiveMsg = gettext('Are you sure you want to set {user_placeholder} inactive?').replace('{user_placeholder}', itemName); // for 'user status' const curStatus = item.active ? 'active' : 'inactive'; @@ -253,6 +262,7 @@ class Item extends Component { options={this.statusOptions} selectOption={this.updateStatus} toggleItemFreezed={this.props.toggleItemFreezed} + operationBeforeSelect={item.active ? this.toggleConfirmInactiveDialog : undefined} /> @@ -298,6 +308,15 @@ class Item extends Component { toggleDialog={this.toggleResetPasswordDialog} /> } + {isConfirmInactiveDialogOpen && + + } ); } diff --git a/frontend/src/pages/sys-admin/users/users-content.js b/frontend/src/pages/sys-admin/users/users-content.js index 5b616581ff..47b352c317 100644 --- a/frontend/src/pages/sys-admin/users/users-content.js +++ b/frontend/src/pages/sys-admin/users/users-content.js @@ -200,7 +200,8 @@ class Item extends Component { isSetQuotaDialogOpen: false, isDeleteUserDialogOpen: false, isResetUserPasswordDialogOpen: false, - isRevokeAdminDialogOpen: false + isRevokeAdminDialogOpen: false, + isConfirmInactiveDialogOpen: false }; } @@ -246,6 +247,10 @@ class Item extends Component { this.setState({isRevokeAdminDialogOpen: !this.state.isRevokeAdminDialogOpen}); }; + toggleConfirmInactiveDialog= () => { + this.setState({isConfirmInactiveDialogOpen: !this.state.isConfirmInactiveDialogOpen}); + }; + onUserSelected = () => { this.props.onUserSelected(this.props.item); }; @@ -258,6 +263,10 @@ class Item extends Component { this.props.updateUser(this.props.item.email, 'is_active', isActive); }; + setUserInactive = () => { + this.props.updateUser(this.props.item.email, 'is_active', false); + }; + updateRole = (roleOption) => { this.props.updateUser(this.props.item.email, 'role', roleOption.value); }; @@ -386,13 +395,15 @@ class Item extends Component { isSetQuotaDialogOpen, isDeleteUserDialogOpen, isResetUserPasswordDialogOpen, - isRevokeAdminDialogOpen + isRevokeAdminDialogOpen, + isConfirmInactiveDialogOpen } = this.state; const itemName = '' + Utils.HTMLescape(item.name) + ''; const deleteDialogMsg = gettext('Are you sure you want to delete {placeholder} ?').replace('{placeholder}', itemName); const resetPasswordDialogMsg = gettext('Are you sure you want to reset the password of {placeholder} ?').replace('{placeholder}', itemName); const revokeAdminDialogMsg = gettext('Are you sure you want to revoke the admin permission of {placeholder} ?').replace('{placeholder}', itemName); + const confirmSetUserInactiveMsg = gettext('Are you sure you want to set {user_placeholder} inactive?').replace('{user_placeholder}', itemName); // for 'user status' const curStatus = item.is_active ? 'active' : 'inactive'; @@ -473,6 +484,7 @@ class Item extends Component { options={this.statusOptions} selectOption={this.updateStatus} toggleItemFreezed={this.props.toggleItemFreezed} + operationBeforeSelect={item.is_active ? this.toggleConfirmInactiveDialog : undefined} /> {isPro && @@ -567,6 +579,15 @@ class Item extends Component { toggleDialog={this.toggleRevokeAdminDialog} /> } + {isConfirmInactiveDialogOpen && + + } ); } diff --git a/seahub/api2/endpoints/admin/account.py b/seahub/api2/endpoints/admin/account.py index a26d7f1e01..37fcd9902a 100644 --- a/seahub/api2/endpoints/admin/account.py +++ b/seahub/api2/endpoints/admin/account.py @@ -282,6 +282,13 @@ class Account(APIView): 'is_active invalid.') user.is_active = is_active + if is_active == False: + # del tokens and personal repo api tokens (not department) + from seahub.utils import inactive_user + try: + inactive_user(email) + except Exception as e: + logger.error("Failed to inactive_user %s: %s." % (email, e)) # update password password = request.data.get("password", None) diff --git a/seahub/api2/endpoints/admin/users.py b/seahub/api2/endpoints/admin/users.py index 27d5e9f6c4..f85ceacf6d 100644 --- a/seahub/api2/endpoints/admin/users.py +++ b/seahub/api2/endpoints/admin/users.py @@ -355,9 +355,18 @@ def update_user_info(request, user, password, is_active, is_staff, role, quota_total_mb, institution_name, upload_rate_limit, download_rate_limit): + email = user.username + # update basic user info if is_active is not None: user.is_active = is_active + if is_active == False: + # del tokens and personal repo api tokens (not department) + from seahub.utils import inactive_user + try: + inactive_user(email) + except Exception as e: + logger.error("Failed to inactive_user %s: %s." % (email, e)) if password: user.set_password(password) @@ -368,8 +377,6 @@ def update_user_info(request, user, password, is_active, is_staff, role, # update user user.save() - email = user.username - # update additional user info if is_pro_version() and role: User.objects.update_role(email, role) diff --git a/seahub/organizations/api/admin/users.py b/seahub/organizations/api/admin/users.py index 36dc38f74b..052700f355 100644 --- a/seahub/organizations/api/admin/users.py +++ b/seahub/organizations/api/admin/users.py @@ -442,6 +442,13 @@ class OrgAdminUser(APIView): user.is_active = is_active == 'true' user.save() + if not is_active == 'true': + # del tokens and personal repo api tokens (not department) + from seahub.utils import inactive_user + try: + inactive_user(email) + except Exception as e: + logger.error("Failed to inactive_user %s: %s." % (email, e)) # update quota_total quota_total_mb = request.data.get("quota_total", None) diff --git a/seahub/utils/__init__.py b/seahub/utils/__init__.py index 760dbb821f..e192ac1ce4 100644 --- a/seahub/utils/__init__.py +++ b/seahub/utils/__init__.py @@ -18,7 +18,7 @@ from urllib.parse import urlparse, urljoin from constance import config import seaserv -from seaserv import seafile_api +from seaserv import seafile_api, ccnet_api from django.urls import reverse from django.core.mail import EmailMessage @@ -1280,6 +1280,38 @@ def clear_token(username): TokenV2.objects.filter(user = username).delete() seafile_api.delete_repo_tokens_by_email(username) + +def inactive_user(username): + # del tokens and personal repo api tokens (not department) + from seahub.repo_api_tokens.models import RepoAPITokens + try: + clear_token(username) + except Exception as e: + logger.error("Failed to delete tokens for user %s: %s." % (username, e)) + try: + from seahub.settings import MULTI_TENANCY + except ImportError: + MULTI_TENANCY = False + try: + org_id = -1 + if MULTI_TENANCY: + orgs = ccnet_api.get_orgs_by_user(username) + if orgs: + org = orgs[0] + org_id = org.org_id + if org_id > 0: + owned_repos = seafile_api.get_org_owned_repo_list( + org_id, username, ret_corrupted=True) + else: + owned_repos = seafile_api.get_owned_repo_list( + username, ret_corrupted=True) + owned_repo_ids = [item.repo_id for item in owned_repos] + RepoAPITokens.objects.filter( + repo_id__in=owned_repo_ids).delete() + except Exception as e: + logger.error("Failed to delete repo api tokens for user %s: %s." % (username, e)) + + def send_perm_audit_msg(etype, from_user, to, repo_id, path, perm): """Send repo permission audit msg.