resolve conflict step1
This commit is contained in:
commit
f39c915c9c
|
@ -1,2 +0,0 @@
|
|||
name email membershipType
|
||||
Pepe user1@example.org individual
|
|
|
@ -1,2 +0,0 @@
|
|||
name surnames email typeOfPerson membershipType organisation affiliatedSince
|
||||
Pepe Gómez user1@example.org individual Member Pangea 01-01-2023
|
|
BIN
examples/membership-card.ods
Normal file
BIN
examples/membership-card.ods
Normal file
Binary file not shown.
BIN
examples/membership-card.xls
Normal file
BIN
examples/membership-card.xls
Normal file
Binary file not shown.
BIN
examples/signerDNIe004.pfx
Normal file
BIN
examples/signerDNIe004.pfx
Normal file
Binary file not shown.
|
@ -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')
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 = ''
|
||||
|
|
|
@ -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')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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()
|
||||
|
||||
|
|
166
idhub/models.py
166
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()
|
||||
|
|
BIN
idhub/static/images/4_Model_Certificat_html_58d7f7eeb828cf29.jpg
Normal file
BIN
idhub/static/images/4_Model_Certificat_html_58d7f7eeb828cf29.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
idhub/static/images/4_Model_Certificat_html_7a0214c6fc8f2309.jpg
Normal file
BIN
idhub/static/images/4_Model_Certificat_html_7a0214c6fc8f2309.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
19
idhub/templates/auth/2fadmin.html
Normal file
19
idhub/templates/auth/2fadmin.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
{% extends "auth/login_base.html" %}
|
||||
{% load i18n django_bootstrap5 %}
|
||||
|
||||
{% block login_content %}
|
||||
|
||||
<div class="well">
|
||||
<div class="row-fluid">
|
||||
<h2>{% trans 'Doble Factor of Authentication' %}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="well">
|
||||
<div class="row-fluid">
|
||||
<div>
|
||||
<span>{% trans "We have sent an email with a link that you have to select in order to login." %}</span>
|
||||
</div>
|
||||
</div><!-- /.row-fluid -->
|
||||
</div><!--/.well-->
|
||||
{% endblock %}
|
26
idhub/templates/auth/2fadmin_email.html
Normal file
26
idhub/templates/auth/2fadmin_email.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
{% load i18n %}{% autoescape off %}
|
||||
<p>
|
||||
{% blocktrans %}You're receiving this email because you try to access in {{ site_name }}.{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{% trans "Please go to the following page" %}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{% block reset_link %}
|
||||
<a href="{{ protocol }}://{{ domain }}{% url 'idhub:admin_2fauth' admin2fauth=token %}">
|
||||
{{ protocol }}://{{ domain }}{% url 'idhub:admin_2fauth' admin2fauth=token %}
|
||||
</a>
|
||||
{% endblock %}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{% trans "Thanks for using our site!" %}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
{% endautoescape %}
|
14
idhub/templates/auth/2fadmin_email.txt
Normal file
14
idhub/templates/auth/2fadmin_email.txt
Normal file
|
@ -0,0 +1,14 @@
|
|||
{% load i18n %}{% autoescape off %}
|
||||
{% blocktrans %}You're receiving this email because you try to access in {{ site_name }}.{% endblocktrans %}
|
||||
|
||||
{% trans "Please go to the following page" %}
|
||||
{% block reset_link %}
|
||||
{{ protocol }}://{{ domain }}{% url 'idhub:admin_2fauth' admin2fauth=token %}
|
||||
{% endblock %}
|
||||
{% trans "Your username, in case you've forgotten:" %} {{ user.username }}
|
||||
|
||||
{% trans "Thanks for using our site!" %}
|
||||
|
||||
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
|
||||
|
||||
{% endautoescape %}
|
3
idhub/templates/auth/2fadmin_email_subject.txt
Normal file
3
idhub/templates/auth/2fadmin_email_subject.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
{% load i18n %}{% autoescape off %}
|
||||
{% blocktrans %}Authentication in {{ site_name }}{% endblocktrans %}
|
||||
{% endautoescape %}
|
|
@ -4,8 +4,6 @@
|
|||
{% block login_content %}
|
||||
<form action="{% url 'idhub:login' %}" role="form" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
|
||||
<div id="div_id_username"
|
||||
class="clearfix control-group {% if form.username.errors %}error{% endif %}">
|
||||
<div class="form-group">
|
||||
|
@ -48,7 +46,7 @@
|
|||
class="btn btn-primary form-control" id="submit-id-submit">
|
||||
</div>
|
||||
</form>
|
||||
<div id="login-footer">
|
||||
<div id="login-footer" class="mt-3">
|
||||
<a href="{% url 'idhub:password_reset' %}" data-toggle="modal" data-target="#forgotPasswordModal">{% trans "Forgot your password? Click here to recover" %}</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
111
idhub/templates/certificates/4_Model_Certificat.html
Normal file
111
idhub/templates/certificates/4_Model_Certificat.html
Normal file
|
@ -0,0 +1,111 @@
|
|||
{% load i18n static %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Certificado</title>
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="content-type" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
<link rel="stylesheet" href= "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
|
||||
<link href="{% static "/css/bootstrap.min.css" %}" rel="stylesheet">
|
||||
<style type="text/css" media="all">
|
||||
@page {
|
||||
size: A4 portrait; /* can use also 'landscape' for orientation */
|
||||
margin: 1.0cm 1.5cm 3.5cm 1.5cm;
|
||||
font-family: "Source Sans Pro", Calibri, Candra, Sans serif;
|
||||
|
||||
@top {
|
||||
content: element(header);
|
||||
}
|
||||
|
||||
@bottom {
|
||||
content: element(footer);
|
||||
}
|
||||
|
||||
}
|
||||
body {
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
color: black;
|
||||
font-size: 100%;
|
||||
line-height: 1.65;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
header {
|
||||
position: running(header);
|
||||
/*height: 100px;*/
|
||||
font-size: 12px;
|
||||
/* color: #000; */
|
||||
font-family: Arial;
|
||||
width: 100%;
|
||||
/* position: relative;*/
|
||||
}
|
||||
|
||||
footer {
|
||||
position: running(footer);
|
||||
/*height: 150px;*/
|
||||
}
|
||||
|
||||
.body_content {
|
||||
position: relative;
|
||||
page-break-inside: auto;
|
||||
width: 100%;
|
||||
/*overflow: hidden;*/
|
||||
}
|
||||
|
||||
img {max-height: 150px; width: auto;}
|
||||
.company-logo {float: left;}
|
||||
.customer-logo {float: right;}
|
||||
.page-break:not(section:first-of-type) {
|
||||
page-break-before: always
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container body-content">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<img style="width: 100%; height: auto;" src="data:image/jpeg;base64,{{ image_header }}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-12 text-center">
|
||||
<span style="color: #ea5e0f;">LAFEDE.CAT – ORGANITZACIONS PER A LA JUSTÍCIA GLOBAL</span><br />CERTIFICA QUE:<br />
|
||||
{{ first_name }} {{ last_name }} amb DNI {{ document_id }}<br/>
|
||||
Ha realitzat el curs {{ course }}, a {{ address }} / de manera virtual/presencial, els dies {{ date_course }}<br />
|
||||
La durada del curs ha estat de {{ n_hours }} hores lectives corresponents a {{ n_lections }} sessions.<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
I per deixar-ne constància als efectes oportuns, signo el present certificat en data de {{ issue_date }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="padding-top: 20px;">
|
||||
<div class="col-12">
|
||||
<img style="width: 129px; height: 88px;" src="data:image/jpeg;base64,{{ image_signature }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="padding-top: 20px;">
|
||||
<div class="col-12">
|
||||
Pepa Martínez Peyrats<br />
|
||||
Directora<br />
|
||||
Lafede.cat - Federació d'Organitzacions per a la Justícia Global
|
||||
</div>
|
||||
</div>
|
||||
{% if qr %}
|
||||
<div class="row" style="padding-top: 20px;">
|
||||
<div class="col-12">
|
||||
<img style="width: 129px; height: 129px;" src="data:image/jpeg;base64,{{ qr }}" />
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
83
idhub/templates/credentials/course-credential.json
Normal file
83
idhub/templates/credentials/course-credential.json
Normal file
|
@ -0,0 +1,83 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/2018/credentials/v1",
|
||||
"https://idhub.pangea.org/credentials/base/v1",
|
||||
{
|
||||
"firstName": "https://idhub.pangea.org/context/#firstName",
|
||||
"lastName": "https://idhub.pangea.org/context/#lastName",
|
||||
"personalIdentifier": "https://idhub.pangea.org/context/#personalIdentifier",
|
||||
"issuedDate": "https://idhub.pangea.org/context/#issuedDate",
|
||||
"modeOfInstruction": "https://idhub.pangea.org/context/#modeOfInstruction",
|
||||
"courseDuration": "https://idhub.pangea.org/context/#courseDuration",
|
||||
"courseDays": "https://idhub.pangea.org/context/#courseDays",
|
||||
"courseName": "https://idhub.pangea.org/context/#courseName",
|
||||
"courseDescription": "https://idhub.pangea.org/context/#courseDescription",
|
||||
"gradingScheme": "https://idhub.pangea.org/context/#gradingScheme",
|
||||
"scoreAwarded": "https://idhub.pangea.org/context/#scoreAwarded",
|
||||
"qualificationAwarded": "https://idhub.pangea.org/context/#qualificationAwarded",
|
||||
"courseLevel": "https://idhub.pangea.org/context/#courseLevel",
|
||||
"courseFramework": "https://idhub.pangea.org/context/#courseFramework",
|
||||
"courseCredits": "https://idhub.pangea.org/context/#courseCredits",
|
||||
"dateOfAssessment": "https://idhub.pangea.org/context/#dateOfAssessment",
|
||||
"evidenceAssessment": "https://idhub.pangea.org/context/#evidenceAssessment"
|
||||
}
|
||||
],
|
||||
"id": "{{ vc_id }}",
|
||||
"type": [
|
||||
"VerifiableCredential",
|
||||
"VerifiableAttestation",
|
||||
"CourseCredential"
|
||||
],
|
||||
"issuer": {
|
||||
"id": "{{ issuer_did }}",
|
||||
"name": "{{ organisation }}"
|
||||
},
|
||||
"issuanceDate": "{{ issuance_date }}",
|
||||
"validFrom": "{{ issuance_date }}",
|
||||
"validUntil": "{{ validUntil }}",
|
||||
"name": [
|
||||
{
|
||||
"value": "NGO Course Credential for participants",
|
||||
"lang": "en"
|
||||
},
|
||||
{
|
||||
"value": "Credencial per participants d'un curs impartit per una ONG",
|
||||
"lang": "ca_ES"
|
||||
},
|
||||
{
|
||||
"value": "Credencial para participantes de un curso impartido por una ONG",
|
||||
"lang": "es"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"value": "A NGO Course Credential Schema awarded by a NGO federation and their NGO members, as proposed by Lafede.cat",
|
||||
"lang": "en"
|
||||
}
|
||||
],
|
||||
"credentialSubject": {
|
||||
"id": "{{ subject_did }}",
|
||||
"firstName": "{{ firstName }}",
|
||||
"lastName": "{{ lastName }}",
|
||||
"personalIdentifier": "{{ personalIdentifier }}",
|
||||
"issuedDate": "{{ issuedDate }}",
|
||||
"modeOfInstruction": "{{ modeOfInstruction }}",
|
||||
"courseDuration": "{{ courseDuration }}",
|
||||
"courseDays": "{{ courseDays }}",
|
||||
"courseName": "{{ courseName }}",
|
||||
"courseDescription": "{{ courseDescription }}",
|
||||
"gradingScheme": "{{ gradingScheme }}",
|
||||
"scoreAwarded": "{{ scoreAwarded }}",
|
||||
"qualificationAwarded": "{{ qualificationAwarded }}",
|
||||
"courseLevel": "{{ courseLevel }}",
|
||||
"courseFramework": "{{ courseFramework }}",
|
||||
"courseCredits": "{{ courseCredits }}",
|
||||
"dateOfAssessment": "{{ dateOfAssessment }}",
|
||||
"evidenceAssessment": "{{ evidenceAssessment }}"
|
||||
},
|
||||
"credentialSchema": {
|
||||
"id": "https://idhub.pangea.org/vc_schemas/course-credential.json",
|
||||
"type": "FullJsonSchemaValidator2021"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/2018/credentials/v1",
|
||||
{
|
||||
"name": "https://schema.org/name",
|
||||
"email": "https://schema.org/email",
|
||||
"membershipType": "https://schema.org/memberOf",
|
||||
"individual": "https://schema.org/Person",
|
||||
"organization": "https://schema.org/Organization",
|
||||
"Member": "https://schema.org/Member",
|
||||
"startDate": "https://schema.org/startDate",
|
||||
"jsonSchema": "https://schema.org/jsonSchema",
|
||||
"street_address": "https://schema.org/streetAddress",
|
||||
"connectivity_option_list": "https://schema.org/connectivityOptionList",
|
||||
"$ref": "https://schema.org/jsonSchemaRef"
|
||||
}
|
||||
],
|
||||
"id": "{{ vc_id }}",
|
||||
"type": ["VerifiableCredential", "HomeConnectivitySurveyCredential"],
|
||||
"issuer": "{{ issuer_did }}",
|
||||
"issuanceDate": "{{ issuance_date }}",
|
||||
"credentialSubject": {
|
||||
"id": "{{ subject_did }}",
|
||||
"street_address": "{{ street_address }}",
|
||||
"connectivity_option_list": "{{ connectivity_option_list }}",
|
||||
"jsonSchema": {
|
||||
"$ref": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/UNDEF.json"
|
||||
}
|
||||
}
|
||||
}
|
92
idhub/templates/credentials/federation-membership.json
Normal file
92
idhub/templates/credentials/federation-membership.json
Normal file
|
@ -0,0 +1,92 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/2018/credentials/v1",
|
||||
"https://idhub.pangea.org/credentials/base/v1",
|
||||
{
|
||||
"federation": "https://idhub.pangea.org/context/#federation",
|
||||
"legalName": "https://idhub.pangea.org/context/#legalName",
|
||||
"shortName": "https://idhub.pangea.org/context/#shortName",
|
||||
"registrationIdentifier": "https://idhub.pangea.org/context/#registrationIdentifier",
|
||||
"publicRegistry": "https://idhub.pangea.org/context/#publicRegistry",
|
||||
"streetAddress": "https://idhub.pangea.org/context/#streetAddress",
|
||||
"postCode": "https://idhub.pangea.org/context/#postCode",
|
||||
"city": "https://idhub.pangea.org/context/#city",
|
||||
"taxReference": "https://idhub.pangea.org/context/#taxReference",
|
||||
"membershipType": "https://idhub.pangea.org/context/#membershipType",
|
||||
"membershipStatus": "https://idhub.pangea.org/context/#membershipStatus",
|
||||
"membershipId": "https://idhub.pangea.org/context/#membershipId",
|
||||
"membershipSince": "https://idhub.pangea.org/context/#membershipSince",
|
||||
"email": "https://idhub.pangea.org/context/#email",
|
||||
"phone": "https://idhub.pangea.org/context/#phone",
|
||||
"website": "https://idhub.pangea.org/context/#website",
|
||||
"evidence": "https://idhub.pangea.org/context/#evidence",
|
||||
"certificationDate": "https://idhub.pangea.org/context/#certificationDate"
|
||||
}
|
||||
],
|
||||
"id": "{{ vc_id }}",
|
||||
"type": [
|
||||
"VerifiableCredential",
|
||||
"VerifiableAttestation",
|
||||
"FederationMembership"
|
||||
],
|
||||
"issuer": {
|
||||
"id": "{{ issuer_did }}",
|
||||
"name": "{{ organisation }}"
|
||||
},
|
||||
"issuanceDate": "{{ issuance_date }}",
|
||||
"validFrom": "{{ issuance_date }}",
|
||||
"validUntil": "{{ validUntil }}",
|
||||
"name": [
|
||||
{
|
||||
"value": "NGO federation membership attestation credential",
|
||||
"lang": "en"
|
||||
},
|
||||
{
|
||||
"value": "Credencial d'atestat de pertinença a federació d'ONG",
|
||||
"lang": "ca_ES"
|
||||
},
|
||||
{
|
||||
"value": "Credencial de atestado de membresía de Federación de ONG",
|
||||
"lang": "es"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"value": "Credential for NGOs that are members of a NGO federation",
|
||||
"lang": "en"
|
||||
},
|
||||
{
|
||||
"value": "Credencial para ONG que son miembros de una federación de ONG",
|
||||
"lang": "es"
|
||||
},
|
||||
{
|
||||
"value": "Credencial per a les ONG que són membres d'una federació d'ONG",
|
||||
"lang": "ca_ES"
|
||||
}
|
||||
],
|
||||
"credentialSubject": {
|
||||
"id": "{{ subject_did }}",
|
||||
"federation": "{{ federation }}",
|
||||
"legalName": "{{ legalName }}",
|
||||
"shortName": "{{ shortName }}",
|
||||
"registrationIdentifier": "{{ registrationIdentifier }}",
|
||||
"publicRegistry": "{{ publicRegistry }}",
|
||||
"streetAddress": "{{ streetAddress }}",
|
||||
"postCode": "{{ postCode }}",
|
||||
"city": "{{ city }}",
|
||||
"taxReference": "{{ taxReference }}",
|
||||
"membershipType": "{{ membershipType }}",
|
||||
"membershipStatus": "{{ membershipStatus }}",
|
||||
"membershipId": "{{ membershipId }}",
|
||||
"membershipSince": "{{ membershipSince }}",
|
||||
"email": "{{ email }}",
|
||||
"phone": "{{ phone }}",
|
||||
"website": "{{ website }}",
|
||||
"evidence": "{{ evidence }}",
|
||||
"certificationDate": "{{ certificationDate }}"
|
||||
},
|
||||
"credentialSchema": {
|
||||
"id": "https://idhub.pangea.org/vc_schemas/federation-membership.json",
|
||||
"type": "FullJsonSchemaValidator2021"
|
||||
}
|
||||
}
|
69
idhub/templates/credentials/financial-vulnerability.json
Normal file
69
idhub/templates/credentials/financial-vulnerability.json
Normal file
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/2018/credentials/v1",
|
||||
"https://idhub.pangea.org/credentials/base/v1",
|
||||
"https://idhub.pangea.org/credentials/financial-vulnerability/v1"
|
||||
],
|
||||
"id": "{{ vc_id }}",
|
||||
"type": [
|
||||
"VerifiableCredential",
|
||||
"VerifiableAttestation",
|
||||
"FinancialVulnerabilityCredential"
|
||||
],
|
||||
"issuer": {
|
||||
"id": "{{ issuer_did }}",
|
||||
"name": "{{ organisation }}"
|
||||
},
|
||||
"issuanceDate": "{{ issuance_date }}",
|
||||
"issued": "{{ issuance_date }}",
|
||||
"validFrom": "{{ issuance_date }}",
|
||||
"validUntil": "{{ validUntil }}",
|
||||
"name": [
|
||||
{
|
||||
"value": "Financial Vulnerability Credential",
|
||||
"lang": "en"
|
||||
},
|
||||
{
|
||||
"value": "Credencial de Vulnerabilitat Financera",
|
||||
"lang": "ca_ES"
|
||||
},
|
||||
{
|
||||
"value": "Credencial de Vulnerabilidad Financiera",
|
||||
"lang": "es"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"value": "The Financial Vulnerability Credential is issued to individuals or families to prove their financial vulnerability based on various factors, with the objective of presenting it to a third party to receive benefits or services.",
|
||||
"lang": "en"
|
||||
},
|
||||
{
|
||||
"value": "La Credencial de Vulnerabilitat Financera és emesa a persones o famílies per acreditar la seva vulnerabilitat financera sobre la base de diversos factors, amb l'objectiu que la presentin a una tercera part per rebre beneficis o serveis.",
|
||||
"lang": "ca_ES"
|
||||
},
|
||||
{
|
||||
"value": "La Credencial de Vulnerabilidad Financiera es emitida a personas o familias para acreditar su vulnerabilidad financiera con base en diversos factores, con el objetivo de que la presenten a una tercera parte para recibir beneficios o servicios.",
|
||||
"lang": "es"
|
||||
}
|
||||
],
|
||||
"credentialSubject": {
|
||||
"id": "{{ subject_did }}",
|
||||
"firstName": "{{ firstName }}",
|
||||
"lastName": "{{ lastName }}",
|
||||
"email": "{{ email }}",
|
||||
"identityDocType": "{{ identityDocType }}",
|
||||
"identityNumber": "{{ identityNumber }}",
|
||||
"phoneNumber": "{{ phoneNumber }}",
|
||||
"streetAddress": "{{ streetAddress }}",
|
||||
"socialWorkerName": "{{ socialWorkerName }}",
|
||||
"socialWorkerSurname": "{{ socialWorkerSurname }}",
|
||||
"financialVulnerabilityScore": "{{ financialVulnerabilityScore }}",
|
||||
"amountCoveredByOtherAids": "{{ amountCoveredByOtherAids }}",
|
||||
"connectivityOptionList": "{{ connectivityOptionList }}",
|
||||
"assessmentDate": "{{ assessmentDate }}"
|
||||
},
|
||||
"credentialSchema": {
|
||||
"id": "https://idhub.pangea.org/vc_schemas/financial-vulnerability.json",
|
||||
"type": "FullJsonSchemaValidator2021"
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/2018/credentials/v1",
|
||||
{
|
||||
"name": "https://schema.org/name",
|
||||
"email": "https://schema.org/email",
|
||||
"membershipType": "https://schema.org/memberOf",
|
||||
"individual": "https://schema.org/Person",
|
||||
"organization": "https://schema.org/Organization",
|
||||
"Member": "https://schema.org/Member",
|
||||
"startDate": "https://schema.org/startDate",
|
||||
"jsonSchema": "https://schema.org/jsonSchema",
|
||||
"$ref": "https://schema.org/jsonSchemaRef"
|
||||
}
|
||||
],
|
||||
"type": ["VerifiableCredential", "VerifiableAttestation"],
|
||||
"id": "{{ vc_id }}",
|
||||
"issuer": "{{ issuer_did }}",
|
||||
"issuanceDate": "{{ issuance_date }}",
|
||||
"credentialSubject": {
|
||||
"id": "{{ subject_did }}",
|
||||
"Member": {
|
||||
"name": "{{ name }}",
|
||||
"email": "{{ email }}",
|
||||
"membershipType": "{{ membershipType }}",
|
||||
"startDate": "{{ startDate }}"
|
||||
},
|
||||
"jsonSchema": {
|
||||
"$ref": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/member-schema.json"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,31 +1,8 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/2018/credentials/v1",
|
||||
{
|
||||
"individual": "https://schema.org/Person",
|
||||
"Member": "https://schema.org/Member",
|
||||
"startDate": "https://schema.org/startDate",
|
||||
"jsonSchema": "https://schema.org/jsonSchema",
|
||||
"$ref": "https://schema.org/jsonSchemaRef",
|
||||
"credentialSchema": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#credentialSchema",
|
||||
"organisation": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#organisation",
|
||||
"membershipType": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#membershipType",
|
||||
"membershipId": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#membershipId",
|
||||
"typeOfPerson": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#typeOfPerson",
|
||||
"identityDocType": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#identityDocType",
|
||||
"identityNumber": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#identityNumber",
|
||||
"name": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#name",
|
||||
"description": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#description",
|
||||
"value": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#value",
|
||||
"lang": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#lang",
|
||||
"surnames": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#surnames",
|
||||
"email": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#email",
|
||||
"affiliatedSince": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#affiliatedSince",
|
||||
"affiliatedUntil": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/contexts/vocab#affiliatedUntil",
|
||||
"issued": "https://ec.europa.eu/digital-building-blocks/wikis/display/EBSIDOC/Verifiable+Attestation#issued",
|
||||
"validFrom": "https://ec.europa.eu/digital-building-blocks/wikis/display/EBSIDOC/Verifiable+Attestation#validFrom",
|
||||
"validUntil": "https://ec.europa.eu/digital-building-blocks/wikis/display/EBSIDOC/Verifiable+Attestation#validUntil"
|
||||
}
|
||||
"https://idhub.pangea.org/credentials/base/v1",
|
||||
"https://idhub.pangea.org/credentials/membership-card/v1"
|
||||
],
|
||||
"type": [
|
||||
"VerifiableCredential",
|
||||
|
@ -35,22 +12,7 @@
|
|||
"id": "{{ vc_id }}",
|
||||
"issuer": {
|
||||
"id": "{{ issuer_did }}",
|
||||
"name": "Pangea",
|
||||
"description": [
|
||||
{
|
||||
"value": "Pangea.org is a service provider leveraging open-source technologies to provide affordable and accessible solutions for social enterprises and solidarity organisations.",
|
||||
"lang": "en"
|
||||
},
|
||||
{
|
||||
"value": "Pangea.org és un proveïdor de serveis que aprofita les tecnologies de codi obert per oferir solucions assequibles i accessibles per a empreses socials i organitzacions solidàries.",
|
||||
"lang": "ca_ES"
|
||||
},
|
||||
{
|
||||
"value": "Pangea.org es un proveedor de servicios que aprovecha tecnologías de código abierto para proporcionar soluciones asequibles y accesibles para empresas sociales y organizaciones solidarias.",
|
||||
"lang": "es"
|
||||
}
|
||||
|
||||
]
|
||||
"name": "{{ organisation }}"
|
||||
},
|
||||
"issuanceDate": "{{ issuance_date }}",
|
||||
"issued": "{{ issuance_date }}",
|
||||
|
@ -86,20 +48,20 @@
|
|||
],
|
||||
"credentialSubject": {
|
||||
"id": "{{ subject_did }}",
|
||||
"organisation": "Pangea",
|
||||
"membershipType": "{{ membershipType }}",
|
||||
"membershipId": "{{ vc_id }}",
|
||||
"affiliatedSince": "{{ affiliatedSince }}",
|
||||
"affiliatedUntil": "{{ affiliatedUntil }}",
|
||||
"firstName": "{{ firstName }}",
|
||||
"lastName": "{{ lastName }}",
|
||||
"email": "{{ email }}",
|
||||
"typeOfPerson": "{{ typeOfPerson }}",
|
||||
"identityDocType": "{{ identityDocType }}",
|
||||
"identityNumber": "{{ identityNumber }}",
|
||||
"name": "{{ first_name }}",
|
||||
"surnames": "{{ last_name }}",
|
||||
"email": "{{ email }}",
|
||||
"credentialSchema": {
|
||||
"id": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/membership-card-schema.json",
|
||||
"type": "JsonSchema"
|
||||
}
|
||||
"organisation": "{{ organisation }}",
|
||||
"membershipType": "{{ membershipType }}",
|
||||
"membershipId": "{{ vc_id }}",
|
||||
"affiliatedSince": "{{ affiliatedSince }}",
|
||||
"affiliatedUntil": "{{ affiliatedUntil }}"
|
||||
},
|
||||
"credentialSchema": {
|
||||
"id": "https://idhub.pangea.org/vc_schemas/membership-card.json",
|
||||
"type": "FullJsonSchemaValidator2021"
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/2018/credentials/v1",
|
||||
{
|
||||
"name": "https://schema.org/name",
|
||||
"email": "https://schema.org/email",
|
||||
"membershipType": "https://schema.org/memberOf",
|
||||
"individual": "https://schema.org/Person",
|
||||
"organization": "https://schema.org/Organization",
|
||||
"Member": "https://schema.org/Member",
|
||||
"startDate": "https://schema.org/startDate",
|
||||
"jsonSchema": "https://schema.org/jsonSchema",
|
||||
"destination_country": "https://schema.org/destinationCountry",
|
||||
"offboarding_date": "https://schema.org/offboardingDate",
|
||||
"$ref": "https://schema.org/jsonSchemaRef"
|
||||
}
|
||||
],
|
||||
"id": "{{ vc_id }}",
|
||||
"type": ["VerifiableCredential", "MigrantRescueCredential"],
|
||||
"issuer": "{{ issuer_did }}",
|
||||
"issuanceDate": "{{ issuance_date }}",
|
||||
"credentialSubject": {
|
||||
"id": "{{ subject_did }}",
|
||||
"name": "{{ name }}",
|
||||
"country_of_origin": "{{ country_of_origin }}",
|
||||
"rescue_date": "{{ rescue_date }}",
|
||||
"destination_country": "{{ destination_country }}",
|
||||
"offboarding_date": "{{ offboarding_date }}",
|
||||
"jsonSchema": {
|
||||
"$ref": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/UNDEF.json"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/2018/credentials/v1",
|
||||
{
|
||||
"name": "https://schema.org/name",
|
||||
"email": "https://schema.org/email",
|
||||
"membershipType": "https://schema.org/memberOf",
|
||||
"individual": "https://schema.org/Person",
|
||||
"organization": "https://schema.org/Organization",
|
||||
"Member": "https://schema.org/Member",
|
||||
"startDate": "https://schema.org/startDate",
|
||||
"jsonSchema": "https://schema.org/jsonSchema",
|
||||
"street_address": "https://schema.org/streetAddress",
|
||||
"financial_vulnerability_score": "https://schema.org/financialVulnerabilityScore",
|
||||
"$ref": "https://schema.org/jsonSchemaRef"
|
||||
}
|
||||
],
|
||||
"id": "{{ vc_id }}",
|
||||
"type": ["VerifiableCredential", "FinancialSituationCredential"],
|
||||
"issuer": "{{ issuer_did }}",
|
||||
"issuanceDate": "{{ issuance_date }}",
|
||||
"credentialSubject": {
|
||||
"id": "{{ subject_did }}",
|
||||
"street_address": "{{ street_address }}",
|
||||
"financial_vulnerability_score": "{{ financial_vulnerability_score }}",
|
||||
"jsonSchema": {
|
||||
"$ref": "https://gitea.pangea.org/trustchain-oc1-orchestral/schemas/UNDEF.json"
|
||||
}
|
||||
}
|
||||
}
|
57
idhub/templates/idhub/admin/terms_conditions.html
Normal file
57
idhub/templates/idhub/admin/terms_conditions.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
{% extends "idhub/base_admin.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h3>
|
||||
<i class="{{ icon }}"></i>
|
||||
{{ subtitle }}
|
||||
</h3>
|
||||
{% load django_bootstrap5 %}
|
||||
<form role="form" method="post">
|
||||
{% csrf_token %}
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger alert-icon alert-icon-border alert-dismissible" role="alert">
|
||||
<div class="icon"><span class="mdi mdi-close-circle-o"></span></div>
|
||||
<div class="message">
|
||||
{% for field, error in form.errors.items %}
|
||||
{{ error }}<br />
|
||||
{% endfor %}
|
||||
<button class="btn-close" type="button" data-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
You must read the terms and conditions of this service and accept the
|
||||
<a class="btn btn-green-admin" href="jacascript:void()" data-bs-toggle="modal" data-bs-target="#gdpr" title="{% trans 'GDPR' %}">Read GDPR</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
{% bootstrap_form form %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions-no-box">
|
||||
<a class="btn btn-grey" href="{% url 'idhub:admin_dashboard' %}">{% translate "Cancel" %}</a>
|
||||
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Save' %}" />
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<!-- Modal -->
|
||||
<div class="modal" id="gdpr" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="exampleModalLabel">{% trans 'GDPR info' %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Here we write the info about GDPR</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans 'Close' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -6,4 +6,27 @@
|
|||
<i class="{{ icon }}"></i>
|
||||
{{ subtitle }}
|
||||
</h3>
|
||||
{% load django_bootstrap5 %}
|
||||
<form role="form" method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger alert-icon alert-icon-border alert-dismissible" role="alert">
|
||||
<div class="icon"><span class="mdi mdi-close-circle-o"></span></div>
|
||||
<div class="message">
|
||||
<button class="close" type="button" data-dismiss="alert" aria-label="Close">
|
||||
<span class="mdi mdi-close" aria-hidden="true"></span>
|
||||
</button>
|
||||
{% for field, error in form.errors.items %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% bootstrap_form form %}
|
||||
<div class="form-actions-no-box">
|
||||
<a class="btn btn-grey" href="{% url 'idhub:admin_import' %}">{% translate "Cancel" %}</a>
|
||||
<input class="btn btn-green-admin" type="submit" name="submit" value="{% translate 'Upload' %}" />
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
|
@ -109,11 +109,13 @@
|
|||
{% trans 'My credentials' %}
|
||||
</a>
|
||||
</li>
|
||||
{% if admin_validated %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if path == 'user_credentials_request' %}active2{% endif %}" href="{% url 'idhub:user_credentials_request' %}">
|
||||
{% trans 'Request a credential' %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if path in 'user_demand_authorization, authorize' %}active2{% endif %}" href="{% url 'idhub:user_demand_authorization' %}">
|
||||
{% trans 'Present a credential' %}
|
||||
|
@ -138,9 +140,6 @@
|
|||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">{{ title }}</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<input class="form-control form-control-grey " type="text" placeholder="{% trans 'Search' %}" aria-label="Search">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -170,9 +170,6 @@
|
|||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">{{ title }}</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<input class="form-control form-control-grey " type="text" placeholder="{% trans 'Search' %}" aria-label="Search">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -36,11 +36,16 @@
|
|||
{{ object.get_status}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col text-center">
|
||||
<a class="btn btn-green-user" href="{% url 'idhub:user_credential_json' object.id %}">{% trans 'View in JSON format' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
{% if object.eidas1_did and admin_validated %}
|
||||
<div class="col text-center">
|
||||
<a class="btn btn-green-user" href="{% url 'idhub:user_credential_pdf' object.id %}">{% trans 'Sign credential in PDF format' %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col text-center">
|
||||
<a class="btn btn-green-user" href="{% url 'idhub:user_credential_json' object.id %}">{% trans 'View credential in JSON format' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -6,4 +6,7 @@
|
|||
<i class="{{ icon }}"></i>
|
||||
{{ subtitle }}
|
||||
</h3>
|
||||
Gdpr info<br/>
|
||||
If you want accept or revoke the Gdpr go to:
|
||||
<a class="btn btn-green-user" href="{% url 'idhub:user_terms_and_conditions' %}">Terms and conditions</a>
|
||||
{% endblock %}
|
||||
|
|
57
idhub/templates/idhub/user/terms_conditions.html
Normal file
57
idhub/templates/idhub/user/terms_conditions.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
{% extends "idhub/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h3>
|
||||
<i class="{{ icon }}"></i>
|
||||
{{ subtitle }}
|
||||
</h3>
|
||||
{% load django_bootstrap5 %}
|
||||
<form role="form" method="post">
|
||||
{% csrf_token %}
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger alert-icon alert-icon-border alert-dismissible" role="alert">
|
||||
<div class="icon"><span class="mdi mdi-close-circle-o"></span></div>
|
||||
<div class="message">
|
||||
{% for field, error in form.errors.items %}
|
||||
{{ error }}<br />
|
||||
{% endfor %}
|
||||
<button class="btn-close" type="button" data-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
You must read the terms and conditions of this service and accept the
|
||||
<a class="btn btn-green-user" href="jacascript:void()" data-bs-toggle="modal" data-bs-target="#gdpr" title="{% trans 'GDPR' %}">Read GDPR</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-4">
|
||||
{% bootstrap_form form %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions-no-box">
|
||||
<a class="btn btn-grey" href="{% url 'idhub:user_dashboard' %}">{% translate "Cancel" %}</a>
|
||||
<input class="btn btn-green-user" type="submit" name="submit" value="{% translate 'Save' %}" />
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<!-- Modal -->
|
||||
<div class="modal" id="gdpr" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="exampleModalLabel">{% trans 'GDPR info' %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Here we write the info about GDPR</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans 'Close' %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,12 +0,0 @@
|
|||
{% extends "musician/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<p>{% blocktrans with address_name=object.full_address_name %}Are you sure that you want remove the address: "{{ address_name }}"?{% endblocktrans %}</p>
|
||||
<p class="alert alert-warning"><strong>{% trans 'WARNING: This action cannot be undone.' %}</strong></p>
|
||||
<input class="btn btn-danger" type="submit" value="{% trans 'Delete' %}">
|
||||
<a class="btn btn-secondary" href="{% url 'musician:address-update' view.kwargs.pk %}">{% trans 'Cancel' %}</a>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -1,20 +0,0 @@
|
|||
{% extends "musician/base.html" %}
|
||||
{% load bootstrap4 i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="service-name">{{ service.verbose_name }}</h1>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% buttons %}
|
||||
<a class="btn btn-light mr-2" href="{% url 'musician:address-list' %}">{% trans "Cancel" %}</a>
|
||||
<button type="submit" class="btn btn-secondary">{% trans "Save" %}</button>
|
||||
{% if form.instance %}
|
||||
<div class="float-right">
|
||||
<a class="btn btn-danger" href="{% url 'musician:address-delete' view.kwargs.pk %}">{% trans "Delete" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -1,41 +0,0 @@
|
|||
{% extends "musician/mail_base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block tabcontent %}
|
||||
<div class="tab-pane fade show active" id="addresses" role="tabpanel" aria-labelledby="addresses-tab">
|
||||
<table class="table service-list">
|
||||
<colgroup>
|
||||
<col span="1" style="width: 25%;">
|
||||
<col span="1" style="width: 25%;">
|
||||
<col span="1" style="width: 25%;">
|
||||
<col span="1" style="width: 25%;">
|
||||
</colgroup>
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th scope="col">{% trans "Email" %}</th>
|
||||
<th scope="col">{% trans "Domain" %}</th>
|
||||
<th scope="col">{% trans "Mailboxes" %}</th>
|
||||
<th scope="col">{% trans "Forward" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for obj in object_list %}
|
||||
<tr>
|
||||
<td><a href="{% url 'musician:address-update' obj.id %}">{{ obj.full_address_name }}</a></td>
|
||||
<td>{{ obj.domain.name }}</td>
|
||||
<td>
|
||||
{% for mailbox in obj.mailboxes %}
|
||||
<a href="{% url 'musician:mailbox-update' mailbox.id %}">{{ mailbox.name }}</a>
|
||||
{% if not forloop.last %}<br/> {% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td>{{ obj.forward }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% include "musician/components/table_paginator.html" %}
|
||||
</table>
|
||||
<a class="btn btn-primary mt-4 mb-4" href="{% url 'musician:address-create' %}">{% trans "New mail address" %}</a>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,41 +0,0 @@
|
|||
{% extends "musician/base.html" %}
|
||||
{% load i18n l10n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="service-name">{% trans "Billing" %}</h1>
|
||||
<p class="service-description">{% trans "Billing page description." %}</p>
|
||||
|
||||
<table class="table service-list">
|
||||
<colgroup>
|
||||
<col span="1" style="width: 15%;">
|
||||
<col span="1" style="width: 15%;">
|
||||
<col span="1" style="width: 40%;">
|
||||
<col span="1" style="width: 10%;">
|
||||
<col span="1" style="width: 10%;">
|
||||
</colgroup>
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th scope="col">{% trans "Number" %}</th>
|
||||
<th scope="col">{% trans "Bill date" %}</th>
|
||||
<th scope="col">{% trans "Type" %}</th>
|
||||
<th scope="col">{% trans "Total" %}</th>
|
||||
<th scope="col">{% trans "Download PDF" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for bill in object_list %}
|
||||
<tr>
|
||||
<th scope="row">{{ bill.number }}</th>
|
||||
<td>{{ bill.created_on|date:"SHORT_DATE_FORMAT" }}</td>
|
||||
<td>{{ bill.type }}</td>
|
||||
<td>{{ bill.total|floatformat:2|localize }}€</td>
|
||||
<td><a class="text-dark" href="{% url 'musician:bill-download' bill.id %}" target="_blank" rel="noopener noreferrer"><i class="fas fa-file-pdf"></i></a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{# TODO: define proper colspan #}
|
||||
{% include "musician/components/table_paginator.html" %}
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
|
@ -1,29 +0,0 @@
|
|||
{# <!-- paginator component --> #}
|
||||
<div class="row object-list-paginator">
|
||||
<div class="col-md-4">{{ page_obj.paginator.count }} items in total</div>
|
||||
<div class="col-md-4 text-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page=1&per_page={{ page_obj.paginator.per_page }}">«</a>
|
||||
<a href="?page={{ page_obj.previous_page_number }}&per_page={{ page_obj.paginator.per_page }}">‹</a>
|
||||
{% endif %}
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}&per_page={{ page_obj.paginator.per_page }}">›</a>
|
||||
<a href="?page={{ page_obj.paginator.num_pages }}&per_page={{ page_obj.paginator.per_page }}">»</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4 text-right">
|
||||
<form method="get">
|
||||
Showing
|
||||
<select name="{{ per_page_param }}">
|
||||
{% for value in per_page_values %}
|
||||
{% with page_obj.paginator.per_page as per_page %}
|
||||
<option value="{{ value }}" {% if value == per_page %}selected{% endif %}>{{ value }}</option>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
per page
|
||||
<input type="submit" value="apply" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
|
@ -1,50 +0,0 @@
|
|||
{# <!-- table footer based paginator for ListView --> #}
|
||||
{% load i18n %}
|
||||
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="2">{{ page_obj.paginator.count }} items in total</td>
|
||||
<td class="text-center">
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
<li class="page-item {{ page_obj.has_previous|yesno:',disabled' }}">
|
||||
<a class="page-link" {% if page_obj.has_previous %}
|
||||
href="?page={{ page_obj.previous_page_number }}&per_page={{ page_obj.paginator.per_page }}"
|
||||
{% else %} href="#" {% endif %} tabindex="-1">
|
||||
<span aria-hidden="true">‹</span>
|
||||
<span class="sr-only">{% trans "Previous" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% for page_number in page_obj.paginator.page_range %}
|
||||
<li class="page-item {% if page_number == page_obj.number %}active {% endif %}">
|
||||
<a class="page-link"
|
||||
href="?page={{ page_number }}&per_page={{ page_obj.paginator.per_page }}">{{ page_number }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li class="page-item {{ page_obj.has_next|yesno:',disabled' }}">
|
||||
<a class="page-link" {% if page_obj.has_next %}
|
||||
href="?page={{ page_obj.next_page_number }}&per_page={{ page_obj.paginator.per_page }}"
|
||||
{% else %} href="#" {% endif %}>
|
||||
<span aria-hidden="true">›</span>
|
||||
<span class="sr-only">{% trans "Next" %}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</td>
|
||||
<td colspan="2" class="text-right">
|
||||
<form method="get">
|
||||
Showing
|
||||
<select name="{{ per_page_param }}">
|
||||
{% for value in per_page_values %}
|
||||
{% with page_obj.paginator.per_page as per_page %}
|
||||
<option value="{{ value }}" {% if value == per_page %}selected{% endif %}>{{ value }}</option>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
per page
|
||||
<input type="submit" value="apply" />
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
|
@ -1,22 +0,0 @@
|
|||
{% comment %}
|
||||
Resource usage rendered as bootstrap progress bar
|
||||
|
||||
Expected parameter: detail
|
||||
Expected structure: dictionary or object with attributes:
|
||||
- usage (int): 125
|
||||
- total (int): 200
|
||||
- unit (string): 'MB'
|
||||
- percent (int: [0, 25, 50, 75, 100]: 75
|
||||
{% endcomment %}
|
||||
|
||||
<div class="text-center">
|
||||
{% if detail %}
|
||||
{{ detail.usage }} {{ detail.unit }}
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar bg-secondary w-{{ detail.percent }}" role="progressbar" aria-valuenow="{{ detail.usage }}"
|
||||
aria-valuemin="0" aria-valuemax="{{ detail.total }}"></div>
|
||||
</div>
|
|
@ -1,161 +0,0 @@
|
|||
{% extends "musician/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h2 style="margin-top: 10px;">{% trans "Welcome back" %} <strong>{{ profile.username }}</strong></h2>
|
||||
{% if profile.last_login %}
|
||||
<p>{% blocktrans with last_login=profile.last_login|date:"SHORT_DATE_FORMAT" %}Last time you logged in was: {{ last_login }}{% endblocktrans %}</p>
|
||||
{% else %}
|
||||
<p>{% trans "It's the first time you log into the system, welcome on board!" %}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="card-deck">
|
||||
{% for resource, usage in resource_usage.items %}
|
||||
<div class="card resource-usage resource-{{ resource }}">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ usage.verbose_name }}</h5>
|
||||
{% include "musician/components/usage_progress_bar.html" with detail=usage.data %}
|
||||
{% if usage.data.alert %}
|
||||
<div class="text-center mt-4">
|
||||
{{ usage.data.alert }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="card resource-usage resource-notifications">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{% trans "Notifications" %}</h5>
|
||||
{% for message in notifications %}
|
||||
<p class="card-text">{{ message }}</p>
|
||||
{% empty %}
|
||||
<p class="card-text">{% trans "There is no notifications at this time." %}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<h1 class="service-name">{% trans "Your domains and websites" %}</h1>
|
||||
<p class="service-description">{% trans "Dashboard page description." %}</p>
|
||||
|
||||
{% for domain in domains %}
|
||||
<div class="card service-card">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<strong>{{ domain.name }}</strong>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
{% with domain.websites.0 as website %}
|
||||
{% with website.contents.0 as content %}
|
||||
<button type="button" class="btn text-secondary" data-toggle="modal" data-target="#configDetailsModal"
|
||||
data-domain="{{ domain.name }}" data-website="{{ website|yesno:'true,false' }}" data-webapp-type="{{ content.webapp.type }}" data-root-path="{{ content.path }}"
|
||||
data-url="{% url 'musician:domain-detail' domain.id %}">
|
||||
{% trans "view configuration" %} <strong class="fas fa-tools"></strong>
|
||||
</button>
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="col-md text-right">
|
||||
{% comment "@slamora: orchestra doesn't have this information [won't fix] See issue #2" %}
|
||||
{% trans "Expiration date" %}: <strong>{{ domain.expiration_date|date:"SHORT_DATE_FORMAT" }}</strong>
|
||||
{% endcomment %}
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /card-header-->
|
||||
<div class="card-body row text-center">
|
||||
<div class="col-6 col-md-3 col-lg-2 border-right">
|
||||
<h4>{% trans "Mail" %}</h4>
|
||||
<p class="card-text"><i class="fas fa-envelope fa-3x"></i></p>
|
||||
<p class="card-text text-dark">
|
||||
{{ domain.addresses|length }} {% trans "mail addresses created" %}
|
||||
</p>
|
||||
<a class="stretched-link" href="{% url 'musician:address-list' %}?domain={{ domain.id }}"></a>
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-lg-2 border-right">
|
||||
<h4>{% trans "Mail list" %}</h4>
|
||||
<p class="card-text"><i class="fas fa-mail-bulk fa-3x"></i></p>
|
||||
<a class="stretched-link" href="{% url 'musician:mailing-lists' %}?domain={{ domain.id }}"></a>
|
||||
</div>
|
||||
<div class="col-6 col-md-3 col-lg-2 border-right">
|
||||
<h4>{% trans "Software as a Service" %}</h4>
|
||||
<p class="card-text"><i class="fas fa-fire fa-3x"></i></p>
|
||||
<p class="card-text text-dark">{% trans "Nothing installed" %}</p>
|
||||
<a class="stretched-link" href="{% url 'musician:saas-list' %}?domain={{ domain.id }}"></a>
|
||||
</div>
|
||||
<div class="d-none d-lg-block col-lg-1"></div>
|
||||
<div class="col-6 col-md-3 col-lg-4">
|
||||
<h4>{% trans "Disk usage" %}</h4>
|
||||
<p class="card-text"><i class="fas fa-hdd fa-3x"></i></p>
|
||||
<div class="w-75 m-auto">
|
||||
{% include "musician/components/usage_progress_bar.html" with detail=domain.usage %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-none d-lg-block col-lg-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
<!-- configuration details modal -->
|
||||
<div class="modal fade" id="configDetailsModal" tabindex="-1" role="dialog" aria-labelledby="configDetailsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title text-secondary" id="configDetailsModalLabel">{% trans "Configuration details" %}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="domain-ftp pb-3 border-bottom">
|
||||
<h6 class="pl-4 mb-4">{% trans "FTP access:" %}</h6>
|
||||
{# Translators: domain configuration detail modal #}
|
||||
<p>{% trans "Contact with the support team to get details concerning FTP access." %}</p>
|
||||
{% comment %}
|
||||
<!-- hidden until API provides FTP information -->
|
||||
<label>{% trans "Username" %}:</label> <span id="config-username" class="font-weight-bold">username</span><br/>
|
||||
<label>{% trans "Password:" %}</label> <span id="config-password" class="font-weight-bold">password</span>
|
||||
{% endcomment %}
|
||||
</div>
|
||||
<div class="domain-website pt-4">
|
||||
<div id="no-website"><h6 class="pl-4">{% trans "No website configured." %}</h6></div>
|
||||
<div id="config-website">
|
||||
<label>{% trans "Root directory:" %}</label> <span id="config-root-path" class="font-weight-bold">root directory</span>
|
||||
<label>{% trans "Type:" %}</label><span id="config-webapp-type" class="font-weight-bold">type</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#domain-detail" class="btn btn-primary">{% trans "View DNS records" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block extrascript %}
|
||||
<script>
|
||||
$('#configDetailsModal').on('show.bs.modal', function (event) {
|
||||
var button = $(event.relatedTarget); // Button that triggered the modal
|
||||
var modal = $(this);
|
||||
|
||||
// Extract info from data-* attributes
|
||||
modal.find('.modal-title').text(button.data('domain'));
|
||||
modal.find('.modal-body #config-webapp-type').text(button.data('webapp-type'));
|
||||
modal.find('.modal-body #config-root-path').text(button.data('root-path'));
|
||||
modal.find('.modal-footer .btn').attr('href', button.data('url'));
|
||||
|
||||
var nowebsite = modal.find('.modal-body #no-website');
|
||||
var websitecfg = modal.find('.modal-body #config-website');
|
||||
if(button.data('website')) {
|
||||
nowebsite.hide();
|
||||
websitecfg.show();
|
||||
} else {
|
||||
nowebsite.show();
|
||||
websitecfg.hide();
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -1,68 +0,0 @@
|
|||
{% extends "musician/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="service-name">{{ service.verbose_name }}</h1>
|
||||
<p class="service-description">{{ service.description }}</p>
|
||||
|
||||
{% for database in object_list %}
|
||||
<div class="card service-card">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<strong>{{ database.name }}</strong>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
{% trans "Type" %}: <strong>{{ database.type }}</strong>
|
||||
</div>
|
||||
<div class="col-md text-right">
|
||||
{% comment "@slamora: orchestra doesn't provide this information [won't fix] See issue #3" %}
|
||||
{% trans "associated to" %}: <strong>{{ database.domain|default:"-" }}</strong>
|
||||
{% endcomment %}
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /card-header-->
|
||||
<div class="card-body row">
|
||||
<div class="col-md-4">
|
||||
<h4>Database users</h4>
|
||||
<ul class="list-unstyled pl-2">
|
||||
{% for user in database.users %}
|
||||
{# TODO(@slamora) render in two columns #}
|
||||
<li><span class="d-inline-block w-25">{{ user.username }}</span> <i class="fas fa-user-edit"></i></li>
|
||||
{% empty %}
|
||||
<li>{% trans "No users for this database." %}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-3 border-left border-right">
|
||||
<h4>Database usage</h4>
|
||||
<p class="text-center"><i class="fas fa-database fa-3x"></i></p>
|
||||
{% include "musician/components/usage_progress_bar.html" with detail=database.usage %}
|
||||
</div>
|
||||
<div class="col-md-5 text-right">
|
||||
<div class="service-manager-link">
|
||||
<a class="btn btn-primary" href="{{ database.manager_url }}" target="_blank" rel="noopener noreferrer">{% trans "Open database manager" %} <i class="fas fa-external-link-alt"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% empty %}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card service-card shadow p-3 mb-5 bg-white rounded">
|
||||
<div class="card-body text-center">
|
||||
<p class="mb-4"><i class="fas fa-database fa-5x"></i></p>
|
||||
{# Translators: database page when there isn't any database. #}
|
||||
<h5 class="card-title text-dark">{% trans "Ooops! Looks like there is nothing here!" %}</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if object_list|length > 0 %}
|
||||
{% include "musician/components/paginator.html" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -1,30 +0,0 @@
|
|||
{% extends "musician/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<a class="btn-arrow-left" href="{% url 'musician:dashboard' %}">{% trans "Go back" %}</a>
|
||||
|
||||
<h1 class="service-name">{% trans "DNS settings for" %} <span class="font-weight-light">{{ object.name }}</span></h1>
|
||||
<p class="service-description">{% trans "DNS settings page description." %}</p>
|
||||
|
||||
<table class="table service-list">
|
||||
<colgroup>
|
||||
<col span="1" style="width: 12%;">
|
||||
<col span="1" style="width: 88%;">
|
||||
</colgroup>
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th scope="col">{% trans "Type" %}</th>
|
||||
<th scope="col">{% trans "Value" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for record in object.records %}
|
||||
<tr>
|
||||
<td>{{ record.type }}</td>
|
||||
<td>{{ record.value }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
|
@ -1,32 +0,0 @@
|
|||
{% extends "musician/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% if active_domain %}
|
||||
<a class="btn-arrow-left" href="{% url 'musician:address-list' %}">{% trans "Go to global" %}</a>
|
||||
{% endif %}
|
||||
|
||||
<h1 class="service-name">{{ service.verbose_name }}
|
||||
{% if active_domain %}<span class="font-weight-light">{% trans "for" %} {{ active_domain.name }}</span>{% endif %}
|
||||
</h1>
|
||||
<p class="service-description">{{ service.description }}</p>
|
||||
|
||||
{% with request.resolver_match.url_name as url_name %}
|
||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if url_name == 'address-list' %}active{% endif %}" href="{% url 'musician:address-list' %}" role="tab"
|
||||
aria-selected="{% if url_name == 'address-list' %}true{% else %}false{% endif %}">{% trans "Addresses" %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if url_name == 'mailbox-list' %}active{% endif %}" href="{% url 'musician:mailbox-list' %}" role="tab"
|
||||
aria-selected="{% if url_name == 'mailbox-list' %}true{% else %}false{% endif %}">{% trans "Mailboxes" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% endwith %}
|
||||
|
||||
<div class="tab-content" id="myTabContent">
|
||||
{% block tabcontent %}
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
|
@ -1,15 +0,0 @@
|
|||
{% extends "musician/base.html" %}
|
||||
{% load bootstrap4 i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="service-name">{% trans "Change password" %}: <span class="font-weight-light">{{ object.name }}</span></h1>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% buttons %}
|
||||
<a class="btn btn-light mr-2" href="{% url 'musician:mailbox-list' %}">{% trans "Cancel" %}</a>
|
||||
<button type="submit" class="btn btn-secondary">{% trans "Save" %}</button>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -1,15 +0,0 @@
|
|||
{% extends "musician/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<p>{% blocktrans with name=object.name %}Are you sure that you want remove the mailbox: "{{ name }}"?{% endblocktrans %}</p>
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{% trans "All mailbox's messages will be <strong>deleted and cannot be recovered</strong>." %}
|
||||
</div>
|
||||
<p class="alert alert-warning"><strong>{% trans 'WARNING: This action cannot be undone.' %}</strong></p>
|
||||
<input class="btn btn-danger" type="submit" value="{% trans 'Delete' %}">
|
||||
<a class="btn btn-secondary" href="{% url 'musician:mailbox-list' %}">{% trans 'Cancel' %}</a>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -1,30 +0,0 @@
|
|||
{% extends "musician/base.html" %}
|
||||
{% load bootstrap4 i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="service-name">{{ service.verbose_name }}</h1>
|
||||
|
||||
{% if extra_mailbox %}
|
||||
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
||||
<strong>{% trans "Warning!" %}</strong> {% trans "You have reached the limit of mailboxes of your subscription so <strong>extra fees</strong> may apply." %}
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="{% trans 'Close' %}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% buttons %}
|
||||
<a class="btn btn-light mr-2" href="{% url 'musician:mailbox-list' %}">{% trans "Cancel" %}</a>
|
||||
<button type="submit" class="btn btn-secondary">{% trans "Save" %}</button>
|
||||
{% if form.instance %}
|
||||
<div class="float-right">
|
||||
<a class="btn btn-outline-warning" href="{% url 'musician:mailbox-password' view.kwargs.pk %}"><i class="fas fa-key"></i> {% trans "Change password" %}</a>
|
||||
<a class="btn btn-danger" href="{% url 'musician:mailbox-delete' view.kwargs.pk %}">{% trans "Delete" %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -1,46 +0,0 @@
|
|||
{% extends "musician/mail_base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block tabcontent %}
|
||||
<div class="tab-pane fade show active" id="mailboxes" role="tabpanel" aria-labelledby="mailboxes-tab">
|
||||
<table class="table service-list">
|
||||
<colgroup>
|
||||
<col span="1" style="width: 25%;">
|
||||
<col span="1" style="width: 10%;">
|
||||
<col span="1" style="width: 65%;">
|
||||
</colgroup>
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th scope="col">{% trans "Name" %}</th>
|
||||
<th scope="col">{% trans "Filtering" %}</th>
|
||||
<th scope="col">{% trans "Addresses" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for mailbox in object_list %}
|
||||
{# <!-- Exclude (don't render) inactive mailboxes -->#}
|
||||
{% if mailbox.is_active %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'musician:mailbox-update' mailbox.id %}">{{ mailbox.name }}</a>
|
||||
<a class="roll-hover btn btn-outline-warning" href="{% url 'musician:mailbox-password' mailbox.id %}">
|
||||
<i class="fas fa-key"></i> {% trans "Update password" %}</a>
|
||||
</td>
|
||||
<td>{{ mailbox.filtering }}</td>
|
||||
<td>
|
||||
{% for addr in mailbox.addresses %}
|
||||
<a href="{% url 'musician:address-update' addr.data.id %}">
|
||||
{{ addr.full_address_name }}
|
||||
</a><br/>
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}{# <!-- /is_active --> #}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% include "musician/components/table_paginator.html" %}
|
||||
</table>
|
||||
<a class="btn btn-primary mt-4 mb-4" href="{% url 'musician:mailbox-create' %}">{% trans "New mailbox" %}</a>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,46 +0,0 @@
|
|||
{% extends "musician/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% if active_domain %}
|
||||
<a class="btn-arrow-left" href="{% url 'musician:mailing-lists' %}">{% trans "Go to global" %}</a>
|
||||
{% endif %}
|
||||
|
||||
<h1 class="service-name">{{ service.verbose_name }}{% if active_domain %} <span class="font-weight-light">{% trans "for" %} {{ active_domain.name }}</span>{% endif %}</h1>
|
||||
<p class="service-description">{{ service.description }}</p>
|
||||
|
||||
<table class="table service-list">
|
||||
<colgroup>
|
||||
<col span="1" style="width: 13%;">
|
||||
<col span="1" style="width: 12%;">
|
||||
<col span="1" style="width: 50%;">
|
||||
<col span="1" style="width: 15%;">
|
||||
<col span="1" style="width: 10%;">
|
||||
</colgroup>
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Address</th>
|
||||
<th scope="col">Admin email</th>
|
||||
<th scope="col">Configure</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for resource in object_list %}
|
||||
<tr>
|
||||
<th scope="row">{{ resource.name }}</th>
|
||||
{% if resource.is_active %}
|
||||
<td class="text-primary font-weight-bold">{% trans "Active" %}</td>
|
||||
{% else %}
|
||||
<td class="text-danger font-weight-bold">{% trans "Inactive" %}</td>
|
||||
{% endif %}
|
||||
<td>{{ resource.address_name}}</td>
|
||||
<td>{{ resource.admin_email }}</td>
|
||||
<td><a href="{{ resource.manager_url }}" target="_blank" rel="noopener noreferrer">Mailtrain <i class="fas fa-external-link-alt"></i></a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% include "musician/components/table_paginator.html" %}
|
||||
</table>
|
||||
{% endblock %}
|
|
@ -1,65 +0,0 @@
|
|||
{% extends "musician/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="service-name">{% trans "Profile" %}</h1>
|
||||
<p class="service-description">{% trans "Little description on profile page." %}</p>
|
||||
|
||||
<div class="card-deck">
|
||||
<div class="card card-profile">
|
||||
<h5 class="card-header">{% trans "User information" %}</h5>
|
||||
<div class="card-body row">
|
||||
<div class="col-md">
|
||||
<div class="border-primary rounded-circle d-inline-block p-1" style="background-color: white; border: 5px solid grey">
|
||||
<img id="user-avatar" width="160" height="160" src="/static/musician/images/default-profile-picture-primary-color.png" alt="user-profile-picture">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<p class="card-text">{{ profile.username }}</p>
|
||||
<p class="card-text">{{ profile.type }}</p>
|
||||
<p class="card-text">{% trans "Preferred language:" %} {{ profile.language|language_name_local }}</p>
|
||||
</div>
|
||||
{% comment %}
|
||||
<!-- disabled until set_password is implemented -->
|
||||
<div class="col-md-12 text-right">
|
||||
<a class="btn btn-primary pl-5 pr-5" href="#">{% trans "Set new password" %}</a>
|
||||
</div>
|
||||
{% endcomment %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% with profile.billing as contact %}
|
||||
<div class="card card-profile">
|
||||
<h5 class="card-header">{% trans "Billing information" %}</h5>
|
||||
<div class="card-body">
|
||||
<div class="form-group">{{ contact.name }}</div>
|
||||
<div class="form-group">{{ contact.address }}</div>
|
||||
<div class="form-group">
|
||||
{{ contact.zipcode }}
|
||||
{{ contact.city }}
|
||||
{{ contact.country }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{{ contact.vat }}
|
||||
</div>
|
||||
<!-- payment method -->
|
||||
<div class="form-group">
|
||||
{% trans "payment method:" %} {{ payment.method }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{% if payment.method == 'SEPADirectDebit' %}
|
||||
IBAN {{ payment.data.iban }}
|
||||
{% else %}
|
||||
{# <!-- "TODO handle Credit Card" --> #}
|
||||
Details: {{ payment.data }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<a href="{% url 'musician:billing' %}">{% trans "Check your last bills" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endblock %}
|
|
@ -1,56 +0,0 @@
|
|||
{% extends "musician/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1 class="service-name">{{ service.verbose_name }}</h1>
|
||||
<p class="service-description">{{ service.description }}</p>
|
||||
|
||||
{% for saas in object_list %}
|
||||
<div class="card service-card">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<strong>{{ saas.name }}</strong>
|
||||
</div>
|
||||
{% comment "Hidden until API provides this information" %}
|
||||
<div class="col-md text-right">
|
||||
{% trans "Installed on" %}: <strong>{{ saas.domain|default:"-" }}</strong>
|
||||
</div>
|
||||
{% endcomment %}
|
||||
</div>
|
||||
</div><!-- /card-header-->
|
||||
<div class="card-body row">
|
||||
<div class="col-md-4">
|
||||
<h4>{{ saas.service|capfirst }}</h4>
|
||||
<p class="text-center service-brand"><i class="fab fa-{{ saas.service }} fa-10x"></i></p>
|
||||
</div>
|
||||
<div class="col-md-3 border-left border-right">
|
||||
<h4 class="mb-3">{% trans "Service info" %}</h4>
|
||||
<label class="w-25">{% trans "active" %}:</label> <strong>{{ saas.is_active|yesno }}</strong><br/>
|
||||
{% for key, value in saas.data.items %}
|
||||
<label class="w-25">{{ key }}:</label> <strong>{{ value }}</strong><br/>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="col-md-5 text-right">
|
||||
<div class="service-manager-link">
|
||||
<a class="btn btn-primary" href="{{ saas.manager_url }}" target="_blank" rel="noopener noreferrer">{% trans "Open service admin panel" %} <i class="fas fa-external-link-alt"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card service-card shadow p-3 mb-5 bg-white rounded">
|
||||
<div class="card-body text-center">
|
||||
<p class="mb-4"><i class="fas fa-fire fa-5x"></i></p>
|
||||
{# Translators: saas page when there isn't any saas. #}
|
||||
<h5 class="card-title text-dark">{% trans "Ooops! Looks like there is nothing here!" %}</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
|
@ -1,28 +0,0 @@
|
|||
{% extends "musician/base.html" %}
|
||||
{% load i18n musician %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>{{ service.verbose_name }}</h1>
|
||||
<p>{{ service.description }}</p>
|
||||
<table class="table table-hover">
|
||||
<thead class="thead-dark">
|
||||
|
||||
<tr>
|
||||
{% for field_name in service.fields %}
|
||||
<th scope="col">{{ field_name }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for resource in object_list %}
|
||||
<tr>
|
||||
{% for field_name in service.fields %}
|
||||
<td>{{ resource|get_item:field_name }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% include "musician/components/table_paginator.html" %}
|
||||
</table>
|
||||
{% endblock %}
|
|
@ -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/<uidb64>/<token>/',
|
||||
auth_views.PasswordResetConfirmView.as_view(
|
||||
template_name='auth/password_reset_confirm.html',
|
||||
success_url=reverse_lazy('idhub:password_reset_complete')
|
||||
),
|
||||
path('auth/reset/<uidb64>/<token>/', PasswordResetConfirmView.as_view(),
|
||||
name='password_reset_confirm'
|
||||
),
|
||||
# path('auth/reset/<uidb64>/<token>/',
|
||||
# auth_views.PasswordResetConfirmView.as_view(
|
||||
# template_name='auth/password_reset_confirm.html',
|
||||
# success_url=reverse_lazy('idhub:password_reset_complete')
|
||||
# ),
|
||||
# name='password_reset_confirm'
|
||||
# ),
|
||||
path('auth/reset/done/',
|
||||
auth_views.PasswordResetCompleteView.as_view(
|
||||
template_name='auth/password_reset_complete.html'
|
||||
|
@ -80,14 +88,20 @@ urlpatterns = [
|
|||
name='user_credentials'),
|
||||
path('user/credentials/<int:pk>', views_user.CredentialView.as_view(),
|
||||
name='user_credential'),
|
||||
path('user/credentials/<int:pk>/json', views_user.CredentialJsonView.as_view(),
|
||||
path('user/credentials/<int:pk>/pdf', views_user.CredentialPdfView.as_view(),
|
||||
name='user_credential_pdf'),
|
||||
path('credentials/<int:pk>/', views_user.CredentialJsonView.as_view(),
|
||||
name='user_credential_json'),
|
||||
path('public/credentials/<str:pk>/', 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/<uuid:admin2fauth>', views_admin.DobleFactorAuthView.as_view(),
|
||||
name='admin_2fauth'),
|
||||
path('admin/auth/2f/', DobleFactorSendView.as_view(), name='confirm_send_2f'),
|
||||
|
||||
path('did-registry/<str:did_id>/did.json', serve_did)
|
||||
|
||||
# path('verification_portal/verify/', views_verification_portal.verify,
|
||||
# name="verification_portal_verify")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'))
|
||||
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
class VPVerifyRequest(models.Model):
|
||||
"""
|
||||
`nonce` is an opaque random string used to lookup verification requests. URL-safe.
|
||||
Example: "UPBQ3JE2DGJYHP5CPSCRIGTHRTCYXMQPNQ"
|
||||
`expected_credentials` is a JSON list of credential types that must be present in this VP.
|
||||
Example: ["FinancialSituationCredential", "HomeConnectivitySurveyCredential"]
|
||||
`expected_contents` is a JSON object that places optional constraints on the contents of the
|
||||
returned VP.
|
||||
Example: [{"FinancialSituationCredential": {"financial_vulnerability_score": "7"}}]
|
||||
`action` is (for now) a JSON object describing the next steps to take if this verification
|
||||
is successful. For example "send mail to <destination> with <subject> and <body>"
|
||||
Example: {"action": "send_mail", "params": {"to": "orders@somconnexio.coop", "subject": "New client", "body": ...}
|
||||
`response` is a URL that the user's wallet will redirect the user to.
|
||||
`submitted_on` is used (by a cronjob) to purge old entries that didn't complete verification
|
||||
"""
|
||||
nonce = models.CharField(max_length=50)
|
||||
expected_credentials = models.CharField(max_length=255)
|
||||
expected_contents = models.TextField()
|
||||
action = models.TextField()
|
||||
response_or_redirect = models.CharField(max_length=255)
|
||||
submitted_on = models.DateTimeField(auto_now=True)
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -31,4 +31,3 @@ class ProfileForm(forms.ModelForm):
|
|||
|
||||
return last_name
|
||||
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -87,7 +87,7 @@
|
|||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% trans 'Are you sure that you want delete this user?' %}
|
||||
{% trans 'Are you sure that you want share the info of this credentials?' %}
|
||||
</div>
|
||||
<div class="modal-footer"></div>
|
||||
</div>
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
130
schemas/course-credential.json
Normal file
130
schemas/course-credential.json
Normal file
|
@ -0,0 +1,130 @@
|
|||
{
|
||||
"$id": "https://idhub.pangea.org/vc_schemas/courseCredential",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "NGO Course Credential Schema",
|
||||
"description": "A NGO Course Credential Schema awarded by a NGO federation and their NGO members, as proposed by Lafede.cat",
|
||||
"name": [
|
||||
{
|
||||
"value": "NGO Course Credential for participants",
|
||||
"lang": "en"
|
||||
},
|
||||
{
|
||||
"value": "Credencial per participants d'un curs impartit per una ONG",
|
||||
"lang": "ca_ES"
|
||||
},
|
||||
{
|
||||
"value": "Credencial para participantes de un curso impartido por una ONG",
|
||||
"lang": "es"
|
||||
}
|
||||
],
|
||||
"type": "object",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "https://idhub.pangea.org/vc_schemas/ebsi/attestation.json"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"credentialSubject": {
|
||||
"description": "Defines additional properties on credentialSubject: the given course followed by a student",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Defines a unique identifier (DID) of the credential subject: the credential of a completed course by a student",
|
||||
"type": "string"
|
||||
},
|
||||
"firstName": {
|
||||
"type": "string",
|
||||
"description": "The first name of the student"
|
||||
},
|
||||
"lastName": {
|
||||
"type": "string",
|
||||
"description": "The family name of the student"
|
||||
},
|
||||
"personalIdentifier": {
|
||||
"type": "string",
|
||||
"description": "The personal identifier of the student, such as ID number"
|
||||
},
|
||||
"issuedDate": {
|
||||
"type": "string",
|
||||
"description": "The date the credential was issued",
|
||||
"format": "date"
|
||||
},
|
||||
"modeOfInstruction": {
|
||||
"type": "string",
|
||||
"description": "The mode of instruction: online, in-person, etc."
|
||||
},
|
||||
"courseDuration": {
|
||||
"type": "integer",
|
||||
"description": "The duration of the course in hours"
|
||||
},
|
||||
"courseDays": {
|
||||
"type": "integer",
|
||||
"description": "The number of days the course lasts"
|
||||
},
|
||||
"courseName": {
|
||||
"type": "string",
|
||||
"description": "The name of the course"
|
||||
},
|
||||
"courseDescription": {
|
||||
"type": "string",
|
||||
"description": "The description of the course"
|
||||
},
|
||||
"gradingScheme": {
|
||||
"type": "string",
|
||||
"description": "The grading scheme used for the course"
|
||||
},
|
||||
"scoreAwarded": {
|
||||
"type": "integer",
|
||||
"description": "The score awarded to the student",
|
||||
"minimum": 0,
|
||||
"maximum": 10
|
||||
},
|
||||
"qualificationAwarded": {
|
||||
"type": "string",
|
||||
"description": "The qualification awarded to the student",
|
||||
"enum": [
|
||||
"A+",
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"D"
|
||||
]
|
||||
},
|
||||
"courseLevel": {
|
||||
"type": "string",
|
||||
"description": "The level of the course"
|
||||
},
|
||||
"courseFramework": {
|
||||
"type": "string",
|
||||
"description": "The framework in which the course belongs to"
|
||||
},
|
||||
"courseCredits": {
|
||||
"type": "integer",
|
||||
"description": "The number of (ECTS) credits awarded for the course"
|
||||
},
|
||||
"dateOfAssessment": {
|
||||
"type": "string",
|
||||
"description": "The date of assessment",
|
||||
"format": "date"
|
||||
},
|
||||
"evidenceAssessment": {
|
||||
"type": "string",
|
||||
"description": "The evidence of the assessment: final exam, presence, participation"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"firstName",
|
||||
"lastName",
|
||||
"personalIdentifier",
|
||||
"issuedDate",
|
||||
"modeOfInstruction",
|
||||
"courseDuration",
|
||||
"courseDays",
|
||||
"courseName"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
176
schemas/device-purchase.json
Normal file
176
schemas/device-purchase.json
Normal file
|
@ -0,0 +1,176 @@
|
|||
{
|
||||
"$id": "https://idhub.pangea.org/vc_schemas/devicePurchase.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Purchase of an eReuse device",
|
||||
"description": "A device purchase credential is a proof of purchase of a device from a seller by a buyer",
|
||||
"name": [
|
||||
{
|
||||
"value": "Device purchase credential",
|
||||
"lang": "en"
|
||||
},
|
||||
{
|
||||
"value": "Credencial d'adquisició d'un dispositiu",
|
||||
"lang": "ca_ES"
|
||||
},
|
||||
{
|
||||
"value": "Credencial de adquisición de un dispositivo",
|
||||
"lang": "es"
|
||||
}
|
||||
],
|
||||
"type": "object",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "https://idhub.pangea.org/vc_schemas/ebsi/attestation.json"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"credentialSubject": {
|
||||
"description": "Defines additional properties on credentialSubject: the purchase act, to qualify as simplified invoice (ES)",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Defines a unique identifier (DID) of the credential subject: the purchase act/transaction",
|
||||
"type": "string"
|
||||
},
|
||||
"invoiceNumber": {
|
||||
"description": "The invoice number of the purchase act/transaction",
|
||||
"type": "string"
|
||||
},
|
||||
"totalAmount": {
|
||||
"description": "The total amount of the transaction in local currency units: Euro by default",
|
||||
"type": "string"
|
||||
},
|
||||
"sellerId": {
|
||||
"description": "Defines a unique identifier (DID) of the seller actor",
|
||||
"type": "string"
|
||||
},
|
||||
"sellerBusinessName": {
|
||||
"description": "Business name of the credential subject in the seller role",
|
||||
"type": "string"
|
||||
},
|
||||
"sellerName": {
|
||||
"description": "Name of the credential subject in the seller role",
|
||||
"type": "string"
|
||||
},
|
||||
"sellerSurname": {
|
||||
"description": "Surname of the credential subject in the seller role, if natural person",
|
||||
"type": "string"
|
||||
},
|
||||
"sellerEmail": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"sellerPhoneNumber": {
|
||||
"type": "string"
|
||||
},
|
||||
"sellerIdentityDocType": {
|
||||
"description": "Type of the Identity Document of the credential subject in the seller role",
|
||||
"type": "string"
|
||||
},
|
||||
"sellerIdentityNumber": {
|
||||
"description": "Number of the Identity Document of the credential subject in the seller role",
|
||||
"type": "string"
|
||||
},
|
||||
"buyerId": {
|
||||
"description": "Defines a unique identifier (DID) of the credential subject: the buyer actor",
|
||||
"type": "string"
|
||||
},
|
||||
"buyerBusinessName": {
|
||||
"description": "Business name of the credential subject in the buyer role",
|
||||
"type": "string"
|
||||
},
|
||||
"buyerName": {
|
||||
"description": "Name of the credential subject in the buyer role",
|
||||
"type": "string"
|
||||
},
|
||||
"buyerSurname": {
|
||||
"description": "Surname of the credential subject in the buyer role, if natural person",
|
||||
"type": "string"
|
||||
},
|
||||
"buyerEmail": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"buyerPhoneNumber": {
|
||||
"type": "string"
|
||||
},
|
||||
"buyerIdentityDocType": {
|
||||
"description": "Type of the Identity Document of the credential subject in the buyer role",
|
||||
"type": "string"
|
||||
},
|
||||
"buyerIdentityNumber": {
|
||||
"description": "Number of the Identity Document of the credential subject in the buyer role",
|
||||
"type": "string"
|
||||
},
|
||||
"deliveryStreetAddress": {
|
||||
"description": "Postal address of the credential Subject in the buyer role",
|
||||
"type": "string"
|
||||
},
|
||||
"deliveryPostCode": {
|
||||
"description": "Postal code of the credential Subject in the buyer role",
|
||||
"type": "string"
|
||||
},
|
||||
"deliveryCity": {
|
||||
"description": "City of the credential Subject in the buyer role",
|
||||
"type": "string"
|
||||
},
|
||||
"supplyDescription": {
|
||||
"description": "Description of the product/device supplied, needed in a simplified invoice",
|
||||
"type": "string"
|
||||
},
|
||||
"taxRate": {
|
||||
"description": "Description of Tax rate (VAT) and optionally also the expression VAT included, or special circumstances such as REBU, needed in a simplified invoice",
|
||||
"type": "string"
|
||||
},
|
||||
"deviceChassisId": {
|
||||
"description": "Chassis identifier of the device",
|
||||
"type": "string"
|
||||
},
|
||||
"devicePreciseHardwareId": {
|
||||
"description": "Chassis precise hardware configuration identifier of the device",
|
||||
"type": "string"
|
||||
},
|
||||
"depositId": {
|
||||
"description": "Identifier of an economic deposit left on loan to be returned under conditions",
|
||||
"type": "string"
|
||||
},
|
||||
"sponsorId": {
|
||||
"description": "Identifier of the sponsor of this purchase that paid the economic cost of the purchase",
|
||||
"type": "string"
|
||||
},
|
||||
"sponsorName": {
|
||||
"description": "Name of the sponsor of this purchase that paid the economic cost of the purchase",
|
||||
"type": "string"
|
||||
},
|
||||
"purchaseDate": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"invoiceDate": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"invoiceNumber",
|
||||
"totalAmount",
|
||||
"sellerId",
|
||||
"sellerName",
|
||||
"sellerBusinessName",
|
||||
"sellerSurname",
|
||||
"sellerEmail",
|
||||
"sellerIdentityDocType",
|
||||
"sellerIdentityNumber",
|
||||
"buyerId",
|
||||
"buyerEmail",
|
||||
"supplyDescription",
|
||||
"taxRate",
|
||||
"deviceChassisId",
|
||||
"purchaseDate"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
122
schemas/federation-membership.json
Normal file
122
schemas/federation-membership.json
Normal file
|
@ -0,0 +1,122 @@
|
|||
{
|
||||
"$id": "https://idhub.pangea.org/vc_schemas/federationMembership.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Federation membership",
|
||||
"description": "The federation membership specifies participation of a NGO into a NGO federation, as proposed by Lafede.cat",
|
||||
"name": [
|
||||
{
|
||||
"value": "NGO federation membership",
|
||||
"lang": "en"
|
||||
},
|
||||
{
|
||||
"value": "Membre de federació ONGs",
|
||||
"lang": "ca_ES"
|
||||
},
|
||||
{
|
||||
"value": "Miembro de federación de ONGs",
|
||||
"lang": "es"
|
||||
}
|
||||
],
|
||||
"type": "object",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "https://idhub.pangea.org/vc_schemas/ebsi/attestation.json"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"credentialSubject": {
|
||||
"description": "Defines additional properties on credentialSubject",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Defines a unique identifier of the credential subject",
|
||||
"type": "string"
|
||||
},
|
||||
"federation": {
|
||||
"description": "Federation the credential subject is affiliated with",
|
||||
"type": "string"
|
||||
},
|
||||
"legalName": {
|
||||
"description": "Legal name of the affiliated organisation",
|
||||
"type": "string"
|
||||
},
|
||||
"shortName": {
|
||||
"description": "Short name of the organisation of the affiliated organisation",
|
||||
"type": "string"
|
||||
},
|
||||
"registrationIdentifier": {
|
||||
"description": "Registration identifier of the affiliated organisation",
|
||||
"type": "string"
|
||||
},
|
||||
"publicRegistry": {
|
||||
"description": "Registry where the affiliated organisation is registered: 'Generalitat de Catalunya', 'Ministerio del interior de España'",
|
||||
"type": "string"
|
||||
},
|
||||
"streetAddress": {
|
||||
"description": "Postal address of the member organisation: legal address",
|
||||
"type": "string"
|
||||
},
|
||||
"postCode": {
|
||||
"description": "Postal code of the member organisation",
|
||||
"type": "string"
|
||||
},
|
||||
"city": {
|
||||
"description": "City of the member organisation",
|
||||
"type": "string"
|
||||
},
|
||||
"taxReference": {
|
||||
"description": "Tax reference as VAT registration of the member organisation",
|
||||
"type": "string"
|
||||
},
|
||||
"membershipType": {
|
||||
"description": "Type of membership: full / observer",
|
||||
"type": "string"
|
||||
},
|
||||
"membershipStatus": {
|
||||
"description": "Type of membership: active / suspended, etc.",
|
||||
"type": "string"
|
||||
},
|
||||
"membershipId": {
|
||||
"description": "Membership identifier: an internal unique number or code",
|
||||
"type": "string"
|
||||
},
|
||||
"membershipSince": {
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string"
|
||||
},
|
||||
"website": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"evidence": {
|
||||
"description": "Type of evidence used for attestation",
|
||||
"type": "string"
|
||||
},
|
||||
"certificationDate": {
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"legalName",
|
||||
"postCode",
|
||||
"city",
|
||||
"membershipType",
|
||||
"membershipStatus",
|
||||
"federation",
|
||||
"membershipSince",
|
||||
"certificationDate"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
103
schemas/financial-vulnerability.json
Normal file
103
schemas/financial-vulnerability.json
Normal file
|
@ -0,0 +1,103 @@
|
|||
{
|
||||
"$id": "https://idhub.pangea.org/vc_schemas/financial-vulnerability.json",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Financial Vulnerability Credential",
|
||||
"description": "A Financial Vulnerability Credential is issued to individuals or families to prove their financial vulnerability based on various factors, with the objective of presenting it to a third party to receive benefits or services.",
|
||||
"name": [
|
||||
{
|
||||
"value": "Financial Vulnerability Credential",
|
||||
"lang": "en"
|
||||
},
|
||||
{
|
||||
"value": "Credencial de Vulnerabilitat Financera",
|
||||
"lang": "ca_ES"
|
||||
},
|
||||
{
|
||||
"value": "Credencial de Vulnerabilidad Financiera",
|
||||
"lang": "es"
|
||||
}
|
||||
],
|
||||
"type": "object",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "https://idhub.pangea.org/vc_schemas/ebsi/attestation.json"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"credentialSubject": {
|
||||
"description": "Defines additional properties on credentialSubject",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Defines a unique identifier (DID) of the credential subject",
|
||||
"type": "string"
|
||||
},
|
||||
"firstName": {
|
||||
"description": "Name of the credential subject",
|
||||
"type": "string"
|
||||
},
|
||||
"lastName": {
|
||||
"description": "Surname of the credential subject",
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"phoneNumber": {
|
||||
"type": "string"
|
||||
},
|
||||
"identityDocType": {
|
||||
"description": "Type of the Identity Document of the credential subject",
|
||||
"type": "string"
|
||||
},
|
||||
"identityNumber": {
|
||||
"description": "Number of the Identity Document of the credential subject",
|
||||
"type": "string"
|
||||
},
|
||||
"streetAddress": {
|
||||
"description": "Postal address of the credential Subject",
|
||||
"type": "string"
|
||||
},
|
||||
"socialWorkerName": {
|
||||
"description": "Name of the social worker that support the vulnerable person/family",
|
||||
"type": "string"
|
||||
},
|
||||
"socialWorkerSurname": {
|
||||
"description": "Surname of the social worker that support the vulnerable person/family",
|
||||
"type": "string"
|
||||
},
|
||||
"financialVulnerabilityScore": {
|
||||
"description": "Measure of an individual's susceptibility to financial hardship",
|
||||
"type": "string"
|
||||
},
|
||||
"amountCoveredByOtherAids": {
|
||||
"type": "string"
|
||||
},
|
||||
"connectivityOptionList": {
|
||||
"type": "string"
|
||||
},
|
||||
"assessmentDate": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"firstName",
|
||||
"lastName",
|
||||
"email",
|
||||
"identityDocType",
|
||||
"identityNumber",
|
||||
"streetAddress",
|
||||
"socialWorkerName",
|
||||
"socialWorkerSurname",
|
||||
"financialVulnerabilityScore",
|
||||
"amountCoveredByOtherAids",
|
||||
"assessmentDate"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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')
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
/user/event-log [GET] -> vista d'esdeveniments
|
||||
sense enllaços rapids a les accions
|
||||
/user/dashboard [GET, POST] -> vista de dades personals
|
||||
/user/roles [GET] -> vista de rols (????)
|
||||
/user/gdpr [GET] -> info de la gdpr
|
||||
|
||||
/user/wallet/dids [GET, POST]
|
||||
/user/wallet/dids/<id:integer> [GET, DELETE]
|
||||
/user/credentials [GET]
|
||||
/user/credentials/<id:integer> [GET, DELETE]
|
||||
/user/credentials/request [GET, POST]
|
||||
*** falta "present credentials" ??? ***
|
||||
|
||||
|
||||
/admin/
|
36
utils/certs.py
Normal file
36
utils/certs.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
from pyhanko.sign.signers import SimpleSigner
|
||||
from cryptography.hazmat.primitives.serialization import pkcs12
|
||||
from pyhanko_certvalidator.registry import SimpleCertificateStore
|
||||
from pyhanko.keys import _translate_pyca_cryptography_cert_to_asn1
|
||||
from pyhanko.keys import _translate_pyca_cryptography_key_to_asn1
|
||||
|
||||
|
||||
def load_cert(pfx_bytes, passphrase):
|
||||
try:
|
||||
(
|
||||
private_key,
|
||||
cert,
|
||||
other_certs_pkcs12,
|
||||
) = pkcs12.load_key_and_certificates(pfx_bytes, passphrase)
|
||||
except (IOError, ValueError, TypeError) as e:
|
||||
# logger.error(
|
||||
# 'Could not load key material from PKCS#12 file', exc_info=e
|
||||
# )
|
||||
return None
|
||||
|
||||
kinfo = _translate_pyca_cryptography_key_to_asn1(private_key)
|
||||
cert = _translate_pyca_cryptography_cert_to_asn1(cert)
|
||||
other_certs_pkcs12 = set(
|
||||
map(_translate_pyca_cryptography_cert_to_asn1, other_certs_pkcs12)
|
||||
)
|
||||
|
||||
cs = SimpleCertificateStore()
|
||||
certs_to_register = set(other_certs_pkcs12)
|
||||
cs.register_multiple(certs_to_register)
|
||||
return SimpleSigner(
|
||||
signing_key=kinfo,
|
||||
signing_cert=cert,
|
||||
cert_registry=cs,
|
||||
signature_mechanism=None,
|
||||
prefer_pss=False,
|
||||
)
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue