resolve conflict

This commit is contained in:
Cayo Puigdefabregas 2024-01-20 15:28:42 +01:00
commit a93563a5d0
46 changed files with 426 additions and 1518 deletions

View File

@ -23,6 +23,33 @@ from idhub.models import (
from idhub_auth.models import User
class TermsConditionsForm(forms.Form):
accept = forms.BooleanField(
label=_("Accept terms and conditions of the service"),
required=False
)
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
def clean(self):
data = self.cleaned_data
if data.get("accept"):
self.user.accept_gdpr = True
else:
self.user.accept_gdpr = False
return data
def save(self, commit=True):
if commit:
self.user.save()
return self.user
return
class ImportForm(forms.Form):
did = forms.ChoiceField(label=_("Did"), choices=[])
eidas1 = forms.ChoiceField(

View File

@ -9,7 +9,7 @@ from django_tables2 import SingleTableView
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from django.views.generic.base import TemplateView
from django.views.generic.base import TemplateView, View
from django.views.generic.edit import (
CreateView,
DeleteView,
@ -29,6 +29,7 @@ from idhub.email.views import NotifyActivateUserByEmail
from idhub.admin.forms import (
ImportForm,
MembershipForm,
TermsConditionsForm,
SchemaForm,
UserRolForm,
ImportCertificateForm,
@ -49,6 +50,41 @@ from idhub.models import (
)
class TermsAndConditionsView(AdminView, FormView):
template_name = "idhub/admin/terms_conditions.html"
title = _("GDPR")
section = ""
subtitle = _('Accept Terms and Conditions')
icon = 'bi bi-file-earmark-medical'
form_class = TermsConditionsForm
success_url = reverse_lazy('idhub:admin_dashboard')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
kwargs['initial'] = {"accept": self.request.user.accept_gdpr}
return kwargs
def form_valid(self, form):
user = form.save()
return super().form_valid(form)
class DobleFactorAuthView(AdminView, View):
url = reverse_lazy('idhub:admin_dashboard')
def get(self, request, *args, **kwargs):
self.check_valid_user()
if not self.request.session.get("2fauth"):
return redirect(self.url)
if self.request.session.get("2fauth") == str(kwargs.get("admin2fauth")):
self.request.session.pop("2fauth", None)
return redirect(self.url)
return redirect(reverse_lazy("idhub:login"))
class DashboardView(AdminView, SingleTableView):
template_name = "idhub/admin/dashboard.html"
table_class = DashboardTable
@ -119,6 +155,7 @@ class PeopleView(People, TemplateView):
class PeopleActivateView(PeopleView):
def get(self, request, *args, **kwargs):
self.check_valid_user()
self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk)
@ -140,6 +177,7 @@ class PeopleActivateView(PeopleView):
class PeopleDeleteView(PeopleView):
def get(self, request, *args, **kwargs):
self.check_valid_user()
self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk)
@ -304,6 +342,7 @@ class PeopleMembershipDeleteView(PeopleView):
model = Membership
def get(self, request, *args, **kwargs):
self.check_valid_user()
self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk)
@ -391,6 +430,7 @@ class PeopleRolDeleteView(PeopleView):
model = UserRol
def get(self, request, *args, **kwargs):
self.check_valid_user()
self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk)
user = self.object.user
@ -454,6 +494,7 @@ class RolDeleteView(AccessControl):
model = Rol
def get(self, request, *args, **kwargs):
self.check_valid_user()
self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk)
@ -527,6 +568,7 @@ class ServiceDeleteView(AccessControl):
model = Service
def get(self, request, *args, **kwargs):
self.check_valid_user()
self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk)
@ -571,6 +613,7 @@ class CredentialView(Credentials):
class CredentialJsonView(Credentials):
def get(self, request, *args, **kwargs):
self.check_valid_user()
pk = kwargs['pk']
self.object = get_object_or_404(
VerificableCredential,
@ -585,6 +628,7 @@ class RevokeCredentialsView(Credentials):
success_url = reverse_lazy('idhub:admin_credentials')
def get(self, request, *args, **kwargs):
self.check_valid_user()
pk = kwargs['pk']
self.object = get_object_or_404(
VerificableCredential,
@ -604,6 +648,7 @@ class DeleteCredentialsView(Credentials):
success_url = reverse_lazy('idhub:admin_credentials')
def get(self, request, *args, **kwargs):
self.check_valid_user()
pk = kwargs['pk']
self.object = get_object_or_404(
VerificableCredential,
@ -683,6 +728,7 @@ class DidDeleteView(Credentials, DeleteView):
success_url = reverse_lazy('idhub:admin_dids')
def get(self, request, *args, **kwargs):
self.check_valid_user()
self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk)
Event.set_EV_ORG_DID_DELETED_BY_ADMIN(self.object)
@ -737,6 +783,7 @@ class SchemasView(SchemasMix):
class SchemasDeleteView(SchemasMix):
def get(self, request, *args, **kwargs):
self.check_valid_user()
self.pk = kwargs['pk']
self.object = get_object_or_404(Schemas, pk=self.pk)
self.object.delete()
@ -747,6 +794,7 @@ class SchemasDeleteView(SchemasMix):
class SchemasDownloadView(SchemasMix):
def get(self, request, *args, **kwargs):
self.check_valid_user()
self.pk = kwargs['pk']
self.object = get_object_or_404(Schemas, pk=self.pk)
@ -825,6 +873,7 @@ class SchemasImportView(SchemasMix):
class SchemasImportAddView(SchemasMix):
def get(self, request, *args, **kwargs):
self.check_valid_user()
file_name = kwargs['file_schema']
schemas_files = os.listdir(settings.SCHEMAS_DIR)
if not file_name in schemas_files:

View File

@ -13,7 +13,11 @@ logger = logging.getLogger(__name__)
class NotifyActivateUserByEmail:
def get_email_context(self, user):
subject_template_name = 'idhub/admin/registration/activate_user_subject.txt'
email_template_name = 'idhub/admin/registration/activate_user_email.txt'
html_email_template_name = 'idhub/admin/registration/activate_user_email.html'
def get_email_context(self, user, token):
"""
Define a new context with a token for put in a email
when send a email for add a new password
@ -22,35 +26,35 @@ class NotifyActivateUserByEmail:
current_site = get_current_site(self.request)
site_name = current_site.name
domain = current_site.domain
if not token:
token = default_token_generator.make_token(user)
context = {
'email': user.email,
'domain': domain,
'site_name': site_name,
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
'user': user,
'token': default_token_generator.make_token(user),
'token': token,
'protocol': protocol,
}
return context
def send_email(self, user):
def send_email(self, user, token=None):
"""
Send a email when a user is activated.
"""
context = self.get_email_context(user)
subject_template_name = 'idhub/admin/registration/activate_user_subject.txt'
email_template_name = 'idhub/admin/registration/activate_user_email.txt'
html_email_template_name = 'idhub/admin/registration/activate_user_email.html'
subject = loader.render_to_string(subject_template_name, context)
context = self.get_email_context(user, token)
subject = loader.render_to_string(self.subject_template_name, context)
# Email subject *must not* contain newlines
subject = ''.join(subject.splitlines())
body = loader.render_to_string(email_template_name, context)
body = loader.render_to_string(self.email_template_name, context)
from_email = settings.DEFAULT_FROM_EMAIL
to_email = user.email
email_message = EmailMultiAlternatives(
subject, body, from_email, [to_email])
html_email = loader.render_to_string(html_email_template_name, context)
html_email = loader.render_to_string(self.html_email_template_name, context)
email_message.attach_alternative(html_email, 'text/html')
try:
if settings.DEVELOPMENT:

View File

@ -1,367 +0,0 @@
# Generated by Django 4.2.5 on 2024-01-18 11:32
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='DID',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
(
'type',
models.PositiveSmallIntegerField(
choices=[(1, 'Key'), (2, 'Web')], verbose_name='Type'
),
),
('created_at', models.DateTimeField(auto_now=True)),
('label', models.CharField(max_length=50, verbose_name='Label')),
('did', models.CharField(max_length=250)),
('key_material', models.TextField()),
('eidas1', models.BooleanField(default=False)),
('didweb_document', models.TextField()),
(
'user',
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='dids',
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name='File_datas',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('file_name', models.CharField(max_length=250)),
('success', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='Rol',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('name', models.CharField(max_length=250, verbose_name='name')),
(
'description',
models.CharField(
max_length=250, null=True, verbose_name='Description'
),
),
],
),
migrations.CreateModel(
name='Schemas',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('type', models.CharField(max_length=250)),
('file_schema', models.CharField(max_length=250)),
('data', models.TextField()),
('created_at', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='Service',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('domain', models.CharField(max_length=250, verbose_name='Domain')),
(
'description',
models.CharField(max_length=250, verbose_name='Description'),
),
('rol', models.ManyToManyField(to='idhub.rol')),
],
),
migrations.CreateModel(
name='VCTemplate',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('wkit_template_id', models.CharField(max_length=250)),
('data', models.TextField()),
],
),
migrations.CreateModel(
name='VerificableCredential',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('id_string', models.CharField(max_length=250)),
('verified', models.BooleanField()),
('created_on', models.DateTimeField(auto_now=True)),
('issued_on', models.DateTimeField(null=True)),
('data', models.TextField()),
('csv_data', models.TextField()),
('hash', models.CharField(max_length=260)),
(
'status',
models.PositiveSmallIntegerField(
choices=[
(1, 'Enabled'),
(2, 'Issued'),
(3, 'Revoked'),
(4, 'Expired'),
],
default=1,
),
),
(
'eidas1_did',
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to='idhub.did',
),
),
(
'issuer_did',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='vcredentials',
to='idhub.did',
),
),
(
'schema',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='vcredentials',
to='idhub.schemas',
),
),
(
'subject_did',
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='subject_credentials',
to='idhub.did',
),
),
(
'user',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='vcredentials',
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name='Membership',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
(
'type',
models.PositiveSmallIntegerField(
choices=[(1, 'Beneficiary'), (2, 'Employee'), (3, 'Member')],
verbose_name='Type of membership',
),
),
(
'start_date',
models.DateField(
blank=True,
help_text='What date did the membership start?',
null=True,
verbose_name='Start date',
),
),
(
'end_date',
models.DateField(
blank=True,
help_text='What date will the membership end?',
null=True,
verbose_name='End date',
),
),
(
'user',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='memberships',
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name='Event',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('created', models.DateTimeField(auto_now=True, verbose_name='Date')),
(
'message',
models.CharField(max_length=350, verbose_name='Description'),
),
(
'type',
models.PositiveSmallIntegerField(
choices=[
(1, 'User registered'),
(2, 'User welcomed'),
(3, 'Data update requested by user'),
(
4,
'Data update requested. Pending approval by administrator',
),
(5, "User's data updated by admin"),
(6, 'Your data updated by admin'),
(7, 'User deactivated by admin'),
(8, 'DID created by user'),
(9, 'DID created'),
(10, 'DID deleted'),
(11, 'Credential deleted by user'),
(12, 'Credential deleted'),
(13, 'Credential issued for user'),
(14, 'Credential issued'),
(15, 'Credential presented by user'),
(16, 'Credential presented'),
(17, 'Credential enabled'),
(18, 'Credential available'),
(19, 'Credential revoked by admin'),
(20, 'Credential revoked'),
(21, 'Role created by admin'),
(22, 'Role modified by admin'),
(23, 'Role deleted by admin'),
(24, 'Service created by admin'),
(25, 'Service modified by admin'),
(26, 'Service deleted by admin'),
(27, 'Organisational DID created by admin'),
(28, 'Organisational DID deleted by admin'),
(29, 'User deactivated'),
(30, 'User activated'),
],
verbose_name='Event',
),
),
(
'user',
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='events',
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name='UserRol',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
(
'service',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='users',
to='idhub.service',
verbose_name='Service',
),
),
(
'user',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='roles',
to=settings.AUTH_USER_MODEL,
),
),
],
options={
'unique_together': {('user', 'service')},
},
),
]

View File

@ -1,14 +1,48 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth import views as auth_views
from django.urls import reverse_lazy, resolve
from django.utils.translation import gettext_lazy as _
from django.contrib.auth import views as auth_views
from django.core.exceptions import PermissionDenied
from django.urls import reverse_lazy, resolve
from django.shortcuts import redirect
from django.core.cache import cache
class Http403(PermissionDenied):
status_code = 403
default_detail = _('Permission denied. User is not authenticated')
default_code = 'forbidden'
def __init__(self, detail=None, code=None):
if detail is not None:
self.detail = details or self.default_details
if code is not None:
self.code = code or self.default_code
class UserView(LoginRequiredMixin):
login_url = "/login/"
wallet = False
path_terms = [
'admin_terms_and_conditions',
'user_terms_and_conditions',
'user_gdpr',
]
def get(self, request, *args, **kwargs):
response = super().get(request, *args, **kwargs)
url = self.check_gdpr()
if url:
return url
return response
def post(self, request, *args, **kwargs):
response = super().post(request, *args, **kwargs)
url = self.check_gdpr()
if url:
return url
return response
def get(self, request, *args, **kwargs):
self.admin_validated = cache.get("KEY_DIDS")
@ -32,12 +66,29 @@ class UserView(LoginRequiredMixin):
})
return context
def check_gdpr(self):
if not self.request.user.accept_gdpr:
url = reverse_lazy("idhub:user_terms_and_conditions")
if self.request.user.is_admin:
url = reverse_lazy("idhub:admin_terms_and_conditions")
if resolve(self.request.path).url_name not in self.path_terms:
return redirect(url)
class AdminView(UserView):
def get(self, request, *args, **kwargs):
if not request.user.is_admin:
url = reverse_lazy('idhub:user_dashboard')
return redirect(url)
self.check_valid_user()
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.check_valid_user()
return super().post(request, *args, **kwargs)
def check_valid_user(self):
if not self.request.user.is_admin:
raise Http403()
if self.request.session.get("2fauth"):
raise Http403()

View File

@ -0,0 +1,19 @@
{% extends "auth/login_base.html" %}
{% load i18n django_bootstrap5 %}
{% block login_content %}
<div class="well">
<div class="row-fluid">
<h2>{% trans 'Doble Factor of Authentication' %}</h2>
</div>
</div>
<div class="well">
<div class="row-fluid">
<div>
<span>{% trans "We have sent an email with a link that you have to select in order to login." %}</span>
</div>
</div><!-- /.row-fluid -->
</div><!--/.well-->
{% endblock %}

View File

@ -0,0 +1,26 @@
{% load i18n %}{% autoescape off %}
<p>
{% blocktrans %}You're receiving this email because you try to access in {{ site_name }}.{% endblocktrans %}
</p>
<p>
{% trans "Please go to the following page" %}
</p>
<p>
{% block reset_link %}
<a href="{{ protocol }}://{{ domain }}{% url 'idhub:admin_2fauth' admin2fauth=token %}">
{{ protocol }}://{{ domain }}{% url 'idhub:admin_2fauth' admin2fauth=token %}
</a>
{% endblock %}
</p>
<p>
{% trans "Thanks for using our site!" %}
</p>
<p>
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
</p>
{% endautoescape %}

View File

@ -0,0 +1,14 @@
{% load i18n %}{% autoescape off %}
{% blocktrans %}You're receiving this email because you try to access in {{ site_name }}.{% endblocktrans %}
{% trans "Please go to the following page" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'idhub:admin_2fauth' admin2fauth=token %}
{% endblock %}
{% trans "Your username, in case you've forgotten:" %} {{ user.username }}
{% trans "Thanks for using our site!" %}
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
{% endautoescape %}

View File

@ -0,0 +1,3 @@
{% load i18n %}{% autoescape off %}
{% blocktrans %}Authentication in {{ site_name }}{% endblocktrans %}
{% endautoescape %}

View File

@ -0,0 +1,57 @@
{% extends "idhub/base_admin.html" %}
{% load i18n %}
{% block content %}
<h3>
<i class="{{ icon }}"></i>
{{ subtitle }}
</h3>
{% load django_bootstrap5 %}
<form role="form" method="post">
{% csrf_token %}
{% if form.errors %}
<div class="alert alert-danger alert-icon alert-icon-border alert-dismissible" role="alert">
<div class="icon"><span class="mdi mdi-close-circle-o"></span></div>
<div class="message">
{% for field, error in form.errors.items %}
{{ error }}<br />
{% endfor %}
<button class="btn-close" type="button" data-dismiss="alert" aria-label="Close"></button>
</div>
</div>
{% endif %}
<div class="row">
<div class="col">
You must read the terms and conditions of this service and accept the
<a class="btn btn-green-admin" href="jacascript:void()" data-bs-toggle="modal" data-bs-target="#gdpr" title="{% trans 'GDPR' %}">Read GDPR</a>
</div>
</div>
<div class="row">
<div class="col-sm-4">
{% bootstrap_form form %}
</div>
</div>
<div class="form-actions-no-box">
<a class="btn btn-grey" href="{% url 'idhub:admin_dashboard' %}">{% translate "Cancel" %}</a>
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
</div>
</form>
<!-- Modal -->
<div class="modal" id="gdpr" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">{% trans 'GDPR info' %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Here we write the info about GDPR</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans 'Close' %}</button>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -140,9 +140,6 @@
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">{{ title }}</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<input class="form-control form-control-grey " type="text" placeholder="{% trans 'Search' %}" aria-label="Search">
</div>
</div>
</div>

View File

@ -170,9 +170,6 @@
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">{{ title }}</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<input class="form-control form-control-grey " type="text" placeholder="{% trans 'Search' %}" aria-label="Search">
</div>
</div>
</div>

View File

@ -6,4 +6,7 @@
<i class="{{ icon }}"></i>
{{ subtitle }}
</h3>
Gdpr info<br/>
If you want accept or revoke the Gdpr go to:
<a class="btn btn-green-user" href="{% url 'idhub:user_terms_and_conditions' %}">Terms and conditions</a>
{% endblock %}

View File

@ -0,0 +1,57 @@
{% extends "idhub/base.html" %}
{% load i18n %}
{% block content %}
<h3>
<i class="{{ icon }}"></i>
{{ subtitle }}
</h3>
{% load django_bootstrap5 %}
<form role="form" method="post">
{% csrf_token %}
{% if form.errors %}
<div class="alert alert-danger alert-icon alert-icon-border alert-dismissible" role="alert">
<div class="icon"><span class="mdi mdi-close-circle-o"></span></div>
<div class="message">
{% for field, error in form.errors.items %}
{{ error }}<br />
{% endfor %}
<button class="btn-close" type="button" data-dismiss="alert" aria-label="Close"></button>
</div>
</div>
{% endif %}
<div class="row">
<div class="col">
You must read the terms and conditions of this service and accept the
<a class="btn btn-green-user" href="jacascript:void()" data-bs-toggle="modal" data-bs-target="#gdpr" title="{% trans 'GDPR' %}">Read GDPR</a>
</div>
</div>
<div class="row">
<div class="col-sm-4">
{% bootstrap_form form %}
</div>
</div>
<div class="form-actions-no-box">
<a class="btn btn-grey" href="{% url 'idhub:user_dashboard' %}">{% translate "Cancel" %}</a>
<input class="btn btn-green-user" type="submit" name="submit" value="{% translate 'Save' %}" />
</div>
</form>
<!-- Modal -->
<div class="modal" id="gdpr" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">{% trans 'GDPR info' %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Here we write the info about GDPR</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans 'Close' %}</button>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,12 +0,0 @@
{% extends "musician/base.html" %}
{% load i18n %}
{% block content %}
<form method="post">
{% csrf_token %}
<p>{% blocktrans with address_name=object.full_address_name %}Are you sure that you want remove the address: "{{ address_name }}"?{% endblocktrans %}</p>
<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:address-update' view.kwargs.pk %}">{% trans 'Cancel' %}</a>
</form>
{% endblock %}

View File

@ -1,20 +0,0 @@
{% 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 %}
<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

@ -1,41 +0,0 @@
{% extends "musician/mail_base.html" %}
{% load i18n %}
{% block tabcontent %}
<div class="tab-pane fade show active" id="addresses" role="tabpanel" aria-labelledby="addresses-tab">
<table class="table service-list">
<colgroup>
<col span="1" style="width: 25%;">
<col span="1" style="width: 25%;">
<col span="1" style="width: 25%;">
<col span="1" style="width: 25%;">
</colgroup>
<thead class="thead-dark">
<tr>
<th scope="col">{% trans "Email" %}</th>
<th scope="col">{% trans "Domain" %}</th>
<th scope="col">{% trans "Mailboxes" %}</th>
<th scope="col">{% trans "Forward" %}</th>
</tr>
</thead>
<tbody>
{% for obj in object_list %}
<tr>
<td><a href="{% url 'musician:address-update' obj.id %}">{{ obj.full_address_name }}</a></td>
<td>{{ obj.domain.name }}</td>
<td>
{% for mailbox in obj.mailboxes %}
<a href="{% url 'musician:mailbox-update' mailbox.id %}">{{ mailbox.name }}</a>
{% if not forloop.last %}<br/> {% endif %}
{% endfor %}
</td>
<td>{{ obj.forward }}</td>
</tr>
{% endfor %}
</tbody>
{% include "musician/components/table_paginator.html" %}
</table>
<a class="btn btn-primary mt-4 mb-4" href="{% url 'musician:address-create' %}">{% trans "New mail address" %}</a>
</div>
{% endblock %}

View File

@ -1,41 +0,0 @@
{% extends "musician/base.html" %}
{% load i18n l10n %}
{% block content %}
<h1 class="service-name">{% trans "Billing" %}</h1>
<p class="service-description">{% trans "Billing page description." %}</p>
<table class="table service-list">
<colgroup>
<col span="1" style="width: 15%;">
<col span="1" style="width: 15%;">
<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 "Number" %}</th>
<th scope="col">{% trans "Bill date" %}</th>
<th scope="col">{% trans "Type" %}</th>
<th scope="col">{% trans "Total" %}</th>
<th scope="col">{% trans "Download PDF" %}</th>
</tr>
</thead>
<tbody>
{% for bill in object_list %}
<tr>
<th scope="row">{{ bill.number }}</th>
<td>{{ bill.created_on|date:"SHORT_DATE_FORMAT" }}</td>
<td>{{ bill.type }}</td>
<td>{{ bill.total|floatformat:2|localize }}€</td>
<td><a class="text-dark" href="{% url 'musician:bill-download' bill.id %}" target="_blank" rel="noopener noreferrer"><i class="fas fa-file-pdf"></i></a></td>
</tr>
{% endfor %}
</tbody>
{# TODO: define proper colspan #}
{% include "musician/components/table_paginator.html" %}
</table>
{% endblock %}

View File

@ -1,29 +0,0 @@
{# <!-- paginator component --> #}
<div class="row object-list-paginator">
<div class="col-md-4">{{ page_obj.paginator.count }} items in total</div>
<div class="col-md-4 text-center">
{% if page_obj.has_previous %}
<a href="?page=1&per_page={{ page_obj.paginator.per_page }}">&laquo;</a>
<a href="?page={{ page_obj.previous_page_number }}&per_page={{ page_obj.paginator.per_page }}">&lsaquo;</a>
{% endif %}
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}&per_page={{ page_obj.paginator.per_page }}">&rsaquo;</a>
<a href="?page={{ page_obj.paginator.num_pages }}&per_page={{ page_obj.paginator.per_page }}">&raquo;</a>
{% endif %}
</div>
<div class="col-md-4 text-right">
<form method="get">
Showing
<select name="{{ per_page_param }}">
{% for value in per_page_values %}
{% with page_obj.paginator.per_page as per_page %}
<option value="{{ value }}" {% if value == per_page %}selected{% endif %}>{{ value }}</option>
{% endwith %}
{% endfor %}
</select>
per page
<input type="submit" value="apply" />
</form>
</div>
</div>

View File

@ -1,50 +0,0 @@
{# <!-- table footer based paginator for ListView --> #}
{% load i18n %}
<tfoot>
<tr>
<td colspan="2">{{ page_obj.paginator.count }} items in total</td>
<td class="text-center">
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
<li class="page-item {{ page_obj.has_previous|yesno:',disabled' }}">
<a class="page-link" {% if page_obj.has_previous %}
href="?page={{ page_obj.previous_page_number }}&per_page={{ page_obj.paginator.per_page }}"
{% else %} href="#" {% endif %} tabindex="-1">
<span aria-hidden="true">&lsaquo;</span>
<span class="sr-only">{% trans "Previous" %}</span>
</a>
</li>
{% for page_number in page_obj.paginator.page_range %}
<li class="page-item {% if page_number == page_obj.number %}active {% endif %}">
<a class="page-link"
href="?page={{ page_number }}&per_page={{ page_obj.paginator.per_page }}">{{ page_number }}</a>
</li>
{% endfor %}
<li class="page-item {{ page_obj.has_next|yesno:',disabled' }}">
<a class="page-link" {% if page_obj.has_next %}
href="?page={{ page_obj.next_page_number }}&per_page={{ page_obj.paginator.per_page }}"
{% else %} href="#" {% endif %}>
<span aria-hidden="true">&rsaquo;</span>
<span class="sr-only">{% trans "Next" %}</span>
</a>
</li>
</ul>
</nav>
</td>
<td colspan="2" class="text-right">
<form method="get">
Showing
<select name="{{ per_page_param }}">
{% for value in per_page_values %}
{% with page_obj.paginator.per_page as per_page %}
<option value="{{ value }}" {% if value == per_page %}selected{% endif %}>{{ value }}</option>
{% endwith %}
{% endfor %}
</select>
per page
<input type="submit" value="apply" />
</form>
</td>
</tr>
</tfoot>

View File

@ -1,22 +0,0 @@
{% comment %}
Resource usage rendered as bootstrap progress bar
Expected parameter: detail
Expected structure: dictionary or object with attributes:
- usage (int): 125
- total (int): 200
- unit (string): 'MB'
- percent (int: [0, 25, 50, 75, 100]: 75
{% endcomment %}
<div class="text-center">
{% if detail %}
{{ detail.usage }} {{ detail.unit }}
{% else %}
N/A
{% endif %}
</div>
<div class="progress">
<div class="progress-bar bg-secondary w-{{ detail.percent }}" role="progressbar" aria-valuenow="{{ detail.usage }}"
aria-valuemin="0" aria-valuemax="{{ detail.total }}"></div>
</div>

View File

@ -1,161 +0,0 @@
{% extends "musician/base.html" %}
{% load i18n %}
{% block content %}
<h2 style="margin-top: 10px;">{% trans "Welcome back" %} <strong>{{ profile.username }}</strong></h2>
{% if profile.last_login %}
<p>{% blocktrans with last_login=profile.last_login|date:"SHORT_DATE_FORMAT" %}Last time you logged in was: {{ last_login }}{% endblocktrans %}</p>
{% else %}
<p>{% trans "It's the first time you log into the system, welcome on board!" %}</p>
{% endif %}
<div class="card-deck">
{% for resource, usage in resource_usage.items %}
<div class="card resource-usage resource-{{ resource }}">
<div class="card-body">
<h5 class="card-title">{{ usage.verbose_name }}</h5>
{% include "musician/components/usage_progress_bar.html" with detail=usage.data %}
{% if usage.data.alert %}
<div class="text-center mt-4">
{{ usage.data.alert }}
</div>
{% endif %}
</div>
</div>
{% endfor %}
<div class="card resource-usage resource-notifications">
<div class="card-body">
<h5 class="card-title">{% trans "Notifications" %}</h5>
{% for message in notifications %}
<p class="card-text">{{ message }}</p>
{% empty %}
<p class="card-text">{% trans "There is no notifications at this time." %}</p>
{% endfor %}
</div>
</div>
</div>
<h1 class="service-name">{% trans "Your domains and websites" %}</h1>
<p class="service-description">{% trans "Dashboard page description." %}</p>
{% for domain in domains %}
<div class="card service-card">
<div class="card-header">
<div class="row">
<div class="col-md">
<strong>{{ domain.name }}</strong>
</div>
<div class="col-md-8">
{% with domain.websites.0 as website %}
{% with website.contents.0 as content %}
<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-url="{% url 'musician:domain-detail' domain.id %}">
{% trans "view configuration" %} <strong class="fas fa-tools"></strong>
</button>
{% endwith %}
{% endwith %}
</div>
<div class="col-md text-right">
{% comment "@slamora: orchestra doesn't have this information [won't fix] See issue #2" %}
{% trans "Expiration date" %}: <strong>{{ domain.expiration_date|date:"SHORT_DATE_FORMAT" }}</strong>
{% endcomment %}
</div>
</div>
</div><!-- /card-header-->
<div class="card-body row text-center">
<div class="col-6 col-md-3 col-lg-2 border-right">
<h4>{% trans "Mail" %}</h4>
<p class="card-text"><i class="fas fa-envelope fa-3x"></i></p>
<p class="card-text text-dark">
{{ domain.addresses|length }} {% trans "mail addresses created" %}
</p>
<a class="stretched-link" href="{% url 'musician:address-list' %}?domain={{ domain.id }}"></a>
</div>
<div class="col-6 col-md-3 col-lg-2 border-right">
<h4>{% trans "Mail list" %}</h4>
<p class="card-text"><i class="fas fa-mail-bulk fa-3x"></i></p>
<a class="stretched-link" href="{% url 'musician:mailing-lists' %}?domain={{ domain.id }}"></a>
</div>
<div class="col-6 col-md-3 col-lg-2 border-right">
<h4>{% trans "Software as a Service" %}</h4>
<p class="card-text"><i class="fas fa-fire fa-3x"></i></p>
<p class="card-text text-dark">{% trans "Nothing installed" %}</p>
<a class="stretched-link" href="{% url 'musician:saas-list' %}?domain={{ domain.id }}"></a>
</div>
<div class="d-none d-lg-block col-lg-1"></div>
<div class="col-6 col-md-3 col-lg-4">
<h4>{% trans "Disk usage" %}</h4>
<p class="card-text"><i class="fas fa-hdd fa-3x"></i></p>
<div class="w-75 m-auto">
{% include "musician/components/usage_progress_bar.html" with detail=domain.usage %}
</div>
</div>
<div class="d-none d-lg-block col-lg-1"></div>
</div>
</div>
{% endfor %}
<!-- configuration details modal -->
<div class="modal fade" id="configDetailsModal" tabindex="-1" role="dialog" aria-labelledby="configDetailsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title text-secondary" id="configDetailsModalLabel">{% trans "Configuration details" %}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="domain-ftp pb-3 border-bottom">
<h6 class="pl-4 mb-4">{% trans "FTP access:" %}</h6>
{# Translators: domain configuration detail modal #}
<p>{% trans "Contact with the support team to get details concerning FTP access." %}</p>
{% comment %}
<!-- hidden until API provides FTP information -->
<label>{% trans "Username" %}:</label> <span id="config-username" class="font-weight-bold">username</span><br/>
<label>{% trans "Password:" %}</label> <span id="config-password" class="font-weight-bold">password</span>
{% endcomment %}
</div>
<div class="domain-website pt-4">
<div id="no-website"><h6 class="pl-4">{% trans "No website configured." %}</h6></div>
<div id="config-website">
<label>{% trans "Root directory:" %}</label> <span id="config-root-path" class="font-weight-bold">root directory</span>
<label>{% trans "Type:" %}</label><span id="config-webapp-type" class="font-weight-bold">type</span>
</div>
</div>
</div>
<div class="modal-footer">
<a href="#domain-detail" class="btn btn-primary">{% trans "View DNS records" %}</a>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extrascript %}
<script>
$('#configDetailsModal').on('show.bs.modal', function (event) {
var button = $(event.relatedTarget); // Button that triggered the modal
var modal = $(this);
// Extract info from data-* attributes
modal.find('.modal-title').text(button.data('domain'));
modal.find('.modal-body #config-webapp-type').text(button.data('webapp-type'));
modal.find('.modal-body #config-root-path').text(button.data('root-path'));
modal.find('.modal-footer .btn').attr('href', button.data('url'));
var nowebsite = modal.find('.modal-body #no-website');
var websitecfg = modal.find('.modal-body #config-website');
if(button.data('website')) {
nowebsite.hide();
websitecfg.show();
} else {
nowebsite.show();
websitecfg.hide();
}
})
</script>
{% endblock %}

View File

@ -1,68 +0,0 @@
{% extends "musician/base.html" %}
{% load i18n %}
{% block content %}
<h1 class="service-name">{{ service.verbose_name }}</h1>
<p class="service-description">{{ service.description }}</p>
{% for database in object_list %}
<div class="card service-card">
<div class="card-header">
<div class="row">
<div class="col-md-8">
<strong>{{ database.name }}</strong>
</div>
<div class="col-md">
{% trans "Type" %}: <strong>{{ database.type }}</strong>
</div>
<div class="col-md text-right">
{% comment "@slamora: orchestra doesn't provide this information [won't fix] See issue #3" %}
{% trans "associated to" %}: <strong>{{ database.domain|default:"-" }}</strong>
{% endcomment %}
</div>
</div>
</div><!-- /card-header-->
<div class="card-body row">
<div class="col-md-4">
<h4>Database users</h4>
<ul class="list-unstyled pl-2">
{% for user in database.users %}
{# TODO(@slamora) render in two columns #}
<li><span class="d-inline-block w-25">{{ user.username }}</span> <i class="fas fa-user-edit"></i></li>
{% empty %}
<li>{% trans "No users for this database." %}</li>
{% endfor %}
</ul>
</div>
<div class="col-md-3 border-left border-right">
<h4>Database usage</h4>
<p class="text-center"><i class="fas fa-database fa-3x"></i></p>
{% include "musician/components/usage_progress_bar.html" with detail=database.usage %}
</div>
<div class="col-md-5 text-right">
<div class="service-manager-link">
<a class="btn btn-primary" href="{{ database.manager_url }}" target="_blank" rel="noopener noreferrer">{% trans "Open database manager" %} <i class="fas fa-external-link-alt"></i></a>
</div>
</div>
</div>
</div>
{% empty %}
<div class="row">
<div class="col-md-4">
<div class="card service-card shadow p-3 mb-5 bg-white rounded">
<div class="card-body text-center">
<p class="mb-4"><i class="fas fa-database fa-5x"></i></p>
{# Translators: database page when there isn't any database. #}
<h5 class="card-title text-dark">{% trans "Ooops! Looks like there is nothing here!" %}</h5>
</div>
</div>
</div>
</div>
{% endfor %}
{% if object_list|length > 0 %}
{% include "musician/components/paginator.html" %}
{% endif %}
{% endblock %}

View File

@ -1,30 +0,0 @@
{% extends "musician/base.html" %}
{% load i18n %}
{% block content %}
<a class="btn-arrow-left" href="{% url 'musician:dashboard' %}">{% trans "Go back" %}</a>
<h1 class="service-name">{% trans "DNS settings for" %} <span class="font-weight-light">{{ object.name }}</span></h1>
<p class="service-description">{% trans "DNS settings page description." %}</p>
<table class="table service-list">
<colgroup>
<col span="1" style="width: 12%;">
<col span="1" style="width: 88%;">
</colgroup>
<thead class="thead-dark">
<tr>
<th scope="col">{% trans "Type" %}</th>
<th scope="col">{% trans "Value" %}</th>
</tr>
</thead>
<tbody>
{% for record in object.records %}
<tr>
<td>{{ record.type }}</td>
<td>{{ record.value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@ -1,32 +0,0 @@
{% extends "musician/base.html" %}
{% load i18n %}
{% block content %}
{% if active_domain %}
<a class="btn-arrow-left" href="{% url 'musician:address-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 == 'address-list' %}active{% endif %}" href="{% url 'musician:address-list' %}" role="tab"
aria-selected="{% if url_name == 'address-list' %}true{% else %}false{% endif %}">{% trans "Addresses" %}</a>
</li>
<li class="nav-item">
<a class="nav-link {% if url_name == 'mailbox-list' %}active{% endif %}" href="{% url 'musician:mailbox-list' %}" role="tab"
aria-selected="{% if url_name == 'mailbox-list' %}true{% else %}false{% endif %}">{% trans "Mailboxes" %}</a>
</li>
</ul>
{% endwith %}
<div class="tab-content" id="myTabContent">
{% block tabcontent %}
{% endblock %}
{% endblock %}

View File

@ -1,15 +0,0 @@
{% 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:mailbox-list' %}">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-secondary">{% trans "Save" %}</button>
{% endbuttons %}
</form>
{% endblock %}

View File

@ -1,15 +0,0 @@
{% extends "musician/base.html" %}
{% load i18n %}
{% block content %}
<form method="post">
{% csrf_token %}
<p>{% blocktrans with name=object.name %}Are you sure that you want remove the mailbox: "{{ name }}"?{% endblocktrans %}</p>
<div class="alert alert-danger" role="alert">
{% trans "All mailbox's messages will be <strong>deleted and cannot be recovered</strong>." %}
</div>
<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:mailbox-list' %}">{% trans 'Cancel' %}</a>
</form>
{% endblock %}

View File

@ -1,30 +0,0 @@
{% extends "musician/base.html" %}
{% load bootstrap4 i18n %}
{% block content %}
<h1 class="service-name">{{ service.verbose_name }}</h1>
{% if extra_mailbox %}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<strong>{% trans "Warning!" %}</strong> {% trans "You have reached the limit of mailboxes of your subscription so <strong>extra fees</strong> may apply." %}
<button type="button" class="close" data-dismiss="alert" aria-label="{% trans 'Close' %}">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% endif %}
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<a class="btn btn-light mr-2" href="{% url 'musician:mailbox-list' %}">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-secondary">{% trans "Save" %}</button>
{% if form.instance %}
<div class="float-right">
<a class="btn btn-outline-warning" href="{% url 'musician:mailbox-password' view.kwargs.pk %}"><i class="fas fa-key"></i> {% trans "Change password" %}</a>
<a class="btn btn-danger" href="{% url 'musician:mailbox-delete' view.kwargs.pk %}">{% trans "Delete" %}</a>
</div>
{% endif %}
{% endbuttons %}
</form>
{% endblock %}

View File

@ -1,46 +0,0 @@
{% extends "musician/mail_base.html" %}
{% load i18n %}
{% block tabcontent %}
<div class="tab-pane fade show active" id="mailboxes" role="tabpanel" aria-labelledby="mailboxes-tab">
<table class="table service-list">
<colgroup>
<col span="1" style="width: 25%;">
<col span="1" style="width: 10%;">
<col span="1" style="width: 65%;">
</colgroup>
<thead class="thead-dark">
<tr>
<th scope="col">{% trans "Name" %}</th>
<th scope="col">{% trans "Filtering" %}</th>
<th scope="col">{% trans "Addresses" %}</th>
</tr>
</thead>
<tbody>
{% for mailbox in object_list %}
{# <!-- Exclude (don't render) inactive mailboxes -->#}
{% if mailbox.is_active %}
<tr>
<td>
<a href="{% url 'musician:mailbox-update' mailbox.id %}">{{ mailbox.name }}</a>
<a class="roll-hover btn btn-outline-warning" href="{% url 'musician:mailbox-password' mailbox.id %}">
<i class="fas fa-key"></i> {% trans "Update password" %}</a>
</td>
<td>{{ mailbox.filtering }}</td>
<td>
{% for addr in mailbox.addresses %}
<a href="{% url 'musician:address-update' addr.data.id %}">
{{ addr.full_address_name }}
</a><br/>
{% endfor %}
</td>
</tr>
{% endif %}{# <!-- /is_active --> #}
{% endfor %}
</tbody>
{% include "musician/components/table_paginator.html" %}
</table>
<a class="btn btn-primary mt-4 mb-4" href="{% url 'musician:mailbox-create' %}">{% trans "New mailbox" %}</a>
</div>
{% endblock %}

View File

@ -1,46 +0,0 @@
{% extends "musician/base.html" %}
{% load i18n %}
{% block content %}
{% if active_domain %}
<a class="btn-arrow-left" href="{% url 'musician:mailing-lists' %}">{% 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>
<table class="table service-list">
<colgroup>
<col span="1" style="width: 13%;">
<col span="1" style="width: 12%;">
<col span="1" style="width: 50%;">
<col span="1" style="width: 15%;">
<col span="1" style="width: 10%;">
</colgroup>
<thead class="thead-dark">
<tr>
<th scope="col">Name</th>
<th scope="col">Status</th>
<th scope="col">Address</th>
<th scope="col">Admin email</th>
<th scope="col">Configure</th>
</tr>
</thead>
<tbody>
{% for resource in object_list %}
<tr>
<th scope="row">{{ resource.name }}</th>
{% if resource.is_active %}
<td class="text-primary font-weight-bold">{% trans "Active" %}</td>
{% else %}
<td class="text-danger font-weight-bold">{% trans "Inactive" %}</td>
{% endif %}
<td>{{ resource.address_name}}</td>
<td>{{ resource.admin_email }}</td>
<td><a href="{{ resource.manager_url }}" target="_blank" rel="noopener noreferrer">Mailtrain <i class="fas fa-external-link-alt"></i></a></td>
</tr>
{% endfor %}
</tbody>
{% include "musician/components/table_paginator.html" %}
</table>
{% endblock %}

View File

@ -1,65 +0,0 @@
{% extends "musician/base.html" %}
{% load i18n %}
{% block content %}
<h1 class="service-name">{% trans "Profile" %}</h1>
<p class="service-description">{% trans "Little description on profile page." %}</p>
<div class="card-deck">
<div class="card card-profile">
<h5 class="card-header">{% trans "User information" %}</h5>
<div class="card-body row">
<div class="col-md">
<div class="border-primary rounded-circle d-inline-block p-1" style="background-color: white; border: 5px solid grey">
<img id="user-avatar" width="160" height="160" src="/static/musician/images/default-profile-picture-primary-color.png" alt="user-profile-picture">
</div>
</div>
<div class="col-md-9">
<p class="card-text">{{ profile.username }}</p>
<p class="card-text">{{ profile.type }}</p>
<p class="card-text">{% trans "Preferred language:" %} {{ profile.language|language_name_local }}</p>
</div>
{% comment %}
<!-- disabled until set_password is implemented -->
<div class="col-md-12 text-right">
<a class="btn btn-primary pl-5 pr-5" href="#">{% trans "Set new password" %}</a>
</div>
{% endcomment %}
</div>
</div>
{% with profile.billing as contact %}
<div class="card card-profile">
<h5 class="card-header">{% trans "Billing information" %}</h5>
<div class="card-body">
<div class="form-group">{{ contact.name }}</div>
<div class="form-group">{{ contact.address }}</div>
<div class="form-group">
{{ contact.zipcode }}
{{ contact.city }}
{{ contact.country }}
</div>
<div class="form-group">
{{ contact.vat }}
</div>
<!-- payment method -->
<div class="form-group">
{% trans "payment method:" %} {{ payment.method }}
</div>
<div class="form-group">
{% if payment.method == 'SEPADirectDebit' %}
IBAN {{ payment.data.iban }}
{% else %}
{# <!-- "TODO handle Credit Card" --> #}
Details: {{ payment.data }}
{% endif %}
</div>
<div class="text-right">
<a href="{% url 'musician:billing' %}">{% trans "Check your last bills" %}</a>
</div>
</div>
</div>
</div>
{% endwith %}
{% endblock %}

View File

@ -1,56 +0,0 @@
{% extends "musician/base.html" %}
{% load i18n %}
{% block content %}
<h1 class="service-name">{{ service.verbose_name }}</h1>
<p class="service-description">{{ service.description }}</p>
{% for saas in object_list %}
<div class="card service-card">
<div class="card-header">
<div class="row">
<div class="col-md-8">
<strong>{{ saas.name }}</strong>
</div>
{% comment "Hidden until API provides this information" %}
<div class="col-md text-right">
{% trans "Installed on" %}: <strong>{{ saas.domain|default:"-" }}</strong>
</div>
{% endcomment %}
</div>
</div><!-- /card-header-->
<div class="card-body row">
<div class="col-md-4">
<h4>{{ saas.service|capfirst }}</h4>
<p class="text-center service-brand"><i class="fab fa-{{ saas.service }} fa-10x"></i></p>
</div>
<div class="col-md-3 border-left border-right">
<h4 class="mb-3">{% trans "Service info" %}</h4>
<label class="w-25">{% trans "active" %}:</label> <strong>{{ saas.is_active|yesno }}</strong><br/>
{% for key, value in saas.data.items %}
<label class="w-25">{{ key }}:</label> <strong>{{ value }}</strong><br/>
{% endfor %}
</div>
<div class="col-md-5 text-right">
<div class="service-manager-link">
<a class="btn btn-primary" href="{{ saas.manager_url }}" target="_blank" rel="noopener noreferrer">{% trans "Open service admin panel" %} <i class="fas fa-external-link-alt"></i></a>
</div>
</div>
</div>
</div>
{% empty %}
<div class="row">
<div class="col-md-4">
<div class="card service-card shadow p-3 mb-5 bg-white rounded">
<div class="card-body text-center">
<p class="mb-4"><i class="fas fa-fire fa-5x"></i></p>
{# Translators: saas page when there isn't any saas. #}
<h5 class="card-title text-dark">{% trans "Ooops! Looks like there is nothing here!" %}</h5>
</div>
</div>
</div>
</div>
{% endfor %}
{% endblock %}

View File

@ -1,28 +0,0 @@
{% extends "musician/base.html" %}
{% load i18n musician %}
{% block content %}
<h1>{{ service.verbose_name }}</h1>
<p>{{ service.description }}</p>
<table class="table table-hover">
<thead class="thead-dark">
<tr>
{% for field_name in service.fields %}
<th scope="col">{{ field_name }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for resource in object_list %}
<tr>
{% for field_name in service.fields %}
<td>{{ resource|get_item:field_name }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
{% include "musician/components/table_paginator.html" %}
</table>
{% endblock %}

View File

@ -17,7 +17,12 @@ Including another URLconf
from django.contrib.auth import views as auth_views
from django.views.generic import RedirectView
from django.urls import path, reverse_lazy
from .views import LoginView, PasswordResetConfirmView, serve_did
from .views import (
LoginView,
PasswordResetConfirmView,
serve_did,
DobleFactorSendView,
)
from .admin import views as views_admin
from .user import views as views_user
# from .verification_portal import views as views_verification_portal
@ -95,6 +100,8 @@ urlpatterns = [
path('user/credentials_presentation/demand',
views_user.DemandAuthorizationView.as_view(),
name='user_demand_authorization'),
path('user/terms/', views_user.TermsAndConditionsView.as_view(),
name='user_terms_and_conditions'),
# Admin
path('admin/dashboard/', views_admin.DashboardView.as_view(),
@ -177,8 +184,13 @@ urlpatterns = [
name='admin_schemas_import_add'),
path('admin/import', views_admin.ImportView.as_view(),
name='admin_import'),
path('admin/terms/', views_admin.TermsAndConditionsView.as_view(),
name='admin_terms_and_conditions'),
path('admin/import/new', views_admin.ImportAddView.as_view(),
name='admin_import_add'),
path('admin/auth/<uuid:admin2fauth>', views_admin.DobleFactorAuthView.as_view(),
name='admin_2fauth'),
path('admin/auth/2f/', DobleFactorSendView.as_view(), name='confirm_send_2f'),
path('did-registry/<str:did_id>/did.json', serve_did)

View File

@ -16,6 +16,33 @@ class ProfileForm(forms.ModelForm):
fields = ('first_name', 'last_name', 'email')
class TermsConditionsForm(forms.Form):
accept = forms.BooleanField(
label=_("Accept terms and conditions of the service"),
required=False
)
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
def clean(self):
data = self.cleaned_data
if data.get("accept"):
self.user.accept_gdpr = True
else:
self.user.accept_gdpr = False
return data
def save(self, commit=True):
if commit:
self.user.save()
return self.user
return
class RequestCredentialForm(forms.Form):
did = forms.ChoiceField(label=_("Did"), choices=[])
credential = forms.ChoiceField(label=_("Credential"), choices=[])

View File

@ -31,7 +31,8 @@ from django.conf import settings
from idhub.user.forms import (
ProfileForm,
RequestCredentialForm,
DemandAuthorizationForm
DemandAuthorizationForm,
TermsConditionsForm
)
from utils import certs
from idhub.mixins import UserView
@ -105,6 +106,26 @@ class CredentialsView(MyWallet, TemplateView):
})
return context
class TermsAndConditionsView(UserView, FormView):
template_name = "idhub/user/terms_conditions.html"
title = _("GDPR")
section = ""
subtitle = _('Accept Terms and Conditions')
icon = 'bi bi-file-earmark-medical'
form_class = TermsConditionsForm
success_url = reverse_lazy('idhub:user_dashboard')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
kwargs['initial'] = {"accept": self.request.user.accept_gdpr}
return kwargs
def form_valid(self, form):
user = form.save()
return super().form_valid(form)
class CredentialView(MyWallet, TemplateView):
template_name = "idhub/user/credential.html"

View File

@ -1,24 +0,0 @@
from django.db import models
class VPVerifyRequest(models.Model):
"""
`nonce` is an opaque random string used to lookup verification requests. URL-safe.
Example: "UPBQ3JE2DGJYHP5CPSCRIGTHRTCYXMQPNQ"
`expected_credentials` is a JSON list of credential types that must be present in this VP.
Example: ["FinancialSituationCredential", "HomeConnectivitySurveyCredential"]
`expected_contents` is a JSON object that places optional constraints on the contents of the
returned VP.
Example: [{"FinancialSituationCredential": {"financial_vulnerability_score": "7"}}]
`action` is (for now) a JSON object describing the next steps to take if this verification
is successful. For example "send mail to <destination> with <subject> and <body>"
Example: {"action": "send_mail", "params": {"to": "orders@somconnexio.coop", "subject": "New client", "body": ...}
`response` is a URL that the user's wallet will redirect the user to.
`submitted_on` is used (by a cronjob) to purge old entries that didn't complete verification
"""
nonce = models.CharField(max_length=50)
expected_credentials = models.CharField(max_length=255)
expected_contents = models.TextField()
action = models.TextField()
response_or_redirect = models.CharField(max_length=255)
submitted_on = models.DateTimeField(auto_now=True)

View File

@ -1,49 +0,0 @@
import json
from django.core.mail import send_mail
from django.http import HttpResponse, HttpResponseRedirect
from utils.idhub_ssikit import verify_presentation
from .models import VPVerifyRequest
from django.shortcuts import get_object_or_404
from more_itertools import flatten, unique_everseen
def verify(request):
assert request.method == "POST"
# TODO: incorporate request.POST["presentation_submission"] as schema definition
(presentation_valid, _) = verify_presentation(request.POST["vp_token"])
if not presentation_valid:
raise Exception("Failed to verify signature on the given Verifiable Presentation.")
vp = json.loads(request.POST["vp_token"])
nonce = vp["nonce"]
# "vr" = verification_request
vr = get_object_or_404(VPVerifyRequest, nonce=nonce) # TODO: return meaningful error, not 404
# Get a list of all included verifiable credential types
included_credential_types = unique_everseen(flatten([
vc["type"] for vc in vp["verifiableCredential"]
]))
# Check that it matches what we requested
for requested_vc_type in json.loads(vr.expected_credentials):
if requested_vc_type not in included_credential_types:
raise Exception("You're missing some credentials we requested!") # TODO: return meaningful error
# Perform whatever action we have to do
action = json.loads(vr.action)
if action["action"] == "send_mail":
subject = action["params"]["subject"]
to_email = action["params"]["to"]
from_email = "noreply@verifier-portal"
body = request.POST["vp-token"]
send_mail(
subject,
body,
from_email,
[to_email]
)
elif action["action"] == "something-else":
pass
else:
raise Exception("Unknown action!")
# OK! Your verifiable presentation was successfully presented.
return HttpResponseRedirect(vr.response_or_redirect)

View File

@ -1,13 +1,18 @@
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
import uuid
from django.conf import settings
from django.core.cache import cache
from django.utils.translation import gettext_lazy as _
from django.urls import reverse_lazy
from django.views.generic.base import TemplateView
from django.contrib.auth import views as auth_views
from django.contrib.auth import login as auth_login
from django.http import HttpResponseRedirect, HttpResponse
from django.utils.translation import gettext_lazy as _
from django.shortcuts import get_object_or_404, redirect
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseRedirect, HttpResponse, Http404
from idhub.models import DID
from idhub.email.views import NotifyActivateUserByEmail
from trustchain_idhub import settings
@ -41,6 +46,9 @@ class LoginView(auth_views.LoginView):
# )
# cache.set("KEY_DIDS", encryption_key, None)
cache.set("KEY_DIDS", sensitive_data_encryption_key, None)
if not settings.DEVELOPMENT:
self.request.session["2fauth"] = str(uuid.uuid4())
return redirect(reverse_lazy('idhub:confirm_send_2f'))
self.request.session["key_did"] = user.encrypt_data(
sensitive_data_encryption_key,
@ -69,3 +77,23 @@ def serve_did(request, did_id):
retval = HttpResponse(document)
retval.headers["Content-Type"] = "application/json"
return retval
class DobleFactorSendView(LoginRequiredMixin, NotifyActivateUserByEmail, TemplateView):
template_name = 'auth/2fadmin.html'
subject_template_name = 'auth/2fadmin_email_subject.txt'
email_template_name = 'auth/2fadmin_email.txt'
html_email_template_name = 'auth/2fadmin_email.html'
def get(self, request, *args, **kwargs):
if not request.user.is_admin:
raise Http404
f2auth = self.request.session.get("2fauth")
if not f2auth:
raise Http404
self.send_email(self.request.user, token=f2auth)
return super().get(request, *args, **kwargs)

View File

@ -1,58 +0,0 @@
# Generated by Django 4.2.5 on 2024-01-18 11:32
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name='User',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('password', models.CharField(max_length=128, verbose_name='password')),
(
'last_login',
models.DateTimeField(
blank=True, null=True, verbose_name='last login'
),
),
(
'email',
models.EmailField(
max_length=255, unique=True, verbose_name='Email address'
),
),
('is_active', models.BooleanField(default=True)),
('is_admin', models.BooleanField(default=False)),
(
'first_name',
models.CharField(
blank=True, max_length=255, null=True, verbose_name='First name'
),
),
(
'last_name',
models.CharField(
blank=True, max_length=255, null=True, verbose_name='Last name'
),
),
('encrypted_sensitive_data', models.CharField(max_length=255)),
('salt', models.CharField(max_length=255)),
],
options={
'abstract': False,
},
),
]

View File

@ -51,6 +51,7 @@ class User(AbstractBaseUser):
last_name = models.CharField(_("Last name"), max_length=255, blank=True, null=True)
encrypted_sensitive_data = models.CharField(max_length=255)
salt = models.CharField(max_length=255)
accept_gdpr = models.BooleanField(default=False)
objects = UserManager()

View File

@ -1,137 +0,0 @@
# Generated by Django 4.2.5 on 2024-01-18 11:32
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import oidc4vp.models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Authorization',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
(
'code',
models.CharField(default=oidc4vp.models.set_code, max_length=24),
),
('code_used', models.BooleanField(default=False)),
('created', models.DateTimeField(auto_now=True)),
('presentation_definition', models.CharField(max_length=250)),
],
),
migrations.CreateModel(
name='Organization',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('name', models.CharField(max_length=250)),
(
'client_id',
models.CharField(
default=oidc4vp.models.set_client_id, max_length=24, unique=True
),
),
(
'client_secret',
models.CharField(
default=oidc4vp.models.set_client_secret, max_length=48
),
),
('my_client_id', models.CharField(max_length=24)),
('my_client_secret', models.CharField(max_length=48)),
(
'response_uri',
models.URLField(
help_text='Url where to send the verificable presentation',
max_length=250,
),
),
],
),
migrations.CreateModel(
name='OAuth2VPToken',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('created', models.DateTimeField(auto_now=True)),
('result_verify', models.CharField(max_length=255)),
('vp_token', models.TextField()),
(
'authorization',
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='vp_tokens',
to='oidc4vp.authorization',
),
),
(
'organization',
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='vp_tokens',
to='oidc4vp.organization',
),
),
(
'user',
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='vp_tokens',
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.AddField(
model_name='authorization',
name='organization',
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='authorizations',
to='oidc4vp.organization',
),
),
migrations.AddField(
model_name='authorization',
name='user',
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
]

View File

@ -1,45 +0,0 @@
# Generated by Django 4.2.5 on 2024-01-18 11:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('oidc4vp', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Promotion',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('name', models.CharField(max_length=250)),
(
'discount',
models.PositiveSmallIntegerField(
choices=[(1, 'Financial vulnerability')]
),
),
(
'authorize',
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name='promotions',
to='oidc4vp.authorization',
),
),
],
),
]

View File

@ -26,4 +26,4 @@ uharfbuzz==0.38.0
fontTools==4.47.0
weasyprint==60.2
ujson==5.9.0
didkit-0.3.2-cp311-cp311-manylinux_2_34_x86_64.whl
./didkit-0.3.2-cp311-cp311-manylinux_2_34_x86_64.whl

View File

@ -2,6 +2,7 @@ import asyncio
import datetime
import didkit
import json
import urllib
import jinja2
from django.template.backends.django import Template
from django.template.loader import get_template
@ -29,7 +30,8 @@ def webdid_from_controller_key(key):
keydid = keydid_from_controller_key(key) # "did:key:<...>"
pubkeyid = keydid.rsplit(":")[-1] # <...>
document = json.loads(asyncio.run(resolve_keydid(keydid))) # Documento DID en terminos "key"
webdid_url = f"did:web:{settings.DOMAIN}:did-registry:{pubkeyid}" # nueva URL: "did:web:idhub.pangea.org:<...>"
domain = urllib.parse.urlencode({"domain": settings.DOMAIN})[7:]
webdid_url = f"did:web:{domain}:did-registry:{pubkeyid}" # nueva URL: "did:web:idhub.pangea.org:<...>"
webdid_url_owner = webdid_url + "#owner"
# Reemplazamos los campos del documento DID necesarios:
document["id"] = webdid_url