1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-11 03:41:12 +00:00

update inactive user (#6019)

* update inactive user

* is_active del RepoAPITokens

* update del_repo_api_token

* add inactive_user

* [user admin] add confirmation to 'set user inactive'

* system admin - users
* system admin - organizations - org - member
* org admin - users

---------

Co-authored-by: llj <lingjun.li1@gmail.com>
This commit is contained in:
欢乐马
2024-04-23 16:26:31 +08:00
committed by GitHub
parent aa40b202d1
commit cd7329e711
8 changed files with 171 additions and 48 deletions

View File

@@ -8,6 +8,7 @@ const propTypes = {
currentSelectedOption: PropTypes.object.isRequired, currentSelectedOption: PropTypes.object.isRequired,
options: PropTypes.array.isRequired, options: PropTypes.array.isRequired,
selectOption: PropTypes.func.isRequired, selectOption: PropTypes.func.isRequired,
operationBeforeSelect: PropTypes.func,
toggleItemFreezed: PropTypes.func toggleItemFreezed: PropTypes.func
}; };
@@ -52,7 +53,11 @@ class Selector extends Component {
selectItem = (e, targetItem) => { selectItem = (e, targetItem) => {
e.stopPropagation(); e.stopPropagation();
this.props.selectOption(targetItem); if (this.props.operationBeforeSelect) {
this.props.operationBeforeSelect();
} else {
this.props.selectOption(targetItem);
}
this.togglePopover(); this.togglePopover();
}; };

View File

@@ -6,6 +6,7 @@ import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils'; import { Utils } from '../../utils/utils';
import toaster from '../../components/toast'; import toaster from '../../components/toast';
import Selector from '../../components/single-selector'; import Selector from '../../components/single-selector';
import CommonOperationConfirmationDialog from '../../components/dialog/common-operation-confirmation-dialog';
const propTypes = { const propTypes = {
user: PropTypes.object, user: PropTypes.object,
@@ -84,6 +85,11 @@ class UserItem extends React.Component {
this.props.changeStatus(this.props.user.email, isActive); this.props.changeStatus(this.props.user.email, isActive);
}; };
setUserInactive = () => {
const isActive = false;
this.props.changeStatus(this.props.user.email, isActive);
};
onDropdownToggleClick = (e) => { onDropdownToggleClick = (e) => {
e.preventDefault(); e.preventDefault();
this.toggleOperationMenu(e); this.toggleOperationMenu(e);
@@ -126,8 +132,12 @@ class UserItem extends React.Component {
} }
}; };
toggleConfirmInactiveDialog= () => {
this.setState({isConfirmInactiveDialogOpen: !this.state.isConfirmInactiveDialogOpen});
};
render() { render() {
const { highlight } = this.state; const { highlight, isConfirmInactiveDialogOpen } = this.state;
let { user, currentTab } = this.props; let { user, currentTab } = this.props;
let href = siteRoot + 'org/useradmin/info/' + encodeURIComponent(user.email) + '/'; let href = siteRoot + 'org/useradmin/info/' + encodeURIComponent(user.email) + '/';
let isOperationMenuShow = (user.email !== username) && this.state.showMenu; 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 currentSelectedStatusOption = this.statusOptions.filter(item => item.isSelected)[0];
const itemName = '<span class="op-target">' + Utils.HTMLescape(user.name) + '</span>';
const confirmSetUserInactiveMsg = gettext('Are you sure you want to set {user_placeholder} inactive?').replace('{user_placeholder}', itemName);
return ( return (
<tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}> <>
<td> <tr className={this.state.highlight ? 'tr-highlight' : ''} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
<a href={href}>{user.name}</a> <td>
</td> <a href={href}>{user.name}</a>
<td> </td>
<Selector <td>
isDropdownToggleShown={highlight} <Selector
currentSelectedOption={currentSelectedStatusOption} isDropdownToggleShown={highlight}
options={this.statusOptions} currentSelectedOption={currentSelectedStatusOption}
selectOption={this.changeStatus} options={this.statusOptions}
toggleItemFreezed={this.props.toggleItemFreezed} selectOption={this.changeStatus}
/> toggleItemFreezed={this.props.toggleItemFreezed}
</td> operationBeforeSelect={user.is_active ? this.toggleConfirmInactiveDialog : undefined}
<td>{`${Utils.formatSize({bytes: user.quota_usage})} / ${this.getQuotaTotal(user.quota_total)}`}</td> />
<td> </td>
{user.ctime} / <td>{`${Utils.formatSize({bytes: user.quota_usage})} / ${this.getQuotaTotal(user.quota_total)}`}</td>
<br /> <td>
{user.last_login ? user.last_login : '--'} {user.ctime} /
</td> <br />
<td className="text-center cursor-pointer"> {user.last_login ? user.last_login : '--'}
{isOperationMenuShow && ( </td>
<Dropdown isOpen={this.state.isItemMenuShow} toggle={this.toggleOperationMenu}> <td className="text-center cursor-pointer">
<DropdownToggle {isOperationMenuShow && (
tag="a" <Dropdown isOpen={this.state.isItemMenuShow} toggle={this.toggleOperationMenu}>
className="attr-action-icon fas fa-ellipsis-v" <DropdownToggle
title={gettext('More operations')} tag="a"
aria-label={gettext('More operations')} className="attr-action-icon fas fa-ellipsis-v"
data-toggle="dropdown" title={gettext('More operations')}
aria-expanded={this.state.isItemMenuShow} aria-label={gettext('More operations')}
onClick={this.onDropdownToggleClick} data-toggle="dropdown"
/> aria-expanded={this.state.isItemMenuShow}
<DropdownMenu> onClick={this.onDropdownToggleClick}
<DropdownItem onClick={this.toggleDelete}>{gettext('Delete')}</DropdownItem> />
<DropdownItem onClick={this.toggleResetPW}>{gettext('ResetPwd')}</DropdownItem> <DropdownMenu>
{currentTab == 'admins' && <DropdownItem onClick={this.toggleRevokeAdmin}>{gettext('Revoke Admin')}</DropdownItem>} <DropdownItem onClick={this.toggleDelete}>{gettext('Delete')}</DropdownItem>
</DropdownMenu> <DropdownItem onClick={this.toggleResetPW}>{gettext('ResetPwd')}</DropdownItem>
</Dropdown> {currentTab == 'admins' && <DropdownItem onClick={this.toggleRevokeAdmin}>{gettext('Revoke Admin')}</DropdownItem>}
)} </DropdownMenu>
</td> </Dropdown>
</tr> )}
</td>
</tr>
{isConfirmInactiveDialogOpen &&
<CommonOperationConfirmationDialog
title={gettext('Set user inactive')}
message={confirmSetUserInactiveMsg}
executeOperation={this.setUserInactive}
confirmBtnText={gettext('Set')}
toggleDialog={this.toggleConfirmInactiveDialog}
/>
}
</>
); );
} }
} }

