diff --git a/frontend/src/pages/sys-admin/orgs/orgs-content.js b/frontend/src/pages/sys-admin/orgs/orgs-content.js index b8806ad2c6..e53a407299 100644 --- a/frontend/src/pages/sys-admin/orgs/orgs-content.js +++ b/frontend/src/pages/sys-admin/orgs/orgs-content.js @@ -8,7 +8,7 @@ import EmptyTip from '../../../components/empty-tip'; import Loading from '../../../components/loading'; import Paginator from '../../../components/paginator'; import { systemAdminAPI } from '../../../utils/system-admin-api'; -import RoleSelector from '../../../components/single-selector'; +import Selector from '../../../components/single-selector'; import CommonOperationConfirmationDialog from '../../../components/dialog/common-operation-confirmation-dialog'; import UserLink from '../user-link'; import toaster from '../../../components/toast'; @@ -52,8 +52,9 @@ class Content extends Component { - - + + + @@ -66,6 +67,7 @@ class Content extends Component { key={index} item={item} updateRole={this.props.updateRole} + updateStatus={this.props.updateStatus} deleteOrg={this.props.deleteOrg} isItemFreezed={this.state.isItemFreezed} toggleItemFreezed={this.toggleItemFreezed} @@ -97,6 +99,7 @@ Content.propTypes = { currentPage: PropTypes.number, items: PropTypes.array.isRequired, updateRole: PropTypes.func.isRequired, + updateStatus: PropTypes.func.isRequired, deleteOrg: PropTypes.func.isRequired, hasNextPage: PropTypes.bool, resetPerPage: PropTypes.func, @@ -111,6 +114,7 @@ class Item extends Component { highlighted: false, isDeleteDialogOpen: false, deleteDialogMsg: '', + isConfirmInactiveDialogOpen: false }; } @@ -148,6 +152,31 @@ class Item extends Component { }); }; + toggleConfirmInactiveDialog = () => { + this.setState({ isConfirmInactiveDialogOpen: !this.state.isConfirmInactiveDialogOpen }); + }; + + translateStatus = (status) => { + switch (status) { + case 'active': + return gettext('Active'); + case 'inactive': + return gettext('Inactive'); + } + }; + + updateStatus = (statusOption) => { + const isActive = statusOption.value == 'active'; + if (isActive) { + toaster.notify(gettext('It may take some time, please wait.')); + } + this.props.updateStatus(this.props.item.org_id, isActive); + }; + + setOrgInactive = () => { + this.props.updateStatus(this.props.item.org_id, false); + }; + translateRole = (role) => { switch (role) { case 'default': @@ -170,7 +199,12 @@ class Item extends Component { render() { const { item } = this.props; - const { highlighted, isDeleteDialogOpen, deleteDialogMsg } = this.state; + const { + highlighted, + isDeleteDialogOpen, + deleteDialogMsg, + isConfirmInactiveDialogOpen + } = this.state; const { role: curRole } = item; this.roleOptions = availableRoles.map(item => { @@ -182,6 +216,18 @@ class Item extends Component { }); const currentSelectedOption = this.roleOptions.filter(item => item.isSelected)[0]; + // edit status + const curStatus = item.is_active ? 'active' : 'inactive'; + this.statusOptions = ['active', 'inactive'].map(item => { + return { + value: item, + text: this.translateStatus(item), + isSelected: item == curStatus + }; + }); + const currentSelectedStatusOption = this.statusOptions.filter(item => item.isSelected)[0]; + const confirmSetUserInactiveMsg = gettext('Are you sure you want to set {user_placeholder} inactive?').replace('{user_placeholder}', item.org_name); + return ( @@ -190,7 +236,17 @@ class Item extends Component { +
{gettext('Name')}{gettext('Creator')}{gettext('Name')}{gettext('Creator')}{gettext('Status')} {gettext('Role')} {gettext('Space Used')} {gettext('Created At')}
- + + } + {isConfirmInactiveDialogOpen && + + } ); } @@ -221,6 +286,7 @@ class Item extends Component { Item.propTypes = { item: PropTypes.object.isRequired, updateRole: PropTypes.func.isRequired, + updateStatus: PropTypes.func.isRequired, deleteOrg: PropTypes.func.isRequired, isItemFreezed: PropTypes.bool.isRequired, toggleItemFreezed: PropTypes.func.isRequired diff --git a/frontend/src/pages/sys-admin/orgs/orgs.js b/frontend/src/pages/sys-admin/orgs/orgs.js index 026bbda294..f80057dbb2 100644 --- a/frontend/src/pages/sys-admin/orgs/orgs.js +++ b/frontend/src/pages/sys-admin/orgs/orgs.js @@ -66,6 +66,24 @@ class Orgs extends Component { this.setState({ isAddOrgDialogOpen: !this.state.isAddOrgDialogOpen }); }; + updateStatus = (orgID, isActive) => { + let orgInfo = {}; + orgInfo.isActive = isActive; + systemAdminAPI.sysAdminUpdateOrg(orgID, orgInfo).then(res => { + let newOrgList = this.state.orgList.map(org => { + if (org.org_id == orgID) { + org.is_active = isActive; + } + return org; + }); + this.setState({ orgList: newOrgList }); + toaster.success(gettext('Edit succeeded')); + }).catch((error) => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + }; + updateRole = (orgID, role) => { let orgInfo = {}; orgInfo.role = role; @@ -143,6 +161,7 @@ class Orgs extends Component { resetPerPage={this.resetPerPage} getListByPage={this.getItemsByPage} updateRole={this.updateRole} + updateStatus={this.updateStatus} deleteOrg={this.deleteOrg} /> diff --git a/frontend/src/utils/system-admin-api.js b/frontend/src/utils/system-admin-api.js index 7403955d7d..47579f8038 100644 --- a/frontend/src/utils/system-admin-api.js +++ b/frontend/src/utils/system-admin-api.js @@ -608,6 +608,9 @@ class SystemAdminAPI { if (orgInfo.role) { formData.append('role', orgInfo.role); } + if (orgInfo.isActive != undefined) { + formData.append('is_active', orgInfo.isActive); + } return this.req.put(url, formData); } diff --git a/media/img/organization-inactive.png b/media/img/organization-inactive.png new file mode 100644 index 0000000000..9ab8718f01 Binary files /dev/null and b/media/img/organization-inactive.png differ diff --git a/seahub/api2/endpoints/admin/organizations.py b/seahub/api2/endpoints/admin/organizations.py index 173129eb9f..85b86af096 100644 --- a/seahub/api2/endpoints/admin/organizations.py +++ b/seahub/api2/endpoints/admin/organizations.py @@ -29,7 +29,7 @@ from seahub.organizations.models import OrgSAMLConfig try: from seahub.settings import ORG_MEMBER_QUOTA_ENABLED except ImportError: - ORG_MEMBER_QUOTA_ENABLED= False + ORG_MEMBER_QUOTA_ENABLED = False if ORG_MEMBER_QUOTA_ENABLED: from seahub.organizations.models import OrgMemberQuota @@ -47,6 +47,7 @@ except ImportError: logger = logging.getLogger(__name__) + def get_org_info(org): org_id = org.org_id @@ -56,6 +57,7 @@ def get_org_info(org): org_info['ctime'] = timestamp_to_isoformat_timestr(org.ctime) org_info['org_url_prefix'] = org.url_prefix org_info['role'] = OrgSettings.objects.get_role_by_org(org) + org_info['is_active'] = OrgSettings.objects.get_is_active_by_org(org) creator = org.creator org_info['creator_email'] = creator @@ -70,7 +72,9 @@ def get_org_info(org): return org_info + def get_org_detailed_info(org): + org_id = org.org_id org_info = get_org_info(org) @@ -99,6 +103,7 @@ def get_org_detailed_info(org): return org_info + def gen_org_url_prefix(max_trial=None, length=20): """Generate organization url prefix automatically. If ``max_trial`` is large than 0, then re-try that times if failed. @@ -234,7 +239,7 @@ class AdminOrganizations(APIView): logger.error(e) error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) - + quota = request.data.get('quota', None) if quota: try: @@ -244,7 +249,7 @@ class AdminOrganizations(APIView): except ValueError as e: logger.error(e) return api_error(status.HTTP_400_BAD_REQUEST, "Quota is not valid") - + if ORG_MEMBER_QUOTA_ENABLED: member_limit = request.data.get('member_limit', ORG_MEMBER_QUOTA_DEFAULT) OrgMemberQuota.objects.set_quota(org_id, member_limit) @@ -367,7 +372,6 @@ class AdminOrganization(APIView): error_msg = 'quota invalid.' return api_error(status.HTTP_400_BAD_REQUEST, error_msg) - quota = quota_mb * get_file_size_unit('MB') try: seafile_api.set_org_quota(org_id, quota) @@ -384,6 +388,15 @@ class AdminOrganization(APIView): OrgSettings.objects.add_or_update(org, role) + is_active = request.data.get('is_active', None) + if is_active: + is_active = is_active.lower() + if is_active not in ('true', 'false'): + error_msg = 'is_active invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + OrgSettings.objects.add_or_update(org, is_active=is_active == 'true') + org = ccnet_api.get_org_by_id(org_id) org_info = get_org_info(org) return Response(org_info) @@ -493,14 +506,14 @@ class AdminOrganizationsBaseInfo(APIView): error_msg = 'Feature is not enabled.' return api_error(status.HTTP_403_FORBIDDEN, error_msg) - org_ids = request.GET.getlist('org_ids',[]) + org_ids = request.GET.getlist('org_ids', []) orgs = [] for org_id in org_ids: try: org = ccnet_api.get_org_by_id(int(org_id)) if not org: continue - except: + except Exception: continue base_info = {'org_id': org.org_id, 'org_name': org.org_name} orgs.append(base_info) diff --git a/seahub/base/middleware.py b/seahub/base/middleware.py index 708e7b53f7..6209760155 100644 --- a/seahub/base/middleware.py +++ b/seahub/base/middleware.py @@ -1,17 +1,23 @@ # Copyright (c) 2012-2016 Seafile Ltd. import re -from django.utils.deprecation import MiddlewareMixin -from django.core.cache import cache +from rest_framework import status from django.urls import reverse +from django.core.cache import cache from django.http import HttpResponseRedirect +from django.utils.translation import gettext as _ +from django.utils.deprecation import MiddlewareMixin from seaserv import ccnet_api +from seahub.auth import logout +from seahub.utils import render_error +from seahub.organizations.models import OrgSettings from seahub.notifications.models import Notification from seahub.notifications.utils import refresh_cache -from seahub.constants import DEFAULT_ADMIN +from seahub.api2.utils import api_error +from seahub.settings import SITE_ROOT, SUPPORT_EMAIL try: from seahub.settings import CLOUD_MODE except ImportError: @@ -20,7 +26,7 @@ try: from seahub.settings import MULTI_TENANCY except ImportError: MULTI_TENANCY = False -from seahub.settings import SITE_ROOT + class BaseMiddleware(MiddlewareMixin): """ @@ -38,6 +44,16 @@ class BaseMiddleware(MiddlewareMixin): orgs = ccnet_api.get_orgs_by_user(username) if orgs: request.user.org = orgs[0] + if not OrgSettings.objects.get_is_active_by_org(request.user.org): + org_name = request.user.org.org_name + error_msg = _(f"Team {org_name} is inactive.") + if SUPPORT_EMAIL: + error_msg += " " + _(f"Please contact {SUPPORT_EMAIL} if you want to activate the team.") + logout(request) + if "api2/" in request.path or "api/v2.1/" in request.path: + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + return render_error(request, error_msg, {"organization_inactive": True}) + else: request.cloud_mode = False @@ -46,6 +62,7 @@ class BaseMiddleware(MiddlewareMixin): def process_response(self, request, response): return response + class InfobarMiddleware(MiddlewareMixin): """Query info bar close status, and store into request.""" @@ -99,6 +116,7 @@ class ForcePasswdChangeMiddleware(MiddlewareMixin): if self._request_in_black_list(request): return HttpResponseRedirect(reverse('auth_password_change')) + class UserAgentMiddleWare(MiddlewareMixin): user_agents_test_match = ( "w3c ", "acs-", "alav", "alca", "amoi", "audi", @@ -151,7 +169,7 @@ class UserAgentMiddleWare(MiddlewareMixin): # Test common mobile values. if self.user_agents_test_search_regex.search(user_agent) and \ - not self.user_agents_exception_search_regex.search(user_agent): + not self.user_agents_exception_search_regex.search(user_agent): is_mobile = True else: # Nokia like test for WAP browsers. diff --git a/seahub/organizations/migrations/0002_orgsettings_is_active.py b/seahub/organizations/migrations/0002_orgsettings_is_active.py new file mode 100644 index 0000000000..6b979f8d7c --- /dev/null +++ b/seahub/organizations/migrations/0002_orgsettings_is_active.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.16 on 2025-01-16 04:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('organizations', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='orgsettings', + name='is_active', + field=models.BooleanField(db_index=True, default=False), + ), + ] diff --git a/seahub/organizations/models.py b/seahub/organizations/models.py index 4d2556a506..116aa1a22f 100644 --- a/seahub/organizations/models.py +++ b/seahub/organizations/models.py @@ -18,7 +18,9 @@ FORCE_ADFS_LOGIN = 'force_adfs_login' DISABLE_ORG_USER_CLEAN_TRASH = 'disable_org_user_clean_trash' DISABLE_ORG_ENCRYPTED_LIBRARY = 'disable_org_encrypted_library' + class OrgMemberQuotaManager(models.Manager): + def get_quota(self, org_id): try: return self.get(org_id=org_id).quota @@ -63,7 +65,15 @@ class OrgSettingsManager(models.Manager): logger.warning('Role %s is not valid' % role) return DEFAULT_ORG - def add_or_update(self, org, role=None): + def get_is_active_by_org(self, org): + org_id = org.org_id + try: + is_active = self.get(org_id=org_id).is_active + return is_active + except OrgSettings.DoesNotExist: + return True + + def add_or_update(self, org, role=None, is_active=None): org_id = org.org_id try: settings = self.get(org_id=org_id) @@ -76,6 +86,9 @@ class OrgSettingsManager(models.Manager): else: logger.warning('Role %s is not valid' % role) + if is_active is not None: + settings.is_active = is_active + settings.save(using=self._db) return settings @@ -83,6 +96,7 @@ class OrgSettingsManager(models.Manager): class OrgSettings(models.Model): org_id = models.IntegerField(unique=True) role = models.CharField(max_length=100, null=True, blank=True) + is_active = models.BooleanField(default=False, db_index=True) objects = OrgSettingsManager() diff --git a/seahub/settings.py b/seahub/settings.py index 081f3d42a0..d99bed648b 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -504,6 +504,7 @@ ENABLE_SHOW_ABOUT = True # enable show wechat support SHOW_WECHAT_SUPPORT_GROUP = False +SUPPORT_EMAIL = '' # File preview FILE_PREVIEW_MAX_SIZE = 30 * 1024 * 1024 diff --git a/seahub/templates/error.html b/seahub/templates/error.html index c07fcb367d..c93c2c546f 100644 --- a/seahub/templates/error.html +++ b/seahub/templates/error.html @@ -5,6 +5,9 @@ {% if unable_view_file %}

{{ error_msg }}

+ {% elif organization_inactive %} + +

{{ error_msg }}

{% else %}

{{ error_msg }}

{% endif %}