1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-25 23:02:26 +00:00

org admin delete org (#8167)

* org admin delete org

* code-optimize

* add ORG_ENABLE_ADMIN_DELETE_ORG

* Organization -> Team

---------

Co-authored-by: r350178982 <32759763+r350178982@users.noreply.github.com>
This commit is contained in:
lian
2025-09-09 12:27:43 +08:00
committed by GitHub
parent aa3c318384
commit 39eaa2f87b
12 changed files with 221 additions and 8 deletions

View File

@@ -0,0 +1,79 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Modal, Input, ModalBody, ModalFooter, Label, Form, FormGroup } from 'reactstrap';
import { gettext } from '../../utils/constants';
import SeahubModalHeader from '@/components/common/seahub-modal-header';
const propTypes = {
organizationName: PropTypes.string.isRequired,
toggle: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired,
};
class OrgAdminDeleteOrgDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
name: '',
errMessage: '',
};
}
toggle = () => {
this.props.toggle();
};
inputName = (e) => {
let name = e.target.value;
this.setState({ name: name });
};
validateInputParams() {
let errMessage;
let name = this.state.name.trim();
if (!name.length) {
errMessage = gettext('Name is required');
this.setState({ errMessage: errMessage });
return false;
}
if (name !== this.props.organizationName) {
errMessage = gettext('Names don\'t match');
this.setState({ errMessage: errMessage });
return false;
}
return true;
}
handleSubmit = () => {
let isValid = this.validateInputParams();
if (isValid) {
this.props.handleSubmit();
}
};
render() {
return (
<Modal isOpen={true} toggle={this.toggle}>
<SeahubModalHeader toggle={this.toggle}>{gettext('Delete Team')}</SeahubModalHeader>
<ModalBody>
<Form>
<FormGroup>
<Label for="orgName">{gettext('To confirm, type "{placeholder}" in the box below').replace('{placeholder}', this.props.organizationName)}</Label>
<Input id="orgName" value={this.state.name || ''} onChange={this.inputName} />
</FormGroup>
</Form>
{this.state.errMessage && <Label className="err-message">{this.state.errMessage}</Label>}
</ModalBody>
<ModalFooter>
<Button color="primary" onClick={this.handleSubmit} >{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);
}
}
OrgAdminDeleteOrgDialog.propTypes = propTypes;
export default OrgAdminDeleteOrgDialog;

View File

