resolve conflict step1

This commit is contained in:
Cayo Puigdefabregas 2024-01-22 14:04:06 +01:00
commit f39c915c9c
80 changed files with 2252 additions and 1854 deletions

View File

@ -1,2 +0,0 @@
name email membershipType
Pepe user1@example.org individual
1 name email membershipType
2 Pepe user1@example.org individual

View File

@ -1,2 +0,0 @@
name surnames email typeOfPerson membershipType organisation affiliatedSince
Pepe Gómez user1@example.org individual Member Pangea 01-01-2023
1 name surnames email typeOfPerson membershipType organisation affiliatedSince
2 Pepe Gómez user1@example.org individual Member Pangea 01-01-2023

Binary file not shown.

Binary file not shown.

BIN
examples/signerDNIe004.pfx Normal file

Binary file not shown.

View File

@ -1,11 +1,16 @@
import csv import csv
import json import json
import base64
import copy
import pandas as pd import pandas as pd
from pyhanko.sign import signers
from django import forms from django import forms
from django.core.cache import cache
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from utils import credtools from utils import credtools, certs
from idhub.models import ( from idhub.models import (
DID, DID,
File_datas, File_datas,
@ -18,26 +23,69 @@ from idhub.models import (
from idhub_auth.models import User 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): class ImportForm(forms.Form):
did = forms.ChoiceField(label=_("Did"), choices=[]) did = forms.ChoiceField(label=_("Did"), choices=[])
eidas1 = forms.ChoiceField(
label=_("Signature with Eidas1"),
choices=[],
required=False
)
schema = forms.ChoiceField(label=_("Schema"), choices=[]) schema = forms.ChoiceField(label=_("Schema"), choices=[])
file_import = forms.FileField(label=_("File import")) file_import = forms.FileField(label=_("File import"))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self._schema = None self._schema = None
self._did = None self._did = None
self._eidas1 = None
self.rows = {} self.rows = {}
self.properties = {} self.properties = {}
self.users = []
self.user = kwargs.pop('user', None) self.user = kwargs.pop('user', None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
dids = DID.objects.filter(user=self.user)
self.fields['did'].choices = [ self.fields['did'].choices = [
(x.did, x.label) for x in DID.objects.filter(user=self.user) (x.did, x.label) for x in dids.filter(eidas1=False)
] ]
self.fields['schema'].choices = [ self.fields['schema'].choices = [
(x.id, x.name) for x in Schemas.objects.filter() (x.id, x.name) for x in Schemas.objects.filter()
] ]
if dids.filter(eidas1=True).exists():
choices = [("", "")]
choices.extend([
(x.did, x.label) for x in dids.filter(eidas1=True)
])
self.fields['eidas1'].choices = choices
else:
self.fields.pop('eidas1')
def clean_did(self): def clean(self):
data = self.cleaned_data["did"] data = self.cleaned_data["did"]
did = DID.objects.filter( did = DID.objects.filter(
user=self.user, user=self.user,
@ -49,6 +97,14 @@ class ImportForm(forms.Form):
self._did = did.first() self._did = did.first()
eidas1 = self.cleaned_data.get('eidas1')
if eidas1:
self._eidas1 = DID.objects.filter(
user=self.user,
eidas1=True,
did=eidas1
).first()
return data return data
def clean_schema(self): def clean_schema(self):
@ -62,7 +118,8 @@ class ImportForm(forms.Form):
self._schema = schema.first() self._schema = schema.first()
try: try:
self.json_schema = json.loads(self._schema.data) self.json_schema = json.loads(self._schema.data)
prop = self.json_schema['properties'] props = [x for x in self.json_schema["allOf"] if 'properties' in x.keys()]
prop = props[0]['properties']
self.properties = prop['credentialSubject']['properties'] self.properties = prop['credentialSubject']['properties']
except Exception: except Exception:
raise ValidationError("Schema is not valid!") raise ValidationError("Schema is not valid!")
@ -70,7 +127,10 @@ class ImportForm(forms.Form):
if not self.properties: if not self.properties:
raise ValidationError("Schema is not valid!") raise ValidationError("Schema is not valid!")
# TODO we need filter "$ref" of schema for can validate a csv
self.json_schema_filtered = copy.copy(self.json_schema)
allOf = [x for x in self.json_schema["allOf"] if '$ref' not in x.keys()]
self.json_schema_filtered["allOf"] = allOf
return data return data
def clean_file_import(self): def clean_file_import(self):
@ -79,7 +139,8 @@ class ImportForm(forms.Form):
if File_datas.objects.filter(file_name=self.file_name, success=True).exists(): if File_datas.objects.filter(file_name=self.file_name, success=True).exists():
raise ValidationError("This file already exists!") raise ValidationError("This file already exists!")
df = pd.read_csv (data, delimiter="\t", quotechar='"', quoting=csv.QUOTE_ALL) # df = pd.read_csv (data, delimiter="\t", quotechar='"', quoting=csv.QUOTE_ALL)
df = pd.read_excel(data)
data_pd = df.fillna('').to_dict() data_pd = df.fillna('').to_dict()
if not data_pd: if not data_pd:
@ -111,18 +172,18 @@ class ImportForm(forms.Form):
def validate_jsonld(self, line, row): def validate_jsonld(self, line, row):
try: try:
credtools.validate_json(row, self.json_schema) check = credtools.validate_json(row, self.json_schema_filtered)
if check is not True:
raise ValidationError("Not valid row")
except Exception as e: except Exception as e:
msg = "line {}: {}".format(line+1, e) msg = "line {}: {}".format(line+1, e)
self.exception(msg) self.exception(msg)
user = User.objects.filter(email=row.get('email')) user, new = User.objects.get_or_create(email=row.get('email'))
if not user: if new:
txt = _('The user does not exist!') self.users.append(user)
msg = "line {}: {}".format(line+1, txt)
self.exception(msg)
return user.first() return user
def create_credential(self, user, row): def create_credential(self, user, row):
return VerificableCredential( return VerificableCredential(
@ -131,6 +192,7 @@ class ImportForm(forms.Form):
csv_data=json.dumps(row), csv_data=json.dumps(row),
issuer_did=self._did, issuer_did=self._did,
schema=self._schema, schema=self._schema,
eidas1_did=self._eidas1
) )
def exception(self, msg): def exception(self, msg):
@ -216,3 +278,70 @@ class UserRolForm(forms.ModelForm):
raise forms.ValidationError(msg) raise forms.ValidationError(msg)
return data['service'] return data['service']
class ImportCertificateForm(forms.Form):
label = forms.CharField(label=_("Label"))
password = forms.CharField(
label=_("Password of certificate"),
widget=forms.PasswordInput
)
file_import = forms.FileField(label=_("File import"))
def __init__(self, *args, **kwargs):
self._did = None
self._s = None
self._label = None
self.user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
def clean(self):
data = super().clean()
file_import = data.get('file_import')
self.pfx_file = file_import.read()
self.file_name = file_import.name
self._pss = data.get('password')
self._label = data.get('label')
if not self.pfx_file or not self._pss:
msg = _("Is not a valid certificate")
raise forms.ValidationError(msg)
self.signer_init()
if not self._s:
msg = _("Is not a valid certificate")
raise forms.ValidationError(msg)
self.new_did()
return data
def new_did(self):
cert = self.pfx_file
keys = {
"cert": base64.b64encode(self.pfx_file).decode('utf-8'),
"passphrase": self._pss
}
key_material = json.dumps(keys)
self._did = DID(
key_material=key_material,
did=self.file_name,
label=self._label,
eidas1=True,
user=self.user,
type=DID.Types.KEY
)
pw = cache.get("KEY_DIDS")
self._did.set_key_material(key_material, pw)
def save(self, commit=True):
if commit:
self._did.save()
return self._did
return
def signer_init(self):
self._s = certs.load_cert(
self.pfx_file, self._pss.encode('utf-8')
)

View File

@ -10,7 +10,7 @@ from django_tables2 import SingleTableView
from django.conf import settings from django.conf import settings
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.translation import gettext_lazy as _ 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 ( from django.views.generic.edit import (
CreateView, CreateView,
DeleteView, DeleteView,
@ -18,6 +18,7 @@ from django.views.generic.edit import (
UpdateView, UpdateView,
) )
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.core.cache import cache
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.http import HttpResponse from django.http import HttpResponse
from django.contrib import messages from django.contrib import messages
@ -29,8 +30,10 @@ from idhub.email.views import NotifyActivateUserByEmail
from idhub.admin.forms import ( from idhub.admin.forms import (
ImportForm, ImportForm,
MembershipForm, MembershipForm,
TermsConditionsForm,
SchemaForm, SchemaForm,
UserRolForm, UserRolForm,
ImportCertificateForm,
) )
from idhub.admin.tables import ( from idhub.admin.tables import (
DashboardTable, DashboardTable,
@ -55,6 +58,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): class DashboardView(AdminView, SingleTableView):
template_name = "idhub/admin/dashboard.html" template_name = "idhub/admin/dashboard.html"
table_class = DashboardTable table_class = DashboardTable
@ -132,6 +170,7 @@ class PeopleView(People, TemplateView):
class PeopleActivateView(PeopleView): class PeopleActivateView(PeopleView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.check_valid_user()
self.pk = kwargs['pk'] self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk) self.object = get_object_or_404(self.model, pk=self.pk)
@ -153,6 +192,7 @@ class PeopleActivateView(PeopleView):
class PeopleDeleteView(PeopleView): class PeopleDeleteView(PeopleView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.check_valid_user()
self.pk = kwargs['pk'] self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk) self.object = get_object_or_404(self.model, pk=self.pk)
@ -317,6 +357,7 @@ class PeopleMembershipDeleteView(PeopleView):
model = Membership model = Membership
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.check_valid_user()
self.pk = kwargs['pk'] self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk) self.object = get_object_or_404(self.model, pk=self.pk)
@ -404,6 +445,7 @@ class PeopleRolDeleteView(PeopleView):
model = UserRol model = UserRol
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.check_valid_user()
self.pk = kwargs['pk'] self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk) self.object = get_object_or_404(self.model, pk=self.pk)
user = self.object.user user = self.object.user
@ -470,6 +512,7 @@ class RolDeleteView(AccessControl):
model = Rol model = Rol
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.check_valid_user()
self.pk = kwargs['pk'] self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk) self.object = get_object_or_404(self.model, pk=self.pk)
@ -546,6 +589,7 @@ class ServiceDeleteView(AccessControl):
model = Service model = Service
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.check_valid_user()
self.pk = kwargs['pk'] self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk) self.object = get_object_or_404(self.model, pk=self.pk)
@ -592,6 +636,7 @@ class CredentialView(Credentials):
class CredentialJsonView(Credentials): class CredentialJsonView(Credentials):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.check_valid_user()
pk = kwargs['pk'] pk = kwargs['pk']
self.object = get_object_or_404( self.object = get_object_or_404(
VerificableCredential, VerificableCredential,
@ -606,6 +651,7 @@ class RevokeCredentialsView(Credentials):
success_url = reverse_lazy('idhub:admin_credentials') success_url = reverse_lazy('idhub:admin_credentials')
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.check_valid_user()
pk = kwargs['pk'] pk = kwargs['pk']
self.object = get_object_or_404( self.object = get_object_or_404(
VerificableCredential, VerificableCredential,
@ -625,6 +671,7 @@ class DeleteCredentialsView(Credentials):
success_url = reverse_lazy('idhub:admin_credentials') success_url = reverse_lazy('idhub:admin_credentials')
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.check_valid_user()
pk = kwargs['pk'] pk = kwargs['pk']
self.object = get_object_or_404( self.object = get_object_or_404(
VerificableCredential, VerificableCredential,
@ -669,13 +716,13 @@ class DidRegisterView(Credentials, CreateView):
icon = 'bi bi-patch-check-fill' icon = 'bi bi-patch-check-fill'
wallet = True wallet = True
model = DID model = DID
fields = ('label',) fields = ('label', 'type')
success_url = reverse_lazy('idhub:admin_dids') success_url = reverse_lazy('idhub:admin_dids')
object = None object = None
def form_valid(self, form): def form_valid(self, form):
form.instance.user = self.request.user form.instance.user = self.request.user
form.instance.set_did() form.instance.set_did(cache.get("KEY_DIDS"))
form.save() form.save()
messages.success(self.request, _('DID created successfully')) messages.success(self.request, _('DID created successfully'))
Event.set_EV_ORG_DID_CREATED_BY_ADMIN(form.instance) Event.set_EV_ORG_DID_CREATED_BY_ADMIN(form.instance)
@ -710,6 +757,7 @@ class DidDeleteView(Credentials, DeleteView):
success_url = reverse_lazy('idhub:admin_dids') success_url = reverse_lazy('idhub:admin_dids')
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.check_valid_user()
self.pk = kwargs['pk'] self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk) self.object = get_object_or_404(self.model, pk=self.pk)
Event.set_EV_ORG_DID_DELETED_BY_ADMIN(self.object) Event.set_EV_ORG_DID_DELETED_BY_ADMIN(self.object)
@ -725,11 +773,27 @@ class WalletCredentialsView(Credentials):
wallet = True wallet = True
class WalletConfigIssuesView(Credentials): class WalletConfigIssuesView(Credentials, FormView):
template_name = "idhub/admin/wallet_issues.html" template_name = "idhub/admin/wallet_issues.html"
subtitle = _('Configure credential issuance') subtitle = _('Configure credential issuance')
icon = 'bi bi-patch-check-fill' icon = 'bi bi-patch-check-fill'
wallet = True wallet = True
form_class = ImportCertificateForm
success_url = reverse_lazy('idhub:admin_dids')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def form_valid(self, form):
cred = form.save()
if cred:
messages.success(self.request, _("The credential was imported successfully!"))
Event.set_EV_ORG_DID_CREATED_BY_ADMIN(cred)
else:
messages.error(self.request, _("Error importing the credential!"))
return super().form_valid(form)
class SchemasView(SchemasMix, SingleTableView): class SchemasView(SchemasMix, SingleTableView):
@ -750,6 +814,7 @@ class SchemasView(SchemasMix, SingleTableView):
class SchemasDeleteView(SchemasMix): class SchemasDeleteView(SchemasMix):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.check_valid_user()
self.pk = kwargs['pk'] self.pk = kwargs['pk']
self.object = get_object_or_404(Schemas, pk=self.pk) self.object = get_object_or_404(Schemas, pk=self.pk)
self.object.delete() self.object.delete()
@ -760,6 +825,7 @@ class SchemasDeleteView(SchemasMix):
class SchemasDownloadView(SchemasMix): class SchemasDownloadView(SchemasMix):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.check_valid_user()
self.pk = kwargs['pk'] self.pk = kwargs['pk']
self.object = get_object_or_404(Schemas, pk=self.pk) self.object = get_object_or_404(Schemas, pk=self.pk)
@ -838,6 +904,7 @@ class SchemasImportView(SchemasMix):
class SchemasImportAddView(SchemasMix): class SchemasImportAddView(SchemasMix):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.check_valid_user()
file_name = kwargs['file_schema'] file_name = kwargs['file_schema']
schemas_files = os.listdir(settings.SCHEMAS_DIR) schemas_files = os.listdir(settings.SCHEMAS_DIR)
if file_name not in schemas_files: if file_name not in schemas_files:
@ -855,12 +922,16 @@ class SchemasImportAddView(SchemasMix):
ldata = json.loads(data) ldata = json.loads(data)
assert credtools.validate_schema(ldata) assert credtools.validate_schema(ldata)
name = ldata.get('name') name = ldata.get('name')
title = ldata.get('title')
assert name assert name
assert title
except Exception: except Exception:
messages.error(self.request, _('This is not a valid schema!')) messages.error(self.request, _('This is not a valid schema!'))
return return
schema = Schemas.objects.create(file_schema=file_name, schema = Schemas.objects.create(
data=data, type=name, file_schema=file_name,
data=data,
type=name,
template_description=self.get_description() template_description=self.get_description()
) )
schema.save() schema.save()
@ -917,7 +988,7 @@ class ImportStep2View(ImportExport, TemplateView):
return context return context
class ImportAddView(ImportExport, FormView): class ImportAddView(NotifyActivateUserByEmail, ImportExport, FormView):
template_name = "idhub/admin/import_add.html" template_name = "idhub/admin/import_add.html"
subtitle = _('Import') subtitle = _('Import')
icon = '' icon = ''
@ -938,4 +1009,11 @@ class ImportAddView(ImportExport, FormView):
Event.set_EV_CREDENTIAL_CAN_BE_REQUESTED(cred) Event.set_EV_CREDENTIAL_CAN_BE_REQUESTED(cred)
else: else:
messages.error(self.request, _("Error importing the file!")) messages.error(self.request, _("Error importing the file!"))
for user in form.users:
try:
self.send_email(user)
except SMTPException as e:
messages.error(self.request, e)
return super().form_valid(form) return super().form_valid(form)

View File

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

View File

@ -7,6 +7,7 @@ from utils import credtools
from django.conf import settings from django.conf import settings
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.cache import cache
from decouple import config from decouple import config
from idhub.models import DID, Schemas from idhub.models import DID, Schemas
from oidc4vp.models import Organization from oidc4vp.models import Organization
@ -36,17 +37,25 @@ class Command(BaseCommand):
self.create_organizations(r[0].strip(), r[1].strip()) self.create_organizations(r[0].strip(), r[1].strip())
self.sync_credentials_organizations("pangea.org", "somconnexio.coop") self.sync_credentials_organizations("pangea.org", "somconnexio.coop")
self.sync_credentials_organizations("local 8000", "local 9000") self.sync_credentials_organizations("local 8000", "local 9000")
self.create_defaults_dids()
self.create_schemas() self.create_schemas()
def create_admin_users(self, email, password): def create_admin_users(self, email, password):
User.objects.create_superuser(email=email, password=password) su = User.objects.create_superuser(email=email, password=password)
su.set_encrypted_sensitive_data(password)
su.save()
key = su.decrypt_sensitive_data(password)
key_dids = {su.id: key}
cache.set("KEY_DIDS", key_dids, None)
self.create_defaults_dids(su, key)
def create_users(self, email, password): def create_users(self, email, password):
u= User.objects.create(email=email, password=password) u = User.objects.create(email=email, password=password)
u.set_password(password) u.set_password(password)
u.set_encrypted_sensitive_data(password)
u.save() u.save()
key = u.decrypt_sensitive_data(password)
self.create_defaults_dids(u, key)
def create_organizations(self, name, url): def create_organizations(self, name, url):
@ -61,11 +70,9 @@ class Command(BaseCommand):
org1.my_client_secret = org2.client_secret org1.my_client_secret = org2.client_secret
org1.save() org1.save()
org2.save() org2.save()
def create_defaults_dids(self, u, password):
def create_defaults_dids(self): did = DID(label="Default", user=u, type=DID.Types.KEY)
for u in User.objects.all(): did.set_did(password)
did = DID(label="Default", user=u)
did.set_did()
did.save() did.save()
def create_schemas(self): def create_schemas(self):
@ -82,11 +89,22 @@ class Command(BaseCommand):
try: try:
ldata = json.loads(data) ldata = json.loads(data)
assert credtools.validate_schema(ldata) assert credtools.validate_schema(ldata)
name = ldata.get('name') dname = ldata.get('name')
assert name title = ldata.get('title')
assert dname
assert title
except Exception:
title = ''
return
name = ''
try:
for x in dname:
if settings.LANGUAGE_CODE in x['lang']:
name = x.get('value', '')
except Exception: except Exception:
return return
Schemas.objects.create(file_schema=file_name, data=data, type=name)
Schemas.objects.create(file_schema=file_name, data=data, type=title)
def open_file(self, file_name): def open_file(self, file_name):
data = '' data = ''

View File

@ -1,361 +0,0 @@
# Generated by Django 4.2.5 on 2024-01-22 12:15
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',
),
),
('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.CharField(max_length=250)),
(
'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)),
(
'_name',
models.CharField(db_column='name', max_length=250, null=True),
),
(
'_description',
models.CharField(
db_column='description', max_length=250, null=True
),
),
('template_description', models.TextField(null=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()),
(
'status',
models.PositiveSmallIntegerField(
choices=[
(1, 'Enabled'),
(2, 'Issued'),
(3, 'Revoked'),
(4, 'Expired'),
],
default=1,
),
),
(
'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,13 +1,46 @@
from django.contrib.auth.mixins import LoginRequiredMixin 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.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.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): class UserView(LoginRequiredMixin):
login_url = "/login/" login_url = "/login/"
wallet = False wallet = False
path_terms = [
'admin_terms_and_conditions',
'user_terms_and_conditions',
'user_gdpr',
]
def get(self, request, *args, **kwargs):
self.admin_validated = cache.get("KEY_DIDS")
response = super().get(request, *args, **kwargs)
url = self.check_gdpr()
return url or response
def post(self, request, *args, **kwargs):
self.admin_validated = cache.get("KEY_DIDS")
response = super().post(request, *args, **kwargs)
url = self.check_gdpr()
return url or response
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -19,15 +52,33 @@ class UserView(LoginRequiredMixin):
'path': resolve(self.request.path).url_name, 'path': resolve(self.request.path).url_name,
'user': self.request.user, 'user': self.request.user,
'wallet': self.wallet, 'wallet': self.wallet,
'admin_validated': True if self.admin_validated else False
}) })
return context 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): class AdminView(UserView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if not request.user.is_admin: self.check_valid_user()
url = reverse_lazy('idhub:user_dashboard')
return redirect(url)
return super().get(request, *args, **kwargs) 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

@ -1,14 +1,21 @@
import json import json
import ujson
import pytz import pytz
import hashlib
import datetime import datetime
from collections import OrderedDict
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from nacl import secret
from utils.idhub_ssikit import ( from utils.idhub_ssikit import (
generate_did_controller_key, generate_did_controller_key,
keydid_from_controller_key, keydid_from_controller_key,
sign_credential, sign_credential,
webdid_from_controller_key,
) )
from idhub_auth.models import User from idhub_auth.models import User
@ -45,6 +52,7 @@ class Event(models.Model):
EV_ORG_DID_DELETED_BY_ADMIN = 28, "Organisational DID deleted by admin" EV_ORG_DID_DELETED_BY_ADMIN = 28, "Organisational DID deleted by admin"
EV_USR_DEACTIVATED_BY_ADMIN = 29, "User deactivated" EV_USR_DEACTIVATED_BY_ADMIN = 29, "User deactivated"
EV_USR_ACTIVATED_BY_ADMIN = 30, "User activated" EV_USR_ACTIVATED_BY_ADMIN = 30, "User activated"
EV_USR_SEND_VP = 31, "User send Verificable Presentation"
created = models.DateTimeField(_("Date"), auto_now=True) created = models.DateTimeField(_("Date"), auto_now=True)
message = models.CharField(_("Description"), max_length=350) message = models.CharField(_("Description"), max_length=350)
@ -401,21 +409,44 @@ class Event(models.Model):
message=msg, message=msg,
) )
@classmethod
def set_EV_USR_SEND_VP(cls, msg, user):
cls.objects.create(
type=cls.Types.EV_USR_SEND_VP,
message=msg,
user=user
)
class DID(models.Model): class DID(models.Model):
class Types(models.IntegerChoices):
KEY = 1, "Key"
WEB = 2, "Web"
type = models.PositiveSmallIntegerField(
_("Type"),
choices=Types.choices,
)
created_at = models.DateTimeField(auto_now=True) created_at = models.DateTimeField(auto_now=True)
label = models.CharField(_("Label"), max_length=50) label = models.CharField(_("Label"), max_length=50)
did = models.CharField(max_length=250) did = models.CharField(max_length=250)
# In JWK format. Must be stored as-is and passed whole to library functions. # In JWK format. Must be stored as-is and passed whole to library functions.
# Example key material: # Example key material:
# '{"kty":"OKP","crv":"Ed25519","x":"oB2cPGFx5FX4dtS1Rtep8ac6B__61HAP_RtSzJdPxqs","d":"OJw80T1CtcqV0hUcZdcI-vYNBN1dlubrLaJa0_se_gU"}' # '{"kty":"OKP","crv":"Ed25519","x":"oB2cPGFx5FX4dtS1Rtep8ac6B__61HAP_RtSzJdPxqs","d":"OJw80T1CtcqV0hUcZdcI-vYNBN1dlubrLaJa0_se_gU"}'
key_material = models.CharField(max_length=250) key_material = models.TextField()
eidas1 = models.BooleanField(default=False)
user = models.ForeignKey( user = models.ForeignKey(
User, User,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='dids', related_name='dids',
null=True, null=True,
) )
didweb_document = models.TextField()
def get_key_material(self, password):
return self.user.decrypt_data(self.key_material, password)
def set_key_material(self, value, password):
self.key_material = self.user.encrypt_data(value, password)
@property @property
def is_organization_did(self): def is_organization_did(self):
@ -423,14 +454,20 @@ class DID(models.Model):
return True return True
return False return False
def set_did(self): def set_did(self, password):
self.key_material = generate_did_controller_key() new_key_material = generate_did_controller_key()
self.did = keydid_from_controller_key(self.key_material) self.set_key_material(new_key_material, password)
if self.type == self.Types.KEY:
self.did = keydid_from_controller_key(new_key_material)
elif self.type == self.Types.WEB:
didurl, document = webdid_from_controller_key(new_key_material)
self.did = didurl
self.didweb_document = document
def get_key(self): def get_key(self):
return json.loads(self.key_material) return json.loads(self.key_material)
class Schemas(models.Model): class Schemas(models.Model):
type = models.CharField(max_length=250) type = models.CharField(max_length=250)
file_schema = models.CharField(max_length=250) file_schema = models.CharField(max_length=250)
@ -446,6 +483,7 @@ class Schemas(models.Model):
return {} return {}
return json.loads(self.data) return json.loads(self.data)
#<<<<<<< HEAD
def _update_and_get_field(self, field_attr, schema_key): def _update_and_get_field(self, field_attr, schema_key):
field_value = getattr(self, field_attr) field_value = getattr(self, field_attr)
if not field_value: if not field_value:
@ -467,6 +505,21 @@ class Schemas(models.Model):
self._name = value self._name = value
@property @property
#=======
def name(self, request=None):
names = {}
for name in self.get_schema.get('name', []):
lang = name.get('lang')
if 'ca' in lang:
lang = 'ca'
names[lang]= name.get('value')
if request and request.LANGUAGE_CODE in names.keys():
return names[request.LANGUAGE_CODE]
return names[settings.LANGUAGE_CODE]
#>>>>>>> main
def description(self): def description(self):
return self._update_and_get_field('_description', 'description') return self._update_and_get_field('_description', 'description')
@ -490,6 +543,7 @@ class VerificableCredential(models.Model):
issued_on = models.DateTimeField(null=True) issued_on = models.DateTimeField(null=True)
data = models.TextField() data = models.TextField()
csv_data = models.TextField() csv_data = models.TextField()
hash = models.CharField(max_length=260)
status = models.PositiveSmallIntegerField( status = models.PositiveSmallIntegerField(
choices=Status.choices, choices=Status.choices,
default=Status.ENABLED default=Status.ENABLED
@ -510,17 +564,54 @@ class VerificableCredential(models.Model):
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='vcredentials', related_name='vcredentials',
) )
eidas1_did = models.ForeignKey(
DID,
on_delete=models.CASCADE,
null=True
)
schema = models.ForeignKey( schema = models.ForeignKey(
Schemas, Schemas,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='vcredentials', related_name='vcredentials',
) )
def get_data(self, password):
if not self.data:
return ""
if self.eidas1_did:
return self.data
return self.user.decrypt_data(self.data, password)
def set_data(self, value, password):
self.data = self.user.encrypt_data(value, password)
def type(self): def type(self):
return self.schema.type return self.schema.type
#<<<<<<< HEAD
def get_description(self): def get_description(self):
return self.schema.template_description return self.schema.template_description
#=======
def description(self):
for des in json.loads(self.render("")).get('description', []):
if settings.LANGUAGE_CODE in des.get('lang'):
return des.get('value', '')
return ''
#>>>>>>> main
def get_type(self, lang=None):
schema = json.loads(self.schema.data)
if not schema.get('name'):
return ''
try:
for x in schema['name']:
if lang or settings.LANGUAGE_CODE in x['lang']:
return x.get('value', '')
except:
return self.schema.type
return ''
def get_status(self): def get_status(self):
return self.Status(self.status).label return self.Status(self.status).label
@ -529,43 +620,72 @@ class VerificableCredential(models.Model):
data = json.loads(self.csv_data).items() data = json.loads(self.csv_data).items()
return data return data
def issue(self, did): def issue(self, did, password, domain=settings.DOMAIN.strip("/")):
if self.status == self.Status.ISSUED: if self.status == self.Status.ISSUED:
return return
self.status = self.Status.ISSUED self.status = self.Status.ISSUED
self.subject_did = did self.subject_did = did
self.issued_on = datetime.datetime.now().astimezone(pytz.utc) self.issued_on = datetime.datetime.now().astimezone(pytz.utc)
self.data = sign_credential( issuer_pass = cache.get("KEY_DIDS")
self.render(), # issuer_pass = self.user.decrypt_data(
self.issuer_did.key_material # cache.get("KEY_DIDS"),
) # settings.SECRET_KEY,
# )
def get_context(self): # hash of credential without sign
self.hash = hashlib.sha3_256(self.render(domain).encode()).hexdigest()
data = sign_credential(
self.render(domain),
self.issuer_did.get_key_material(issuer_pass)
)
if self.eidas1_did:
self.data = data
else:
self.data = self.user.encrypt_data(data, password)
def get_context(self, domain):
d = json.loads(self.csv_data) d = json.loads(self.csv_data)
issuance_date = '' issuance_date = ''
if self.issued_on: if self.issued_on:
format = "%Y-%m-%dT%H:%M:%SZ" format = "%Y-%m-%dT%H:%M:%SZ"
issuance_date = self.issued_on.strftime(format) issuance_date = self.issued_on.strftime(format)
cred_path = 'credentials'
sid = self.id
if self.eidas1_did:
cred_path = 'public/credentials'
sid = self.hash
url_id = "{}/{}/{}".format(
domain,
cred_path,
sid
)
context = { context = {
'vc_id': self.id, 'vc_id': url_id,
'issuer_did': self.issuer_did.did, 'issuer_did': self.issuer_did.did,
'subject_did': self.subject_did and self.subject_did.did or '', 'subject_did': self.subject_did and self.subject_did.did or '',
'issuance_date': issuance_date, 'issuance_date': issuance_date,
'first_name': self.user.first_name, 'firstName': self.user.first_name or "",
'last_name': self.user.last_name, 'lastName': self.user.last_name or "",
'email': self.user.email,
'organisation': settings.ORGANIZATION or '',
} }
context.update(d) context.update(d)
context['firstName'] = ""
return context return context
def render(self): def render(self, domain):
context = self.get_context() context = self.get_context(domain)
template_name = 'credentials/{}'.format( template_name = 'credentials/{}'.format(
self.schema.file_schema self.schema.file_schema
) )
tmpl = get_template(template_name) tmpl = get_template(template_name)
return tmpl.render(context) d_ordered = ujson.loads(tmpl.render(context))
d_minimum = self.filter_dict(d_ordered)
return ujson.dumps(d_minimum)
def get_issued_on(self): def get_issued_on(self):
if self.issued_on: if self.issued_on:
@ -573,6 +693,18 @@ class VerificableCredential(models.Model):
return '' return ''
def filter_dict(self, dic):
new_dict = OrderedDict()
for key, value in dic.items():
if isinstance(value, dict):
new_value = self.filter_dict(value)
if new_value:
new_dict[key] = new_value
elif value:
new_dict[key] = value
return new_dict
class VCTemplate(models.Model): class VCTemplate(models.Model):
wkit_template_id = models.CharField(max_length=250) wkit_template_id = models.CharField(max_length=250)
data = models.TextField() data = models.TextField()

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

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

@ -4,8 +4,6 @@
{% block login_content %} {% block login_content %}
<form action="{% url 'idhub:login' %}" role="form" method="post"> <form action="{% url 'idhub:login' %}" role="form" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="next" value="{{ next }}" />
<div id="div_id_username" <div id="div_id_username"
class="clearfix control-group {% if form.username.errors %}error{% endif %}"> class="clearfix control-group {% if form.username.errors %}error{% endif %}">
<div class="form-group"> <div class="form-group">
@ -48,7 +46,7 @@
class="btn btn-primary form-control" id="submit-id-submit"> class="btn btn-primary form-control" id="submit-id-submit">
</div> </div>
</form> </form>
<div id="login-footer"> <div id="login-footer" class="mt-3">
<a href="{% url 'idhub:password_reset' %}" data-toggle="modal" data-target="#forgotPasswordModal">{% trans "Forgot your password? Click here to recover" %}</a> <a href="{% url 'idhub:password_reset' %}" data-toggle="modal" data-target="#forgotPasswordModal">{% trans "Forgot your password? Click here to recover" %}</a>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,111 @@
{% load i18n static %}
<!DOCTYPE html>
<html>
<head>
<title>Certificado</title>
<meta content="text/html; charset=UTF-8" http-equiv="content-type" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<link rel="stylesheet" href= "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<link href="{% static "/css/bootstrap.min.css" %}" rel="stylesheet">
<style type="text/css" media="all">
@page {
size: A4 portrait; /* can use also 'landscape' for orientation */
margin: 1.0cm 1.5cm 3.5cm 1.5cm;
font-family: "Source Sans Pro", Calibri, Candra, Sans serif;
@top {
content: element(header);
}
@bottom {
content: element(footer);
}
}
body {
width: 100% !important;
height: 100%;
background: #fff;
color: black;
font-size: 100%;
line-height: 1.65;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
}
header {
position: running(header);
/*height: 100px;*/
font-size: 12px;
/* color: #000; */
font-family: Arial;
width: 100%;
/* position: relative;*/
}
footer {
position: running(footer);
/*height: 150px;*/
}
.body_content {
position: relative;
page-break-inside: auto;
width: 100%;
/*overflow: hidden;*/
}
img {max-height: 150px; width: auto;}
.company-logo {float: left;}
.customer-logo {float: right;}
.page-break:not(section:first-of-type) {
page-break-before: always
}
}
</style>
</head>
<body>
<div class="container body-content">
<div class="row">
<div class="col">
<img style="width: 100%; height: auto;" src="data:image/jpeg;base64,{{ image_header }}" />
</div>
</div>
<div class="row mt-3">
<div class="col-12 text-center">
<span style="color: #ea5e0f;">LAFEDE.CAT ORGANITZACIONS PER A LA JUSTÍCIA GLOBAL</span><br />CERTIFICA QUE:<br />
{{ first_name }} {{ last_name }} amb DNI {{ document_id }}<br/>
Ha realitzat el curs {{ course }}, a {{ address }} / de manera virtual/presencial, els dies {{ date_course }}<br />
La durada del curs ha estat de {{ n_hours }} hores lectives corresponents a {{ n_lections }} sessions.<br />
<br />
<br />
<br />
I per deixar-ne constància als efectes oportuns, signo el present certificat en data de {{ issue_date }}
</div>
</div>
<div class="row" style="padding-top: 20px;">
<div class="col-12">
<img style="width: 129px; height: 88px;" src="data:image/jpeg;base64,{{ image_signature }}" />
</div>
</div>
<div class="row" style="padding-top: 20px;">
<div class="col-12">
Pepa Martínez Peyrats<br />
Directora<br />
Lafede.cat - Federació d'Organitzacions per a la Justícia Global
</div>
</div>
{% if qr %}
<div class="row" style="padding-top: 20px;">
<div class="col-12">
<img style="width: 129px; height: 129px;" src="data:image/jpeg;base64,{{ qr }}" />
</div>
</div>
{% endif %}
</div>
</body>
</html>

View File

@ -0,0 +1,83 @@
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://idhub.pangea.org/credentials/base/v1",
{
"firstName": "https://idhub.pangea.org/context/#firstName",
"lastName": "https://idhub.pangea.org/context/#lastName",
"personalIdentifier": "https://idhub.pangea.org/context/#personalIdentifier",
"issuedDate": "https://idhub.pangea.org/context/#issuedDate",
"modeOfInstruction": "https://idhub.pangea.org/context/#modeOfInstruction",
"courseDuration": "https://idhub.pangea.org/context/#courseDuration",
"courseDays": "https://idhub.pangea.org/context/#courseDays",
"courseName": "https://idhub.pangea.org/context/#courseName",
"courseDescription": "https://idhub.pangea.org/context/#courseDescription",
"gradingScheme": "https://idhub.pangea.org/context/#gradingScheme",
"scoreAwarded": "https://idhub.pangea.org/context/#scoreAwarded",
"qualificationAwarded": "https://idhub.pangea.org/context/#qualificationAwarded",
"courseLevel": "https://idhub.pangea.org/context/#courseLevel",
"courseFramework": "https://idhub.pangea.org/context/#courseFramework",
"courseCredits": "https://idhub.pangea.org/context/#courseCredits",
"dateOfAssessment": "https://idhub.pangea.org/context/#dateOfAssessment",
"evidenceAssessment": "https://idhub.pangea.org/context/#evidenceAssessment"
}
],
"id": "{{ vc_id }}",
"type": [
"VerifiableCredential",
"VerifiableAttestation",
"CourseCredential"
],
"issuer": {
"id": "{{ issuer_did }}",
"name": "{{ organisation }}"
},
"issuanceDate": "{{ issuance_date }}",
"validFrom": "{{ issuance_date }}",
"validUntil": "{{ validUntil }}",
"name": [
{
"value": "NGO Course Credential for participants",
"lang": "en"
},
{
"value": "Credencial per participants d'un curs impartit per una ONG",
"lang": "ca_ES"
},
{
"value": "Credencial para participantes de un curso impartido por una ONG",
"lang": "es"
}
],
"description": [
{
"value": "A NGO Course Credential Schema awarded by a NGO federation and their NGO members, as proposed by Lafede.cat",
"lang": "en"
}
],
"credentialSubject": {
"id": "{{ subject_did }}",
"firstName": "{{ firstName }}",
"lastName": "{{ lastName }}",
"personalIdentifier": "{{ personalIdentifier }}",
"issuedDate": "{{ issuedDate }}",
"modeOfInstruction": "{{ modeOfInstruction }}",
"courseDuration": "{{ courseDuration }}",
"courseDays": "{{ courseDays }}",
"courseName": "{{ courseName }}",
"courseDescription": "{{ courseDescription }}",
"gradingScheme": "{{ gradingScheme }}",
"scoreAwarded": "{{ scoreAwarded }}",
"qualificationAwarded": "{{ qualificationAwarded }}",
"courseLevel": "{{ courseLevel }}",
"courseFramework": "{{ courseFramework }}",
"courseCredits": "{{ courseCredits }}",
"dateOfAssessment": "{{ dateOfAssessment }}",
"evidenceAssessment": "{{ evidenceAssessment }}"
},
"credentialSchema": {
"id": "https://idhub.pangea.org/vc_schemas/course-credential.json",
"type": "FullJsonSchemaValidator2021"
}
}

