From 5fa2c7c622d12205884fef25c99e91af3a6941c9 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Tue, 17 Dec 2024 01:17:16 +0530 Subject: [PATCH] Update the UI to switch user aka become user - Rename to "switch user" - Can switch from the user menu - Switch to choices.js from Select2 - Update the hijacked view, so an alert at the top --- hypha/apply/users/forms.py | 29 +++++++++-- .../apply/users/templates/users/account.html | 38 +++++---------- hypha/apply/users/templates/users/become.html | 48 +++++++++++++++++++ hypha/apply/users/urls.py | 2 + hypha/apply/users/views.py | 25 +++++++--- hypha/core/context_processors.py | 1 + hypha/settings/base.py | 1 + hypha/static_src/tailwind/main.css | 13 +++++ hypha/templates/base.html | 4 ++ hypha/templates/includes/hijack-bar.html | 29 +++++++++++ hypha/templates/includes/user_menu.html | 33 ++++++++++++- 11 files changed, 186 insertions(+), 37 deletions(-) create mode 100644 hypha/apply/users/templates/users/become.html create mode 100644 hypha/templates/includes/hijack-bar.html diff --git a/hypha/apply/users/forms.py b/hypha/apply/users/forms.py index e52415b732..7ed3b0080f 100644 --- a/hypha/apply/users/forms.py +++ b/hypha/apply/users/forms.py @@ -4,7 +4,6 @@ from django.contrib.auth.forms import AuthenticationForm from django.template.defaultfilters import mark_safe from django.utils.translation import gettext_lazy as _ -from django_select2.forms import Select2Widget from rolepermissions import roles from wagtail.users.forms import UserCreationForm, UserEditForm @@ -216,11 +215,33 @@ def clean_full_name(self): return strip_html_and_nerf_urls(self.cleaned_data["full_name"]) +def get_become_user_choices(): + """Returns list of active non-superusers with their roles as choice tuples.""" + active_users = User.objects.filter(is_active=True, is_superuser=False) + choices = [] + + for user in active_users: + role_names = user.get_role_names() + if role_names: + roles_html = ", ".join( + [ + f'{str(g)}' + for g in role_names + ] + ) + label = f"{user} ({roles_html})" + else: + label = str(user) + + choices.append((user.pk, mark_safe(label))) + + return choices + + class BecomeUserForm(forms.Form): - user_pk = forms.ModelChoiceField( - widget=Select2Widget, + user_pk = forms.ChoiceField( help_text=_("Only includes active, non-superusers"), - queryset=User.objects.filter(is_active=True, is_superuser=False), + choices=get_become_user_choices, label="", required=False, ) diff --git a/hypha/apply/users/templates/users/account.html b/hypha/apply/users/templates/users/account.html index e868edc222..eb51573dfe 100644 --- a/hypha/apply/users/templates/users/account.html +++ b/hypha/apply/users/templates/users/account.html @@ -83,33 +83,19 @@

{% trans "Two-Factor Authentication (2FA)" %}

{% trans "Enable 2FA" %} {% endif %} - - - {% if swappable_form %} -
- {% if swappable_form %} -

{% trans "Become" %}:

-
- {{ swappable_form.media }} - {% csrf_token %} - {% for field in swappable_form %} - {% include "forms/includes/field.html" %} - {% endfor %} -
- -
-
- {% endif %} - - {# Remove the comment block tags below when such need arises. e.g. adding new providers #} - {% comment %} - {% can_use_oauth as show_oauth_link %} - {% if show_oauth_link %} - {% trans "Manage OAuth" %} - {% endif %} - {% endcomment %} + {# Remove the comment block tags below when such need arises. e.g. adding new providers #} + {% comment %} + {% can_use_oauth as show_oauth_link %} + {% if show_oauth_link %} +

Manage OAuth

+
+ + {% trans "Manage OAuth" %} +
- {% endif %} + {% endif %} + {% endcomment %} +
{% endblock %} diff --git a/hypha/apply/users/templates/users/become.html b/hypha/apply/users/templates/users/become.html new file mode 100644 index 0000000000..6a1376c5fc --- /dev/null +++ b/hypha/apply/users/templates/users/become.html @@ -0,0 +1,48 @@ +{% load i18n static %} + +{% modal_title %}{% trans "Switch to User..." %}{% endmodal_title %} + +
+ {% csrf_token %} + {% if next %} + + {% endif %} + + + + {{ form.user_pk }} + +

+ {{ form.user_pk.help_text }} +

+ +
+ + + +
+ + + diff --git a/hypha/apply/users/urls.py b/hypha/apply/users/urls.py index 32afa399a6..5750a14a16 100644 --- a/hypha/apply/users/urls.py +++ b/hypha/apply/users/urls.py @@ -23,6 +23,7 @@ account_email_change, create_password, elevate_check_code_view, + hijack_view, oauth, send_confirm_access_email_view, set_password_view, @@ -112,6 +113,7 @@ ActivationView.as_view(), name="activate", ), + path("hijack/", hijack_view, name="hijack"), path("activate/", create_password, name="activate_password"), path("oauth", oauth, name="oauth"), # 2FA diff --git a/hypha/apply/users/views.py b/hypha/apply/users/views.py index d6ee7663be..97f929cd75 100644 --- a/hypha/apply/users/views.py +++ b/hypha/apply/users/views.py @@ -143,23 +143,36 @@ def get_success_url(self): return reverse_lazy("users:account") def get_context_data(self, **kwargs): - if self.request.user.is_superuser and settings.HIJACK_ENABLE: - swappable_form = BecomeUserForm() - else: - swappable_form = None - show_change_password = ( password_management_enabled() and self.request.user.has_usable_password(), ) return super().get_context_data( - swappable_form=swappable_form, default_device=default_device(self.request.user), show_change_password=show_change_password, **kwargs, ) +@login_required +def hijack_view(request): + if not settings.HIJACK_ENABLE: + raise Http404(_("Hijack feature is not enabled.")) + + if not request.user.is_superuser: + raise PermissionDenied() + + next = get_redirect_url(request, "next") + if request.method == "POST": + form = BecomeUserForm(request.POST) + if form.is_valid(): + return AcquireUserView.as_view()(request) + else: + form = BecomeUserForm() + + return render(request, "users/become.html", {"form": form, "next": next}) + + @login_required def account_email_change(request): if request.user.has_usable_password() and not request.is_elevated(): diff --git a/hypha/core/context_processors.py b/hypha/core/context_processors.py index d1efdf6b6b..8986cee206 100644 --- a/hypha/core/context_processors.py +++ b/hypha/core/context_processors.py @@ -22,4 +22,5 @@ def global_vars(request): "SENTRY_DEBUG": settings.SENTRY_DEBUG, "SENTRY_PUBLIC_KEY": settings.SENTRY_PUBLIC_KEY, "SUBMISSIONS_TABLE_EXCLUDED_FIELDS": settings.SUBMISSIONS_TABLE_EXCLUDED_FIELDS, + "HIJACK_ENABLE": settings.HIJACK_ENABLE, } diff --git a/hypha/settings/base.py b/hypha/settings/base.py index f4425b76e7..35e7682258 100644 --- a/hypha/settings/base.py +++ b/hypha/settings/base.py @@ -77,6 +77,7 @@ # Enable staff to "hijack" (become) other users. # Good for testing, might not be a good idea in production. HIJACK_ENABLE = env.bool("HIJACK_ENABLE", False) +HIJACK_INSERT_BEFORE = None # Organisation name and e-mail address etc., used in e-mail templates etc. ORG_EMAIL = env.str("ORG_EMAIL", "info@example.org") diff --git a/hypha/static_src/tailwind/main.css b/hypha/static_src/tailwind/main.css index 107993a6c1..fc85b1379a 100644 --- a/hypha/static_src/tailwind/main.css +++ b/hypha/static_src/tailwind/main.css @@ -9,3 +9,16 @@ @import "./components/django-file-field.css"; @import "tailwindcss/utilities"; + +@layer utilities { + .border-stripe { + border-image: repeating-linear-gradient( + -55deg, + #000 0px, + #000 20px, + #ffb101 20px, + #ffb101 40px + ) + 10; + } +} diff --git a/hypha/templates/base.html b/hypha/templates/base.html index 1d4d4b2e31..45cf53084c 100644 --- a/hypha/templates/base.html +++ b/hypha/templates/base.html @@ -68,6 +68,10 @@ hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' class="antialiased min-h-full flex flex-col {% block body_class %}template-{{ page.get_verbose_name|slugify }}{% endblock %}" > + {% if request.user.is_hijacked %} + {% include "includes/hijack-bar.html" %} + {% endif %} + {% include "includes/sprites.html" %} {% if messages %} diff --git a/hypha/templates/includes/hijack-bar.html b/hypha/templates/includes/hijack-bar.html new file mode 100644 index 0000000000..0ef64a7e3c --- /dev/null +++ b/hypha/templates/includes/hijack-bar.html @@ -0,0 +1,29 @@ +{% load i18n heroicons %} + + + {% csrf_token %} + + + + {% heroicon_micro "exclamation-triangle" size=18 class="inline align-text-bottom" aria_hidden=true %} + + {% blocktrans trimmed with user=request.user %} + You are currently working on behalf of {{ user }} + {% endblocktrans %} + <{{ request.user.email }}> + {% for role in request.user.get_role_names %} + + {{ role }} + + {% endfor %} + + + + + +
diff --git a/hypha/templates/includes/user_menu.html b/hypha/templates/includes/user_menu.html index 0fac8e6435..571646a7c3 100644 --- a/hypha/templates/includes/user_menu.html +++ b/hypha/templates/includes/user_menu.html @@ -66,11 +66,42 @@ {% endif %} {% endif %} + {% if HIJACK_ENABLE and not user.is_hijacked and user.is_superuser %} + + {% heroicon_outline "arrows-right-left" size=18 class="stroke-gray-500 inline group-hover:scale-110 group-hover:stroke-2 group-hover:stroke-dark-blue transition-transform" aria_hidden=true %} + {% trans "Switch User" %} + + {% endif %} + {% if user.is_hijacked %} +
+ {% csrf_token %} + + + +
+ {% endif %} +
{% trans "Log out" %}