From b44a442b3ac943098e76691d35862494412c1932 Mon Sep 17 00:00:00 2001 From: Vlad0n20 Date: Mon, 15 Sep 2025 16:38:58 +0300 Subject: [PATCH 1/3] 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 | 8 +++++++ admin/users/urls.py | 1 + admin/users/views.py | 32 +++++++++++++++++++++++++++- 5 files changed, 68 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 9bd963cb49b..80cbee0abf3 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 01327a4eb73..7dccb2c57a9 100644 --- a/admin/users/forms.py +++ b/admin/users/forms.py @@ -19,3 +19,11 @@ class UserSearchForm(forms.Form): class MergeUserForm(forms.Form): user_guid_to_be_merged = forms.CharField(label='user_guid_to_be_merged', min_length=5, max_length=5, required=True) # TODO: Move max to 6 when needed + + +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 3c87ab1e332..8eac060ba41 100644 --- a/admin/users/urls.py +++ b/admin/users/urls.py @@ -26,6 +26,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]+)/reindex_share_user/$', views.UserShareReindex.as_view(), name='reindex-share-user'), re_path(r'^(?P[a-z0-9]+)/merge_accounts/$', views.UserMergeAccounts.as_view(), name='merge-accounts'), diff --git a/admin/users/views.py b/admin/users/views.py index b15eace9cf4..73c970e52a9 100644 --- a/admin/users/views.py +++ b/admin/users/views.py @@ -43,7 +43,8 @@ from admin.users.forms import ( EmailResetForm, UserSearchForm, - MergeUserForm + MergeUserForm, + AddEmailForm ) from admin.nodes.views import NodeAddSystemTag, NodeRemoveSystemTag from admin.base.views import GuidView @@ -396,6 +397,35 @@ class UserRemoveSystemTag(UserMixin, NodeRemoveSystemTag): permission_required = 'osf.change_osfuser' +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. """ From 5112d00bed84abc42056b26b4f1ba9a64d02027e Mon Sep 17 00:00:00 2001 From: Vlad0n20 Date: Thu, 30 Oct 2025 15:51:26 +0200 Subject: [PATCH 2/3] add email check --- admin/users/views.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/admin/users/views.py b/admin/users/views.py index 73c970e52a9..0f0fa3a2f4e 100644 --- a/admin/users/views.py +++ b/admin/users/views.py @@ -18,7 +18,7 @@ from osf.exceptions import UserStateError from osf.models.base import Guid -from osf.models.user import OSFUser +from osf.models.user import OSFUser, Email from osf.models.spam import SpamStatus from framework.auth import get_user from framework.auth.core import generate_verification_key @@ -411,6 +411,19 @@ def form_valid(self, form): user = self.get_object() address = form.cleaned_data['new_email'].strip().lower() + + existing_email = Email.objects.filter(address=address).first() + if existing_email: + if existing_email.user == user: + messages.error(self.request, f'Email {address} is already confirmed for this user.') + else: + messages.error(self.request, f'Email {address} already exists in the system and is associated with another user.') + return super().form_valid(form) + + if address in user.unconfirmed_emails: + messages.error(self.request, f'Email {address} is already pending confirmation for this user.') + return super().form_valid(form) + try: user.add_unconfirmed_email(address) From 21ed3c811e76658c74265e5b16a656e472d19c5b Mon Sep 17 00:00:00 2001 From: Vlad0n20 Date: Thu, 30 Oct 2025 15:56:05 +0200 Subject: [PATCH 3/3] Update imports --- admin/users/views.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/admin/users/views.py b/admin/users/views.py index 0f0fa3a2f4e..434f76bfbbf 100644 --- a/admin/users/views.py +++ b/admin/users/views.py @@ -11,17 +11,19 @@ from django.contrib import messages from django.contrib.auth.mixins import PermissionRequiredMixin from django.urls import reverse +from django.utils import timezone from django.core.exceptions import PermissionDenied from django.shortcuts import redirect from django.core.paginator import Paginator from django.core.exceptions import ValidationError -from osf.exceptions import UserStateError +from osf.exceptions import UserStateError, BlockedEmailError from osf.models.base import Guid from osf.models.user import OSFUser, Email from osf.models.spam import SpamStatus from framework.auth import get_user from framework.auth.core import generate_verification_key +from framework.auth.views import send_confirm_email_async from website import search from website.settings import EXTERNAL_IDENTITY_PROFILE @@ -404,10 +406,6 @@ class UserAddEmail(UserMixin, FormView): 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() @@ -431,7 +429,7 @@ def form_valid(self, form): 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: + except (ValidationError, 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.')