View File

@ -1,30 +0,0 @@
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
{
"name": "https://schema.org/name",
"email": "https://schema.org/email",
"membershipType": "https://schema.org/memberOf",
"individual": "https://schema.org/Person",
"organization": "https://schema.org/Organization",
"Member": "https://schema.org/Member",
"startDate": "https://schema.org/startDate",
"jsonSchema": "https://schema.org/jsonSchema",
"street_address": "https://schema.org/streetAddress",
"connectivity_option_list": "https://schema.org/connectivityOptionList",
"$ref": "https://schema.org/jsonSchemaRef"
}
],
"id": "{{ vc_id }}",
"type": ["VerifiableCredential", "HomeConnectivitySurveyCredential"],
"issuer": "{{ issuer_did }}",
"issuanceDate": "{{ issuance_date }}",
"credentialSubject": {
"id": "{{ subject_did }}",
"street_address": "{{ street_address }}",
"connectivity_option_list": "{{ connectivity_option_list }}",
"jsonSchema": {
"$ref": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/UNDEF.json"
}
}
}

View File

@ -0,0 +1,92 @@
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://idhub.pangea.org/credentials/base/v1",
{
"federation": "https://idhub.pangea.org/context/#federation",
"legalName": "https://idhub.pangea.org/context/#legalName",
"shortName": "https://idhub.pangea.org/context/#shortName",
"registrationIdentifier": "https://idhub.pangea.org/context/#registrationIdentifier",
"publicRegistry": "https://idhub.pangea.org/context/#publicRegistry",
"streetAddress": "https://idhub.pangea.org/context/#streetAddress",
"postCode": "https://idhub.pangea.org/context/#postCode",
"city": "https://idhub.pangea.org/context/#city",
"taxReference": "https://idhub.pangea.org/context/#taxReference",
"membershipType": "https://idhub.pangea.org/context/#membershipType",
"membershipStatus": "https://idhub.pangea.org/context/#membershipStatus",
"membershipId": "https://idhub.pangea.org/context/#membershipId",
"membershipSince": "https://idhub.pangea.org/context/#membershipSince",
"email": "https://idhub.pangea.org/context/#email",
"phone": "https://idhub.pangea.org/context/#phone",
"website": "https://idhub.pangea.org/context/#website",
"evidence": "https://idhub.pangea.org/context/#evidence",
"certificationDate": "https://idhub.pangea.org/context/#certificationDate"
}
],
"id": "{{ vc_id }}",
"type": [
"VerifiableCredential",
"VerifiableAttestation",
"FederationMembership"
],
"issuer": {
"id": "{{ issuer_did }}",
"name": "{{ organisation }}"
},
"issuanceDate": "{{ issuance_date }}",
"validFrom": "{{ issuance_date }}",
"validUntil": "{{ validUntil }}",
"name": [
{
"value": "NGO federation membership attestation credential",
"lang": "en"
},
{
"value": "Credencial d'atestat de pertinença a federació d'ONG",
"lang": "ca_ES"
},
{
"value": "Credencial de atestado de membresía de Federación de ONG",
"lang": "es"
}
],
"description": [
{
"value": "Credential for NGOs that are members of a NGO federation",
"lang": "en"
},
{
"value": "Credencial para ONG que son miembros de una federación de ONG",
"lang": "es"
},
{
"value": "Credencial per a les ONG que són membres d'una federació d'ONG",
"lang": "ca_ES"
}
],
"credentialSubject": {
"id": "{{ subject_did }}",
"federation": "{{ federation }}",
"legalName": "{{ legalName }}",
"shortName": "{{ shortName }}",
"registrationIdentifier": "{{ registrationIdentifier }}",
"publicRegistry": "{{ publicRegistry }}",
"streetAddress": "{{ streetAddress }}",
"postCode": "{{ postCode }}",
"city": "{{ city }}",
"taxReference": "{{ taxReference }}",
"membershipType": "{{ membershipType }}",
"membershipStatus": "{{ membershipStatus }}",
"membershipId": "{{ membershipId }}",
"membershipSince": "{{ membershipSince }}",
"email": "{{ email }}",
"phone": "{{ phone }}",
"website": "{{ website }}",
"evidence": "{{ evidence }}",
"certificationDate": "{{ certificationDate }}"
},
"credentialSchema": {
"id": "https://idhub.pangea.org/vc_schemas/federation-membership.json",
"type": "FullJsonSchemaValidator2021"
}
}