@@ -2,7 +2,7 @@ import React, { Component, Fragment } from 'react';
import { InputGroupText } from 'reactstrap'; import { InputGroupText } from 'reactstrap';
import { Utils } from '../../../utils/utils'; import { Utils } from '../../../utils/utils';
import { orgAdminAPI } from '../../../utils/org-admin-api'; import { orgAdminAPI } from '../../../utils/org-admin-api';
import { gettext, mediaUrl, logoPath, orgID, orgEnableAdminCustomLogo, orgEnableAdminCustomName, enableMultiADFS } from '../../../utils/constants'; import { gettext, mediaUrl, logoPath, orgID, orgEnableAdminCustomLogo, orgEnableAdminCustomName, orgEnableAdminDeleteOrg, enableMultiADFS } from '../../../utils/constants';
import Loading from '../../../components/loading'; import Loading from '../../../components/loading';
import toaster from '../../../components/toast'; import toaster from '../../../components/toast';
import MainPanelTopbar from '../main-panel-topbar'; import MainPanelTopbar from '../main-panel-topbar';
@@ -10,6 +10,7 @@ import Section from '../../common-admin/web-settings/section';
import CheckboxItem from '../../common-admin/web-settings/checkbox-item'; import CheckboxItem from '../../common-admin/web-settings/checkbox-item';
import FileItem from '../../common-admin/web-settings/file-item'; import FileItem from '../../common-admin/web-settings/file-item';
import InputItem from './input-item'; import InputItem from './input-item';
import DeleteOrganizationDialog from '../../../components/dialog/org-admin-delete-org-dialog';
import '../../../css/system-admin-web-settings.css'; import '../../../css/system-admin-web-settings.css';
@@ -29,7 +30,8 @@ class OrgWebSettings extends Component {
force_adfs_login: false, force_adfs_login: false,
disable_org_encrypted_library: false, disable_org_encrypted_library: false,
disable_org_user_clean_trash: false, disable_org_user_clean_trash: false,
user_default_quota: 0 user_default_quota: 0,
isDeleteOrganizationDialogShow: false,
}; };
} }
@@ -104,6 +106,19 @@ class OrgWebSettings extends Component {
}); });
}; };
toggleDeleteOrganization = () => {
this.setState({ isDeleteOrganizationDialogShow: !this.state.isDeleteOrganizationDialogShow });
};
deleteOrganization = () => {
orgAdminAPI.orgAdminDeleteOrg(orgID).then((res) => {
window.location.href = '/';
}).catch((error) => {
let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage);
});
};
render() { render() {
const { loading, errorMsg, config_dict, file_ext_white_list, force_adfs_login, disable_org_encrypted_library, disable_org_user_clean_trash, user_default_quota } = this.state; const { loading, errorMsg, config_dict, file_ext_white_list, force_adfs_login, disable_org_encrypted_library, disable_org_user_clean_trash, user_default_quota } = this.state;
let logoPath = this.state.logoPath; let logoPath = this.state.logoPath;
@@ -206,11 +221,27 @@ class OrgWebSettings extends Component {
/> />
</Fragment> </Fragment>
</Section> </Section>
{orgEnableAdminDeleteOrg &&
<Section headingText={gettext('Delete')}>
<Fragment>
<button onClick={this.toggleDeleteOrganization.bind(this, null)} className="btn btn-outline-primary" >
{gettext('Delete Team')}
</button>
</Fragment>
</Section>
}
</Fragment> </Fragment>
} }
</div> </div>
</div> </div>
</div> </div>
{this.state.isDeleteOrganizationDialogShow &&
<DeleteOrganizationDialog
organizationName={config_dict['org_name']}
toggle={this.toggleDeleteOrganization}
handleSubmit={this.deleteOrganization}
/>
}
</Fragment> </Fragment>
); );
} }

View File

@@ -176,6 +176,7 @@ export const orgMemberQuotaEnabled = window.org ? window.org.pageOptions.orgMemb
export const orgEnableAdminCustomLogo = window.org ? window.org.pageOptions.orgEnableAdminCustomLogo === 'True' : false; export const orgEnableAdminCustomLogo = window.org ? window.org.pageOptions.orgEnableAdminCustomLogo === 'True' : false;
export const orgEnableAdminCustomName = window.org ? window.org.pageOptions.orgEnableAdminCustomName === 'True' : false; export const orgEnableAdminCustomName = window.org ? window.org.pageOptions.orgEnableAdminCustomName === 'True' : false;
export const orgEnableAdminInviteUser = window.org ? window.org.pageOptions.orgEnableAdminInviteUser === 'True' : false; export const orgEnableAdminInviteUser = window.org ? window.org.pageOptions.orgEnableAdminInviteUser === 'True' : false;
export const orgEnableAdminDeleteOrg = window.org ? window.org.pageOptions.orgEnableAdminDeleteOrg === 'True' : false;
export const enableMultiADFS = window.org ? window.org.pageOptions.enableMultiADFS === 'True' : false; export const enableMultiADFS = window.org ? window.org.pageOptions.enableMultiADFS === 'True' : false;
export const enableSubscription = window.org ? window.org.pageOptions.enableSubscription : false; export const enableSubscription = window.org ? window.org.pageOptions.enableSubscription : false;
export const enableExternalBillingService = window.org ? window.org.pageOptions.enableExternalBillingService : false; export const enableExternalBillingService = window.org ? window.org.pageOptions.enableExternalBillingService : false;

View File

@@ -566,6 +566,11 @@ class OrgAdminAPI {
return this.req.put(url, form); return this.req.put(url, form);
} }
orgAdminDeleteOrg(orgID) {
const url = this.server + '/api/v2.1/org/' + orgID + '/admin/delete-org/';
return this.req.delete(url);
}
} }
let orgAdminAPI = new OrgAdminAPI(); let orgAdminAPI = new OrgAdminAPI();