View File

@@ -158,10 +158,18 @@ class Item extends Component {
this.setState({isResetPasswordDialogOpen: !this.state.isResetPasswordDialogOpen}); this.setState({isResetPasswordDialogOpen: !this.state.isResetPasswordDialogOpen});
}; };
toggleConfirmInactiveDialog= () => {
this.setState({isConfirmInactiveDialogOpen: !this.state.isConfirmInactiveDialogOpen});
};
updateStatus= (statusOption) => { updateStatus= (statusOption) => {
this.props.updateStatus(this.props.item.email, statusOption.value); this.props.updateStatus(this.props.item.email, statusOption.value);
}; };
setUserInactive = () => {
this.props.updateStatus(this.props.item.email, 'inactive');
};
updateMembership= (membershipOption) => { updateMembership= (membershipOption) => {
this.props.updateMembership(this.props.item.email, membershipOption.value); this.props.updateMembership(this.props.item.email, membershipOption.value);
}; };
@@ -214,11 +222,12 @@ class Item extends Component {
render() { render() {
const { item } = this.props; const { item } = this.props;
const { highlight, isOpIconShown, isDeleteDialogOpen, isResetPasswordDialogOpen } = this.state; const { highlight, isOpIconShown, isDeleteDialogOpen, isResetPasswordDialogOpen, isConfirmInactiveDialogOpen } = this.state;
const itemName = '<span class="op-target">' + Utils.HTMLescape(item.name) + '</span>'; const itemName = '<span class="op-target">' + Utils.HTMLescape(item.name) + '</span>';
let deleteDialogMsg = gettext('Are you sure you want to delete {placeholder} ?').replace('{placeholder}', itemName); 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); 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' // for 'user status'
const curStatus = item.active ? 'active' : 'inactive'; const curStatus = item.active ? 'active' : 'inactive';
@@ -253,6 +262,7 @@ class Item extends Component {
options={this.statusOptions} options={this.statusOptions}
selectOption={this.updateStatus} selectOption={this.updateStatus}
toggleItemFreezed={this.props.toggleItemFreezed} toggleItemFreezed={this.props.toggleItemFreezed}
operationBeforeSelect={item.active ? this.toggleConfirmInactiveDialog : undefined}
/> />
</td> </td>
<td> <td>
@@ -298,6 +308,15 @@ class Item extends Component {
toggleDialog={this.toggleResetPasswordDialog} toggleDialog={this.toggleResetPasswordDialog}
/> />
} }
{isConfirmInactiveDialogOpen &&
<CommonOperationConfirmationDialog
title={gettext('Set user inactive')}
message={confirmSetUserInactiveMsg}
executeOperation={this.setUserInactive}
confirmBtnText={gettext('Set')}
toggleDialog={this.toggleConfirmInactiveDialog}
/>
}
</Fragment> </Fragment>
); );
} }

View File

