From 744624cf3fd5c1d54673eef27123c5656f3279c2 Mon Sep 17 00:00:00 2001 From: Vlad0n20 Date: Mon, 15 Sep 2025 16:38:58 +0300 Subject: [PATCH] Add "add email" button to admin user profile --- admin/templates/users/add_email.html | 27 +++++++++++++++++++++++ admin/templates/users/user.html | 1 + admin/users/forms.py | 4 ++++ admin/users/urls.py | 1 + admin/users/views.py | 32 +++++++++++++++++++++++++++- 5 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 admin/templates/users/add_email.html diff --git a/admin/templates/users/add_email.html b/admin/templates/users/add_email.html new file mode 100644 index 00000000000..79ee05bb2fb --- /dev/null +++ b/admin/templates/users/add_email.html @@ -0,0 +1,27 @@ +{% if perms.osf.change_osfuser %} + Add email + +{% endif %} + + diff --git a/admin/templates/users/user.html b/admin/templates/users/user.html index 71bb25a5989..8a5b7e9f87e 100644 --- a/admin/templates/users/user.html +++ b/admin/templates/users/user.html @@ -20,6 +20,7 @@
{% include "users/reset_password.html" with user=user %} + {% include "users/add_email.html" with user=user %} {% if perms.osf.change_osfuser %} Get password reset link {% if user.confirmed %} diff --git a/admin/users/forms.py b/admin/users/forms.py index e64a648ff77..7dccb2c57a9 100644 --- a/admin/users/forms.py +++ b/admin/users/forms.py @@ -23,3 +23,7 @@ class MergeUserForm(forms.Form): class AddSystemTagForm(forms.Form): system_tag_to_add = forms.CharField(label='system_tag_to_add', min_length=1, max_length=1024, required=True) + + +class AddEmailForm(forms.Form): + new_email = forms.EmailField(label='new_email', required=True) diff --git a/admin/users/urls.py b/admin/users/urls.py index 7f5d55ddb9b..31516a736d2 100644 --- a/admin/users/urls.py +++ b/admin/users/urls.py @@ -25,6 +25,7 @@ re_path(r'^(?P[a-z0-9]+)/get_reset_password/$', views.GetPasswordResetLink.as_view(), name='get-reset-password'), re_path(r'^(?P[a-z0-9]+)/reindex_elastic_user/$', views.UserReindexElastic.as_view(), name='reindex-elastic-user'), + re_path(r'^(?P[a-z0-9]+)/add_email/$', views.UserAddEmail.as_view(), name='add-email'), re_path(r'^(?P[a-z0-9]+)/merge_accounts/$', views.UserMergeAccounts.as_view(), name='merge-accounts'), re_path(r'^(?P[a-z0-9]+)/draft_registrations/$', views.UserDraftRegistrationsList.as_view(), name='draft-registrations'), ] diff --git a/admin/users/views.py b/admin/users/views.py index 41ac9583fe8..1e9bc0ae9b9 100644 --- a/admin/users/views.py +++ b/admin/users/views.py @@ -44,7 +44,8 @@ EmailResetForm, UserSearchForm, MergeUserForm, - AddSystemTagForm + AddSystemTagForm, + AddEmailForm ) from admin.base.views import GuidView from api.users.services import send_password_reset_email @@ -408,6 +409,35 @@ def form_valid(self, form): return super().form_valid(form) +class UserAddEmail(UserMixin, FormView): + """Allows authorized users to add an email to a user's account and trigger confirmation.""" + permission_required = 'osf.change_osfuser' + raise_exception = True + form_class = AddEmailForm + + def form_valid(self, form): + from osf.exceptions import BlockedEmailError + from django.core.exceptions import ValidationError as DjangoValidationError + from framework.auth.views import send_confirm_email_async + from django.utils import timezone + + user = self.get_object() + address = form.cleaned_data['new_email'].strip().lower() + try: + user.add_unconfirmed_email(address) + + send_confirm_email_async(user, email=address) + user.email_last_sent = timezone.now() + user.save() + messages.success(self.request, f'Added unconfirmed email {address} and sent confirmation email.') + except (DjangoValidationError, ValueError) as e: + messages.error(self.request, f'Invalid email: {getattr(e, "message", str(e))}') + except BlockedEmailError: + messages.error(self.request, 'This email address domain is blocked.') + + return super().form_valid(form) + + class UserMergeAccounts(UserMixin, FormView): """ Allows authorized users to merge a user's accounts using their guid. """