View File

@ -0,0 +1,69 @@
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://idhub.pangea.org/credentials/base/v1",
"https://idhub.pangea.org/credentials/financial-vulnerability/v1"
],
"id": "{{ vc_id }}",
"type": [
"VerifiableCredential",
"VerifiableAttestation",
"FinancialVulnerabilityCredential"
],
"issuer": {
"id": "{{ issuer_did }}",
"name": "{{ organisation }}"
},
"issuanceDate": "{{ issuance_date }}",
"issued": "{{ issuance_date }}",
"validFrom": "{{ issuance_date }}",
"validUntil": "{{ validUntil }}",
"name": [
{
"value": "Financial Vulnerability Credential",
"lang": "en"
},
{
"value": "Credencial de Vulnerabilitat Financera",
"lang": "ca_ES"
},
{
"value": "Credencial de Vulnerabilidad Financiera",
"lang": "es"
}
],
"description": [
{
"value": "The Financial Vulnerability Credential is issued to individuals or families to prove their financial vulnerability based on various factors, with the objective of presenting it to a third party to receive benefits or services.",
"lang": "en"
},
{
"value": "La Credencial de Vulnerabilitat Financera és emesa a persones o famílies per acreditar la seva vulnerabilitat financera sobre la base de diversos factors, amb l'objectiu que la presentin a una tercera part per rebre beneficis o serveis.",
"lang": "ca_ES"
},
{
"value": "La Credencial de Vulnerabilidad Financiera es emitida a personas o familias para acreditar su vulnerabilidad financiera con base en diversos factores, con el objetivo de que la presenten a una tercera parte para recibir beneficios o servicios.",
"lang": "es"
}
],
"credentialSubject": {
"id": "{{ subject_did }}",
"firstName": "{{ firstName }}",
"lastName": "{{ lastName }}",
"email": "{{ email }}",
"identityDocType": "{{ identityDocType }}",
"identityNumber": "{{ identityNumber }}",
"phoneNumber": "{{ phoneNumber }}",
"streetAddress": "{{ streetAddress }}",
"socialWorkerName": "{{ socialWorkerName }}",
"socialWorkerSurname": "{{ socialWorkerSurname }}",
"financialVulnerabilityScore": "{{ financialVulnerabilityScore }}",
"amountCoveredByOtherAids": "{{ amountCoveredByOtherAids }}",
"connectivityOptionList": "{{ connectivityOptionList }}",
"assessmentDate": "{{ assessmentDate }}"
},
"credentialSchema": {
"id": "https://idhub.pangea.org/vc_schemas/financial-vulnerability.json",
"type": "FullJsonSchemaValidator2021"
}
}

