From 9b293072c3154be164a41cd76cb61b103cb221a2 Mon Sep 17 00:00:00 2001 From: lian Date: Mon, 16 Jun 2025 10:27:56 +0800 Subject: [PATCH] Org reactivate (#7925) * org reactivate * update * update * update * remove print --- .../sys-admin/invitations/invitations.js | 3 + seahub/api2/endpoints/admin/organizations.py | 28 ++++++++-- seahub/base/middleware.py | 55 +++++++++++++------ seahub/invitations/models.py | 1 + seahub/organizations/__init__.py | 8 ++- seahub/organizations/settings.py | 2 + seahub/organizations/signals.py | 1 + .../organizations/org_deactivate_email.html | 23 ++++++++ seahub/organizations/urls.py | 1 + seahub/organizations/utils.py | 12 ++++ seahub/organizations/views.py | 26 ++++++++- seahub/templates/error.html | 8 +++ 12 files changed, 144 insertions(+), 24 deletions(-) create mode 100644 seahub/organizations/templates/organizations/org_deactivate_email.html diff --git a/frontend/src/pages/sys-admin/invitations/invitations.js b/frontend/src/pages/sys-admin/invitations/invitations.js index 0af170c8e1..e1b2125194 100644 --- a/frontend/src/pages/sys-admin/invitations/invitations.js +++ b/frontend/src/pages/sys-admin/invitations/invitations.js @@ -186,6 +186,9 @@ class Item extends Component { case 'default': translateResult = gettext('Default'); break; + case 'org': + translateResult = gettext('Organization'); + break; } return translateResult; }; diff --git a/seahub/api2/endpoints/admin/organizations.py b/seahub/api2/endpoints/admin/organizations.py index 7e380e4f57..05543786ca 100644 --- a/seahub/api2/endpoints/admin/organizations.py +++ b/seahub/api2/endpoints/admin/organizations.py @@ -1,6 +1,7 @@ # Copyright (c) 2012-2016 Seafile Ltd. import logging +from django.utils.translation import gettext as _ from django.utils.crypto import get_random_string from rest_framework.authentication import SessionAuthentication @@ -12,8 +13,10 @@ from rest_framework import status from seaserv import ccnet_api, seafile_api from seahub.auth.utils import get_virtual_id_by_email -from seahub.organizations.settings import ORG_MEMBER_QUOTA_DEFAULT -from seahub.utils import is_valid_email +from seahub.organizations.settings import ORG_MEMBER_QUOTA_DEFAULT, \ + ORG_ENABLE_REACTIVATE +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.file_size import get_file_size_unit from seahub.utils.timeutils import timestamp_to_isoformat_timestr, datetime_to_isoformat_timestr from seahub.base.templatetags.seahub_tags import email2nickname, \ @@ -182,7 +185,7 @@ class AdminOrganizations(APIView): result = [] org_ids = [org.org_id for org in orgs] orgs_last_activity = OrgLastActivityTime.objects.filter(org_id__in=org_ids) - orgs_last_activity_dict = {org.org_id:org.timestamp for org in orgs_last_activity} + orgs_last_activity_dict = {org.org_id: org.timestamp for org in orgs_last_activity} for org in orgs: org_info = get_org_info(org) org_id = org_info['org_id'] @@ -408,6 +411,23 @@ class AdminOrganization(APIView): OrgSettings.objects.add_or_update(org, is_active=is_active == 'true') + if is_active == 'false' and IS_EMAIL_CONFIGURED and ORG_ENABLE_REACTIVATE: + + subject = _(f'Your team {org.org_name} has been deactivated') + email_template = 'organizations/org_deactivate_email.html' + from_email = None + + org_users = ccnet_api.get_org_emailusers(org.url_prefix, -1, -1) + org_admin_users = [org_user.email for org_user in org_users + if ccnet_api.is_org_staff(org_id, org_user.email)] + for email in org_admin_users: + con_context = { + 'org_name': org.org_name, + 'reactivate_link': generate_org_reactivate_link(org_id) + } + send_html_email(subject, email_template, con_context, + from_email, [email2contact_email(email)]) + org = ccnet_api.get_org_by_id(org_id) org_info = get_org_info(org) return Response(org_info) @@ -517,7 +537,7 @@ 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', []) include_org_staffs = to_python_boolean(request.GET.get('include_org_staffs', 'false')) orgs = [] for org_id in org_ids: diff --git a/seahub/base/middleware.py b/seahub/base/middleware.py index 6209760155..64753b945a 100644 --- a/seahub/base/middleware.py +++ b/seahub/base/middleware.py @@ -13,11 +13,13 @@ from seaserv import ccnet_api from seahub.auth import logout from seahub.utils import render_error from seahub.organizations.models import OrgSettings +from seahub.organizations.utils import generate_org_reactivate_link from seahub.notifications.models import Notification from seahub.notifications.utils import refresh_cache from seahub.api2.utils import api_error from seahub.settings import SITE_ROOT, SUPPORT_EMAIL +from seahub.organizations.settings import ORG_ENABLE_REACTIVATE try: from seahub.settings import CLOUD_MODE except ImportError: @@ -34,30 +36,47 @@ class BaseMiddleware(MiddlewareMixin): """ def process_request(self, request): + username = request.user.username request.user.org = None - if CLOUD_MODE: - request.cloud_mode = True + # not enable multi-tenancy + request.cloud_mode = CLOUD_MODE + if not CLOUD_MODE or not MULTI_TENANCY: + return None - if MULTI_TENANCY: - 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}) + # not org user + orgs = ccnet_api.get_orgs_by_user(username) + if not orgs: + return None - else: - request.cloud_mode = False + # org is active + request.user.org = orgs[0] + if OrgSettings.objects.get_is_active_by_org(request.user.org): + return None - return None + # handle inactive org + org_id = request.user.org.org_id + org_name = request.user.org.org_name + is_org_staff = request.user.org.is_staff + logout(request) + + error_msg = _(f"Team {org_name} is inactive.") + if "api2/" in request.path or "api/v2.1/" in request.path: + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + extra_ctx = {"organization_inactive": True} + + if ORG_ENABLE_REACTIVATE and is_org_staff: + reactivate_link = generate_org_reactivate_link(org_id) + extra_ctx["reactivate_link"] = reactivate_link + return render_error(request, error_msg, extra_ctx) + + if SUPPORT_EMAIL: + contact_msg = _(f"Please contact {SUPPORT_EMAIL} if you want to activate the team.") + error_msg = f"{error_msg} {contact_msg}" + + return render_error(request, error_msg, extra_ctx) def process_response(self, request, response): return response diff --git a/seahub/invitations/models.py b/seahub/invitations/models.py index 7726448c8a..61ab661eb9 100644 --- a/seahub/invitations/models.py +++ b/seahub/invitations/models.py @@ -41,6 +41,7 @@ class Invitation(models.Model): INVITE_TYPE_CHOICES = ( (GUEST_USER, 'guest'), (DEFAULT_USER, 'default'), + ('Organization', 'org'), ) token = models.CharField(max_length=40, db_index=True) diff --git a/seahub/organizations/__init__.py b/seahub/organizations/__init__.py index accff5da26..d6b222a89e 100644 --- a/seahub/organizations/__init__.py +++ b/seahub/organizations/__init__.py @@ -1,7 +1,7 @@ import os import sys import logging -from seahub.organizations.signals import org_created +from seahub.organizations.signals import org_created, org_reactivated logger = logging.getLogger(__name__) try: @@ -12,5 +12,11 @@ try: org_created.connect(org_created_callback) except ImportError as e: logger.debug(e) + + try: + from seahub_custom_functions import org_reactivated_callback + org_reactivated.connect(org_reactivated_callback) + except ImportError as e: + logger.debug(e) except KeyError as e: logger.debug(e) diff --git a/seahub/organizations/settings.py b/seahub/organizations/settings.py index c4d8b112c8..a3b4cf9570 100644 --- a/seahub/organizations/settings.py +++ b/seahub/organizations/settings.py @@ -17,6 +17,8 @@ ORG_ENABLE_ADMIN_INVITE_USER_VIA_WEIXIN = getattr(settings, 'ORG_ENABLE_ADMIN_INVITE_USER_VIA_WEIXIN', False) +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_CUSTOM_NAME = getattr(settings, 'ORG_ENABLE_ADMIN_CUSTOM_NAME', True) ORG_ENABLE_ADMIN_CUSTOM_LOGO = getattr(settings, 'ORG_ENABLE_ADMIN_CUSTOM_LOGO', True) diff --git a/seahub/organizations/signals.py b/seahub/organizations/signals.py index 2bb6788311..eafd4490e8 100644 --- a/seahub/organizations/signals.py +++ b/seahub/organizations/signals.py @@ -3,4 +3,5 @@ from django.dispatch import Signal # A new org is created org_created = Signal() +org_reactivated = Signal() org_last_activity = Signal() diff --git a/seahub/organizations/templates/organizations/org_deactivate_email.html b/seahub/organizations/templates/organizations/org_deactivate_email.html new file mode 100644 index 0000000000..940aaa8ead --- /dev/null +++ b/seahub/organizations/templates/organizations/org_deactivate_email.html @@ -0,0 +1,23 @@ +{% extends 'email_base.html' %} + +{% load i18n seahub_tags %} + +{% block email_con %} + +{% autoescape off %} + +

{% trans "Hi," %}

+ +

+{% blocktrans with org_name=org_name %}You are receiving this email because your team {{org_name}} has been deactivated.{% endblocktrans %} +

+ +

+{% blocktrans with link=reactivate_link %} +If you like to continue using our site, click here. Otherwise your account will be deleted in 60 days. +{% endblocktrans %} +

+ +{% endautoescape %} + +{% endblock %} diff --git a/seahub/organizations/urls.py b/seahub/organizations/urls.py index 7cb2447369..9f2461436e 100644 --- a/seahub/organizations/urls.py +++ b/seahub/organizations/urls.py @@ -6,6 +6,7 @@ from .views import * urlpatterns = [ path('add/', org_add, name='org_add'), path('register/', org_register, name='org_register'), + re_path('reactivate/(?P[a-f0-9]{32})/', org_reactivate, name='org_reactivate'), path('statistics-admin/file/', react_fake_view, name='org_statistics_admin_file'), path('statistics-admin/total-storage/', react_fake_view, name='org_statistics_admin_total_storage'), diff --git a/seahub/organizations/utils.py b/seahub/organizations/utils.py index 8e1bc7d71f..ca663fc552 100644 --- a/seahub/organizations/utils.py +++ b/seahub/organizations/utils.py @@ -2,6 +2,7 @@ from django.core.cache import cache from django.urls import reverse +from seahub.invitations.models import Invitation from seahub.utils import gen_token, get_service_url @@ -38,3 +39,14 @@ def get_or_create_invitation_link(org_id): link = '%s/weixin/oauth-login/?next=%s' % ( get_service_url().rstrip('/'), reverse('org_associate', args=[token])) return link + + +def generate_org_reactivate_link(org_id): + i = Invitation.objects.add(inviter='Administrator', + accepter=org_id, + invite_type='org') + + service_url = get_service_url().strip('/') + url = reverse('org_reactivate', args=[i.token]) + url = f'{service_url}{url}' + return url diff --git a/seahub/organizations/views.py b/seahub/organizations/views.py index d5a67be087..0d4208221b 100644 --- a/seahub/organizations/views.py +++ b/seahub/organizations/views.py @@ -27,7 +27,8 @@ from seahub.profile.models import Profile from seahub.utils import get_service_url, render_error from seahub.utils.auth import get_login_bg_image_path -from seahub.organizations.signals import org_created +from seahub.organizations.models import OrgSettings +from seahub.organizations.signals import org_created, org_reactivated from seahub.organizations.decorators import org_staff_required from seahub.organizations.forms import OrgRegistrationForm from seahub.organizations.settings import ORG_AUTO_URL_PREFIX, \ @@ -39,6 +40,8 @@ from seahub.subscription.utils import subscription_check from seahub.billing.settings import ENABLE_EXTERNAL_BILLING_SERVICE from registration.models import RegistrationProfile +from seahub.invitations.models import Invitation + # Get an instance of a logger logger = logging.getLogger(__name__) @@ -337,3 +340,24 @@ def org_associate(request, token): set_org_user(org_id, username) return HttpResponseRedirect(settings.LOGIN_REDIRECT_URL) + + +def org_reactivate(request, token): + + invite = Invitation.objects.get_by_token(token=token) + if not invite: + return render_error(request, _('Invalid token.')) + + if invite.is_expired(): + return render_error(request, _('Expired token.')) + + org_id = invite.accepter + org_id = int(org_id) + org = ccnet_api.get_org_by_id(org_id) + if not org: + return render_error(request, f'Organization {org_id} not found') + + invite.accept() + OrgSettings.objects.add_or_update(org, is_active=True) + org_reactivated.send(sender=None, email=None, org=org) + return HttpResponseRedirect(settings.SITE_ROOT) diff --git a/seahub/templates/error.html b/seahub/templates/error.html index 52012b768c..e0d0279388 100644 --- a/seahub/templates/error.html +++ b/seahub/templates/error.html @@ -1,10 +1,18 @@ {% extends 'base.html' %} +{% load i18n %} {% block main_content %}
{% if organization_inactive %}

{{ error_msg }}

+ {% if reactivate_link %} +

+ {% blocktrans with link=reactivate_link %} + Click here to reactivate your team and relogin. + {% endblocktrans %} +

+ {% endif %} {% else %}

{{ error_msg }}