View File

@@ -15,6 +15,7 @@ from seaserv import ccnet_api, seafile_api
from seahub.auth.utils import get_virtual_id_by_email from seahub.auth.utils import get_virtual_id_by_email
from seahub.organizations.settings import ORG_MEMBER_QUOTA_DEFAULT, \ from seahub.organizations.settings import ORG_MEMBER_QUOTA_DEFAULT, \
ORG_ENABLE_REACTIVATE ORG_ENABLE_REACTIVATE
from seahub.organizations.signals import org_deleted
from seahub.organizations.utils import generate_org_reactivate_link from seahub.organizations.utils import generate_org_reactivate_link
from seahub.utils import is_valid_email, IS_EMAIL_CONFIGURED, send_html_email from seahub.utils import is_valid_email, IS_EMAIL_CONFIGURED, send_html_email
from seahub.utils.file_size import get_file_size_unit from seahub.utils.file_size import get_file_size_unit
@@ -472,12 +473,12 @@ class AdminOrganization(APIView):
# remove org repos # remove org repos
seafile_api.remove_org_repo_by_org_id(org_id) seafile_api.remove_org_repo_by_org_id(org_id)
# remove org saml config
OrgSAMLConfig.objects.filter(org_id=org_id).delete()
# remove org # remove org
ccnet_api.remove_org(org_id) ccnet_api.remove_org(org_id)
# handle signal
org_deleted.send(sender=None, org_id=org_id)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
error_msg = 'Internal Server Error' error_msg = 'Internal Server Error'

View File

@@ -0,0 +1,73 @@
import logging
from seaserv import ccnet_api, seafile_api
from rest_framework.authentication import SessionAuthentication
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status
from seahub.api2.permissions import IsProVersion, IsOrgAdminUser
from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error
from seahub.base.accounts import User
from seahub.organizations.signals import org_deleted
from seahub.organizations.settings import ORG_ENABLE_ADMIN_DELETE_ORG
try:
from seahub.settings import MULTI_TENANCY
except ImportError:
MULTI_TENANCY = False
logger = logging.getLogger(__name__)
class OrgAdminDeleteOrg(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
throttle_classes = (UserRateThrottle,)
permission_classes = (IsProVersion, IsOrgAdminUser)
def delete(self, request, org_id):
if not MULTI_TENANCY or not ORG_ENABLE_ADMIN_DELETE_ORG:
error_msg = 'Feature is not enabled.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
org_id = int(org_id)
if org_id == 0:
error_msg = 'org_id invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
org = ccnet_api.get_org_by_id(org_id)
if not org:
error_msg = 'Organization %s not found.' % org_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
try:
# remove org users
users = ccnet_api.get_org_emailusers(org.url_prefix, -1, -1)
for u in users:
ccnet_api.remove_org_user(org_id, u.email)
User.objects.get(email=u.email).delete()
# remove org groups
groups = ccnet_api.get_org_groups(org_id, -1, -1)
for g in groups:
ccnet_api.remove_org_group(org_id, g.gid)
# remove org repos
seafile_api.remove_org_repo_by_org_id(org_id)
# remove org
ccnet_api.remove_org(org_id)
# handle signal
org_deleted.send(sender=None, org_id=org_id)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response({'success': True})

View File

@@ -26,6 +26,7 @@ from .api.admin.user_repos import OrgAdminUserRepos, OrgAdminUserBesharedRepos
from .api.admin.devices import OrgAdminDevices, OrgAdminDevicesErrors from .api.admin.devices import OrgAdminDevices, OrgAdminDevicesErrors
from .api.admin.logo import OrgAdminLogo from .api.admin.logo import OrgAdminLogo
from .api.admin.delete_org import OrgAdminDeleteOrg
from .api.admin.statistics import OrgFileOperationsView, OrgTotalStorageView, \ from .api.admin.statistics import OrgFileOperationsView, OrgTotalStorageView, \
OrgActiveUsersView, OrgSystemTrafficView, OrgUserTrafficView, \ OrgActiveUsersView, OrgSystemTrafficView, OrgUserTrafficView, \
@@ -65,6 +66,7 @@ urlpatterns = [
OrgVerifyDomain.as_view(), OrgVerifyDomain.as_view(),
name='api-v2.1-org-admin-verify-domain'), name='api-v2.1-org-admin-verify-domain'),
path('<int:org_id>/admin/delete-org/', OrgAdminDeleteOrg.as_view(), name='api-v2.1-org-admin-delete-org'),
path('<int:org_id>/admin/logo/', OrgAdminLogo.as_view(), name='api-v2.1-org-admin-logo'), path('<int:org_id>/admin/logo/', OrgAdminLogo.as_view(), name='api-v2.1-org-admin-logo'),
path('<int:org_id>/admin/devices/', OrgAdminDevices.as_view(), name='api-v2.1-org-admin-devices'), path('<int:org_id>/admin/devices/', OrgAdminDevices.as_view(), name='api-v2.1-org-admin-devices'),
path('<int:org_id>/admin/devices-errors/', OrgAdminDevicesErrors.as_view(), name='api-v2.1-org-admin-devices-errors'), path('<int:org_id>/admin/devices-errors/', OrgAdminDevicesErrors.as_view(), name='api-v2.1-org-admin-devices-errors'),