View File

@ -1,32 +0,0 @@
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
{
"name": "https://schema.org/name",
"email": "https://schema.org/email",
"membershipType": "https://schema.org/memberOf",
"individual": "https://schema.org/Person",
"organization": "https://schema.org/Organization",
"Member": "https://schema.org/Member",
"startDate": "https://schema.org/startDate",
"jsonSchema": "https://schema.org/jsonSchema",
"$ref": "https://schema.org/jsonSchemaRef"
}
],
"type": ["VerifiableCredential", "VerifiableAttestation"],
"id": "{{ vc_id }}",
"issuer": "{{ issuer_did }}",
"issuanceDate": "{{ issuance_date }}",
"credentialSubject": {
"id": "{{ subject_did }}",
"Member": {
"name": "{{ name }}",
"email": "{{ email }}",
"membershipType": "{{ membershipType }}",
"startDate": "{{ startDate }}"
},
"jsonSchema": {
"$ref": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/member-schema.json"
}
}
}

View File

@ -1,31 +1,8 @@
{ {
"@context": [ "@context": [
"https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/v1",
{ "https://idhub.pangea.org/credentials/base/v1",
"individual": "https://schema.org/Person", "https://idhub.pangea.org/credentials/membership-card/v1"
"Member": "https://schema.org/Member",
"startDate": "https://schema.org/startDate",
"jsonSchema": "https://schema.org/jsonSchema",
"$ref": "https://schema.org/jsonSchemaRef",
"credentialSchema": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#credentialSchema",
"organisation": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#organisation",
"membershipType": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#membershipType",
"membershipId": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#membershipId",
"typeOfPerson": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#typeOfPerson",
"identityDocType": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#identityDocType",
"identityNumber": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#identityNumber",
"name": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#name",
"description": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#description",
"value": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#value",
"lang": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#lang",
"surnames": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#surnames",
"email": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#email",
"affiliatedSince": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#affiliatedSince",
"affiliatedUntil": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#affiliatedUntil",
"issued": "https://ec.europa.eu/digital-building-blocks/wikis/display/EBSIDOC/Verifiable+Attestation#issued",
"validFrom": "https://ec.europa.eu/digital-building-blocks/wikis/display/EBSIDOC/Verifiable+Attestation#validFrom",
"validUntil": "https://ec.europa.eu/digital-building-blocks/wikis/display/EBSIDOC/Verifiable+Attestation#validUntil"
}
], ],
"type": [ "type": [
"VerifiableCredential", "VerifiableCredential",
@ -35,22 +12,7 @@
"id": "{{ vc_id }}", "id": "{{ vc_id }}",
"issuer": { "issuer": {
"id": "{{ issuer_did }}", "id": "{{ issuer_did }}",
"name": "Pangea", "name": "{{ organisation }}"
"description": [
{
"value": "Pangea.org is a service provider leveraging open-source technologies to provide affordable and accessible solutions for social enterprises and solidarity organisations.",
"lang": "en"
},
{
"value": "Pangea.org és un proveïdor de serveis que aprofita les tecnologies de codi obert per oferir solucions assequibles i accessibles per a empreses socials i organitzacions solidàries.",
"lang": "ca_ES"
},
{
"value": "Pangea.org es un proveedor de servicios que aprovecha tecnologías de código abierto para proporcionar soluciones asequibles y accesibles para empresas sociales y organizaciones solidarias.",
"lang": "es"
}
]
}, },
"issuanceDate": "{{ issuance_date }}", "issuanceDate": "{{ issuance_date }}",
"issued": "{{ issuance_date }}", "issued": "{{ issuance_date }}",
@ -86,20 +48,20 @@
], ],
"credentialSubject": { "credentialSubject": {
"id": "{{ subject_did }}", "id": "{{ subject_did }}",
"organisation": "Pangea", "firstName": "{{ firstName }}",
"membershipType": "{{ membershipType }}", "lastName": "{{ lastName }}",
"membershipId": "{{ vc_id }}", "email": "{{ email }}",
"affiliatedSince": "{{ affiliatedSince }}",
"affiliatedUntil": "{{ affiliatedUntil }}",
"typeOfPerson": "{{ typeOfPerson }}", "typeOfPerson": "{{ typeOfPerson }}",
"identityDocType": "{{ identityDocType }}", "identityDocType": "{{ identityDocType }}",
"identityNumber": "{{ identityNumber }}", "identityNumber": "{{ identityNumber }}",
"name": "{{ first_name }}", "organisation": "{{ organisation }}",
"surnames": "{{ last_name }}", "membershipType": "{{ membershipType }}",
"email": "{{ email }}", "membershipId": "{{ vc_id }}",
"affiliatedSince": "{{ affiliatedSince }}",
"affiliatedUntil": "{{ affiliatedUntil }}"
},
"credentialSchema": { "credentialSchema": {
"id": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/membership-card-schema.json", "id": "https://idhub.pangea.org/vc_schemas/membership-card.json",
"type": "JsonSchema" "type": "FullJsonSchemaValidator2021"
}
} }
} }

