Compare commits

...

10 Commits

20 changed files with 737 additions and 42 deletions

View File

@ -2,12 +2,21 @@ from django import forms
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.encoding import force_str
from orchestra.forms.widgets import DynamicHelpTextSelect
from django.contrib.auth.hashers import make_password
from orchestra.contrib.domains.models import Domain, Record from orchestra.contrib.domains.models import Domain, Record
from orchestra.contrib.mailboxes.models import Address, Mailbox from orchestra.contrib.mailboxes.models import Address, Mailbox
from orchestra.contrib.systemusers.models import WebappUsers, SystemUser
from orchestra.contrib.musician.validators import ValidateZoneMixin from orchestra.contrib.musician.validators import ValidateZoneMixin
from orchestra.contrib.webapps.models import WebApp, WebAppOption
from orchestra.contrib.webapps.options import AppOption
from orchestra.contrib.webapps.types import AppType
from . import api from . import api
from .settings import MUSICIAN_EDIT_ENABLE_PHP_OPTIONS
class LoginForm(AuthenticationForm): class LoginForm(AuthenticationForm):
@ -27,6 +36,42 @@ class LoginForm(AuthenticationForm):
return self.cleaned_data return self.cleaned_data
class ChangePasswordForm(forms.ModelForm):
error_messages = {
'password_mismatch': _('The two password fields didnt 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 MailForm(forms.ModelForm):
class Meta: class Meta:
@ -53,36 +98,12 @@ class MailForm(forms.ModelForm):
return instance return instance
class MailboxChangePasswordForm(forms.ModelForm): class MailboxChangePasswordForm(ChangePasswordForm):
error_messages = {
'password_mismatch': _('The two password fields didnt 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: class Meta:
fields = ("password",) fields = ("password",)
model = Mailbox 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): class MailboxCreateForm(forms.ModelForm):
error_messages = { error_messages = {
@ -120,7 +141,13 @@ class MailboxCreateForm(forms.ModelForm):
self.error_messages['password_mismatch'], self.error_messages['password_mismatch'],
code='password_mismatch', code='password_mismatch',
) )
return password2 return password
def clean(self):
cleaned_data = super().clean()
password = cleaned_data.get("password")
cleaned_data['password'] = make_password(password)
return cleaned_data
def save(self, commit=True): def save(self, commit=True):
instance = super().save(commit=False) instance = super().save(commit=False)
@ -169,3 +196,75 @@ class RecordUpdateForm(ValidateZoneMixin, forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.domain = self.instance.domain self.domain = self.instance.domain
class WebappUsersChangePasswordForm(ChangePasswordForm):
class Meta:
fields = ("password",)
model = WebappUsers
class SystemUsersChangePasswordForm(ChangePasswordForm):
class Meta:
fields = ("password",)
model = SystemUser
class WebappOptionForm(forms.ModelForm):
OPTIONS_HELP_TEXT = {
op.name: force_str(op.help_text) for op in AppOption.get_plugins()
}
class Meta:
model = WebAppOption
fields = ("name", "value")
def __init__(self, *args, **kwargs):
try:
self.webapp = kwargs.pop('webapp')
super().__init__(*args, **kwargs)
except:
super().__init__(*args, **kwargs)
self.webapp = self.instance.webapp
target = 'this.id.replace("name", "value")'
self.fields['name'].widget.attrs = DynamicHelpTextSelect(target, self.OPTIONS_HELP_TEXT).attrs
def save(self, commit=True):
instance = super().save(commit=False)
instance.webapp = self.webapp
if commit:
super().save(commit=True)
self.webapp.save()
return instance
class WebappOptionCreateForm(WebappOptionForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
plugin = AppType.get(self.webapp.type)
choices = list(plugin.get_group_options_choices())
for grupo, opciones in enumerate(choices):
if isinstance(opciones[1], list):
nueva_lista = [opc for opc in opciones[1] if opc[0] in MUSICIAN_EDIT_ENABLE_PHP_OPTIONS]
choices[grupo] = (opciones[0], nueva_lista)
self.fields['name'].widget.choices = choices
def clean(self):
cleaned_data = super().clean()
name = self.cleaned_data.get("name")
if WebAppOption.objects.filter(webapp=self.webapp, name=name).exists():
raise ValidationError(_("This option already exist."))
return cleaned_data
class WebappOptionUpdateForm(WebappOptionForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['name'].widget.choices = [(self.initial['name'], self.initial['name'])]

View File

@ -14,11 +14,13 @@ class CustomContextMixin(ContextMixin):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
# generate services menu items # generate services menu items
services_menu = [ services_menu = [
{'icon': 'globe-europe', 'pattern_name': 'musician:dashboard', 'title': _('Domains & websites')}, {'icon': 'globe-europe', 'pattern_name': 'musician:dashboard', 'title': _('Domains')},
{'icon': 'envelope', 'pattern_name': 'musician:address-list', 'title': _('Mails')}, {'icon': 'envelope', 'pattern_name': 'musician:address-list', 'title': _('Mails')},
{'icon': 'mail-bulk', 'pattern_name': 'musician:mailing-lists', 'title': _('Mailing lists')}, {'icon': 'mail-bulk', 'pattern_name': 'musician:mailing-lists', 'title': _('Mailing lists')},
{'icon': 'database', 'pattern_name': 'musician:database-list', 'title': _('Databases')}, {'icon': 'database', 'pattern_name': 'musician:database-list', 'title': _('Databases')},
{'icon': 'fire', 'pattern_name': 'musician:saas-list', 'title': _('SaaS')}, {'icon': 'fire', 'pattern_name': 'musician:saas-list', 'title': _('SaaS')},
{'icon': 'globe', 'pattern_name': 'musician:website-list', 'title': _('Websites')},
{'icon': 'folder', 'pattern_name': 'musician:webapp-list', 'title': _('Webapps'), 'indent': True},
] ]
context.update({ context.update({
'services_menu': services_menu, 'services_menu': services_menu,

View File

@ -1,3 +1,4 @@
from orchestra.contrib.settings import Setting
from collections import defaultdict from collections import defaultdict
from django.conf import settings from django.conf import settings
@ -46,3 +47,14 @@ URL_SAAS_GITLAB = getsetting("URL_SAAS_GITLAB")
URL_SAAS_OWNCLOUD = getsetting("URL_SAAS_OWNCLOUD") URL_SAAS_OWNCLOUD = getsetting("URL_SAAS_OWNCLOUD")
URL_SAAS_WORDPRESS = getsetting("URL_SAAS_WORDPRESS") URL_SAAS_WORDPRESS = getsetting("URL_SAAS_WORDPRESS")
MUSICIAN_EDIT_ENABLE_PHP_OPTIONS = Setting('MUSICIAN_EDIT_ENABLE_PHP_OPTIONS', (
'public-root',
'timeout',
'max_input_time',
'max_input_vars',
'memory_limit',
'post_max_size',
'upload_max_filesize',
))

View File

@ -46,7 +46,11 @@
{# <!-- services menu --> #} {# <!-- services menu --> #}
<ul id="sidebar-services" class="nav flex-column"> <ul id="sidebar-services" class="nav flex-column">
{% for item in services_menu %} {% for item in services_menu %}
{% if item.indent %}
<li class="nav-item ml-3">
{% else %}
<li class="nav-item"> <li class="nav-item">
{% endif %}
<a class="nav-link text-light active" href="{% url item.pattern_name %}"> <a class="nav-link text-light active" href="{% url item.pattern_name %}">
<i class="fas fa-{{ item.icon }}"></i> <i class="fas fa-{{ item.icon }}"></i>
{{ item.title }} {{ item.title }}

View File

@ -50,11 +50,12 @@
<div class="col-md-8"> <div class="col-md-8">
{% with domain.websites.0 as website %} {% with domain.websites.0 as website %}
{% with website.contents.0 as content %} {% with website.contents.0 as content %}
<button type="button" class="btn text-secondary" data-toggle="modal" data-target="#configDetailsModal" <a href="{% url 'musician:domain-detail' domain.id %}" class="btn btn-primary">{% trans "View DNS records" %}</a>
<!-- <button type="button" class="btn text-secondary" data-toggle="modal" data-target="#configDetailsModal"
data-domain="{{ domain.name }}" data-website="{{ website|yesno:'true,false' }}" data-webapp-type="{{ content.webapp.type }}" data-root-path="{{ content.path }}" data-domain="{{ domain.name }}" data-website="{{ website|yesno:'true,false' }}" data-webapp-type="{{ content.webapp.type }}" data-root-path="{{ content.path }}"
data-url="{% url 'musician:domain-detail' domain.id %}"> data-url="{% url 'musician:domain-detail' domain.id %}">
{% trans "view configuration" %} <strong class="fas fa-tools"></strong> {% trans "view configuration" %} <strong class="fas fa-tools"></strong>
</button> </button> -->
{% endwith %} {% endwith %}
{% endwith %} {% endwith %}
</div> </div>

View File

@ -0,0 +1,15 @@
{% extends "musician/base.html" %}
{% load bootstrap4 i18n %}
{% block content %}
<h1 class="service-name">{% trans "Change password" %}: <span class="font-weight-light">{{ object.name }}</span></h1>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<a class="btn btn-light mr-2" href="{% url 'musician:systemuser-list' %}">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-secondary">{% trans "Save" %}</button>
{% endbuttons %}
</form>
{% endblock %}

View File

@ -0,0 +1,41 @@
{% extends "musician/users_base.html" %}
{% load bootstrap4 i18n %}
{% block tabcontent %}
<p></p>
<p>{% 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." %}</p>
<p>{% trans "This user only has write permissions in their own directory." %}</p>
<table class="table service-list">
<colgroup>
<col span="1" style="width: 15%;">
<col span="1" style="width: 25%;">
<col span="1" style="width: 60%;">
</colgroup>
<thead class="thead-dark">
<tr>
<th scope="col">{% trans "Username" %}</th>
<th scope="col">{% trans "Path" %}</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for systemuser in object_list %}
{% if systemuser.is_main %}
<tr>
<td>{{ systemuser.username }}</td>
<td>{{ systemuser.home }}/{{ systemuser.username }}</td>
<td>
<div class="d-flex justify-content-end">
<a class="btn btn-outline-warning" href="{% url 'musician:systemuser-password' systemuser.id %}">
<i class="fas fa-key"></i> {% trans "Update password" %}</a>
</div>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@ -0,0 +1,31 @@
{% extends "musician/base.html" %}
{% load i18n %}
{% block content %}
{% if active_domain %}
<a class="btn-arrow-left" href="{% url 'musician:systemuser-list' %}">{% trans "Go to global" %}</a>
{% endif %}
<h1 class="service-name">{{ service.verbose_name }}
{% if active_domain %}<span class="font-weight-light">{% trans "for" %} {{ active_domain.name }}</span>{% endif %}
</h1>
<p class="service-description">{{ service.description }}</p>
{% with request.resolver_match.url_name as url_name %}
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item">
<a class="nav-link {% if url_name == 'systemuser-list' %}active{% endif %}" href="{% url 'musician:systemuser-list' %}" role="tab"
aria-selected="{% if url_name == 'systemuser-list' %}true{% else %}false{% endif %}">{% trans "Main User" %}</a>
</li>
<li class="nav-item">
<a class="nav-link {% if url_name == 'webappuser-list' %}active{% endif %}" href="{% url 'musician:webappuser-list' %}" role="tab"
aria-selected="{% if url_name == 'webappuser-list' %}true{% else %}false{% endif %}">{% trans "SFTP Users" %}</a>
</li>
</ul>
{% endwith %}
<div class="tab-content" id="myTabContent">
{% block tabcontent %}
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,46 @@
{% extends "musician/base.html" %}
{% load i18n %}
{% block content %}
<a class="btn-arrow-left" href="{% url 'musician:webapp-list' %}">{% trans "Go back" %}</a>
<h1 class="service-name">
{% trans "PHP settings for" %} <span class="font-weight-light">{{ object.name }}</span>
</h1>
<p class="service-description">{% trans "PHP settings page description." %}</p>
<table class="table service-list">
<colgroup>
<col span="1" style="width: 12%;">
<col span="1" style="width: 10%;">
<col span="1" style="width: 78%;">
</colgroup>
<thead class="thead-dark">
<tr>
<th scope="col">{% trans "Type" %}</th>
<th scope="col">{% trans "Value" %}</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for option in object.options.all %}
<tr>
<td>{{ option.name }}</td>
<td>{{ option.value }}</td>
<td class="text-right">
{% if option.name in edit_allowed_PHP_options %}
<a href="{% url 'musician:webapp-update-option' object.pk option.pk %}">
<i class="ml-3 fas fa-edit"></i></a>
{% endif %}
<a href="{% url 'musician:webapp-delete-option' object.pk option.pk %}">
<i class="ml-3 text-danger fas fa-trash"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<a class="btn btn-primary mt-4 mb-4" href="{% url 'musician:webapp-add-option' object.pk %}">{% trans "Add new option" %}</a></td>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "musician/base.html" %}
{% load bootstrap4 i18n %}
{% block content %}
<h1 class="service-name">{{ service.verbose_name }}</h1>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<a class="btn btn-light mr-2" href="{% url 'musician:address-list' %}">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-secondary">{% trans "Save" %}</button>
{% if form.instance.pk %}
<div class="float-right">
<a class="btn btn-danger" href="{% url 'musician:address-delete' view.kwargs.pk %}">{% trans "Delete" %}</a>
</div>
{% endif %}
{% endbuttons %}
</form>
{% endblock %}

View File

@ -0,0 +1,55 @@
{% extends "musician/base.html" %}
{% load bootstrap4 i18n %}
{% block content %}
<p>
{{ description }}
</p>
<p>
{{ description2 }}
</p>
<table class="table service-list">
<colgroup>
<col span="1" style="width: 19%;">
<col span="1" style="width: 19%;">
<col span="1" style="width: 19%;">
<col span="1" style="width: 19%;">
<col span="1" style="width: 19%;">
<col span="1" style="width: 5%;">
</colgroup>
<thead class="thead-dark">
<tr>
<th scope="col">{% trans "Name" %}</th>
<th scope="col">{% trans "Type" %}</th>
<th scope="col">{% trans "Version" %}</th>
<th scope="col">{% trans "SFTP User" %}</th>
<th scope="col">{% trans "Server" %}</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for webapp in object_list %}
<tr>
<td>{{ webapp.name }}</td>
<td>{{ webapp.type }}</td>
<td>{{ webapp.type_instance.get_detail }}</td>
<td>
{% if webapp.sftpuser %}
<a href="{% url 'musician:webappuser-list'%}">{{ webapp.sftpuser }}</a>
{% else %}
<a href="{% url 'musician:systemuser-list'%}">{{ webapp.account.main_systemuser }}</a>
{% endif %}
</td>
<td>{{ webapp.target_server }}</td>
<td>
<a class="btn btn-outline-warning" href="{% url 'musician:webapp-detail' webapp.id %}">
<i class="fas fa-tools"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@ -0,0 +1,22 @@
{% extends "musician/base.html" %}
{% load bootstrap4 i18n %}
{% block content %}
<a class="btn-arrow-left" href="{% url 'musician:webapp-detail' view.kwargs.pk %}">{% trans "Go back" %}</a>
<h1 class="service-name">
{% if form.instance.pk %}{% trans "Update Option of" %}{% else %}{% trans "Add Option to" %}{% endif %}
<span class="font-weight-light">{{ form.webapp.name }}</span>
</h1>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<a class="btn btn-light mr-2" href="{% url 'musician:webapp-detail' view.kwargs.pk %}">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-secondary">{% trans "Save" %}</button>
{% endbuttons %}
</form>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends "musician/base.html" %}
{% load i18n %}
{% block content %}
<form method="post">
{% csrf_token %}
<p>{% blocktrans %}Are you sure that you want remove the following option"?{% endblocktrans %}</p>
<pre>{{ object.name}} {{ object.value}}</pre>
<p class="alert alert-warning"><strong>{% trans 'WARNING: This action cannot be undone.' %}</strong></p>
<input class="btn btn-danger" type="submit" value="{% trans 'Delete' %}">
<a class="btn btn-secondary" href="{% url 'musician:webapp-detail' view.kwargs.pk %}">{% trans 'Cancel' %}</a>
</form>
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends "musician/base.html" %}
{% load bootstrap4 i18n %}
{% block content %}
<h1 class="service-name">{% trans "Change password" %}: <span class="font-weight-light">{{ object.name }}</span></h1>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<a class="btn btn-light mr-2" href="{% url 'musician:webappuser-list' %}">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-secondary">{% trans "Save" %}</button>
{% endbuttons %}
</form>
{% endblock %}

View File

@ -0,0 +1,37 @@
{% extends "musician/users_base.html" %}
{% load bootstrap4 i18n %}
{% block tabcontent %}
<table class="table service-list">
<colgroup>
<col span="1" style="width: 15%;">
<col span="1" style="width: 25%;">
<col span="1" style="width: 40%;">
<col span="1" style="width: 20%;">
</colgroup>
<thead class="thead-dark">
<tr>
<th scope="col">{% trans "Username" %}</th>
<th scope="col">{% trans "Path" %}</th>
<th scope="col">{% trans "Server" %}</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for webappuser in object_list %}
<tr>
<td>{{ webappuser.username }}</td>
<td>/home/{{ webappuser.account }}/webapps/{{ webappuser.home }}</td>
<td>{{ webappuser.target_server }}</td>
<td>
<a class="btn btn-outline-warning" href="{% url 'musician:webappuser-password' webappuser.id %}">
<i class="fas fa-key"></i> {% trans "Update password" %}</a>
</td>
</tr>
{% endfor %}
</tbody>
{% include "musician/components/table_paginator.html" %}
</table>
{% endblock %}

View File

@ -0,0 +1,128 @@
{% extends "musician/base.html" %}
{% load bootstrap4 i18n %}
{% block content %}
<p>
{{ description }}
</p>
<table class="table service-list table-hover">
<colgroup>
<col span="1" style="width: 15%;">
<col span="1" style="width: 25%;">
<col span="1" style="width: 40%;">
<col span="1" style="width: 10%;">
<col span="1" style="width: 10%;">
</colgroup>
<thead class="thead-dark">
<tr>
<th scope="col">{% trans "Name" %}</th>
<th scope="col">{% trans "Url" %}</th>
<th scope="col">{% trans "Server" %}</th>
<th scope="col">{% trans "Is active?" %}</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for website in object_list %}
<tr class="fila-principal" data-toggle="collapse" data-target=".detalles{{ website.id }}">
<td>{{ website.name }}</td>
<td>
{% for domain in website.domains.all %}
<a href="{{ website.get_protocol }}://{{ domain }}">{{ website.get_protocol }}://{{ domain }}</a><br>
{% endfor %}
</td>
<td>{{ website.target_server }}</td>
<td class="text-{{website.is_active|yesno:'success,danger'}}">
<i class="fa fa-{{ website.is_active|yesno:'check,times' }}"></i>
<span class="sr-only">{{ website.is_active|yesno }}</span>
</td>
<td>
<!-- <a class="btn btn-outline-warning" href="#">
<i class="fas fa-tools"></i></a> -->
<button type="button" class="btn btn-outline-warning" data-toggle="modal" data-target="#exampleModal">
<i class="fas fa-tools"></i>
</button>
</td>
</tr>
<!-- Fila oculta de webapp -->
<tr class="collapse detalles{{ website.id }}">
<td colspan="12"class="p-5">
<table class="table">
<tbody>
{% for content in website.content_set.all %}
<tr class="table-active">
<td>Webapp Dir</td>
<td>/home/{{ content.webapp.account }}/webapps/{{ content.webapp }}</td>
<td class="text-right">
<a class="btn btn-outline-secondary" href="{% url 'musician:webapp-list'%}">
<i class="fas fa-tools"></i></a>
</td>
</tr>
<tr>
<td>Url</td>
{% if website.domains.first %}
<td><a href="{{ website.get_protocol }}://{{ website.domains.first }}{{ content.path }}">
{{ website.get_protocol }}://{{ website.domains.first }}{{ content.path }}</a>
</td>
{% else %}
<td></td>
{% endif %}
<td></td>
</tr>
<tr>
<td>Type</td>
{% if content.webapp.type == "php" %}
<td>PHP {{ content.webapp.type_instance.get_detail }}</td>
{% else %}
<td>{{ content.webapp.type }}</td>
{% endif %}
<td></td>
</tr>
<tr>
{% if content.webapp.sftpuser %}
<td>SFTP user</td>
<td>{{ content.webapp.sftpuser }}</td>
<td class="text-right">
<a class="btn btn-outline-warning" href="{% url 'musician:webappuser-password' content.webapp.sftpuser.id %}">
<i class="fas fa-key"></i> {% trans "Update password" %}</a>
</td>
{% else %}
<td>FTP user</td>
<td>{{ content.webapp.account.main_systemuser }}</td>
<td class="text-right">
<a class="btn btn-outline-warning" href="{% url 'musician:systemuser-password' content.webapp.account.main_systemuser.id %}">
<i class="fas fa-key"></i> {% trans "Update password" %}</a>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- Modal -->
<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Sorry!</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{% trans "This section is under development." %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -39,4 +39,16 @@ urlpatterns = [
path('mailing-lists/', views.MailingListsView.as_view(), name='mailing-lists'), path('mailing-lists/', views.MailingListsView.as_view(), name='mailing-lists'),
path('databases/', views.DatabaseListView.as_view(), name='database-list'), path('databases/', views.DatabaseListView.as_view(), name='database-list'),
path('saas/', views.SaasListView.as_view(), name='saas-list'), path('saas/', views.SaasListView.as_view(), name='saas-list'),
path('webappusers/', views.WebappUserListView.as_view(), name='webappuser-list'),
path('webappuser/<int:pk>/change-password/', views.WebappUserChangePasswordView.as_view(), name='webappuser-password'),
path('systemusers/', views.SystemUserListView.as_view(), name='systemuser-list'),
path('systemuser/<int:pk>/change-password/', views.SystemUserChangePasswordView.as_view(), name='systemuser-password'),
path('websites/', views.WebsiteListView.as_view(), name='website-list'),
path('webapps/', views.WebappListView.as_view(), name='webapp-list'),
path('webapps/<int:pk>/', views.WebappDetailView.as_view(), name='webapp-detail'),
path('webapps/<int:pk>/add-option/', views.WebappAddOptionView.as_view(), name='webapp-add-option'),
path('webapps/<int:pk>/option/<int:option_pk>/delete/', views.WebappDeleteOptionView.as_view(), name='webapp-delete-option'),
path('webapps/<int:pk>/option/<int:option_pk>/update/', views.WebappUpdateOptionView.as_view(), name='webapp-update-option'),
] ]

View File

@ -32,12 +32,16 @@ from orchestra.contrib.lists.models import List
from orchestra.contrib.mailboxes.models import Address, Mailbox from orchestra.contrib.mailboxes.models import Address, Mailbox
from orchestra.contrib.resources.models import Resource, ResourceData from orchestra.contrib.resources.models import Resource, ResourceData
from orchestra.contrib.saas.models import SaaS from orchestra.contrib.saas.models import SaaS
from orchestra.contrib.systemusers.models import WebappUsers, SystemUser
from orchestra.contrib.websites.models import Website
from orchestra.contrib.webapps.models import WebApp, WebAppOption
from orchestra.utils.html import html_to_pdf from orchestra.utils.html import html_to_pdf
from .auth import logout as auth_logout from .auth import logout as auth_logout
from .forms import (LoginForm, MailboxChangePasswordForm, MailboxCreateForm, from .forms import (LoginForm, MailboxChangePasswordForm, MailboxCreateForm,
MailboxSearchForm, MailboxUpdateForm, MailForm, MailboxSearchForm, MailboxUpdateForm, MailForm,
RecordCreateForm, RecordUpdateForm) RecordCreateForm, RecordUpdateForm, WebappUsersChangePasswordForm,
SystemUsersChangePasswordForm, WebappOptionCreateForm, WebappOptionUpdateForm)
from .mixins import (CustomContextMixin, ExtendedPaginationMixin, from .mixins import (CustomContextMixin, ExtendedPaginationMixin,
UserTokenRequiredMixin) UserTokenRequiredMixin)
from .models import Address as AddressService from .models import Address as AddressService
@ -45,7 +49,7 @@ from .models import Bill as BillService
from .models import DatabaseService from .models import DatabaseService
from .models import Mailbox as MailboxService from .models import Mailbox as MailboxService
from .models import MailinglistService, SaasService from .models import MailinglistService, SaasService
from .settings import ALLOWED_RESOURCES from .settings import ALLOWED_RESOURCES, MUSICIAN_EDIT_ENABLE_PHP_OPTIONS
from .utils import get_bootstraped_percent from .utils import get_bootstraped_percent
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -612,3 +616,140 @@ class LogoutView(RedirectView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
"""Logout may be done via POST.""" """Logout may be done via POST."""
return self.get(request, *args, **kwargs) 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)
class WebsiteListView(CustomContextMixin, UserTokenRequiredMixin, ListView):
model = Website
template_name = "musician/website_list.html"
extra_context = {
# Translators: This message appears on the page title
'title': _('Websites'),
}
def get_queryset(self):
return self.model.objects.filter(account=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'description': _("A website is the place where a domain is associated with the directory where the web files are located. (WebApp)"),
})
return context
class WebappListView(CustomContextMixin, UserTokenRequiredMixin, ListView):
model = WebApp
template_name = "musician/webapp_list.html"
extra_context = {
# Translators: This message appears on the page title
'title': _('Webapps'),
}
def get_queryset(self):
return self.model.objects.filter(account=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'description': _("A web app is the directory where your website is stored. Through SFTP, you can access this directory and upload/edit/delete files."),
'description2': _("Each Webapp has its own SFTP user, which is created automatically when the Webapp is created.")
})
return context
class WebappDetailView(CustomContextMixin, UserTokenRequiredMixin, DetailView):
template_name = "musician/webapp_detail.html"
extra_context = {
# Translators: This message appears on the page title
'title': _('webapp details'),
}
def get_queryset(self):
return WebApp.objects.filter(account=self.request.user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'edit_allowed_PHP_options': MUSICIAN_EDIT_ENABLE_PHP_OPTIONS
})
return context
class WebappAddOptionView(CustomContextMixin, UserTokenRequiredMixin, CreateView):
model = WebAppOption
form_class = WebappOptionCreateForm
template_name = "musician/webapp_option_form.html"
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
webapp = get_object_or_404(WebApp, account=self.request.user, pk=self.kwargs["pk"])
kwargs['webapp'] = webapp
return kwargs
def get_success_url(self):
return reverse_lazy("musician:webapp-detail", kwargs={"pk": self.kwargs["pk"]})
class WebappDeleteOptionView(CustomContextMixin, UserTokenRequiredMixin, DeleteView):
model = WebAppOption
template_name = "musician/webappoption_check_delete.html"
pk_url_kwarg = "option_pk"
def get_queryset(self):
qs = WebAppOption.objects.filter(webapp__account=self.request.user, webapp=self.kwargs["pk"])
return qs
def get_success_url(self):
return reverse_lazy("musician:webapp-detail", kwargs={"pk": self.kwargs["pk"]})
def delete(self, request, *args, **kwargs):
object = self.get_object()
response = super().delete(request, *args, **kwargs)
object.webapp.save()
return response
class WebappUpdateOptionView(CustomContextMixin, UserTokenRequiredMixin, UpdateView):
model = WebAppOption
form_class = WebappOptionUpdateForm
template_name = "musician/webapp_option_form.html"
pk_url_kwarg = "option_pk"
def get_queryset(self):
qs = WebAppOption.objects.filter(webapp__account=self.request.user, webapp=self.kwargs["pk"])
return qs
def get_success_url(self):
return reverse_lazy("musician:webapp-detail", kwargs={"pk": self.kwargs["pk"]})

View File

@ -42,8 +42,8 @@ class WebAppServiceMixin(object):
# cambios de permisos en servidores nuevos # cambios de permisos en servidores nuevos
perms = Template(textwrap.dedent("""\ perms = Template(textwrap.dedent("""\
{% if sftpuser %} {% if sftpuser %}
chown -R {{ sftpuser }}:{{ sftpuser }} {{ app_path }}/* {% else %} chown -R {{ sftpuser }}:{{ sftpuser }} {{ app_path }} {% else %}
chown -R {{ user }}:{{ group }} {{ app_path }}/* chown -R {{ user }}:{{ group }} {{ app_path }}
{% endif %} {% endif %}
""" """
)) ))

View File

@ -77,6 +77,7 @@ class PublicRoot(AppOption):
def validate(self): def validate(self):
super().validate() super().validate()
if self.instance.webapp_id is not None:
base_path = self.instance.webapp.get_base_path() base_path = self.instance.webapp.get_base_path()
path = os.path.join(base_path, self.instance.value) path = os.path.join(base_path, self.instance.value)
if not os.path.abspath(path).startswith(base_path): if not os.path.abspath(path).startswith(base_path):