diff --git a/orchestra/contrib/musician/forms.py b/orchestra/contrib/musician/forms.py index 67dec100..791f3699 100644 --- a/orchestra/contrib/musician/forms.py +++ b/orchestra/contrib/musician/forms.py @@ -3,8 +3,11 @@ from django.contrib.auth.forms import AuthenticationForm from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ +from django.contrib.auth.hashers import make_password + from orchestra.contrib.domains.models import Domain, Record from orchestra.contrib.mailboxes.models import Address, Mailbox +from orchestra.contrib.systemusers.models import WebappUsers, SystemUser from orchestra.contrib.musician.validators import ValidateZoneMixin from . import api @@ -27,6 +30,42 @@ class LoginForm(AuthenticationForm): return self.cleaned_data +class ChangePasswordForm(forms.ModelForm): + error_messages = { + 'password_mismatch': _('The two password fields didn’t match.'), + } + password = forms.CharField( + label=_("Password"), + strip=False, + widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}), + ) + password2 = forms.CharField( + label=_("Password confirmation"), + widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}), + strip=False, + help_text=_("Enter the same password as before, for verification."), + ) + + class Meta: + fields = ("password",) + model = WebappUsers + + def clean_password2(self): + password = self.cleaned_data.get("password") + password2 = self.cleaned_data.get("password2") + if password and password2 and password != password2: + raise ValidationError( + self.error_messages['password_mismatch'], + code='password_mismatch', + ) + return password2 + + def clean(self): + cleaned_data = super().clean() + password = cleaned_data.get("password") + cleaned_data['password'] = make_password(password) + return cleaned_data + class MailForm(forms.ModelForm): class Meta: @@ -53,37 +92,13 @@ class MailForm(forms.ModelForm): return instance -class MailboxChangePasswordForm(forms.ModelForm): - error_messages = { - 'password_mismatch': _('The two password fields didn’t match.'), - } - password = forms.CharField( - label=_("Password"), - strip=False, - widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}), - ) - password2 = forms.CharField( - label=_("Password confirmation"), - widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}), - strip=False, - help_text=_("Enter the same password as before, for verification."), - ) - +class MailboxChangePasswordForm(ChangePasswordForm): + class Meta: fields = ("password",) model = Mailbox - def clean_password2(self): - password = self.cleaned_data.get("password") - password2 = self.cleaned_data.get("password2") - if password and password2 and password != password2: - raise ValidationError( - self.error_messages['password_mismatch'], - code='password_mismatch', - ) - return password2 - - + class MailboxCreateForm(forms.ModelForm): error_messages = { 'password_mismatch': _('The two password fields didn’t match.'), @@ -120,7 +135,7 @@ class MailboxCreateForm(forms.ModelForm): self.error_messages['password_mismatch'], code='password_mismatch', ) - return password2 + return password def save(self, commit=True): instance = super().save(commit=False) @@ -169,3 +184,14 @@ class RecordUpdateForm(ValidateZoneMixin, forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.domain = self.instance.domain + + +class WebappUsersChangePasswordForm(ChangePasswordForm): + class Meta: + fields = ("password",) + model = WebappUsers + +class SystemUsersChangePasswordForm(ChangePasswordForm): + class Meta: + fields = ("password",) + model = SystemUser diff --git a/orchestra/contrib/musician/templates/musician/systemuser_change_password.html b/orchestra/contrib/musician/templates/musician/systemuser_change_password.html new file mode 100644 index 00000000..c2e7b933 --- /dev/null +++ b/orchestra/contrib/musician/templates/musician/systemuser_change_password.html @@ -0,0 +1,15 @@ +{% extends "musician/base.html" %} +{% load bootstrap4 i18n %} + +{% block content %} +

{% trans "Change password" %}: {{ object.name }}

+ +
+ {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + {% trans "Cancel" %} + + {% endbuttons %} +
+{% endblock %} diff --git a/orchestra/contrib/musician/templates/musician/systemuser_list.html b/orchestra/contrib/musician/templates/musician/systemuser_list.html new file mode 100644 index 00000000..781cf173 --- /dev/null +++ b/orchestra/contrib/musician/templates/musician/systemuser_list.html @@ -0,0 +1,41 @@ +{% extends "musician/users_base.html" %} +{% load bootstrap4 i18n %} + +{% block tabcontent %} +

+

{% trans "The main user is your system's main user on each server. You'll be able to view the logs of your websites at (/home/account/logs) and all web content, but you'll never be able to edit content on a website." %}

+

{% trans "This user only has write permissions in their own directory." %}

+ + + + + + + + + + + + + + + + + {% for systemuser in object_list %} + {% if systemuser.is_main %} + + + + + + {% endif %} + {% endfor %} + +
{% trans "Username" %}{% trans "Path" %}
{{ systemuser.username }}{{ systemuser.home }}/{{ systemuser.username }} + +
+ +{% endblock %} diff --git a/orchestra/contrib/musician/templates/musician/users_base.html b/orchestra/contrib/musician/templates/musician/users_base.html new file mode 100644 index 00000000..92af95f8 --- /dev/null +++ b/orchestra/contrib/musician/templates/musician/users_base.html @@ -0,0 +1,31 @@ +{% extends "musician/base.html" %} +{% load i18n %} + +{% block content %} +{% if active_domain %} + {% trans "Go to global" %} +{% endif %} + +

{{ service.verbose_name }} + {% if active_domain %}{% trans "for" %} {{ active_domain.name }}{% endif %} +

+

{{ service.description }}

+ +{% with request.resolver_match.url_name as url_name %} + +{% endwith %} + +
+ {% block tabcontent %} + {% endblock %} + +{% endblock %} diff --git a/orchestra/contrib/musician/templates/musician/webappuser_change_password.html b/orchestra/contrib/musician/templates/musician/webappuser_change_password.html new file mode 100644 index 00000000..b101c613 --- /dev/null +++ b/orchestra/contrib/musician/templates/musician/webappuser_change_password.html @@ -0,0 +1,15 @@ +{% extends "musician/base.html" %} +{% load bootstrap4 i18n %} + +{% block content %} +

{% trans "Change password" %}: {{ object.name }}

+ +
+ {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + {% trans "Cancel" %} + + {% endbuttons %} +
+{% endblock %} diff --git a/orchestra/contrib/musician/templates/musician/webappuser_list.html b/orchestra/contrib/musician/templates/musician/webappuser_list.html new file mode 100644 index 00000000..bde24e3a --- /dev/null +++ b/orchestra/contrib/musician/templates/musician/webappuser_list.html @@ -0,0 +1,37 @@ +{% extends "musician/users_base.html" %} +{% load bootstrap4 i18n %} + +{% block tabcontent %} + + + + + + + + + + + + + + + + + + {% for webappuser in object_list %} + + + + + + + {% endfor %} + + {% include "musician/components/table_paginator.html" %} +
{% trans "Username" %}{% trans "Path" %}{% trans "Server" %}
{{ webappuser.username }}/home/{{ webappuser.account }}/webapps/{{ webappuser.home }}{{ webappuser.target_server }} + + {% trans "Update password" %} +
+ +{% endblock %} diff --git a/orchestra/contrib/musician/urls.py b/orchestra/contrib/musician/urls.py index 98b81601..c157aac4 100644 --- a/orchestra/contrib/musician/urls.py +++ b/orchestra/contrib/musician/urls.py @@ -39,4 +39,9 @@ urlpatterns = [ path('mailing-lists/', views.MailingListsView.as_view(), name='mailing-lists'), path('databases/', views.DatabaseListView.as_view(), name='database-list'), path('saas/', views.SaasListView.as_view(), name='saas-list'), + + path('webappusers/', views.WebappUserListView.as_view(), name='webappuser-list'), + path('webappuser//change-password/', views.WebappUserChangePasswordView.as_view(), name='webappuser-password'), + path('systemusers/', views.SystemUserListView.as_view(), name='systemuser-list'), + path('systemuser//change-password/', views.SystemUserChangePasswordView.as_view(), name='systemuser-password'), ] diff --git a/orchestra/contrib/musician/views.py b/orchestra/contrib/musician/views.py index fcdb5c5a..00077c52 100644 --- a/orchestra/contrib/musician/views.py +++ b/orchestra/contrib/musician/views.py @@ -32,12 +32,14 @@ from orchestra.contrib.lists.models import List from orchestra.contrib.mailboxes.models import Address, Mailbox from orchestra.contrib.resources.models import Resource, ResourceData from orchestra.contrib.saas.models import SaaS +from orchestra.contrib.systemusers.models import WebappUsers, SystemUser from orchestra.utils.html import html_to_pdf from .auth import logout as auth_logout from .forms import (LoginForm, MailboxChangePasswordForm, MailboxCreateForm, MailboxSearchForm, MailboxUpdateForm, MailForm, - RecordCreateForm, RecordUpdateForm) + RecordCreateForm, RecordUpdateForm, WebappUsersChangePasswordForm, + SystemUsersChangePasswordForm) from .mixins import (CustomContextMixin, ExtendedPaginationMixin, UserTokenRequiredMixin) from .models import Address as AddressService @@ -612,3 +614,39 @@ class LogoutView(RedirectView): def post(self, request, *args, **kwargs): """Logout may be done via POST.""" return self.get(request, *args, **kwargs) + + +class WebappUserListView(ServiceListView): + model = WebappUsers + template_name = "musician/webappuser_list.html" + extra_context = { + # Translators: This message appears on the page title + 'title': _('Webapp users'), + } + +class WebappUserChangePasswordView(CustomContextMixin, UserTokenRequiredMixin, UpdateView): + template_name = "musician/webappuser_change_password.html" + model = WebappUsers + form_class = WebappUsersChangePasswordForm + success_url = reverse_lazy("musician:webappuser-list") + + def get_queryset(self): + return self.model.objects.filter(account=self.request.user) + + +class SystemUserListView(ServiceListView): + model = SystemUser + template_name = "musician/systemuser_list.html" + extra_context = { + # Translators: This message appears on the page title + 'title': _('Main users'), + } + +class SystemUserChangePasswordView(CustomContextMixin, UserTokenRequiredMixin, UpdateView): + template_name = "musician/systemuser_change_password.html" + model = SystemUser + form_class = SystemUsersChangePasswordForm + success_url = reverse_lazy("musician:systemuser-list") + + def get_queryset(self): + return self.model.objects.filter(account=self.request.user) \ No newline at end of file