View File

@ -1,33 +0,0 @@
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
{
"name": "https://schema.org/name",
"email": "https://schema.org/email",
"membershipType": "https://schema.org/memberOf",
"individual": "https://schema.org/Person",
"organization": "https://schema.org/Organization",
"Member": "https://schema.org/Member",
"startDate": "https://schema.org/startDate",
"jsonSchema": "https://schema.org/jsonSchema",
"destination_country": "https://schema.org/destinationCountry",
"offboarding_date": "https://schema.org/offboardingDate",
"$ref": "https://schema.org/jsonSchemaRef"
}
],
"id": "{{ vc_id }}",
"type": ["VerifiableCredential", "MigrantRescueCredential"],
"issuer": "{{ issuer_did }}",
"issuanceDate": "{{ issuance_date }}",
"credentialSubject": {
"id": "{{ subject_did }}",
"name": "{{ name }}",
"country_of_origin": "{{ country_of_origin }}",
"rescue_date": "{{ rescue_date }}",
"destination_country": "{{ destination_country }}",
"offboarding_date": "{{ offboarding_date }}",
"jsonSchema": {
"$ref": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/UNDEF.json"
}
}
}

View File

@ -1,30 +0,0 @@
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
{
"name": "https://schema.org/name",
"email": "https://schema.org/email",
"membershipType": "https://schema.org/memberOf",
"individual": "https://schema.org/Person",
"organization": "https://schema.org/Organization",
"Member": "https://schema.org/Member",
"startDate": "https://schema.org/startDate",
"jsonSchema": "https://schema.org/jsonSchema",
"street_address": "https://schema.org/streetAddress",
"financial_vulnerability_score": "https://schema.org/financialVulnerabilityScore",
"$ref": "https://schema.org/jsonSchemaRef"
}
],
"id": "{{ vc_id }}",
"type": ["VerifiableCredential", "FinancialSituationCredential"],
"issuer": "{{ issuer_did }}",
"issuanceDate": "{{ issuance_date }}",
"credentialSubject": {
"id": "{{ subject_did }}",
"street_address": "{{ street_address }}",
"financial_vulnerability_score": "{{ financial_vulnerability_score }}",
"jsonSchema": {
"$ref": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/UNDEF.json"
}
}
}

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

@ -6,4 +6,27 @@
<i class="{{ icon }}"></i> <i class="{{ icon }}"></i>
{{ subtitle }} {{ subtitle }}
</h3> </h3>
{% load django_bootstrap5 %}
<form role="form" method="post" enctype="multipart/form-data">
{% 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">
<button class="close" type="button" data-dismiss="alert" aria-label="Close">
<span class="mdi mdi-close" aria-hidden="true"></span>
</button>
{% for field, error in form.errors.items %}
{{ error }}
{% endfor %}
</div>
</div>
{% endif %}
{% bootstrap_form form %}
<div class="form-actions-no-box">
<a class="btn btn-grey" href="{% url 'idhub:admin_import' %}">{% translate "Cancel" %}</a>
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Upload' %}" />
</div>
</form>
{% endblock %} {% endblock %}

View File

@ -109,11 +109,13 @@
{% trans 'My credentials' %} {% trans 'My credentials' %}
</a> </a>
</li> </li>
{% if admin_validated %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if path == 'user_credentials_request' %}active2{% endif %}" href="{% url 'idhub:user_credentials_request' %}"> <a class="nav-link {% if path == 'user_credentials_request' %}active2{% endif %}" href="{% url 'idhub:user_credentials_request' %}">
{% trans 'Request a credential' %} {% trans 'Request a credential' %}
</a> </a>
</li> </li>
{% endif %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if path in 'user_demand_authorization, authorize' %}active2{% endif %}" href="{% url 'idhub:user_demand_authorization' %}"> <a class="nav-link {% if path in 'user_demand_authorization, authorize' %}active2{% endif %}" href="{% url 'idhub:user_demand_authorization' %}">
{% trans 'Present a credential' %} {% trans 'Present a credential' %}
@ -138,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"> <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> <h1 class="h2">{{ title }}</h1>
<div class="btn-toolbar mb-2 mb-md-0"> <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>
</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"> <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> <h1 class="h2">{{ title }}</h1>
<div class="btn-toolbar mb-2 mb-md-0"> <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>
</div> </div>

View File

@ -36,11 +36,16 @@
{{ object.get_status}} {{ object.get_status}}
</div> </div>
</div> </div>
<div class="row mt-3"> </div>
</div>
<div class="row mt-3">
{% if object.eidas1_did and admin_validated %}
<div class="col text-center"> <div class="col text-center">
<a class="btn btn-green-user" href="{% url 'idhub:user_credential_json' object.id %}">{% trans 'View in JSON format' %}</a> <a class="btn btn-green-user" href="{% url 'idhub:user_credential_pdf' object.id %}">{% trans 'Sign credential in PDF format' %}</a>
</div>
</div> </div>
{% endif %}
<div class="col text-center">
<a class="btn btn-green-user" href="{% url 'idhub:user_credential_json' object.id %}">{% trans 'View credential in JSON format' %}</a>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -6,4 +6,7 @@
<i class="{{ icon }}"></i> <i class="{{ icon }}"></i>
{{ subtitle }} {{ subtitle }}
</h3> </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 %} {% 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.contrib.auth import views as auth_views
from django.views.generic import RedirectView from django.views.generic import RedirectView
from django.urls import path, reverse_lazy from django.urls import path, reverse_lazy
from .views import LoginView from .views import (
LoginView,
PasswordResetConfirmView,
serve_did,
DobleFactorSendView,
)
from .admin import views as views_admin from .admin import views as views_admin
from .user import views as views_user from .user import views as views_user
# from .verification_portal import views as views_verification_portal # from .verification_portal import views as views_verification_portal
@ -45,13 +50,16 @@ urlpatterns = [
), ),
name='password_reset_done' name='password_reset_done'
), ),
path('auth/reset/<uidb64>/<token>/', path('auth/reset/<uidb64>/<token>/', PasswordResetConfirmView.as_view(),
auth_views.PasswordResetConfirmView.as_view(
template_name='auth/password_reset_confirm.html',
success_url=reverse_lazy('idhub:password_reset_complete')
),
name='password_reset_confirm' name='password_reset_confirm'
), ),
# path('auth/reset/<uidb64>/<token>/',
# auth_views.PasswordResetConfirmView.as_view(
# template_name='auth/password_reset_confirm.html',
# success_url=reverse_lazy('idhub:password_reset_complete')
# ),
# name='password_reset_confirm'
# ),
path('auth/reset/done/', path('auth/reset/done/',
auth_views.PasswordResetCompleteView.as_view( auth_views.PasswordResetCompleteView.as_view(
template_name='auth/password_reset_complete.html' template_name='auth/password_reset_complete.html'
@ -80,14 +88,20 @@ urlpatterns = [
name='user_credentials'), name='user_credentials'),
path('user/credentials/<int:pk>', views_user.CredentialView.as_view(), path('user/credentials/<int:pk>', views_user.CredentialView.as_view(),
name='user_credential'), name='user_credential'),
path('user/credentials/<int:pk>/json', views_user.CredentialJsonView.as_view(), path('user/credentials/<int:pk>/pdf', views_user.CredentialPdfView.as_view(),
name='user_credential_pdf'),
path('credentials/<int:pk>/', views_user.CredentialJsonView.as_view(),
name='user_credential_json'), name='user_credential_json'),
path('public/credentials/<str:pk>/', views_user.PublicCredentialJsonView.as_view(),
name='public_credential_json'),
path('user/credentials/request/', path('user/credentials/request/',
views_user.CredentialsRequestView.as_view(), views_user.CredentialsRequestView.as_view(),
name='user_credentials_request'), name='user_credentials_request'),
path('user/credentials_presentation/demand', path('user/credentials_presentation/demand',
views_user.DemandAuthorizationView.as_view(), views_user.DemandAuthorizationView.as_view(),
name='user_demand_authorization'), name='user_demand_authorization'),
path('user/terms/', views_user.TermsAndConditionsView.as_view(),
name='user_terms_and_conditions'),
# Admin # Admin
path('admin/dashboard/', views_admin.DashboardView.as_view(), path('admin/dashboard/', views_admin.DashboardView.as_view(),
@ -170,8 +184,15 @@ urlpatterns = [
name='admin_schemas_import_add'), name='admin_schemas_import_add'),
path('admin/import', views_admin.ImportView.as_view(), path('admin/import', views_admin.ImportView.as_view(),
name='admin_import'), 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(), path('admin/import/new', views_admin.ImportAddView.as_view(),
name='admin_import_add'), 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)
# path('verification_portal/verify/', views_verification_portal.verify, # path('verification_portal/verify/', views_verification_portal.verify,
# name="verification_portal_verify") # name="verification_portal_verify")

View File

@ -5,18 +5,56 @@ from idhub.models import DID, VerificableCredential
from oidc4vp.models import Organization from oidc4vp.models import Organization
class ProfileForm(forms.ModelForm):
MANDATORY_FIELDS = ['first_name', 'last_name', 'email']
class Meta:
model = User
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): class RequestCredentialForm(forms.Form):
did = forms.ChoiceField(label=_("Did"), choices=[]) did = forms.ChoiceField(label=_("Did"), choices=[])
credential = forms.ChoiceField(label=_("Credential"), choices=[]) credential = forms.ChoiceField(label=_("Credential"), choices=[])
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None) self.user = kwargs.pop('user', None)
self.lang = kwargs.pop('lang', None)
self._domain = kwargs.pop('domain', None)
self.password = kwargs.pop('password', None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['did'].choices = [ self.fields['did'].choices = [
(x.did, x.label) for x in DID.objects.filter(user=self.user) (x.did, x.label) for x in DID.objects.filter(user=self.user)
] ]
self.fields['credential'].choices = [ self.fields['credential'].choices = [
(x.id, x.type()) for x in VerificableCredential.objects.filter( (x.id, x.get_type(lang=self.lang)) for x in VerificableCredential.objects.filter(
user=self.user, user=self.user,
status=VerificableCredential.Status.ENABLED status=VerificableCredential.Status.ENABLED
) )
@ -38,7 +76,8 @@ class RequestCredentialForm(forms.Form):
did = did[0] did = did[0]
cred = cred[0] cred = cred[0]
try: try:
cred.issue(did) if self.password:
cred.issue(did, self.password, domain=self._domain)
except Exception: except Exception:
return return

View File

@ -1,6 +1,20 @@
import os
import json
import base64
import qrcode
import logging import logging
import datetime
import weasyprint
import qrcode.image.svg
from io import BytesIO
from pathlib import Path
from pyhanko.sign import fields, signers
from pyhanko import stamp
from pyhanko.pdf_utils import text
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import View
from django.views.generic.edit import ( from django.views.generic.edit import (
UpdateView, UpdateView,
CreateView, CreateView,
@ -20,10 +34,15 @@ from idhub.user.tables import (
DIDTable, DIDTable,
CredentialsTable CredentialsTable
) )
from django.core.cache import cache
from django.conf import settings
from idhub.user.forms import ( from idhub.user.forms import (
ProfileForm,
RequestCredentialForm, RequestCredentialForm,
DemandAuthorizationForm DemandAuthorizationForm,
TermsConditionsForm
) )
from utils import certs
from idhub.mixins import UserView from idhub.mixins import UserView
from idhub.models import DID, VerificableCredential, Event, Membership from idhub.models import DID, VerificableCredential, Event, Membership
from idhub_auth.models import User from idhub_auth.models import User
@ -114,6 +133,26 @@ class CredentialsView(MyWallet, SingleTableView):
return queryset return queryset
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): class CredentialView(MyWallet, TemplateView):
template_name = "idhub/user/credential.html" template_name = "idhub/user/credential.html"
subtitle = _('Credential') subtitle = _('Credential')
@ -136,6 +175,147 @@ class CredentialView(MyWallet, TemplateView):
return context return context
class CredentialPdfView(MyWallet, TemplateView):
template_name = "certificates/4_Model_Certificat.html"
subtitle = _('Credential management')
icon = 'bi bi-patch-check-fill'
file_name = "certificate.pdf"
def get(self, request, *args, **kwargs):
self.admin_validated = cache.get("KEY_DIDS")
pk = kwargs['pk']
self.user = self.request.user
self.object = get_object_or_404(
VerificableCredential,
pk=pk,
eidas1_did__isnull=False,
user=self.request.user
)
self.url_id = "{}://{}/public/credentials/{}".format(
self.request.scheme,
self.request.get_host(),
self.object.hash
)
data = self.build_certificate()
if self.object.eidas1_did:
doc = self.insert_signature(data)
else:
doc = data
response = HttpResponse(doc, content_type="application/pdf")
response['Content-Disposition'] = 'attachment; filename={}'.format(self.file_name)
return response
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
this_folder = str(Path.cwd())
path_img_sig = "idhub/static/images/4_Model_Certificat_html_58d7f7eeb828cf29.jpg"
img_signature = next(Path.cwd().glob(path_img_sig))
with open(img_signature, 'rb') as _f:
img_sig = base64.b64encode(_f.read()).decode('utf-8')
path_img_head = "idhub/static/images/4_Model_Certificat_html_7a0214c6fc8f2309.jpg"
img_header= next(Path.cwd().glob(path_img_head))
with open(img_header, 'rb') as _f:
img_head = base64.b64encode(_f.read()).decode('utf-8')
qr = self.generate_qr_code(self.url_id)
first_name = self.user.first_name and self.user.first_name.upper() or ""
last_name = self.user.first_name and self.user.last_name.upper() or ""
document_id = "0000000-L"
course = "COURSE 1"
address = "ADDRESS"
date_course = datetime.datetime.now()
n_hours = 40
n_lections = 5
issue_date = datetime.datetime.now()
context.update({
'object': self.object,
"image_signature": img_sig,
"image_header": img_head,
"first_name": first_name,
"last_name": last_name,
"document_id": document_id,
"course": course,
"address": address,
"date_course": date_course,
"n_hours": n_hours,
"n_lections": n_lections,
"issue_date": issue_date,
"qr": qr
})
return context
def build_certificate(self):
doc = self.render_to_response(context=self.get_context_data())
doc.render()
pdf = weasyprint.HTML(string=doc.content)
return pdf.write_pdf()
def generate_qr_code(self, data):
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(data)
qr.make(fit=True)
img_buffer = BytesIO()
img = qr.make_image(fill_color="black", back_color="white")
img.save(img_buffer, format="PNG")
return base64.b64encode(img_buffer.getvalue()).decode('utf-8')
def get_pfx_data(self):
did = self.object.eidas1_did
pw = self.admin_validated
if not did or not pw:
return None, None
key_material = json.loads(did.get_key_material(pw))
cert = key_material.get("cert")
passphrase = key_material.get("passphrase")
if cert and passphrase:
return base64.b64decode(cert), passphrase.encode('utf-8')
return None, None
def signer_init(self):
pfx_data, passphrase = self.get_pfx_data()
if not pfx_data or not passphrase:
return
s = certs.load_cert(
pfx_data, passphrase
)
return s
def insert_signature(self, doc):
sig = self.signer_init()
if not sig:
return
_buffer = BytesIO()
_buffer.write(doc)
w = IncrementalPdfFileWriter(_buffer)
fields.append_signature_field(
w, sig_field_spec=fields.SigFieldSpec(
'Signature', box=(150, 100, 450, 150)
)
)
meta = signers.PdfSignatureMetadata(field_name='Signature')
pdf_signer = signers.PdfSigner(
meta, signer=sig, stamp_style=stamp.TextStampStyle(
stamp_text='Signed by: %(signer)s\nTime: %(ts)s\nURL: %(url)s',
text_box_style=text.TextBoxStyle()
)
)
_bf_out = BytesIO()
pdf_signer.sign_pdf(w, output=_bf_out, appearance_text_params={'url': self.url_id})
return _bf_out.read()
class CredentialJsonView(MyWallet, TemplateView): class CredentialJsonView(MyWallet, TemplateView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@ -145,6 +325,28 @@ class CredentialJsonView(MyWallet, TemplateView):
pk=pk, pk=pk,
user=self.request.user user=self.request.user
) )
pass_enc = self.request.session.get("key_did")
data = ""
if pass_enc:
user_pass = self.request.user.decrypt_data(
pass_enc,
self.request.user.password+self.request.session._session_key
)
data = self.object.get_data(user_pass)
response = HttpResponse(data, content_type="application/json")
response['Content-Disposition'] = 'attachment; filename={}'.format("credential.json")
return response
class PublicCredentialJsonView(View):
def get(self, request, *args, **kwargs):
pk = kwargs['pk']
self.object = get_object_or_404(
VerificableCredential,
hash=pk,
eidas1_did__isnull=False,
)
response = HttpResponse(self.object.data, content_type="application/json") response = HttpResponse(self.object.data, content_type="application/json")
response['Content-Disposition'] = 'attachment; filename={}'.format("credential.json") response['Content-Disposition'] = 'attachment; filename={}'.format("credential.json")
return response return response
@ -157,9 +359,27 @@ class CredentialsRequestView(MyWallet, FormView):
form_class = RequestCredentialForm form_class = RequestCredentialForm
success_url = reverse_lazy('idhub:user_credentials') success_url = reverse_lazy('idhub:user_credentials')
def get(self, request, *args, **kwargs):
response = super().get(request, *args, **kwargs)
if not self.admin_validated:
return redirect(reverse_lazy('idhub:user_dashboard'))
return response
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user kwargs['user'] = self.request.user
kwargs['lang'] = self.request.LANGUAGE_CODE
domain = "{}://{}".format(self.request.scheme, self.request.get_host())
kwargs['domain'] = domain
pass_enc = self.request.session.get("key_did")
if pass_enc:
user_pass = self.request.user.decrypt_data(
pass_enc,
self.request.user.password+self.request.session._session_key
)
else:
pass_enc = None
kwargs['password'] = user_pass
return kwargs return kwargs
def form_valid(self, form): def form_valid(self, form):
@ -230,13 +450,17 @@ class DidRegisterView(MyWallet, CreateView):
icon = 'bi bi-patch-check-fill' icon = 'bi bi-patch-check-fill'
wallet = True wallet = True
model = DID model = DID
fields = ('label',) fields = ('label', 'type')
success_url = reverse_lazy('idhub:user_dids') success_url = reverse_lazy('idhub:user_dids')
object = None object = None
def form_valid(self, form): def form_valid(self, form):
form.instance.user = self.request.user form.instance.user = self.request.user
form.instance.set_did() pw = self.request.user.decrypt_data(
self.request.session.get("key_did"),
self.request.user.password+self.request.session._session_key
)
form.instance.set_did(pw)
form.save() form.save()
messages.success(self.request, _('DID created successfully')) messages.success(self.request, _('DID created successfully'))

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,8 +1,19 @@
import uuid
from django.conf import settings
from django.core.cache import cache
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.views.generic.base import TemplateView
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.contrib.auth import login as auth_login from django.contrib.auth import login as auth_login
from django.http import HttpResponseRedirect 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
class LoginView(auth_views.LoginView): class LoginView(auth_views.LoginView):
@ -13,16 +24,76 @@ class LoginView(auth_views.LoginView):
} }
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if request.GET.get('next'): self.extra_context['success_url'] = request.GET.get(
self.extra_context['success_url'] = request.GET.get('next') 'next',
reverse_lazy('idhub:user_dashboard')
)
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def form_valid(self, form): def form_valid(self, form):
user = form.get_user() user = form.get_user()
if not user.is_anonymous and user.is_admin: password = form.cleaned_data.get("password")
user_dashboard = reverse_lazy('idhub:user_dashboard')
admin_dashboard = reverse_lazy('idhub:admin_dashboard')
if self.extra_context['success_url'] == user_dashboard:
self.extra_context['success_url'] = admin_dashboard
auth_login(self.request, user) auth_login(self.request, user)
sensitive_data_encryption_key = user.decrypt_sensitive_data(password)
if not user.is_anonymous and user.is_admin:
admin_dashboard = reverse_lazy('idhub:admin_dashboard')
self.extra_context['success_url'] = admin_dashboard
# encryption_key = user.encrypt_data(
# sensitive_data_encryption_key,
# settings.SECRET_KEY
# )
# 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,
user.password+self.request.session._session_key
)
return HttpResponseRedirect(self.extra_context['success_url']) return HttpResponseRedirect(self.extra_context['success_url'])
class PasswordResetConfirmView(auth_views.PasswordResetConfirmView):
template_name = 'auth/password_reset_confirm.html'
success_url = reverse_lazy('idhub:password_reset_complete')
def form_valid(self, form):
password = form.cleaned_data.get("password")
user = form.get_user()
user.set_encrypted_sensitive_data(password)
user.save()
return HttpResponseRedirect(self.success_url)
def serve_did(request, did_id):
id_did = f'did:web:{settings.DOMAIN}:did-registry:{did_id}'
did = get_object_or_404(DID, did=id_did)
document = did.didweb_document
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

@ -31,4 +31,3 @@ class ProfileForm(forms.ModelForm):
return last_name return last_name

View File

@ -1,56 +0,0 @@
# Generated by Django 4.2.5 on 2024-01-22 12:15
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'
),
),
],
options={
'abstract': False,
},
),
]