View File

@@ -3,14 +3,16 @@ import os
import uuid import uuid
import logging import logging
from django.db import models from django.db import models
from django.dispatch import receiver
from seahub.base.models import OrgQuotaUsage, OrgLastActivityTime
from .settings import ORG_MEMBER_QUOTA_DEFAULT from .settings import ORG_MEMBER_QUOTA_DEFAULT
from seahub.constants import DEFAULT_ORG from seahub.constants import DEFAULT_ORG
from seahub.role_permissions.utils import get_available_roles from seahub.role_permissions.utils import get_available_roles
from seahub.avatar.util import get_avatar_file_storage from seahub.avatar.util import get_avatar_file_storage
from seahub.avatar.settings import AVATAR_STORAGE_DIR from seahub.avatar.settings import AVATAR_STORAGE_DIR
from seahub.organizations.signals import org_operation_signal from seahub.organizations.signals import org_operation_signal, org_deleted
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -206,3 +208,17 @@ class OrgAdminSettings(models.Model):
class Meta: class Meta:
unique_together = [('org_id', 'key')] unique_together = [('org_id', 'key')]
# handle signal
@receiver(org_deleted)
def org_deleted_cb(sender, **kwargs):
org_id = kwargs['org_id']
OrgSAMLConfig.objects.filter(org_id=org_id).delete()
OrgAdminSettings.objects.filter(org_id=org_id).delete()
OrgSettings.objects.filter(org_id=org_id).delete()
OrgMemberQuota.objects.filter(org_id=org_id).delete()
OrgQuotaUsage.objects.filter(org_id=org_id).delete()
OrgLastActivityTime.objects.filter(org_id=org_id).delete()

View File

@@ -22,3 +22,5 @@ ORG_ENABLE_REACTIVATE = getattr(settings, 'ORG_ENABLE_REACTIVATE', False)
ORG_ENABLE_ADMIN_INVITE_USER = getattr(settings, 'ORG_ENABLE_ADMIN_INVITE_USER', True) ORG_ENABLE_ADMIN_INVITE_USER = getattr(settings, 'ORG_ENABLE_ADMIN_INVITE_USER', True)
ORG_ENABLE_ADMIN_CUSTOM_NAME = getattr(settings, 'ORG_ENABLE_ADMIN_CUSTOM_NAME', True) ORG_ENABLE_ADMIN_CUSTOM_NAME = getattr(settings, 'ORG_ENABLE_ADMIN_CUSTOM_NAME', True)
ORG_ENABLE_ADMIN_CUSTOM_LOGO = getattr(settings, 'ORG_ENABLE_ADMIN_CUSTOM_LOGO', True) ORG_ENABLE_ADMIN_CUSTOM_LOGO = getattr(settings, 'ORG_ENABLE_ADMIN_CUSTOM_LOGO', True)
ORG_ENABLE_ADMIN_DELETE_ORG = getattr(settings, 'ORG_ENABLE_ADMIN_DELETE_ORG', False)

