diff --git a/src/backoffice/locale/de/LC_MESSAGES/django.po b/src/backoffice/locale/de/LC_MESSAGES/django.po index e57363e86ff8f9d1f5b5d93e8bc7c8be4ce5cb7f..d7e394ca93b3a576c1441fa8cba3f3a4f16cbb2f 100644 --- a/src/backoffice/locale/de/LC_MESSAGES/django.po +++ b/src/backoffice/locale/de/LC_MESSAGES/django.po @@ -171,6 +171,9 @@ msgstr "Dieser Badge wird auch bei den Nutzern entfernt. Dies kann nicht rückg msgid "Badge__valid__Permanent__Redeem__tokens" msgstr "Aktive Redeem Tokens (Permanent, Map und Limited)" +msgid "delete" +msgstr "entfernen" + msgid "Badge-new" msgstr "Neues Badge" @@ -258,9 +261,6 @@ msgstr "bitte wählen" msgid "add" msgstr "hinzufügen" -msgid "delete" -msgstr "entfernen" - msgid "assemblyedit_addlink" msgstr "Bezug zu anderer Assembly herstellen" diff --git a/src/backoffice/locale/en/LC_MESSAGES/django.po b/src/backoffice/locale/en/LC_MESSAGES/django.po index 7fad31971447260796ffb544bb069f2a10f7871b..0b4c88cbb30bbbc95e77c4b1afaada2cdb82dafb 100644 --- a/src/backoffice/locale/en/LC_MESSAGES/django.po +++ b/src/backoffice/locale/en/LC_MESSAGES/django.po @@ -171,6 +171,9 @@ msgstr "This badge will be removed from users as well. This cannot be undone. Ar msgid "Badge__valid__Permanent__Redeem__tokens" msgstr "Active Redeem Tokens (Permanent, Map und Limited)" +msgid "delete" +msgstr "delete" + msgid "Badge-new" msgstr "new badge" @@ -258,9 +261,6 @@ msgstr "please select" msgid "add" msgstr "add" -msgid "delete" -msgstr "delete" - msgid "assemblyedit_addlink" msgstr "Associate with another assembly" diff --git a/src/backoffice/templates/backoffice/user-block.html b/src/backoffice/templates/backoffice/user-block.html index a558316489e1108fb18bde21460a7f7d2c8032a2..73be08b4bbd7c6028eb7d24777ed2cfe630da779 100644 --- a/src/backoffice/templates/backoffice/user-block.html +++ b/src/backoffice/templates/backoffice/user-block.html @@ -26,7 +26,7 @@ {% endif %} {% if object.id != user.id %} - <button type="submit" class="btn btn-danger" disabled><strong>{{ object.username }}</strong> aus der Konferenz werfen</button> + <button type="submit" class="btn btn-danger"><strong>{{ object.username }}</strong> aus der Konferenz werfen</button> {% else %} <button type="submit" class="btn btn-danger" onclick="return confirm('virtuellen Selbstmord begehen?!')">eigenen Account (<strong>{{ object.username }}</strong>) aus der Konferenz werfen</button> {% endif %} diff --git a/src/backoffice/templates/backoffice/user-detail.html b/src/backoffice/templates/backoffice/user-detail.html index 936cb8929e3e11b416e5f30fbf9aeebd5d97da66..43af4c58667378f2c49a52d218e196d985320bdb 100644 --- a/src/backoffice/templates/backoffice/user-detail.html +++ b/src/backoffice/templates/backoffice/user-detail.html @@ -17,6 +17,16 @@ {{ object.username }} </div> <div class="card-body"> + {% if not object.is_active %} + <div class="alert alert-danger"> + DISABLED USER + </div> + {% endif %} + {% if object.shadow_banned %} + <div class="alert alert-warning"> + SHADOW BANNED + </div> + {% endif %} <form> <fieldset disabled> <div class="form-row"> @@ -93,7 +103,8 @@ <div class="card"> <div class="card-header">Actions</div> <div class="card-body text-center"> - <p><a href="{% url 'backoffice:user-block' pk=object.pk %}" class="btn btn-default border-danger">{% if object.is_active %}🛑<br>block user{% else %}🛴<br>unblock user{% endif %}</a></p> + {% if can_block %}<p>{% if object.is_active %}<a href="{% url 'backoffice:user-block' pk=object.pk %}" class="btn btn-default border-danger">🛑<br>block user</a></p>{% endif %}{% endif %} + {% if can_rename %}<p><a href="{% url 'backoffice:user-rename' pk=object.pk %}" class="btn btn-default border-primary">🛴<br>rename user</a></p>{% endif %} <p><a href="javascript:alert('TODO')" class="btn btn-default border-primary">✉<br>message</a></p> </div> </div> diff --git a/src/backoffice/templates/backoffice/user-list.html b/src/backoffice/templates/backoffice/user-list.html index b33dd8341f8cf42a3b43e8c45a23afe8e71ccba9..82ca8ced68bc84f2bc341a2fdd6f1b76700fc324 100644 --- a/src/backoffice/templates/backoffice/user-list.html +++ b/src/backoffice/templates/backoffice/user-list.html @@ -5,9 +5,14 @@ <div class="card"> <div class="card-body"> + search {{ usercount }} users: <form action="{% url 'backoffice:users' %}" method="POST" class="form-inline">{% csrf_token %} <input type="text" class="form-control" name="search" placeholder="username, displayname, ..." value="{{ search_term }}"> - <button type="submit" class="btn btn-primary ml-2">🔍</button> + + <button type="submit" class="btn btn-primary mx-2">🔍</button> + + <input type="checkbox" id="idmyconf" name="myconf"{% if myconf %} checked{% endif %}> + <label for="idmyconf">limit to participants</label> </form> </div> </div> @@ -29,7 +34,7 @@ </thead> <tbody> {% for obj in object_list %} - <tr> + <tr{% if not obj.is_active %} style="text-decoration: line-through;"{% endif %}> <td>{% if obj.is_staff or obj.is_conference_staff %}<span title="staff">⚙</span>{% endif %}</td> <td>{% if obj.active_conference_angel %}<span title="angel">🧚</span>{% endif %}</td> <td><a href="{% url 'backoffice:user-detail' pk=obj.pk %}">{{ obj.username }}</a></td> diff --git a/src/backoffice/templates/backoffice/user-rename.html b/src/backoffice/templates/backoffice/user-rename.html new file mode 100644 index 0000000000000000000000000000000000000000..0b7b5dfa06b84e6624b3de2f2a4cff0fa0d4a2fb --- /dev/null +++ b/src/backoffice/templates/backoffice/user-rename.html @@ -0,0 +1,31 @@ +{% extends 'backoffice/base.html' %} +{% load i18n %} + +{% block content %} + +<div class="row"> + <div class="col-md-10"> + <div class="card border-primary"> + <div class="card-header bg-primary"> + <a href="{% url 'backoffice:user-detail' pk=object.pk %}" class="text-white"><< zurück</a> + </div> + <div class="card-body"> + <form action="{% url 'backoffice:user-rename' pk=object.pk %}" method="POST" class="form">{% csrf_token %} + Username: <strong>{{ object.username }}</strong> + + <div class="form-group"> + <label for="idNewName">change to:</label> + <input type="text" id="idNewName" class="form-control" name="new_name" value="" placeholder="{{ object.username }}"> + </div> + + <button type="submit" class="btn btn-primary">Rename user</button> + </form> + </div> + </div> + </div> + + <div class="col-md-2"> + {% include 'backoffice/user-sidebar.html' %} + </div> +</div> +{% endblock %} \ No newline at end of file diff --git a/src/backoffice/urls.py b/src/backoffice/urls.py index 6f7685c07cf6af000434e1d8e1ef4790e576bf0f..3e9495d1c85c39081aac17c3ade8f314403cdde2 100644 --- a/src/backoffice/urls.py +++ b/src/backoffice/urls.py @@ -75,6 +75,7 @@ urlpatterns = [ path('users', users.UsersView.as_view(), name='users'), path('users/<int:pk>', users.UserView.as_view(), name='user-detail'), path('users/<int:pk>/block', users.UserBlockView.as_view(), name='user-block'), + path('users/<int:pk>/rename', users.UserRenameView.as_view(), name='user-rename'), path('set_language', misc.SetLanguageView.as_view(), name='set_language'), ] diff --git a/src/backoffice/views/mixins.py b/src/backoffice/views/mixins.py index 916c1e05b69d8c130095983449bc25dd1718eff4..63b691657dcac1782e28ef93af196a1d278da9d7 100644 --- a/src/backoffice/views/mixins.py +++ b/src/backoffice/views/mixins.py @@ -89,7 +89,7 @@ class ConferenceMixin(PermissionRequiredMixin): context.update({ 'has_assemblies': self.is_assembly_team, 'has_pages': self.request.user.has_conference_staffpermission(self.conference, 'core.static_pages'), - 'has_users': self.request.user.has_conference_staffpermission(self.conference, 'core.view_platformuser', 'core.block_platformuser'), + 'has_users': self.request.user.has_conference_staffpermission(self.conference, 'core.platformusers', 'core.block_platformuser'), }) else: context.update({ diff --git a/src/backoffice/views/users.py b/src/backoffice/views/users.py index cc353f765824ad16ffa405cafaf655a5dbcda77e..3929b0a5a3922d4ed7a665101bc7f3a3aeb3ab11 100644 --- a/src/backoffice/views/users.py +++ b/src/backoffice/views/users.py @@ -1,13 +1,18 @@ import logging from django.contrib import messages +from django.contrib.sessions.exceptions import SuspiciousSession +from django.contrib.sessions.models import Session from django.db.models import F from django.shortcuts import redirect, render from django.views.generic import DetailView, TemplateView +from oauth2_provider.models import AccessToken + from core.models.conference import ConferenceMember from core.models.users import PlatformUser + from .mixins import ConferenceMixin @@ -16,13 +21,19 @@ MAX_ROWS = 42 class UsersView(ConferenceMixin, TemplateView): - permissions_required = ['core.view_platformuser'] + permissions_required = ['core.platformusers'] template_name = 'backoffice/user-list.html' def get_context_data(self, *args, **kwargs): ctx = super().get_context_data(*args, **kwargs) ctx['active_page'] = 'users' ctx['object_list'] = None + ctx['usercount'] = PlatformUser.objects.count() + ctx['myconf'] = self.request.method == 'GET' or 'myconf' in self.request.POST + + ctx['can_block'] = self.request.user.has_conference_staffpermission(self.conference, 'core.block_platformuser') + ctx['can_rename'] = self.request.user.has_conference_staffpermission(self.conference, 'core.rename_platformuser') + return ctx def post(self, *args, **kwargs): @@ -31,7 +42,9 @@ class UsersView(ConferenceMixin, TemplateView): messages.error(self.request, 'Minimum 3 Zeichen!') return self.get(*args, **kwargs) - qs = PlatformUser.objects.filter(conferences__conference=self.conference, user_type=PlatformUser.Type.HUMAN) + qs = PlatformUser.objects.filter(user_type=PlatformUser.Type.HUMAN) + if 'myconf' in self.request.POST: + qs = qs.filter(conferences__conference=self.conference) qs = qs.annotate(is_conference_staff=F('conferences__is_staff'), active_conference_angel=F('conferences__active_angel')) qs = qs.filter(username__icontains=search_term) qs = qs.order_by('username') @@ -51,7 +64,7 @@ class UsersView(ConferenceMixin, TemplateView): class UserView(ConferenceMixin, DetailView): model = PlatformUser - permissions_required = ['core.view_platformuser'] + permissions_required = ['core.platformusers'] template_name = 'backoffice/user-detail.html' def get_context_data(self, *args, **kwargs): @@ -84,6 +97,72 @@ class UserBlockView(ConferenceMixin, DetailView): return ctx def post(self, *args, **kwargs): - obj = self.get_object() - messages.error(self.request, 'User blockieren ist noch nicht implementiert!') - return redirect('backoffice:user-detail', pk=obj.pk) + user = self.get_object() + + user.is_active = False + user.save() + messages.success(self.request, 'User disabled: ' + user.username) + logger.warning('User "%s" was disabled by %s.', user.username, self.request.user) + + log = [] + deleted_access_tokens = 0 + for token in AccessToken.objects.filter(user_id=user.id): + log.append(str(token.application)) + token.delete() + deleted_access_tokens += 1 + + if deleted_access_tokens > 0: + messages.info(self.request, f'OAuth2 access token by {user.username} deleted for application(s):\n' + ',\n'.join(log)) + else: + messages.info(self.request, f'No OAuth2 access tokens by {user.username} to be deleted.') + + deleted_sessions = 0 + for session in Session.objects.all(): + try: + session_data = session.get_decoded() + if session_data.get('_auth_user_id') == str(user.pk) and session_data.get('_auth_user_backend') == 'django.contrib.auth.backends.ModelBackend': + session.delete() + deleted_sessions += 1 + + except SuspiciousSession: + # while we are at it, delete the errornous session, too + session.delete() + + if deleted_sessions > 0: + messages.info(self.request, f'Deleted {deleted_sessions} sessions.') + else: + messages.info(self.request, 'No sessions to delete o.O') + + return redirect('backoffice:user-detail', pk=user.pk) + + +class UserRenameView(ConferenceMixin, DetailView): + model = PlatformUser + permissions_required = ['core.rename_platformuser'] + template_name = 'backoffice/user-rename.html' + + def get_context_data(self, *args, **kwargs): + ctx = super().get_context_data(*args, **kwargs) + + ctx['active_page'] = 'users' + + return ctx + + def post(self, *args, **kwargs): + user = self.get_object() + + new_name = self.request.POST.get('new_name', '').strip() + if new_name == '' or new_name == user.username: + return self.get(*args, **kwargs) + + if PlatformUser.objects.filter(username=new_name): + messages.error(self.request, f'Username "{new_name}" exists, won\'t rename "{user.username}"!') + return self.get(*args, **kwargs) + + old_name = user.username + user.username = new_name + user.save() + messages.success(self.request, f'Renamed user "{old_name}" to "{new_name}".') + logger.warning('Renamed user "%s" to "%s".', old_name, new_name) + + return redirect('backoffice:user-detail', pk=user.pk) diff --git a/src/core/migrations/0055_backoffice_users_perms.py b/src/core/migrations/0055_backoffice_users_perms.py new file mode 100644 index 0000000000000000000000000000000000000000..e1db3877e2b43ebcca6113e08df6a5fe10033033 --- /dev/null +++ b/src/core/migrations/0055_backoffice_users_perms.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1.3 on 2020-12-29 02:00 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0054_UserBadgeGrammar'), + ] + + operations = [ + migrations.AlterModelOptions( + name='conferencemember', + options={'permissions': [('assembly_team', 'ConferenceMember__permission-assembly_team'), ('static_pages', 'ConferenceMember__permission-static_pages'), ('platformusers', 'Orga: Users List'), ('rename_platformuser', 'Orga: Rename User'), ('block_platformuser', 'ConferenceMember__permission-block_platformuser'), ('change_conferencemember__active_angel', 'ConferenceMember__permission-change_conferencemember__active_angel'), ('view_platformuser__guardian', 'ConferenceMember__permission-view_platformuser__guardian')]}, + ), + ] diff --git a/src/core/models/conference.py b/src/core/models/conference.py index 773a09b1e4a4e9d0fb5387147fdf9b95569c7885..5e2204e49f647c07a00e1f897efc4f36003d6fcd 100644 --- a/src/core/models/conference.py +++ b/src/core/models/conference.py @@ -33,6 +33,9 @@ class ConferenceMember(models.Model): ('static_pages', _('ConferenceMember__permission-static_pages')), # Access to static pages, can be further limited by configuring static_page_groups. + ('platformusers', 'Orga: Users List'), + ('rename_platformuser', 'Orga: Rename User'), + ('block_platformuser', _('ConferenceMember__permission-block_platformuser')), # This is the right to block a misbehaving user.