View File

@ -1,4 +1,9 @@
import nacl
import base64
from nacl import pwhash
from django.db import models from django.db import models
from django.core.cache import cache
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser from django.contrib.auth.models import BaseUserManager, AbstractBaseUser
@ -44,6 +49,9 @@ class User(AbstractBaseUser):
is_admin = models.BooleanField(default=False) is_admin = models.BooleanField(default=False)
first_name = models.CharField(_("First name"), max_length=255, blank=True, null=True) first_name = models.CharField(_("First name"), max_length=255, blank=True, null=True)
last_name = models.CharField(_("Last name"), max_length=255, blank=True, null=True) 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() objects = UserManager()
@ -86,3 +94,64 @@ class User(AbstractBaseUser):
for r in s.service.rol.all(): for r in s.service.rol.all():
roles.append(r.name) roles.append(r.name)
return ", ".join(set(roles)) return ", ".join(set(roles))
def derive_key_from_password(self, password):
kdf = pwhash.argon2i.kdf
ops = pwhash.argon2i.OPSLIMIT_INTERACTIVE
mem = pwhash.argon2i.MEMLIMIT_INTERACTIVE
return kdf(
nacl.secret.SecretBox.KEY_SIZE,
password,
self.get_salt(),
opslimit=ops,
memlimit=mem
)
def decrypt_sensitive_data(self, password, data=None):
sb_key = self.derive_key_from_password(password.encode('utf-8'))
sb = nacl.secret.SecretBox(sb_key)
if not data:
data = self.get_encrypted_sensitive_data()
if not isinstance(data, bytes):
data = data.encode('utf-8')
return sb.decrypt(data).decode('utf-8')
def encrypt_sensitive_data(self, password, data):
sb_key = self.derive_key_from_password(password.encode('utf-8'))
sb = nacl.secret.SecretBox(sb_key)
if not isinstance(data, bytes):
data = data.encode('utf-8')
return base64.b64encode(sb.encrypt(data)).decode('utf-8')
def get_salt(self):
return base64.b64decode(self.salt.encode('utf-8'))
def set_salt(self):
self.salt = base64.b64encode(nacl.utils.random(16)).decode('utf-8')
def get_encrypted_sensitive_data(self):
return base64.b64decode(self.encrypted_sensitive_data.encode('utf-8'))
def set_encrypted_sensitive_data(self, password):
key = base64.b64encode(nacl.utils.random(64))
self.set_salt()
key_crypted = self.encrypt_sensitive_data(password, key)
self.encrypted_sensitive_data = key_crypted
def encrypt_data(self, data, password):
sb = self.get_secret_box(password)
value_enc = sb.encrypt(data.encode('utf-8'))
return base64.b64encode(value_enc).decode('utf-8')
def decrypt_data(self, data, password):
sb = self.get_secret_box(password)
value = base64.b64decode(data.encode('utf-8'))
return sb.decrypt(value).decode('utf-8')
def get_secret_box(self, password):
pw = base64.b64decode(password.encode('utf-8')*4)
sb_key = self.derive_key_from_password(pw)
return nacl.secret.SecretBox(sb_key)

View File

