diff --git a/examples/import_credentials.csv b/examples/import_credentials.csv
deleted file mode 100644
index 4a06714..0000000
--- a/examples/import_credentials.csv
+++ /dev/null
@@ -1,2 +0,0 @@
-name email membershipType
-Pepe user1@example.org individual
diff --git a/examples/membership-card.csv b/examples/membership-card.csv
deleted file mode 100644
index 1f4d08d..0000000
--- a/examples/membership-card.csv
+++ /dev/null
@@ -1,2 +0,0 @@
-name surnames email typeOfPerson membershipType organisation affiliatedSince
-Pepe Gómez user1@example.org individual Member Pangea 01-01-2023
diff --git a/examples/membership-card.ods b/examples/membership-card.ods
new file mode 100644
index 0000000..b1fe1ac
Binary files /dev/null and b/examples/membership-card.ods differ
diff --git a/examples/membership-card.xls b/examples/membership-card.xls
new file mode 100644
index 0000000..bd10538
Binary files /dev/null and b/examples/membership-card.xls differ
diff --git a/examples/signerDNIe004.pfx b/examples/signerDNIe004.pfx
new file mode 100644
index 0000000..a141358
Binary files /dev/null and b/examples/signerDNIe004.pfx differ
diff --git a/idhub/admin/forms.py b/idhub/admin/forms.py
index 478d671..4264e03 100644
--- a/idhub/admin/forms.py
+++ b/idhub/admin/forms.py
@@ -1,11 +1,16 @@
import csv
import json
+import base64
+import copy
import pandas as pd
+from pyhanko.sign import signers
+
from django import forms
+from django.core.cache import cache
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
-from utils import credtools
+from utils import credtools, certs
from idhub.models import (
DID,
File_datas,
@@ -18,26 +23,69 @@ from idhub.models import (
from idhub_auth.models import User
+class TermsConditionsForm(forms.Form):
+ accept = forms.BooleanField(
+ label=_("Accept terms and conditions of the service"),
+ required=False
+ )
+
+ def __init__(self, *args, **kwargs):
+ self.user = kwargs.pop('user', None)
+ super().__init__(*args, **kwargs)
+
+ def clean(self):
+ data = self.cleaned_data
+ if data.get("accept"):
+ self.user.accept_gdpr = True
+ else:
+ self.user.accept_gdpr = False
+ return data
+
+ def save(self, commit=True):
+
+ if commit:
+ self.user.save()
+ return self.user
+
+ return
+
+
class ImportForm(forms.Form):
did = forms.ChoiceField(label=_("Did"), choices=[])
+ eidas1 = forms.ChoiceField(
+ label=_("Signature with Eidas1"),
+ choices=[],
+ required=False
+ )
schema = forms.ChoiceField(label=_("Schema"), choices=[])
file_import = forms.FileField(label=_("File import"))
def __init__(self, *args, **kwargs):
self._schema = None
self._did = None
+ self._eidas1 = None
self.rows = {}
self.properties = {}
+ self.users = []
self.user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
+ dids = DID.objects.filter(user=self.user)
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 = [
(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"]
did = DID.objects.filter(
user=self.user,
@@ -48,6 +96,14 @@ class ImportForm(forms.Form):
raise ValidationError("Did is not valid!")
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
@@ -62,7 +118,8 @@ class ImportForm(forms.Form):
self._schema = schema.first()
try:
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']
except Exception:
raise ValidationError("Schema is not valid!")
@@ -70,7 +127,10 @@ class ImportForm(forms.Form):
if not self.properties:
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
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():
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()
if not data_pd:
@@ -111,18 +172,18 @@ class ImportForm(forms.Form):
def validate_jsonld(self, line, row):
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:
msg = "line {}: {}".format(line+1, e)
self.exception(msg)
- user = User.objects.filter(email=row.get('email'))
- if not user:
- txt = _('The user does not exist!')
- msg = "line {}: {}".format(line+1, txt)
- self.exception(msg)
+ user, new = User.objects.get_or_create(email=row.get('email'))
+ if new:
+ self.users.append(user)
- return user.first()
+ return user
def create_credential(self, user, row):
return VerificableCredential(
@@ -131,6 +192,7 @@ class ImportForm(forms.Form):
csv_data=json.dumps(row),
issuer_did=self._did,
schema=self._schema,
+ eidas1_did=self._eidas1
)
def exception(self, msg):
@@ -216,3 +278,70 @@ class UserRolForm(forms.ModelForm):
raise forms.ValidationError(msg)
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')
+ )
diff --git a/idhub/admin/views.py b/idhub/admin/views.py
index 7bb3f8d..2af35dc 100644
--- a/idhub/admin/views.py
+++ b/idhub/admin/views.py
@@ -10,7 +10,7 @@ from django_tables2 import SingleTableView
from django.conf import settings
from django.template.loader import get_template
from django.utils.translation import gettext_lazy as _
-from django.views.generic.base import TemplateView
+from django.views.generic.base import TemplateView, View
from django.views.generic.edit import (
CreateView,
DeleteView,
@@ -18,6 +18,7 @@ from django.views.generic.edit import (
UpdateView,
)
from django.shortcuts import get_object_or_404, redirect
+from django.core.cache import cache
from django.urls import reverse_lazy
from django.http import HttpResponse
from django.contrib import messages
@@ -29,8 +30,10 @@ from idhub.email.views import NotifyActivateUserByEmail
from idhub.admin.forms import (
ImportForm,
MembershipForm,
+ TermsConditionsForm,
SchemaForm,
UserRolForm,
+ ImportCertificateForm,
)
from idhub.admin.tables import (
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):
template_name = "idhub/admin/dashboard.html"
table_class = DashboardTable
@@ -132,6 +170,7 @@ class PeopleView(People, TemplateView):
class PeopleActivateView(PeopleView):
def get(self, request, *args, **kwargs):
+ self.check_valid_user()
self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk)
@@ -153,6 +192,7 @@ class PeopleActivateView(PeopleView):
class PeopleDeleteView(PeopleView):
def get(self, request, *args, **kwargs):
+ self.check_valid_user()
self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk)
@@ -317,6 +357,7 @@ class PeopleMembershipDeleteView(PeopleView):
model = Membership
def get(self, request, *args, **kwargs):
+ self.check_valid_user()
self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk)
@@ -404,6 +445,7 @@ class PeopleRolDeleteView(PeopleView):
model = UserRol
def get(self, request, *args, **kwargs):
+ self.check_valid_user()
self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk)
user = self.object.user
@@ -470,6 +512,7 @@ class RolDeleteView(AccessControl):
model = Rol
def get(self, request, *args, **kwargs):
+ self.check_valid_user()
self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk)
@@ -546,6 +589,7 @@ class ServiceDeleteView(AccessControl):
model = Service
def get(self, request, *args, **kwargs):
+ self.check_valid_user()
self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk)
@@ -592,6 +636,7 @@ class CredentialView(Credentials):
class CredentialJsonView(Credentials):
def get(self, request, *args, **kwargs):
+ self.check_valid_user()
pk = kwargs['pk']
self.object = get_object_or_404(
VerificableCredential,
@@ -606,6 +651,7 @@ class RevokeCredentialsView(Credentials):
success_url = reverse_lazy('idhub:admin_credentials')
def get(self, request, *args, **kwargs):
+ self.check_valid_user()
pk = kwargs['pk']
self.object = get_object_or_404(
VerificableCredential,
@@ -625,6 +671,7 @@ class DeleteCredentialsView(Credentials):
success_url = reverse_lazy('idhub:admin_credentials')
def get(self, request, *args, **kwargs):
+ self.check_valid_user()
pk = kwargs['pk']
self.object = get_object_or_404(
VerificableCredential,
@@ -669,13 +716,13 @@ class DidRegisterView(Credentials, CreateView):
icon = 'bi bi-patch-check-fill'
wallet = True
model = DID
- fields = ('label',)
+ fields = ('label', 'type')
success_url = reverse_lazy('idhub:admin_dids')
object = None
def form_valid(self, form):
form.instance.user = self.request.user
- form.instance.set_did()
+ form.instance.set_did(cache.get("KEY_DIDS"))
form.save()
messages.success(self.request, _('DID created successfully'))
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')
def get(self, request, *args, **kwargs):
+ self.check_valid_user()
self.pk = kwargs['pk']
self.object = get_object_or_404(self.model, pk=self.pk)
Event.set_EV_ORG_DID_DELETED_BY_ADMIN(self.object)
@@ -725,11 +773,27 @@ class WalletCredentialsView(Credentials):
wallet = True
-class WalletConfigIssuesView(Credentials):
+class WalletConfigIssuesView(Credentials, FormView):
template_name = "idhub/admin/wallet_issues.html"
subtitle = _('Configure credential issuance')
icon = 'bi bi-patch-check-fill'
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):
@@ -750,6 +814,7 @@ class SchemasView(SchemasMix, SingleTableView):
class SchemasDeleteView(SchemasMix):
def get(self, request, *args, **kwargs):
+ self.check_valid_user()
self.pk = kwargs['pk']
self.object = get_object_or_404(Schemas, pk=self.pk)
self.object.delete()
@@ -760,6 +825,7 @@ class SchemasDeleteView(SchemasMix):
class SchemasDownloadView(SchemasMix):
def get(self, request, *args, **kwargs):
+ self.check_valid_user()
self.pk = kwargs['pk']
self.object = get_object_or_404(Schemas, pk=self.pk)
@@ -838,6 +904,7 @@ class SchemasImportView(SchemasMix):
class SchemasImportAddView(SchemasMix):
def get(self, request, *args, **kwargs):
+ self.check_valid_user()
file_name = kwargs['file_schema']
schemas_files = os.listdir(settings.SCHEMAS_DIR)
if file_name not in schemas_files:
@@ -855,14 +922,18 @@ class SchemasImportAddView(SchemasMix):
ldata = json.loads(data)
assert credtools.validate_schema(ldata)
name = ldata.get('name')
+ title = ldata.get('title')
assert name
+ assert title
except Exception:
messages.error(self.request, _('This is not a valid schema!'))
return
- schema = Schemas.objects.create(file_schema=file_name,
- data=data, type=name,
- template_description=self.get_description()
- )
+ schema = Schemas.objects.create(
+ file_schema=file_name,
+ data=data,
+ type=name,
+ template_description=self.get_description()
+ )
schema.save()
return schema
@@ -917,7 +988,7 @@ class ImportStep2View(ImportExport, TemplateView):
return context
-class ImportAddView(ImportExport, FormView):
+class ImportAddView(NotifyActivateUserByEmail, ImportExport, FormView):
template_name = "idhub/admin/import_add.html"
subtitle = _('Import')
icon = ''
@@ -938,4 +1009,11 @@ class ImportAddView(ImportExport, FormView):
Event.set_EV_CREDENTIAL_CAN_BE_REQUESTED(cred)
else:
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)
diff --git a/idhub/email/views.py b/idhub/email/views.py
index 72e0daa..f14e2a5 100644
--- a/idhub/email/views.py
+++ b/idhub/email/views.py
@@ -13,7 +13,11 @@ logger = logging.getLogger(__name__)
class NotifyActivateUserByEmail:
- def get_email_context(self, user):
+ subject_template_name = 'idhub/admin/registration/activate_user_subject.txt'
+ email_template_name = 'idhub/admin/registration/activate_user_email.txt'
+ html_email_template_name = 'idhub/admin/registration/activate_user_email.html'
+
+ def get_email_context(self, user, token):
"""
Define a new context with a token for put in a email
when send a email for add a new password
@@ -22,35 +26,35 @@ class NotifyActivateUserByEmail:
current_site = get_current_site(self.request)
site_name = current_site.name
domain = current_site.domain
+ if not token:
+ token = default_token_generator.make_token(user)
+
context = {
'email': user.email,
'domain': domain,
'site_name': site_name,
'uid': urlsafe_base64_encode(force_bytes(user.pk)),
'user': user,
- 'token': default_token_generator.make_token(user),
+ 'token': token,
'protocol': protocol,
}
return context
- def send_email(self, user):
+ def send_email(self, user, token=None):
"""
Send a email when a user is activated.
"""
- context = self.get_email_context(user)
- subject_template_name = 'idhub/admin/registration/activate_user_subject.txt'
- email_template_name = 'idhub/admin/registration/activate_user_email.txt'
- html_email_template_name = 'idhub/admin/registration/activate_user_email.html'
- subject = loader.render_to_string(subject_template_name, context)
+ context = self.get_email_context(user, token)
+ subject = loader.render_to_string(self.subject_template_name, context)
# Email subject *must not* contain newlines
subject = ''.join(subject.splitlines())
- body = loader.render_to_string(email_template_name, context)
+ body = loader.render_to_string(self.email_template_name, context)
from_email = settings.DEFAULT_FROM_EMAIL
to_email = user.email
email_message = EmailMultiAlternatives(
subject, body, from_email, [to_email])
- html_email = loader.render_to_string(html_email_template_name, context)
+ html_email = loader.render_to_string(self.html_email_template_name, context)
email_message.attach_alternative(html_email, 'text/html')
try:
if settings.DEVELOPMENT:
diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py
index 034bc68..04c2df7 100644
--- a/idhub/management/commands/initial_datas.py
+++ b/idhub/management/commands/initial_datas.py
@@ -7,6 +7,7 @@ from utils import credtools
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth import get_user_model
+from django.core.cache import cache
from decouple import config
from idhub.models import DID, Schemas
from oidc4vp.models import Organization
@@ -36,17 +37,25 @@ class Command(BaseCommand):
self.create_organizations(r[0].strip(), r[1].strip())
self.sync_credentials_organizations("pangea.org", "somconnexio.coop")
self.sync_credentials_organizations("local 8000", "local 9000")
- self.create_defaults_dids()
self.create_schemas()
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):
- u= User.objects.create(email=email, password=password)
+ u = User.objects.create(email=email, password=password)
u.set_password(password)
+ u.set_encrypted_sensitive_data(password)
u.save()
+ key = u.decrypt_sensitive_data(password)
+ self.create_defaults_dids(u, key)
def create_organizations(self, name, url):
@@ -61,12 +70,10 @@ class Command(BaseCommand):
org1.my_client_secret = org2.client_secret
org1.save()
org2.save()
-
- def create_defaults_dids(self):
- for u in User.objects.all():
- did = DID(label="Default", user=u)
- did.set_did()
- did.save()
+ def create_defaults_dids(self, u, password):
+ did = DID(label="Default", user=u, type=DID.Types.KEY)
+ did.set_did(password)
+ did.save()
def create_schemas(self):
schemas_files = os.listdir(settings.SCHEMAS_DIR)
@@ -82,11 +89,22 @@ class Command(BaseCommand):
try:
ldata = json.loads(data)
assert credtools.validate_schema(ldata)
- name = ldata.get('name')
- assert name
+ dname = ldata.get('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:
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):
data = ''
diff --git a/idhub/migrations/0001_initial.py b/idhub/migrations/0001_initial.py
deleted file mode 100644
index c0c04d3..0000000
--- a/idhub/migrations/0001_initial.py
+++ /dev/null
@@ -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')},
- },
- ),
- ]
diff --git a/idhub/mixins.py b/idhub/mixins.py
index 68b7344..b2b3fbe 100644
--- a/idhub/mixins.py
+++ b/idhub/mixins.py
@@ -1,13 +1,46 @@
from django.contrib.auth.mixins import LoginRequiredMixin
-from django.contrib.auth import views as auth_views
-from django.urls import reverse_lazy, resolve
from django.utils.translation import gettext_lazy as _
+from django.contrib.auth import views as auth_views
+from django.core.exceptions import PermissionDenied
+from django.urls import reverse_lazy, resolve
from django.shortcuts import redirect
+from django.core.cache import cache
+
+
+class Http403(PermissionDenied):
+ status_code = 403
+ default_detail = _('Permission denied. User is not authenticated')
+ default_code = 'forbidden'
+
+ def __init__(self, detail=None, code=None):
+ if detail is not None:
+ self.detail = details or self.default_details
+ if code is not None:
+ self.code = code or self.default_code
class UserView(LoginRequiredMixin):
login_url = "/login/"
wallet = False
+ path_terms = [
+ 'admin_terms_and_conditions',
+ 'user_terms_and_conditions',
+ 'user_gdpr',
+ ]
+
+ def get(self, request, *args, **kwargs):
+ 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):
context = super().get_context_data(**kwargs)
@@ -19,15 +52,33 @@ class UserView(LoginRequiredMixin):
'path': resolve(self.request.path).url_name,
'user': self.request.user,
'wallet': self.wallet,
+ 'admin_validated': True if self.admin_validated else False
})
return context
+ def check_gdpr(self):
+ if not self.request.user.accept_gdpr:
+ url = reverse_lazy("idhub:user_terms_and_conditions")
+ if self.request.user.is_admin:
+ url = reverse_lazy("idhub:admin_terms_and_conditions")
+ if resolve(self.request.path).url_name not in self.path_terms:
+ return redirect(url)
+
class AdminView(UserView):
def get(self, request, *args, **kwargs):
- if not request.user.is_admin:
- url = reverse_lazy('idhub:user_dashboard')
- return redirect(url)
-
+ self.check_valid_user()
return super().get(request, *args, **kwargs)
+
+ def post(self, request, *args, **kwargs):
+ self.check_valid_user()
+ return super().post(request, *args, **kwargs)
+
+ def check_valid_user(self):
+ if not self.request.user.is_admin:
+ raise Http403()
+
+ if self.request.session.get("2fauth"):
+ raise Http403()
+
diff --git a/idhub/models.py b/idhub/models.py
index 40b0b5d..d6f6f7a 100644
--- a/idhub/models.py
+++ b/idhub/models.py
@@ -1,14 +1,21 @@
import json
+import ujson
import pytz
+import hashlib
import datetime
+from collections import OrderedDict
from django.db import models
from django.conf import settings
+from django.core.cache import cache
from django.template.loader import get_template
from django.utils.translation import gettext_lazy as _
+from nacl import secret
+
from utils.idhub_ssikit import (
generate_did_controller_key,
keydid_from_controller_key,
sign_credential,
+ webdid_from_controller_key,
)
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_USR_DEACTIVATED_BY_ADMIN = 29, "User deactivated"
EV_USR_ACTIVATED_BY_ADMIN = 30, "User activated"
+ EV_USR_SEND_VP = 31, "User send Verificable Presentation"
created = models.DateTimeField(_("Date"), auto_now=True)
message = models.CharField(_("Description"), max_length=350)
@@ -400,37 +408,66 @@ class Event(models.Model):
type=cls.Types.EV_USR_ACTIVATED_BY_ADMIN,
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 Types(models.IntegerChoices):
+ KEY = 1, "Key"
+ WEB = 2, "Web"
+ type = models.PositiveSmallIntegerField(
+ _("Type"),
+ choices=Types.choices,
+ )
created_at = models.DateTimeField(auto_now=True)
label = models.CharField(_("Label"), max_length=50)
did = models.CharField(max_length=250)
# In JWK format. Must be stored as-is and passed whole to library functions.
# Example key material:
# '{"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,
on_delete=models.CASCADE,
related_name='dids',
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
def is_organization_did(self):
if not self.user:
return True
return False
- def set_did(self):
- self.key_material = generate_did_controller_key()
- self.did = keydid_from_controller_key(self.key_material)
+ def set_did(self, password):
+ new_key_material = generate_did_controller_key()
+ 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):
return json.loads(self.key_material)
-
class Schemas(models.Model):
type = models.CharField(max_length=250)
file_schema = models.CharField(max_length=250)
@@ -446,6 +483,7 @@ class Schemas(models.Model):
return {}
return json.loads(self.data)
+#<<<<<<< HEAD
def _update_and_get_field(self, field_attr, schema_key):
field_value = getattr(self, field_attr)
if not field_value:
@@ -467,6 +505,21 @@ class Schemas(models.Model):
self._name = value
@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):
return self._update_and_get_field('_description', 'description')
@@ -490,6 +543,7 @@ class VerificableCredential(models.Model):
issued_on = models.DateTimeField(null=True)
data = models.TextField()
csv_data = models.TextField()
+ hash = models.CharField(max_length=260)
status = models.PositiveSmallIntegerField(
choices=Status.choices,
default=Status.ENABLED
@@ -510,17 +564,54 @@ class VerificableCredential(models.Model):
on_delete=models.CASCADE,
related_name='vcredentials',
)
+ eidas1_did = models.ForeignKey(
+ DID,
+ on_delete=models.CASCADE,
+ null=True
+ )
schema = models.ForeignKey(
Schemas,
on_delete=models.CASCADE,
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):
return self.schema.type
+#<<<<<<< HEAD
def get_description(self):
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):
return self.Status(self.status).label
@@ -529,43 +620,72 @@ class VerificableCredential(models.Model):
data = json.loads(self.csv_data).items()
return data
- def issue(self, did):
+ def issue(self, did, password, domain=settings.DOMAIN.strip("/")):
if self.status == self.Status.ISSUED:
return
self.status = self.Status.ISSUED
self.subject_did = did
self.issued_on = datetime.datetime.now().astimezone(pytz.utc)
- self.data = sign_credential(
- self.render(),
- self.issuer_did.key_material
- )
+ issuer_pass = cache.get("KEY_DIDS")
+ # issuer_pass = self.user.decrypt_data(
+ # 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)
issuance_date = ''
if self.issued_on:
format = "%Y-%m-%dT%H:%M:%SZ"
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 = {
- 'vc_id': self.id,
+ 'vc_id': url_id,
'issuer_did': self.issuer_did.did,
'subject_did': self.subject_did and self.subject_did.did or '',
'issuance_date': issuance_date,
- 'first_name': self.user.first_name,
- 'last_name': self.user.last_name,
+ 'firstName': self.user.first_name or "",
+ 'lastName': self.user.last_name or "",
+ 'email': self.user.email,
+ 'organisation': settings.ORGANIZATION or '',
}
context.update(d)
+ context['firstName'] = ""
return context
- def render(self):
- context = self.get_context()
+ def render(self, domain):
+ context = self.get_context(domain)
template_name = 'credentials/{}'.format(
self.schema.file_schema
)
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):
if self.issued_on:
@@ -573,6 +693,18 @@ class VerificableCredential(models.Model):
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):
wkit_template_id = models.CharField(max_length=250)
data = models.TextField()
diff --git a/idhub/static/images/4_Model_Certificat_html_58d7f7eeb828cf29.jpg b/idhub/static/images/4_Model_Certificat_html_58d7f7eeb828cf29.jpg
new file mode 100644
index 0000000..2a285a9
Binary files /dev/null and b/idhub/static/images/4_Model_Certificat_html_58d7f7eeb828cf29.jpg differ
diff --git a/idhub/static/images/4_Model_Certificat_html_7a0214c6fc8f2309.jpg b/idhub/static/images/4_Model_Certificat_html_7a0214c6fc8f2309.jpg
new file mode 100644
index 0000000..9b954dd
Binary files /dev/null and b/idhub/static/images/4_Model_Certificat_html_7a0214c6fc8f2309.jpg differ
diff --git a/idhub/templates/auth/2fadmin.html b/idhub/templates/auth/2fadmin.html
new file mode 100644
index 0000000..4dc2ae6
--- /dev/null
+++ b/idhub/templates/auth/2fadmin.html
@@ -0,0 +1,19 @@
+{% extends "auth/login_base.html" %}
+{% load i18n django_bootstrap5 %}
+
+{% block login_content %}
+
+
+
+
{% trans 'Doble Factor of Authentication' %}
+
+
+
+
+
+
+ {% trans "We have sent an email with a link that you have to select in order to login." %}
+
+
+
+{% endblock %}
diff --git a/idhub/templates/auth/2fadmin_email.html b/idhub/templates/auth/2fadmin_email.html
new file mode 100644
index 0000000..d2253c5
--- /dev/null
+++ b/idhub/templates/auth/2fadmin_email.html
@@ -0,0 +1,26 @@
+{% 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 "Thanks for using our site!" %}
+
+
+
+{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
+
+
+{% endautoescape %}
diff --git a/idhub/templates/auth/2fadmin_email.txt b/idhub/templates/auth/2fadmin_email.txt
new file mode 100644
index 0000000..a9ef3e5
--- /dev/null
+++ b/idhub/templates/auth/2fadmin_email.txt
@@ -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 %}
diff --git a/idhub/templates/auth/2fadmin_email_subject.txt b/idhub/templates/auth/2fadmin_email_subject.txt
new file mode 100644
index 0000000..6d3bb21
--- /dev/null
+++ b/idhub/templates/auth/2fadmin_email_subject.txt
@@ -0,0 +1,3 @@
+{% load i18n %}{% autoescape off %}
+{% blocktrans %}Authentication in {{ site_name }}{% endblocktrans %}
+{% endautoescape %}
\ No newline at end of file
diff --git a/idhub/templates/auth/login.html b/idhub/templates/auth/login.html
index 28c199e..18abfd8 100644
--- a/idhub/templates/auth/login.html
+++ b/idhub/templates/auth/login.html
@@ -4,8 +4,6 @@
{% block login_content %}
+
+
+
+
+
+
+
Here we write the info about GDPR
+
+
+
+
+
+{% endblock %}
diff --git a/idhub/templates/templates/musician/address_check_delete.html b/idhub/templates/templates/musician/address_check_delete.html
deleted file mode 100644
index 27981d4..0000000
--- a/idhub/templates/templates/musician/address_check_delete.html
+++ /dev/null
@@ -1,12 +0,0 @@
-{% extends "musician/base.html" %}
-{% load i18n %}
-
-{% block content %}
-
-{% endblock %}
diff --git a/idhub/templates/templates/musician/address_form.html b/idhub/templates/templates/musician/address_form.html
deleted file mode 100644
index de21067..0000000
--- a/idhub/templates/templates/musician/address_form.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% extends "musician/base.html" %}
-{% load bootstrap4 i18n %}
-
-{% block content %}
-{{ service.verbose_name }}
-
-
-{% endblock %}
diff --git a/idhub/templates/templates/musician/addresses.html b/idhub/templates/templates/musician/addresses.html
deleted file mode 100644
index 1ebc8b7..0000000
--- a/idhub/templates/templates/musician/addresses.html
+++ /dev/null
@@ -1,41 +0,0 @@
-{% extends "musician/mail_base.html" %}
-{% load i18n %}
-
-{% block tabcontent %}
-
-
-
-
-
-
-
-
-
-
- {% trans "Email" %} |
- {% trans "Domain" %} |
- {% trans "Mailboxes" %} |
- {% trans "Forward" %} |
-
-
-
- {% for obj in object_list %}
-
- {{ obj.full_address_name }} |
- {{ obj.domain.name }} |
-
- {% for mailbox in obj.mailboxes %}
- {{ mailbox.name }}
- {% if not forloop.last %} {% endif %}
- {% endfor %}
- |
- {{ obj.forward }} |
-
- {% endfor %}
-
- {% include "musician/components/table_paginator.html" %}
-
-
{% trans "New mail address" %}
-
-
-{% endblock %}
diff --git a/idhub/templates/templates/musician/billing.html b/idhub/templates/templates/musician/billing.html
deleted file mode 100644
index 75f2580..0000000
--- a/idhub/templates/templates/musician/billing.html
+++ /dev/null
@@ -1,41 +0,0 @@
-{% extends "musician/base.html" %}
-{% load i18n l10n %}
-
-{% block content %}
-
-{% trans "Billing" %}
-{% trans "Billing page description." %}
-
-
-
-
-
-
-
-
-
-
-
- {% trans "Number" %} |
- {% trans "Bill date" %} |
- {% trans "Type" %} |
- {% trans "Total" %} |
- {% trans "Download PDF" %} |
-
-
-
- {% for bill in object_list %}
-
- {{ bill.number }} |
- {{ bill.created_on|date:"SHORT_DATE_FORMAT" }} |
- {{ bill.type }} |
- {{ bill.total|floatformat:2|localize }}€ |
- |
-
- {% endfor %}
-
-{# TODO: define proper colspan #}
-{% include "musician/components/table_paginator.html" %}
-
-
-{% endblock %}
diff --git a/idhub/templates/templates/musician/components/paginator.html b/idhub/templates/templates/musician/components/paginator.html
deleted file mode 100644
index 9a7a406..0000000
--- a/idhub/templates/templates/musician/components/paginator.html
+++ /dev/null
@@ -1,29 +0,0 @@
-{# #}
-
-
{{ page_obj.paginator.count }} items in total
-
- {% if page_obj.has_previous %}
-
«
-
‹
- {% endif %}
- Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
- {% if page_obj.has_next %}
-
›
-
»
- {% endif %}
-
-
-
-
-
diff --git a/idhub/templates/templates/musician/components/table_paginator.html b/idhub/templates/templates/musician/components/table_paginator.html
deleted file mode 100644
index 913d5ae..0000000
--- a/idhub/templates/templates/musician/components/table_paginator.html
+++ /dev/null
@@ -1,50 +0,0 @@
-{# #}
-{% load i18n %}
-
-
-
- {{ page_obj.paginator.count }} items in total |
-
-
- |
-
-
- |
-
-
diff --git a/idhub/templates/templates/musician/components/usage_progress_bar.html b/idhub/templates/templates/musician/components/usage_progress_bar.html
deleted file mode 100644
index b35e84c..0000000
--- a/idhub/templates/templates/musician/components/usage_progress_bar.html
+++ /dev/null
@@ -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 %}
-
-
- {% if detail %}
- {{ detail.usage }} {{ detail.unit }}
- {% else %}
- N/A
- {% endif %}
-
-
diff --git a/idhub/templates/templates/musician/dashboard.html b/idhub/templates/templates/musician/dashboard.html
deleted file mode 100644
index d359fcb..0000000
--- a/idhub/templates/templates/musician/dashboard.html
+++ /dev/null
@@ -1,161 +0,0 @@
-{% extends "musician/base.html" %}
-{% load i18n %}
-
-{% block content %}
-
-{% trans "Welcome back" %} {{ profile.username }}
-{% if profile.last_login %}
-{% blocktrans with last_login=profile.last_login|date:"SHORT_DATE_FORMAT" %}Last time you logged in was: {{ last_login }}{% endblocktrans %}
-{% else %}
-{% trans "It's the first time you log into the system, welcome on board!" %}
-{% endif %}
-
-
- {% for resource, usage in resource_usage.items %}
-
-
-
{{ usage.verbose_name }}
- {% include "musician/components/usage_progress_bar.html" with detail=usage.data %}
- {% if usage.data.alert %}
-
- {{ usage.data.alert }}
-
- {% endif %}
-
-
- {% endfor %}
-
-
-
{% trans "Notifications" %}
- {% for message in notifications %}
-
{{ message }}
- {% empty %}
-
{% trans "There is no notifications at this time." %}
- {% endfor %}
-
-
-
-
-
-{% trans "Your domains and websites" %}
-{% trans "Dashboard page description." %}
-
-{% for domain in domains %}
-
-
-
-
-
{% trans "Mail" %}
-
-
- {{ domain.addresses|length }} {% trans "mail addresses created" %}
-
-
-
-
-
{% trans "Mail list" %}
-
-
-
-
-
{% trans "Software as a Service" %}
-
-
{% trans "Nothing installed" %}
-
-
-
-
-
{% trans "Disk usage" %}
-
-
- {% include "musician/components/usage_progress_bar.html" with detail=domain.usage %}
-
-
-
-
-
-
-{% endfor %}
-
-
-
-
-
-
-
-
-
{% trans "FTP access:" %}
- {# Translators: domain configuration detail modal #}
-
{% trans "Contact with the support team to get details concerning FTP access." %}
- {% comment %}
-
-
username
-
password
- {% endcomment %}
-
-
-
{% trans "No website configured." %}
-
- root directory
- type
-
-
-
-
-
-
-
-{% endblock %}
-{% block extrascript %}
-
-{% endblock %}
diff --git a/idhub/templates/templates/musician/databases.html b/idhub/templates/templates/musician/databases.html
deleted file mode 100644
index cf71d1f..0000000
--- a/idhub/templates/templates/musician/databases.html
+++ /dev/null
@@ -1,68 +0,0 @@
-{% extends "musician/base.html" %}
-{% load i18n %}
-
-{% block content %}
-
-{{ service.verbose_name }}
-{{ service.description }}
-
-{% for database in object_list %}
-
-
-
-
-
Database users
-
- {% for user in database.users %}
- {# TODO(@slamora) render in two columns #}
- - {{ user.username }}
- {% empty %}
- - {% trans "No users for this database." %}
- {% endfor %}
-
-
-
-
Database usage
-
- {% include "musician/components/usage_progress_bar.html" with detail=database.usage %}
-
-
-
-
-
-{% empty %}
-
-
-
-
-
- {# Translators: database page when there isn't any database. #}
-
{% trans "Ooops! Looks like there is nothing here!" %}
-
-
-
-
-{% endfor %}
-
- {% if object_list|length > 0 %}
- {% include "musician/components/paginator.html" %}
- {% endif %}
-{% endblock %}
diff --git a/idhub/templates/templates/musician/domain_detail.html b/idhub/templates/templates/musician/domain_detail.html
deleted file mode 100644
index 761c331..0000000
--- a/idhub/templates/templates/musician/domain_detail.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{% extends "musician/base.html" %}
-{% load i18n %}
-
-{% block content %}
-{% trans "Go back" %}
-
-{% trans "DNS settings for" %} {{ object.name }}
-{% trans "DNS settings page description." %}
-
-
-
-
-
-
-
-
- {% trans "Type" %} |
- {% trans "Value" %} |
-
-
-
- {% for record in object.records %}
-
- {{ record.type }} |
- {{ record.value }} |
-
- {% endfor %}
-
-
-{% endblock %}
diff --git a/idhub/templates/templates/musician/mail_base.html b/idhub/templates/templates/musician/mail_base.html
deleted file mode 100644
index 9445f7f..0000000
--- a/idhub/templates/templates/musician/mail_base.html
+++ /dev/null
@@ -1,32 +0,0 @@
-{% extends "musician/base.html" %}
-{% load i18n %}
-
-{% block content %}
-{% if active_domain %}
-{% trans "Go to global" %}
-{% endif %}
-
-{{ service.verbose_name }}
- {% if active_domain %}{% trans "for" %} {{ active_domain.name }}{% endif %}
-
-{{ service.description }}
-
-{% with request.resolver_match.url_name as url_name %}
-
-
-{% endwith %}
-
-
- {% block tabcontent %}
- {% endblock %}
-
-{% endblock %}
diff --git a/idhub/templates/templates/musician/mailbox_change_password.html b/idhub/templates/templates/musician/mailbox_change_password.html
deleted file mode 100644
index e18b95a..0000000
--- a/idhub/templates/templates/musician/mailbox_change_password.html
+++ /dev/null
@@ -1,15 +0,0 @@
-{% extends "musician/base.html" %}
-{% load bootstrap4 i18n %}
-
-{% block content %}
-
{% trans "Change password" %}: {{ object.name }}
-
-
-{% endblock %}
diff --git a/idhub/templates/templates/musician/mailbox_check_delete.html b/idhub/templates/templates/musician/mailbox_check_delete.html
deleted file mode 100644
index 18b9249..0000000
--- a/idhub/templates/templates/musician/mailbox_check_delete.html
+++ /dev/null
@@ -1,15 +0,0 @@
-{% extends "musician/base.html" %}
-{% load i18n %}
-
-{% block content %}
-
-{% endblock %}
diff --git a/idhub/templates/templates/musician/mailbox_form.html b/idhub/templates/templates/musician/mailbox_form.html
deleted file mode 100644
index 5fb9465..0000000
--- a/idhub/templates/templates/musician/mailbox_form.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{% extends "musician/base.html" %}
-{% load bootstrap4 i18n %}
-
-{% block content %}
-
{{ service.verbose_name }}
-
-{% if extra_mailbox %}
-
- {% trans "Warning!" %} {% trans "You have reached the limit of mailboxes of your subscription so extra fees may apply." %}
-
-
-{% endif %}
-
-
-{% endblock %}
diff --git a/idhub/templates/templates/musician/mailboxes.html b/idhub/templates/templates/musician/mailboxes.html
deleted file mode 100644
index a9f3700..0000000
--- a/idhub/templates/templates/musician/mailboxes.html
+++ /dev/null
@@ -1,46 +0,0 @@
-{% extends "musician/mail_base.html" %}
-{% load i18n %}
-
-{% block tabcontent %}
-
-{% endblock %}
diff --git a/idhub/templates/templates/musician/mailinglists.html b/idhub/templates/templates/musician/mailinglists.html
deleted file mode 100644
index 6ff509e..0000000
--- a/idhub/templates/templates/musician/mailinglists.html
+++ /dev/null
@@ -1,46 +0,0 @@
-{% extends "musician/base.html" %}
-{% load i18n %}
-
-{% block content %}
-{% if active_domain %}
-
{% trans "Go to global" %}
-{% endif %}
-
-
{{ service.verbose_name }}{% if active_domain %} {% trans "for" %} {{ active_domain.name }}{% endif %}
-
{{ service.description }}
-
-
-
-
-
-
-
-
-
-
-
- Name |
- Status |
- Address |
- Admin email |
- Configure |
-
-
-
- {% for resource in object_list %}
-
- {{ resource.name }} |
- {% if resource.is_active %}
- {% trans "Active" %} |
- {% else %}
- {% trans "Inactive" %} |
- {% endif %}
- {{ resource.address_name}} |
- {{ resource.admin_email }} |
- Mailtrain |
-
- {% endfor %}
-
- {% include "musician/components/table_paginator.html" %}
-
-{% endblock %}
diff --git a/idhub/templates/templates/musician/profile.html b/idhub/templates/templates/musician/profile.html
deleted file mode 100644
index f6bdbce..0000000
--- a/idhub/templates/templates/musician/profile.html
+++ /dev/null
@@ -1,65 +0,0 @@
-{% extends "musician/base.html" %}
-{% load i18n %}
-
-{% block content %}
-
-
{% trans "Profile" %}
-
{% trans "Little description on profile page." %}
-
-
-
-
-
-
-
-
-
-
-
-
{{ profile.username }}
-
{{ profile.type }}
-
{% trans "Preferred language:" %} {{ profile.language|language_name_local }}
-
- {% comment %}
-
-
- {% endcomment %}
-
-
-
- {% with profile.billing as contact %}
-
-
-
-
{{ contact.name }}
-
{{ contact.address }}
-
- {{ contact.zipcode }}
- {{ contact.city }}
- {{ contact.country }}
-
-
- {{ contact.vat }}
-
-
-
- {% trans "payment method:" %} {{ payment.method }}
-
-
- {% if payment.method == 'SEPADirectDebit' %}
- IBAN {{ payment.data.iban }}
- {% else %}
- {# #}
- Details: {{ payment.data }}
- {% endif %}
-
-
-
-
-
-{% endwith %}
-{% endblock %}
diff --git a/idhub/templates/templates/musician/saas.html b/idhub/templates/templates/musician/saas.html
deleted file mode 100644
index 4da034f..0000000
--- a/idhub/templates/templates/musician/saas.html
+++ /dev/null
@@ -1,56 +0,0 @@
-{% extends "musician/base.html" %}
-{% load i18n %}
-
-{% block content %}
-
-
{{ service.verbose_name }}
-
{{ service.description }}
-
-{% for saas in object_list %}
-
-
-
-
-
{{ saas.service|capfirst }}
-
-
-
-
{% trans "Service info" %}
- {{ saas.is_active|yesno }}
- {% for key, value in saas.data.items %}
- {{ value }}
- {% endfor %}
-
-
-
-
- {% empty %}
-
-
-
-
-
- {# Translators: saas page when there isn't any saas. #}
-
{% trans "Ooops! Looks like there is nothing here!" %}
-
-
-
-
-{% endfor %}
-
-{% endblock %}
diff --git a/idhub/templates/templates/musician/service_list.html b/idhub/templates/templates/musician/service_list.html
deleted file mode 100644
index d413fc8..0000000
--- a/idhub/templates/templates/musician/service_list.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{% extends "musician/base.html" %}
-{% load i18n musician %}
-
-{% block content %}
-
-
{{ service.verbose_name }}
-
{{ service.description }}
-
-
-
-
- {% for field_name in service.fields %}
- {{ field_name }} |
- {% endfor %}
-
-
-
- {% for resource in object_list %}
-
- {% for field_name in service.fields %}
- {{ resource|get_item:field_name }} |
- {% endfor %}
-
- {% endfor %}
-
- {% include "musician/components/table_paginator.html" %}
-
-{% endblock %}
diff --git a/idhub/urls.py b/idhub/urls.py
index d139c32..d107b3f 100644
--- a/idhub/urls.py
+++ b/idhub/urls.py
@@ -17,7 +17,12 @@ Including another URLconf
from django.contrib.auth import views as auth_views
from django.views.generic import RedirectView
from django.urls import path, reverse_lazy
-from .views import LoginView
+from .views import (
+ LoginView,
+ PasswordResetConfirmView,
+ serve_did,
+ DobleFactorSendView,
+)
from .admin import views as views_admin
from .user import views as views_user
# from .verification_portal import views as views_verification_portal
@@ -45,13 +50,16 @@ urlpatterns = [
),
name='password_reset_done'
),
- path('auth/reset/
//',
- auth_views.PasswordResetConfirmView.as_view(
- template_name='auth/password_reset_confirm.html',
- success_url=reverse_lazy('idhub:password_reset_complete')
- ),
+ path('auth/reset///', PasswordResetConfirmView.as_view(),
name='password_reset_confirm'
),
+ # path('auth/reset///',
+ # 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/',
auth_views.PasswordResetCompleteView.as_view(
template_name='auth/password_reset_complete.html'
@@ -80,14 +88,20 @@ urlpatterns = [
name='user_credentials'),
path('user/credentials/', views_user.CredentialView.as_view(),
name='user_credential'),
- path('user/credentials//json', views_user.CredentialJsonView.as_view(),
+ path('user/credentials//pdf', views_user.CredentialPdfView.as_view(),
+ name='user_credential_pdf'),
+ path('credentials//', views_user.CredentialJsonView.as_view(),
name='user_credential_json'),
+ path('public/credentials//', views_user.PublicCredentialJsonView.as_view(),
+ name='public_credential_json'),
path('user/credentials/request/',
views_user.CredentialsRequestView.as_view(),
name='user_credentials_request'),
path('user/credentials_presentation/demand',
views_user.DemandAuthorizationView.as_view(),
name='user_demand_authorization'),
+ path('user/terms/', views_user.TermsAndConditionsView.as_view(),
+ name='user_terms_and_conditions'),
# Admin
path('admin/dashboard/', views_admin.DashboardView.as_view(),
@@ -170,8 +184,15 @@ urlpatterns = [
name='admin_schemas_import_add'),
path('admin/import', views_admin.ImportView.as_view(),
name='admin_import'),
+ path('admin/terms/', views_admin.TermsAndConditionsView.as_view(),
+ name='admin_terms_and_conditions'),
path('admin/import/new', views_admin.ImportAddView.as_view(),
name='admin_import_add'),
+ path('admin/auth/', views_admin.DobleFactorAuthView.as_view(),
+ name='admin_2fauth'),
+ path('admin/auth/2f/', DobleFactorSendView.as_view(), name='confirm_send_2f'),
+
+ path('did-registry//did.json', serve_did)
# path('verification_portal/verify/', views_verification_portal.verify,
# name="verification_portal_verify")
diff --git a/idhub/user/forms.py b/idhub/user/forms.py
index 07b134d..9c68ff3 100644
--- a/idhub/user/forms.py
+++ b/idhub/user/forms.py
@@ -5,18 +5,56 @@ from idhub.models import DID, VerificableCredential
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):
did = forms.ChoiceField(label=_("Did"), choices=[])
credential = forms.ChoiceField(label=_("Credential"), choices=[])
def __init__(self, *args, **kwargs):
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)
self.fields['did'].choices = [
(x.did, x.label) for x in DID.objects.filter(user=self.user)
]
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,
status=VerificableCredential.Status.ENABLED
)
@@ -38,7 +76,8 @@ class RequestCredentialForm(forms.Form):
did = did[0]
cred = cred[0]
try:
- cred.issue(did)
+ if self.password:
+ cred.issue(did, self.password, domain=self._domain)
except Exception:
return
diff --git a/idhub/user/views.py b/idhub/user/views.py
index 07430f5..82c5337 100644
--- a/idhub/user/views.py
+++ b/idhub/user/views.py
@@ -1,6 +1,20 @@
+import os
+import json
+import base64
+import qrcode
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.views.generic import View
from django.views.generic.edit import (
UpdateView,
CreateView,
@@ -20,10 +34,15 @@ from idhub.user.tables import (
DIDTable,
CredentialsTable
)
+from django.core.cache import cache
+from django.conf import settings
from idhub.user.forms import (
- RequestCredentialForm,
- DemandAuthorizationForm
+ ProfileForm,
+ RequestCredentialForm,
+ DemandAuthorizationForm,
+ TermsConditionsForm
)
+from utils import certs
from idhub.mixins import UserView
from idhub.models import DID, VerificableCredential, Event, Membership
from idhub_auth.models import User
@@ -113,6 +132,26 @@ class CredentialsView(MyWallet, SingleTableView):
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):
template_name = "idhub/user/credential.html"
@@ -136,6 +175,147 @@ class CredentialView(MyWallet, TemplateView):
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):
def get(self, request, *args, **kwargs):
@@ -145,6 +325,28 @@ class CredentialJsonView(MyWallet, TemplateView):
pk=pk,
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['Content-Disposition'] = 'attachment; filename={}'.format("credential.json")
return response
@@ -157,9 +359,27 @@ class CredentialsRequestView(MyWallet, FormView):
form_class = RequestCredentialForm
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):
kwargs = super().get_form_kwargs()
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
def form_valid(self, form):
@@ -230,13 +450,17 @@ class DidRegisterView(MyWallet, CreateView):
icon = 'bi bi-patch-check-fill'
wallet = True
model = DID
- fields = ('label',)
+ fields = ('label', 'type')
success_url = reverse_lazy('idhub:user_dids')
object = None
def form_valid(self, form):
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()
messages.success(self.request, _('DID created successfully'))
diff --git a/idhub/verification_portal/__init__.py b/idhub/verification_portal/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/idhub/verification_portal/models.py b/idhub/verification_portal/models.py
deleted file mode 100644
index 0bd203a..0000000
--- a/idhub/verification_portal/models.py
+++ /dev/null
@@ -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 with and "
- 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)
diff --git a/idhub/verification_portal/views.py b/idhub/verification_portal/views.py
deleted file mode 100644
index 486f4f7..0000000
--- a/idhub/verification_portal/views.py
+++ /dev/null
@@ -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)
-
diff --git a/idhub/views.py b/idhub/views.py
index 53db736..7a525b1 100644
--- a/idhub/views.py
+++ b/idhub/views.py
@@ -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.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 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):
@@ -13,16 +24,76 @@ class LoginView(auth_views.LoginView):
}
def get(self, request, *args, **kwargs):
- if request.GET.get('next'):
- self.extra_context['success_url'] = request.GET.get('next')
+ self.extra_context['success_url'] = request.GET.get(
+ 'next',
+ reverse_lazy('idhub:user_dashboard')
+ )
return super().get(request, *args, **kwargs)
def form_valid(self, form):
user = form.get_user()
- if not user.is_anonymous and user.is_admin:
- 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
+ password = form.cleaned_data.get("password")
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'])
+
+
+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)
+
+
diff --git a/idhub_auth/forms.py b/idhub_auth/forms.py
index f4279b7..d9ff2f7 100644
--- a/idhub_auth/forms.py
+++ b/idhub_auth/forms.py
@@ -31,4 +31,3 @@ class ProfileForm(forms.ModelForm):
return last_name
-
diff --git a/idhub_auth/migrations/0001_initial.py b/idhub_auth/migrations/0001_initial.py
deleted file mode 100644
index bd9b3eb..0000000
--- a/idhub_auth/migrations/0001_initial.py
+++ /dev/null
@@ -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,
- },
- ),
- ]
diff --git a/idhub_auth/models.py b/idhub_auth/models.py
index 07a7896..e89bc35 100644
--- a/idhub_auth/models.py
+++ b/idhub_auth/models.py
@@ -1,4 +1,9 @@
+import nacl
+import base64
+
+from nacl import pwhash
from django.db import models
+from django.core.cache import cache
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser
@@ -44,6 +49,9 @@ class User(AbstractBaseUser):
is_admin = models.BooleanField(default=False)
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)
+ encrypted_sensitive_data = models.CharField(max_length=255)
+ salt = models.CharField(max_length=255)
+ accept_gdpr = models.BooleanField(default=False)
objects = UserManager()
@@ -86,3 +94,64 @@ class User(AbstractBaseUser):
for r in s.service.rol.all():
roles.append(r.name)
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)
diff --git a/oidc4vp/forms.py b/oidc4vp/forms.py
index b3c4c9e..d40c6a1 100644
--- a/oidc4vp/forms.py
+++ b/oidc4vp/forms.py
@@ -19,7 +19,9 @@ class AuthorizeForm(forms.Form):
self.user = kwargs.pop('user', None)
self.org = kwargs.pop('org', None)
self.code = kwargs.pop('code', None)
+ self.pw = kwargs.pop('pw', None)
self.presentation_definition = kwargs.pop('presentation_definition', [])
+ self.subject_did = None
reg = r'({})'.format('|'.join(self.presentation_definition))
@@ -49,7 +51,12 @@ class AuthorizeForm(forms.Form):
txt = _('There are some problems with this credentials')
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:
txt = _("There isn't code in request")
@@ -69,13 +76,14 @@ class AuthorizeForm(forms.Form):
return
def get_verificable_presentation(self):
- did = self.list_credentials[0].subject_did
+ did = self.subject_did
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 = {
"holder_did": did.did,
"verifiable_credential_list": vc_list
}
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)
diff --git a/oidc4vp/migrations/0001_initial.py b/oidc4vp/migrations/0001_initial.py
deleted file mode 100644
index 2f67109..0000000
--- a/oidc4vp/migrations/0001_initial.py
+++ /dev/null
@@ -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,
- ),
- ),
- ]
diff --git a/oidc4vp/models.py b/oidc4vp/models.py
index 66ceff1..42b0d93 100644
--- a/oidc4vp/models.py
+++ b/oidc4vp/models.py
@@ -193,10 +193,15 @@ class OAuth2VPToken(models.Model):
return response
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
def get_redirect_url(self):
+ if not settings.ALLOW_CODE_URI:
+ return
+
data = {
"code": self.authorization.code,
}
diff --git a/oidc4vp/templates/credentials_presentation.html b/oidc4vp/templates/credentials_presentation.html
index 63f54ba..41629c8 100644
--- a/oidc4vp/templates/credentials_presentation.html
+++ b/oidc4vp/templates/credentials_presentation.html
@@ -87,7 +87,7 @@
- {% trans 'Are you sure that you want delete this user?' %}
+ {% trans 'Are you sure that you want share the info of this credentials?' %}
diff --git a/oidc4vp/views.py b/oidc4vp/views.py
index a62e462..d6eec81 100644
--- a/oidc4vp/views.py
+++ b/oidc4vp/views.py
@@ -13,6 +13,7 @@ from django.contrib import messages
from oidc4vp.models import Authorization, Organization, OAuth2VPToken
from idhub.mixins import UserView
+from idhub.models import Event
from oidc4vp.forms import AuthorizeForm
from utils.idhub_ssikit import verify_presentation
@@ -43,6 +44,11 @@ class AuthorizeView(UserView, FormView):
kwargs['presentation_definition'] = vps
kwargs["org"] = self.get_org()
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
def get_form(self, form_class=None):
@@ -55,12 +61,12 @@ class AuthorizeView(UserView, FormView):
authorization = form.save()
if not authorization or authorization.status_code != 200:
messages.error(self.request, _("Error sending credential!"))
- return super().form_valid(form)
+ return redirect(self.success_url)
try:
authorization = authorization.json()
except:
messages.error(self.request, _("Error sending credential!"))
- return super().form_valid(form)
+ return redirect(self.success_url)
verify = authorization.get('verify')
result, msg = verify.split(",")
@@ -69,13 +75,22 @@ class AuthorizeView(UserView, FormView):
if 'ok' in result.lower():
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'):
return redirect(authorization.get('redirect_uri'))
elif authorization.get('response'):
txt = authorization.get('response')
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):
client_id = self.request.GET.get("client_id")
@@ -123,7 +138,6 @@ class VerifyView(View):
response = vp_token.get_response_verify()
vp_token.save()
if not vp_token.authorization.promotions.exists():
- response["redirect_uri"] = ""
response["response"] = "Validation Code {}".format(code)
return JsonResponse(response)
@@ -157,9 +171,10 @@ class AllowCodeView(View):
code=code,
code_used=False
)
- if not self.authorization.promotions.exists():
+
+ promotion = self.authorization.promotions.first()
+ if not promotion:
raise Http404("Page not Found!")
- promotion = self.authorization.promotions.all()[0]
return redirect(promotion.get_url(code))
diff --git a/promotion/migrations/0001_initial.py b/promotion/migrations/0001_initial.py
deleted file mode 100644
index b994f2f..0000000
--- a/promotion/migrations/0001_initial.py
+++ /dev/null
@@ -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',
- ),
- ),
- ],
- ),
- ]
diff --git a/requirements.txt b/requirements.txt
index ae069eb..27dd8c6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,11 +6,25 @@ black==23.9.1
python-decouple==3.8
jsonschema==4.19.1
pandas==2.1.1
+xlrd==2.0.1
+odfpy==1.4.1
requests==2.31.0
-didkit==0.3.2
jinja2==3.1.2
jsonref==1.1.0
pyld==2.0.3
+pynacl==1.5.0
more-itertools==10.1.0
dj-database-url==2.1.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
diff --git a/schemas/course-credential.json b/schemas/course-credential.json
new file mode 100644
index 0000000..eae8f21
--- /dev/null
+++ b/schemas/course-credential.json
@@ -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"
+ ]
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/schemas/device-purchase.json b/schemas/device-purchase.json
new file mode 100644
index 0000000..6267ae9
--- /dev/null
+++ b/schemas/device-purchase.json
@@ -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"
+ ]
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/schemas/federation-membership.json b/schemas/federation-membership.json
new file mode 100644
index 0000000..b1f5a9d
--- /dev/null
+++ b/schemas/federation-membership.json
@@ -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"
+ ]
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/schemas/financial-vulnerability.json b/schemas/financial-vulnerability.json
new file mode 100644
index 0000000..e1960d5
--- /dev/null
+++ b/schemas/financial-vulnerability.json
@@ -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"
+ ]
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/schemas/membership-card.json b/schemas/membership-card.json
index 0d5ff9a..20a5f9b 100644
--- a/schemas/membership-card.json
+++ b/schemas/membership-card.json
@@ -1,65 +1,94 @@
{
- "$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",
- "name": "MembershipCard",
- "description": "MembershipCard credential using JsonSchema",
- "type": "object",
- "properties": {
- "credentialSubject": {
- "type": "object",
- "properties": {
- "organisation": {
- "type": "string"
- },
- "membershipType": {
- "type": "string"
- },
- "affiliatedSince": {
- "type": "string",
- "format": "date-time"
- },
- "affiliatedUntil": {
- "type": "string",
- "format": "date-time"
- },
- "typeOfPerson": {
- "type": "string",
- "enum": [
- "individual",
- "org"
- ]
- },
- "identityDocType": {
- "type": "string",
- "enum": [
- "DNI",
- "NIF",
- "NIE",
- "PASSPORT"
- ]
- },
- "identityNumber": {
- "type": "string"
- },
- "name": {
- "type": "string"
- },
- "surnames": {
- "type": "string"
- },
- "email": {
- "type": "string",
- "format": "email"
- }
- },
- "required": [
- "organisation",
- "affiliatedSince",
- "typeOfPerson",
- "name",
- "surnames",
- "email"
- ]
+ "title": "Membership Card",
+ "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",
+ "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"
+ },
+ "organisation": {
+ "description": "Organisation the credential subject is affiliated with",
+ "type": "string"
+ },
+ "membershipType": {
+ "description": "Type of membership",
+ "type": "string"
+ },
+ "membershipId": {
+ "description": "Membership identifier",
+ "type": "string"
+ },
+ "affiliatedSince": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "affiliatedUntil": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "typeOfPerson": {
+ "type": "string",
+ "enum": [
+ "natural",
+ "legal"
+ ]
+ },
+ "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"
+ },
+ "firstName": {
+ "description": "Name of the natural person or name of the legal person (organisation)",
+ "type": "string"
+ },
+ "lastName": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string",
+ "format": "email"
+ }
+ },
+ "required": [
+ "id",
+ "organisation",
+ "affiliatedSince",
+ "typeOfPerson",
+ "firstName",
+ "email"
+ ]
+ }
+ }
+ }
+ ]
}
\ No newline at end of file
diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py
index 290ff63..1a3675d 100644
--- a/trustchain_idhub/settings.py
+++ b/trustchain_idhub/settings.py
@@ -149,6 +149,7 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
+SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
@@ -222,3 +223,4 @@ LOGGING = {
}
}
+ORGANIZATION = config('ORGANIZATION', 'Pangea')
diff --git a/urls_provisional b/urls_provisional
deleted file mode 100644
index 43f0677..0000000
--- a/urls_provisional
+++ /dev/null
@@ -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/ [GET, DELETE]
-/user/credentials [GET]
-/user/credentials/ [GET, DELETE]
-/user/credentials/request [GET, POST]
- *** falta "present credentials" ??? ***
-
-
-/admin/
diff --git a/utils/certs.py b/utils/certs.py
new file mode 100644
index 0000000..ff2c241
--- /dev/null
+++ b/utils/certs.py
@@ -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,
+ )
diff --git a/utils/idhub_ssikit/__init__.py b/utils/idhub_ssikit/__init__.py
index cc3e9b4..85e6e2f 100644
--- a/utils/idhub_ssikit/__init__.py
+++ b/utils/idhub_ssikit/__init__.py
@@ -2,10 +2,13 @@ import asyncio
import datetime
import didkit
import json
+import urllib
import jinja2
from django.template.backends.django import Template
from django.template.loader import get_template
+from trustchain_idhub import settings
+
def generate_did_controller_key():
return didkit.generate_ed25519_key()
@@ -15,6 +18,31 @@ def keydid_from_controller_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():
# TODO agree on a system for Verifiable Credential IDs
return "https://pangea.org/credentials/42"