@@ -200,7 +200,8 @@ class Item extends Component {
isSetQuotaDialogOpen: false, isSetQuotaDialogOpen: false,
isDeleteUserDialogOpen: false, isDeleteUserDialogOpen: false,
isResetUserPasswordDialogOpen: false, isResetUserPasswordDialogOpen: false,
isRevokeAdminDialogOpen: false isRevokeAdminDialogOpen: false,
isConfirmInactiveDialogOpen: false
}; };
} }
@@ -246,6 +247,10 @@ class Item extends Component {
this.setState({isRevokeAdminDialogOpen: !this.state.isRevokeAdminDialogOpen}); this.setState({isRevokeAdminDialogOpen: !this.state.isRevokeAdminDialogOpen});
}; };
toggleConfirmInactiveDialog= () => {
this.setState({isConfirmInactiveDialogOpen: !this.state.isConfirmInactiveDialogOpen});
};
onUserSelected = () => { onUserSelected = () => {
this.props.onUserSelected(this.props.item); this.props.onUserSelected(this.props.item);
}; };
@@ -258,6 +263,10 @@ class Item extends Component {
this.props.updateUser(this.props.item.email, 'is_active', isActive); this.props.updateUser(this.props.item.email, 'is_active', isActive);
}; };
setUserInactive = () => {
this.props.updateUser(this.props.item.email, 'is_active', false);
};
updateRole = (roleOption) => { updateRole = (roleOption) => {
this.props.updateUser(this.props.item.email, 'role', roleOption.value); this.props.updateUser(this.props.item.email, 'role', roleOption.value);
}; };
@@ -386,13 +395,15 @@ class Item extends Component {
isSetQuotaDialogOpen, isSetQuotaDialogOpen,
isDeleteUserDialogOpen, isDeleteUserDialogOpen,
isResetUserPasswordDialogOpen, isResetUserPasswordDialogOpen,
isRevokeAdminDialogOpen isRevokeAdminDialogOpen,
isConfirmInactiveDialogOpen
} = this.state; } = this.state;
const itemName = '<span class="op-target">' + Utils.HTMLescape(item.name) + '</span>'; const itemName = '<span class="op-target">' + Utils.HTMLescape(item.name) + '</span>';
const deleteDialogMsg = gettext('Are you sure you want to delete {placeholder} ?').replace('{placeholder}', itemName); 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 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 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' // for 'user status'
const curStatus = item.is_active ? 'active' : 'inactive'; const curStatus = item.is_active ? 'active' : 'inactive';
@@ -473,6 +484,7 @@ class Item extends Component {
options={this.statusOptions} options={this.statusOptions}
selectOption={this.updateStatus} selectOption={this.updateStatus}
toggleItemFreezed={this.props.toggleItemFreezed} toggleItemFreezed={this.props.toggleItemFreezed}
operationBeforeSelect={item.is_active ? this.toggleConfirmInactiveDialog : undefined}
/> />
</td> </td>
{isPro && {isPro &&
@@ -567,6 +579,15 @@ class Item extends Component {
toggleDialog={this.toggleRevokeAdminDialog} toggleDialog={this.toggleRevokeAdminDialog}
/> />
} }
{isConfirmInactiveDialogOpen &&
<CommonOperationConfirmationDialog
title={gettext('Set user inactive')}
message={confirmSetUserInactiveMsg}
executeOperation={this.setUserInactive}
confirmBtnText={gettext('Set')}
toggleDialog={this.toggleConfirmInactiveDialog}
/>
}
</Fragment> </Fragment>
); );
} }

View File

@@ -282,6 +282,13 @@ class Account(APIView):
'is_active invalid.') 'is_active invalid.')
user.is_active = is_active 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 # update password
password = request.data.get("password", None) password = request.data.get("password", None)

View File

@@ -355,9 +355,18 @@ def update_user_info(request, user, password, is_active, is_staff, role,
quota_total_mb, institution_name, quota_total_mb, institution_name,
upload_rate_limit, download_rate_limit): upload_rate_limit, download_rate_limit):
email = user.username
# update basic user info # update basic user info
if is_active is not None: if is_active is not None:
user.is_active = is_active 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: if password:
user.set_password(password) user.set_password(password)
@@ -368,8 +377,6 @@ def update_user_info(request, user, password, is_active, is_staff, role,
# update user # update user
user.save() user.save()
email = user.username
# update additional user info # update additional user info
if is_pro_version() and role: if is_pro_version() and role:
User.objects.update_role(email, role) User.objects.update_role(email, role)

View File

@@ -442,6 +442,13 @@ class OrgAdminUser(APIView):
user.is_active = is_active == 'true' user.is_active = is_active == 'true'
user.save() 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 # update quota_total
quota_total_mb = request.data.get("quota_total", None) quota_total_mb = request.data.get("quota_total", None)

View File

@@ -18,7 +18,7 @@ from urllib.parse import urlparse, urljoin
from constance import config from constance import config
import seaserv import seaserv
from seaserv import seafile_api from seaserv import seafile_api, ccnet_api
from django.urls import reverse from django.urls import reverse
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
@@ -1280,6 +1280,38 @@ def clear_token(username):
TokenV2.objects.filter(user = username).delete() TokenV2.objects.filter(user = username).delete()
seafile_api.delete_repo_tokens_by_email(username) 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): def send_perm_audit_msg(etype, from_user, to, repo_id, path, perm):
"""Send repo permission audit msg. """Send repo permission audit msg.