@ -19,7 +19,9 @@ class AuthorizeForm(forms.Form):
self.user = kwargs.pop('user', None) self.user = kwargs.pop('user', None)
self.org = kwargs.pop('org', None) self.org = kwargs.pop('org', None)
self.code = kwargs.pop('code', None) self.code = kwargs.pop('code', None)
self.pw = kwargs.pop('pw', None)
self.presentation_definition = kwargs.pop('presentation_definition', []) self.presentation_definition = kwargs.pop('presentation_definition', [])
self.subject_did = None
reg = r'({})'.format('|'.join(self.presentation_definition)) reg = r'({})'.format('|'.join(self.presentation_definition))
@ -49,7 +51,12 @@ class AuthorizeForm(forms.Form):
txt = _('There are some problems with this credentials') txt = _('There are some problems with this credentials')
raise ValidationError(txt) raise ValidationError(txt)
self.list_credentials.append(c) cred = self.user.decrypt_data(
c.data,
self.pw
)
self.subject_did = c.subject_did
self.list_credentials.append(cred)
if not self.code: if not self.code:
txt = _("There isn't code in request") txt = _("There isn't code in request")
@ -69,13 +76,14 @@ class AuthorizeForm(forms.Form):
return return
def get_verificable_presentation(self): def get_verificable_presentation(self):
did = self.list_credentials[0].subject_did did = self.subject_did
vp_template = get_template('credentials/verifiable_presentation.json') vp_template = get_template('credentials/verifiable_presentation.json')
vc_list = json.dumps([json.loads(x.data) for x in self.list_credentials]) vc_list = json.dumps([json.loads(x) for x in self.list_credentials])
context = { context = {
"holder_did": did.did, "holder_did": did.did,
"verifiable_credential_list": vc_list "verifiable_credential_list": vc_list
} }
unsigned_vp = vp_template.render(context) unsigned_vp = vp_template.render(context)
self.vp = create_verifiable_presentation(did.key_material, unsigned_vp) key_material = did.get_key_material(self.pw)
self.vp = create_verifiable_presentation(key_material, unsigned_vp)

View File

@ -1,137 +0,0 @@
# Generated by Django 4.2.5 on 2024-01-22 12:16
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

@ -193,10 +193,15 @@ class OAuth2VPToken(models.Model):
return response return response
response["verify"] = "Ok, Verification correct" response["verify"] = "Ok, Verification correct"
response["redirect_uri"] = self.get_redirect_url() url = self.get_redirect_url()
if url:
response["redirect_uri"] = url
return response return response
def get_redirect_url(self): def get_redirect_url(self):
if not settings.ALLOW_CODE_URI:
return
data = { data = {
"code": self.authorization.code, "code": self.authorization.code,
} }

View File

@ -87,7 +87,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
{% trans 'Are you sure that you want delete this user?' %} {% trans 'Are you sure that you want share the info of this credentials?' %}
</div> </div>
<div class="modal-footer"></div> <div class="modal-footer"></div>
</div> </div>

View File

@ -13,6 +13,7 @@ from django.contrib import messages
from oidc4vp.models import Authorization, Organization, OAuth2VPToken from oidc4vp.models import Authorization, Organization, OAuth2VPToken
from idhub.mixins import UserView from idhub.mixins import UserView
from idhub.models import Event
from oidc4vp.forms import AuthorizeForm from oidc4vp.forms import AuthorizeForm
from utils.idhub_ssikit import verify_presentation from utils.idhub_ssikit import verify_presentation
@ -43,6 +44,11 @@ class AuthorizeView(UserView, FormView):
kwargs['presentation_definition'] = vps kwargs['presentation_definition'] = vps
kwargs["org"] = self.get_org() kwargs["org"] = self.get_org()
kwargs["code"] = self.request.GET.get('code') kwargs["code"] = self.request.GET.get('code')
enc_pw = self.request.session["key_did"]
kwargs['pw'] = self.request.user.decrypt_data(
enc_pw,
self.request.user.password+self.request.session._session_key
)
return kwargs return kwargs
def get_form(self, form_class=None): def get_form(self, form_class=None):
@ -55,12 +61,12 @@ class AuthorizeView(UserView, FormView):
authorization = form.save() authorization = form.save()
if not authorization or authorization.status_code != 200: if not authorization or authorization.status_code != 200:
messages.error(self.request, _("Error sending credential!")) messages.error(self.request, _("Error sending credential!"))
return super().form_valid(form) return redirect(self.success_url)
try: try:
authorization = authorization.json() authorization = authorization.json()
except: except:
messages.error(self.request, _("Error sending credential!")) messages.error(self.request, _("Error sending credential!"))
return super().form_valid(form) return redirect(self.success_url)
verify = authorization.get('verify') verify = authorization.get('verify')
result, msg = verify.split(",") result, msg = verify.split(",")
@ -69,13 +75,22 @@ class AuthorizeView(UserView, FormView):
if 'ok' in result.lower(): if 'ok' in result.lower():
messages.success(self.request, msg) messages.success(self.request, msg)
cred = form.credentials.first()
verifier = form.org.name
if cred and verifier:
Event.set_EV_CREDENTIAL_PRESENTED(cred, verifier)
if authorization.get('redirect_uri'): if authorization.get('redirect_uri'):
return redirect(authorization.get('redirect_uri')) return redirect(authorization.get('redirect_uri'))
elif authorization.get('response'): elif authorization.get('response'):
txt = authorization.get('response') txt = authorization.get('response')
messages.success(self.request, txt) messages.success(self.request, txt)
txt2 = f"Verifier {verifier} send: " + txt
Event.set_EV_USR_SEND_VP(txt2, self.request.user)
url = reverse_lazy('idhub:user_dashboard')
return redirect(url)
return super().form_valid(form) return redirect(self.success_url)
def get_org(self): def get_org(self):
client_id = self.request.GET.get("client_id") client_id = self.request.GET.get("client_id")
@ -123,7 +138,6 @@ class VerifyView(View):
response = vp_token.get_response_verify() response = vp_token.get_response_verify()
vp_token.save() vp_token.save()
if not vp_token.authorization.promotions.exists(): if not vp_token.authorization.promotions.exists():
response["redirect_uri"] = ""
response["response"] = "Validation Code {}".format(code) response["response"] = "Validation Code {}".format(code)
return JsonResponse(response) return JsonResponse(response)
@ -157,9 +171,10 @@ class AllowCodeView(View):
code=code, code=code,
code_used=False code_used=False
) )
if not self.authorization.promotions.exists():
promotion = self.authorization.promotions.first()
if not promotion:
raise Http404("Page not Found!") raise Http404("Page not Found!")
promotion = self.authorization.promotions.all()[0]
return redirect(promotion.get_url(code)) return redirect(promotion.get_url(code))

View File

@ -1,45 +0,0 @@
# Generated by Django 4.2.5 on 2024-01-22 12:16
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

@ -6,11 +6,25 @@ black==23.9.1
python-decouple==3.8 python-decouple==3.8
jsonschema==4.19.1 jsonschema==4.19.1
pandas==2.1.1 pandas==2.1.1
xlrd==2.0.1
odfpy==1.4.1
requests==2.31.0 requests==2.31.0
didkit==0.3.2
jinja2==3.1.2 jinja2==3.1.2
jsonref==1.1.0 jsonref==1.1.0
pyld==2.0.3 pyld==2.0.3
pynacl==1.5.0
more-itertools==10.1.0 more-itertools==10.1.0
dj-database-url==2.1.0 dj-database-url==2.1.0
faker==21.0.0 faker==21.0.0
PyPDF2
svg2rlg
svglib
cairosvg
pypdf
pyhanko
qrcode
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

View File

@ -0,0 +1,130 @@
{
"$id": "https://idhub.pangea.org/vc_schemas/courseCredential",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "NGO Course Credential Schema",
"description": "A NGO Course Credential Schema awarded by a NGO federation and their NGO members, as proposed by Lafede.cat",
"name": [
{
"value": "NGO Course Credential for participants",
"lang": "en"
},
{
"value": "Credencial per participants d'un curs impartit per una ONG",
"lang": "ca_ES"
},
{
"value": "Credencial para participantes de un curso impartido por una ONG",
"lang": "es"
}
],
"type": "object",
"allOf": [
{
"$ref": "https://idhub.pangea.org/vc_schemas/ebsi/attestation.json"
},
{
"properties": {
"credentialSubject": {
"description": "Defines additional properties on credentialSubject: the given course followed by a student",
"type": "object",
"properties": {
"id": {
"description": "Defines a unique identifier (DID) of the credential subject: the credential of a completed course by a student",
"type": "string"
},
"firstName": {
"type": "string",
"description": "The first name of the student"
},
"lastName": {
"type": "string",
"description": "The family name of the student"
},
"personalIdentifier": {
"type": "string",
"description": "The personal identifier of the student, such as ID number"
},
"issuedDate": {
"type": "string",
"description": "The date the credential was issued",
"format": "date"
},
"modeOfInstruction": {
"type": "string",
"description": "The mode of instruction: online, in-person, etc."
},
"courseDuration": {
"type": "integer",
"description": "The duration of the course in hours"
},
"courseDays": {
"type": "integer",
"description": "The number of days the course lasts"
},
"courseName": {
"type": "string",
"description": "The name of the course"
},
"courseDescription": {
"type": "string",
"description": "The description of the course"
},
"gradingScheme": {
"type": "string",
"description": "The grading scheme used for the course"
},
"scoreAwarded": {
"type": "integer",
"description": "The score awarded to the student",
"minimum": 0,
"maximum": 10
},
"qualificationAwarded": {
"type": "string",
"description": "The qualification awarded to the student",
"enum": [
"A+",
"A",
"B",
"C",
"D"
]
},
"courseLevel": {
"type": "string",
"description": "The level of the course"
},
"courseFramework": {
"type": "string",
"description": "The framework in which the course belongs to"
},
"courseCredits": {
"type": "integer",
"description": "The number of (ECTS) credits awarded for the course"
},
"dateOfAssessment": {
"type": "string",
"description": "The date of assessment",
"format": "date"
},
"evidenceAssessment": {
"type": "string",
"description": "The evidence of the assessment: final exam, presence, participation"
}
},
"required": [
"id",
"firstName",
"lastName",
"personalIdentifier",
"issuedDate",
"modeOfInstruction",
"courseDuration",
"courseDays",
"courseName"
]
}
}
}
]
}

View File

@ -0,0 +1,176 @@
{
"$id": "https://idhub.pangea.org/vc_schemas/devicePurchase.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Purchase of an eReuse device",
"description": "A device purchase credential is a proof of purchase of a device from a seller by a buyer",
"name": [
{
"value": "Device purchase credential",
"lang": "en"
},
{
"value": "Credencial d'adquisició d'un dispositiu",
"lang": "ca_ES"
},
{
"value": "Credencial de adquisición de un dispositivo",
"lang": "es"
}
],
"type": "object",
"allOf": [
{
"$ref": "https://idhub.pangea.org/vc_schemas/ebsi/attestation.json"
},
{
"properties": {
"credentialSubject": {
"description": "Defines additional properties on credentialSubject: the purchase act, to qualify as simplified invoice (ES)",
"type": "object",
"properties": {
"id": {
"description": "Defines a unique identifier (DID) of the credential subject: the purchase act/transaction",
"type": "string"
},
"invoiceNumber": {
"description": "The invoice number of the purchase act/transaction",
"type": "string"
},
"totalAmount": {
"description": "The total amount of the transaction in local currency units: Euro by default",
"type": "string"
},
"sellerId": {
"description": "Defines a unique identifier (DID) of the seller actor",
"type": "string"
},
"sellerBusinessName": {
"description": "Business name of the credential subject in the seller role",
"type": "string"
},
"sellerName": {
"description": "Name of the credential subject in the seller role",
"type": "string"
},
"sellerSurname": {
"description": "Surname of the credential subject in the seller role, if natural person",
"type": "string"
},
"sellerEmail": {
"type": "string",
"format": "email"
},
"sellerPhoneNumber": {
"type": "string"
},
"sellerIdentityDocType": {
"description": "Type of the Identity Document of the credential subject in the seller role",
"type": "string"
},
"sellerIdentityNumber": {
"description": "Number of the Identity Document of the credential subject in the seller role",
"type": "string"
},
"buyerId": {
"description": "Defines a unique identifier (DID) of the credential subject: the buyer actor",
"type": "string"
},
"buyerBusinessName": {
"description": "Business name of the credential subject in the buyer role",
"type": "string"
},
"buyerName": {
"description": "Name of the credential subject in the buyer role",
"type": "string"
},
"buyerSurname": {
"description": "Surname of the credential subject in the buyer role, if natural person",
"type": "string"
},
"buyerEmail": {
"type": "string",
"format": "email"
},
"buyerPhoneNumber": {
"type": "string"
},
"buyerIdentityDocType": {
"description": "Type of the Identity Document of the credential subject in the buyer role",
"type": "string"
},
"buyerIdentityNumber": {
"description": "Number of the Identity Document of the credential subject in the buyer role",
"type": "string"
},
"deliveryStreetAddress": {
"description": "Postal address of the credential Subject in the buyer role",
"type": "string"
},
"deliveryPostCode": {
"description": "Postal code of the credential Subject in the buyer role",
"type": "string"
},
"deliveryCity": {
"description": "City of the credential Subject in the buyer role",
"type": "string"
},
"supplyDescription": {
"description": "Description of the product/device supplied, needed in a simplified invoice",
"type": "string"
},
"taxRate": {
"description": "Description of Tax rate (VAT) and optionally also the expression VAT included, or special circumstances such as REBU, needed in a simplified invoice",
"type": "string"
},
"deviceChassisId": {
"description": "Chassis identifier of the device",
"type": "string"
},
"devicePreciseHardwareId": {
"description": "Chassis precise hardware configuration identifier of the device",
"type": "string"
},
"depositId": {
"description": "Identifier of an economic deposit left on loan to be returned under conditions",
"type": "string"
},
"sponsorId": {
"description": "Identifier of the sponsor of this purchase that paid the economic cost of the purchase",
"type": "string"
},
"sponsorName": {
"description": "Name of the sponsor of this purchase that paid the economic cost of the purchase",
"type": "string"
},
"purchaseDate": {
"type": "string",
"format": "date-time"
},
"invoiceDate": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"invoiceNumber",
"totalAmount",
"sellerId",
"sellerName",
"sellerBusinessName",
"sellerSurname",
"sellerEmail",
"sellerIdentityDocType",
"sellerIdentityNumber",
"buyerId",
"buyerEmail",
"supplyDescription",
"taxRate",
"deviceChassisId",
"purchaseDate"
]
}
}
}
]
}