View File

@@ -4,3 +4,4 @@ from django.dispatch import Signal
# A new org is created # A new org is created
org_operation_signal = Signal() org_operation_signal = Signal()
org_last_activity = Signal() org_last_activity = Signal()
org_deleted = Signal()

View File

@@ -17,6 +17,7 @@
orgEnableAdminCustomLogo: '{{ org_enable_admin_custom_logo }}', orgEnableAdminCustomLogo: '{{ org_enable_admin_custom_logo }}',
orgEnableAdminCustomName: '{{ org_enable_admin_custom_name }}', orgEnableAdminCustomName: '{{ org_enable_admin_custom_name }}',
orgEnableAdminInviteUser: '{{ org_enable_admin_invite_user }}', orgEnableAdminInviteUser: '{{ org_enable_admin_invite_user }}',
orgEnableAdminDeleteOrg: '{{ org_enable_admin_delete_org }}',
enableMultiADFS: '{{ enable_multi_adfs }}', enableMultiADFS: '{{ enable_multi_adfs }}',
isOrgContext: true, isOrgContext: true,
enableSubscription: {% if enable_subscription %} true {% else %} false {% endif %}, enableSubscription: {% if enable_subscription %} true {% else %} false {% endif %},

View File

@@ -34,7 +34,7 @@ from seahub.organizations.forms import OrgRegistrationForm
from seahub.organizations.settings import ORG_AUTO_URL_PREFIX, \ from seahub.organizations.settings import ORG_AUTO_URL_PREFIX, \
ORG_MEMBER_QUOTA_ENABLED, ORG_ENABLE_ADMIN_INVITE_USER_VIA_WEIXIN, \ ORG_MEMBER_QUOTA_ENABLED, ORG_ENABLE_ADMIN_INVITE_USER_VIA_WEIXIN, \
ORG_ENABLE_ADMIN_CUSTOM_LOGO, ORG_ENABLE_ADMIN_CUSTOM_NAME, \ ORG_ENABLE_ADMIN_CUSTOM_LOGO, ORG_ENABLE_ADMIN_CUSTOM_NAME, \
ORG_ENABLE_ADMIN_INVITE_USER ORG_ENABLE_ADMIN_INVITE_USER, ORG_ENABLE_ADMIN_DELETE_ORG
from seahub.organizations.utils import get_or_create_invitation_link, \ from seahub.organizations.utils import get_or_create_invitation_link, \
can_use_sso_in_multi_tenancy can_use_sso_in_multi_tenancy
from seahub.subscription.utils import subscription_check from seahub.subscription.utils import subscription_check
@@ -300,6 +300,7 @@ def react_fake_view(request, **kwargs):
'org_enable_admin_custom_logo': ORG_ENABLE_ADMIN_CUSTOM_LOGO, 'org_enable_admin_custom_logo': ORG_ENABLE_ADMIN_CUSTOM_LOGO,
'org_enable_admin_custom_name': ORG_ENABLE_ADMIN_CUSTOM_NAME, 'org_enable_admin_custom_name': ORG_ENABLE_ADMIN_CUSTOM_NAME,
'org_enable_admin_invite_user': ORG_ENABLE_ADMIN_INVITE_USER, 'org_enable_admin_invite_user': ORG_ENABLE_ADMIN_INVITE_USER,
'org_enable_admin_delete_org': ORG_ENABLE_ADMIN_DELETE_ORG,
'group_id': group_id, 'group_id': group_id,
'invitation_link': invitation_link, 'invitation_link': invitation_link,
'enable_multi_adfs': ENABLE_MULTI_ADFS and can_use_sso_in_multi_tenancy(org.org_id), 'enable_multi_adfs': ENABLE_MULTI_ADFS and can_use_sso_in_multi_tenancy(org.org_id),