View File

@ -0,0 +1,122 @@
{
"$id": "https://idhub.pangea.org/vc_schemas/federationMembership.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Federation membership",
"description": "The federation membership specifies participation of a NGO into a NGO federation, as proposed by Lafede.cat",
"name": [
{
"value": "NGO federation membership",
"lang": "en"
},
{
"value": "Membre de federació ONGs",
"lang": "ca_ES"
},
{
"value": "Miembro de federación de ONGs",
"lang": "es"
}
],
"type": "object",
"allOf": [
{
"$ref": "https://idhub.pangea.org/vc_schemas/ebsi/attestation.json"
},
{
"properties": {
"credentialSubject": {
"description": "Defines additional properties on credentialSubject",
"type": "object",
"properties": {
"id": {
"description": "Defines a unique identifier of the credential subject",
"type": "string"
},
"federation": {
"description": "Federation the credential subject is affiliated with",
"type": "string"
},
"legalName": {
"description": "Legal name of the affiliated organisation",
"type": "string"
},
"shortName": {
"description": "Short name of the organisation of the affiliated organisation",
"type": "string"
},
"registrationIdentifier": {
"description": "Registration identifier of the affiliated organisation",
"type": "string"
},
"publicRegistry": {
"description": "Registry where the affiliated organisation is registered: 'Generalitat de Catalunya', 'Ministerio del interior de España'",
"type": "string"
},
"streetAddress": {
"description": "Postal address of the member organisation: legal address",
"type": "string"
},
"postCode": {
"description": "Postal code of the member organisation",
"type": "string"
},
"city": {
"description": "City of the member organisation",
"type": "string"
},
"taxReference": {
"description": "Tax reference as VAT registration of the member organisation",
"type": "string"
},
"membershipType": {
"description": "Type of membership: full / observer",
"type": "string"
},
"membershipStatus": {
"description": "Type of membership: active / suspended, etc.",
"type": "string"
},
"membershipId": {
"description": "Membership identifier: an internal unique number or code",
"type": "string"
},
"membershipSince": {
"type": "string",
"format": "date"
},
"email": {
"type": "string",
"format": "email"
},
"phone": {
"type": "string"
},
"website": {
"type": "string",
"format": "uri"
},
"evidence": {
"description": "Type of evidence used for attestation",
"type": "string"
},
"certificationDate": {
"type": "string",
"format": "date"
}
},
"required": [
"id",
"legalName",
"postCode",
"city",
"membershipType",
"membershipStatus",
"federation",
"membershipSince",
"certificationDate"
]
}
}
}
]
}

View File

@ -0,0 +1,103 @@
{
"$id": "https://idhub.pangea.org/vc_schemas/financial-vulnerability.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Financial Vulnerability Credential",
"description": "A Financial Vulnerability Credential is issued to individuals or families to prove their financial vulnerability based on various factors, with the objective of presenting it to a third party to receive benefits or services.",
"name": [
{
"value": "Financial Vulnerability Credential",
"lang": "en"
},
{
"value": "Credencial de Vulnerabilitat Financera",
"lang": "ca_ES"
},
{
"value": "Credencial de Vulnerabilidad Financiera",
"lang": "es"
}
],
"type": "object",
"allOf": [
{
"$ref": "https://idhub.pangea.org/vc_schemas/ebsi/attestation.json"
},
{
"properties": {
"credentialSubject": {
"description": "Defines additional properties on credentialSubject",
"type": "object",
"properties": {
"id": {
"description": "Defines a unique identifier (DID) of the credential subject",
"type": "string"
},
"firstName": {
"description": "Name of the credential subject",
"type": "string"
},
"lastName": {
"description": "Surname of the credential subject",
"type": "string"
},
"email": {
"type": "string",
"format": "email"
},
"phoneNumber": {
"type": "string"
},
"identityDocType": {
"description": "Type of the Identity Document of the credential subject",
"type": "string"
},
"identityNumber": {
"description": "Number of the Identity Document of the credential subject",
"type": "string"
},
"streetAddress": {
"description": "Postal address of the credential Subject",
"type": "string"
},
"socialWorkerName": {
"description": "Name of the social worker that support the vulnerable person/family",
"type": "string"
},
"socialWorkerSurname": {
"description": "Surname of the social worker that support the vulnerable person/family",
"type": "string"
},
"financialVulnerabilityScore": {
"description": "Measure of an individual's susceptibility to financial hardship",
"type": "string"
},
"amountCoveredByOtherAids": {
"type": "string"
},
"connectivityOptionList": {
"type": "string"
},
"assessmentDate": {
"type": "string",
"format": "date-time"
}
},
"required": [
"id",
"firstName",
"lastName",
"email",
"identityDocType",
"identityNumber",
"streetAddress",
"socialWorkerName",
"socialWorkerSurname",
"financialVulnerabilityScore",
"amountCoveredByOtherAids",
"assessmentDate"
]
}
}
}
]
}

View File

@ -1,17 +1,47 @@
{ {
"$id": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/membership-card-schema.json", "$id": "https://idhub.pangea.org/vc_schemas/membership-card.json",
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"name": "MembershipCard", "title": "Membership Card",
"description": "MembershipCard credential using JsonSchema", "description": "The membership card specifies an individual's subscription or enrollment in specific services or benefits issued by an organization.",
"name": [
{
"value": "Membership Card",
"lang": "en"
},
{
"value": "Carnet de soci/a",
"lang": "ca_ES"
},
{
"value": "Carnet de socio/a",
"lang": "es"
}
],
"type": "object", "type": "object",
"allOf": [
{
"$ref": "https://idhub.pangea.org/vc_schemas/ebsi/attestation.json"
},
{
"properties": { "properties": {
"credentialSubject": { "credentialSubject": {
"description": "Defines additional properties on credentialSubject",
"type": "object", "type": "object",
"properties": { "properties": {
"id": {
"description": "Defines a unique identifier of the credential subject",
"type": "string"
},
"organisation": { "organisation": {
"description": "Organisation the credential subject is affiliated with",
"type": "string" "type": "string"
}, },
"membershipType": { "membershipType": {
"description": "Type of membership",
"type": "string"
},
"membershipId": {
"description": "Membership identifier",
"type": "string" "type": "string"
}, },
"affiliatedSince": { "affiliatedSince": {
@ -25,26 +55,23 @@
"typeOfPerson": { "typeOfPerson": {
"type": "string", "type": "string",
"enum": [ "enum": [
"individual", "natural",
"org" "legal"
] ]
}, },
"identityDocType": { "identityDocType": {
"type": "string", "description": "Type of the Identity Document of the credential subject",
"enum": [ "type": "string"
"DNI",
"NIF",
"NIE",
"PASSPORT"
]
}, },
"identityNumber": { "identityNumber": {
"description": "Number of the Identity Document of the credential subject",
"type": "string" "type": "string"
}, },
"name": { "firstName": {
"description": "Name of the natural person or name of the legal person (organisation)",
"type": "string" "type": "string"
}, },
"surnames": { "lastName": {
"type": "string" "type": "string"
}, },
"email": { "email": {
@ -53,13 +80,15 @@
} }
}, },
"required": [ "required": [
"id",
"organisation", "organisation",
"affiliatedSince", "affiliatedSince",
"typeOfPerson", "typeOfPerson",
"name", "firstName",
"surnames",
"email" "email"
] ]
} }
} }
}
]
} }

View File

@ -149,6 +149,7 @@ AUTH_PASSWORD_VALIDATORS = [
}, },
] ]
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/ # https://docs.djangoproject.com/en/4.2/topics/i18n/
@ -222,3 +223,4 @@ LOGGING = {
} }
} }
ORGANIZATION = config('ORGANIZATION', 'Pangea')

View File

@ -1,15 +0,0 @@
/user/event-log [GET] -> vista d'esdeveniments
sense enllaços rapids a les accions
/user/dashboard [GET, POST] -> vista de dades personals
/user/roles [GET] -> vista de rols (????)
/user/gdpr [GET] -> info de la gdpr
/user/wallet/dids [GET, POST]
/user/wallet/dids/<id:integer> [GET, DELETE]
/user/credentials [GET]
/user/credentials/<id:integer> [GET, DELETE]
/user/credentials/request [GET, POST]
*** falta "present credentials" ??? ***
/admin/

36
utils/certs.py Normal file
View File

@ -0,0 +1,36 @@
from pyhanko.sign.signers import SimpleSigner
from cryptography.hazmat.primitives.serialization import pkcs12
from pyhanko_certvalidator.registry import SimpleCertificateStore
from pyhanko.keys import _translate_pyca_cryptography_cert_to_asn1
from pyhanko.keys import _translate_pyca_cryptography_key_to_asn1
def load_cert(pfx_bytes, passphrase):
try:
(
private_key,
cert,
other_certs_pkcs12,
) = pkcs12.load_key_and_certificates(pfx_bytes, passphrase)
except (IOError, ValueError, TypeError) as e:
# logger.error(
# 'Could not load key material from PKCS#12 file', exc_info=e
# )
return None
kinfo = _translate_pyca_cryptography_key_to_asn1(private_key)
cert = _translate_pyca_cryptography_cert_to_asn1(cert)
other_certs_pkcs12 = set(
map(_translate_pyca_cryptography_cert_to_asn1, other_certs_pkcs12)
)
cs = SimpleCertificateStore()
certs_to_register = set(other_certs_pkcs12)
cs.register_multiple(certs_to_register)
return SimpleSigner(
signing_key=kinfo,
signing_cert=cert,
cert_registry=cs,
signature_mechanism=None,
prefer_pss=False,
)

View File

@ -2,10 +2,13 @@ import asyncio
import datetime import datetime
import didkit import didkit
import json import json
import urllib
import jinja2 import jinja2
from django.template.backends.django import Template from django.template.backends.django import Template
from django.template.loader import get_template from django.template.loader import get_template
from trustchain_idhub import settings
def generate_did_controller_key(): def generate_did_controller_key():
return didkit.generate_ed25519_key() return didkit.generate_ed25519_key()
@ -15,6 +18,31 @@ def keydid_from_controller_key(key):
return didkit.key_to_did("key", key) return didkit.key_to_did("key", key)
async def resolve_keydid(keydid):
return await didkit.resolve_did(keydid, "{}")
def webdid_from_controller_key(key):
"""
Se siguen los pasos para generar un webdid a partir de un keydid.
Documentado en la docu de spruceid.
"""
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"
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
document["verificationMethod"][0]["id"] = webdid_url_owner
document["verificationMethod"][0]["controller"] = webdid_url
document["authentication"][0] = webdid_url_owner
document["assertionMethod"][0] = webdid_url_owner
document_fixed_serialized = json.dumps(document)
return webdid_url, document_fixed_serialized
def generate_generic_vc_id(): def generate_generic_vc_id():
# TODO agree on a system for Verifiable Credential IDs # TODO agree on a system for Verifiable Credential IDs
return "https://pangea.org/credentials/42" return "https://pangea.org/credentials/42"