From 7db6d1f4e3f73241a0fd008f1352bf2af3b0b06f Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 24 Nov 2023 16:36:05 +0100 Subject: [PATCH 01/70] add oidc4vp module --- idhub/management/commands/initial_datas.py | 2 +- idhub/models.py | 16 ----- idhub/user/forms.py | 5 +- oidc4vp/__init__.py | 0 oidc4vp/admin.py | 3 + oidc4vp/apps.py | 6 ++ oidc4vp/forms.py | 41 ++++++++++++ oidc4vp/migrations/__init__.py | 0 oidc4vp/models.py | 77 ++++++++++++++++++++++ oidc4vp/tests.py | 3 + oidc4vp/views.py | 17 +++++ trustchain_idhub/settings.py | 2 + 12 files changed, 152 insertions(+), 20 deletions(-) create mode 100644 oidc4vp/__init__.py create mode 100644 oidc4vp/admin.py create mode 100644 oidc4vp/apps.py create mode 100644 oidc4vp/forms.py create mode 100644 oidc4vp/migrations/__init__.py create mode 100644 oidc4vp/models.py create mode 100644 oidc4vp/tests.py create mode 100644 oidc4vp/views.py diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py index acdf6c7..5de2603 100644 --- a/idhub/management/commands/initial_datas.py +++ b/idhub/management/commands/initial_datas.py @@ -5,7 +5,7 @@ from pathlib import Path from django.core.management.base import BaseCommand, CommandError from django.contrib.auth import get_user_model from decouple import config -from idhub.models import Organization +from oidc4vp.models import Organization User = get_user_model() diff --git a/idhub/models.py b/idhub/models.py index 53f8186..09ba43b 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -1,6 +1,5 @@ import json import pytz -import requests import datetime from django.db import models from django.conf import settings @@ -639,18 +638,3 @@ class UserRol(models.Model): class Meta: unique_together = ('user', 'service',) - - -class Organization(models.Model): - name = models.CharField(max_length=250) - url = models.CharField( - help_text=_("Url where to send the presentation"), - max_length=250 - ) - - def __str__(self): - return self.name - - def send(self, cred): - return - requests.post(self.url, data=cred.data) diff --git a/idhub/user/forms.py b/idhub/user/forms.py index 53a1149..57e3d36 100644 --- a/idhub/user/forms.py +++ b/idhub/user/forms.py @@ -1,7 +1,7 @@ from django import forms from idhub_auth.models import User -from idhub.models import DID, VerificableCredential, Organization - +from idhub.models import DID, VerificableCredential +from oidc4vp.models import Organization class ProfileForm(forms.ModelForm): @@ -56,7 +56,6 @@ class RequestCredentialForm(forms.Form): return - class CredentialPresentationForm(forms.Form): organization = forms.ChoiceField(choices=[]) credential = forms.ChoiceField(choices=[]) diff --git a/oidc4vp/__init__.py b/oidc4vp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oidc4vp/admin.py b/oidc4vp/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/oidc4vp/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/oidc4vp/apps.py b/oidc4vp/apps.py new file mode 100644 index 0000000..1e00f2b --- /dev/null +++ b/oidc4vp/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class Oidc4VpConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'oidc4vp' diff --git a/oidc4vp/forms.py b/oidc4vp/forms.py new file mode 100644 index 0000000..f4f56d5 --- /dev/null +++ b/oidc4vp/forms.py @@ -0,0 +1,41 @@ +from django import forms + + +class Organization(forms.Form): + wallet = forms.ChoiceField( + "Wallet", + choices=[(x.id, x.name) for x in Organization.objects.all()] + ) + + def clean_wallet(self): + data = self.cleaned_data["wallet"] + organization = Organization.objects.filter( + id=data + ) + + if not organization.exists(): + raise ValidationError("organization is not valid!") + + self.organization = organization.first() + + return data + + def authorize(self): + data = { + "response_type": "vp_token", + "response_mode": "direct_post", + "client_id": self.organization.client_id, + "response_uri": settings.RESPONSE_URI, + "presentation_definition": self.pv_definition(), + "nonce": "" + } + query_dict = QueryDict('', mutable=True) + query_dict.update(data) + + url = '{response_uri}/authorize?{params}'.format( + response_uri=self.organization.response_uri, + params=query_dict.urlencode() + ) + + def pv_definition(self): + return "" diff --git a/oidc4vp/migrations/__init__.py b/oidc4vp/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oidc4vp/models.py b/oidc4vp/models.py new file mode 100644 index 0000000..ac6da6d --- /dev/null +++ b/oidc4vp/models.py @@ -0,0 +1,77 @@ +import requests + +from django.db import models +from django.http import QueryDict +from django.utils.translation import gettext_lazy as _ +from idhub_auth.models import User + + +class Organization(models.Model): + name = models.CharField(max_length=250) + client_id = models.CharField() + client_secret = models.CharField() + response_uri = models.URLField( + help_text=_("Url where to send the presentation"), + max_length=250 + ) + + def __str__(self): + return self.name + + def send(self, vcred): + return requests.post(self.url, data=vcred) + + +class Authorization(models.Model): + created = models.DateTimeField(auto_now=True) + presentation_definition = models.CharField() + organization = models.ForeignKey( + Organization, + on_delete=models.CASCADE, + related_name='vp_tokens', + null=True, + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + null=True, + ) + + def authorize(self): + response_uri = self.__class__.objects.filter( + response_uri=settings.RESPONSE_URI + ) + data = { + "response_type": "vp_token", + "response_mode": "direct_post", + "client_id": "...", + "response_uri": response_uri, + "presentation_definition": "...", + "nonce": "" + } + query_dict = QueryDict('', mutable=True) + query_dict.update(data) + + url = '{response_uri}/authorize?{params}'.format( + response_uri=self.organization.response_uri, + params=query_dict.urlencode() + ) + +class OAuth2VPToken(models.Model): + created = models.DateTimeField(auto_now=True) + response_code = models.CharField() + result_verify = models.BooleanField() + presentation_definition = models.CharField() + organization = models.ForeignKey( + Organization, + on_delete=models.CASCADE, + related_name='vp_tokens', + null=True, + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='vp_tokens', + null=True, + ) + diff --git a/oidc4vp/tests.py b/oidc4vp/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/oidc4vp/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/oidc4vp/views.py b/oidc4vp/views.py new file mode 100644 index 0000000..42e1a58 --- /dev/null +++ b/oidc4vp/views.py @@ -0,0 +1,17 @@ +from django.shortcuts import render + +class PeopleEditView(People, FormView): + template_name = "idhub/admin/user_edit.html" + form_class = ProfileForm + success_url = reverse_lazy('idhub:admin_people_list') + + + def form_valid(self, form): + user = form.save() + messages.success(self.request, _('The credential was sended successfully')) + # Event.set_EV_USR_UPDATED_BY_ADMIN(user) + # Event.set_EV_USR_UPDATED(user) + + return super().form_valid(form) + + diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index 30bbc0a..df36198 100644 --- a/trustchain_idhub/settings.py +++ b/trustchain_idhub/settings.py @@ -71,6 +71,7 @@ INSTALLED_APPS = [ 'django_extensions', 'django_bootstrap5', 'idhub_auth', + 'oidc4vp', 'idhub' ] @@ -183,3 +184,4 @@ USE_I18N = True USE_L10N = True AUTH_USER_MODEL = 'idhub_auth.User' +RESPONSE_URI = config('RESPONSE_URI', default="") From 9e8596f39d07d1b0567647c52c0720ade273cc2c Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 24 Nov 2023 17:53:43 +0100 Subject: [PATCH 02/70] add more details to flow --- oidc4vp/migrations/0001_initial.py | 134 +++++++++++++++++++++++++++++ oidc4vp/models.py | 86 +++++++++++++++--- 2 files changed, 207 insertions(+), 13 deletions(-) create mode 100644 oidc4vp/migrations/0001_initial.py diff --git a/oidc4vp/migrations/0001_initial.py b/oidc4vp/migrations/0001_initial.py new file mode 100644 index 0000000..cc0d8fd --- /dev/null +++ b/oidc4vp/migrations/0001_initial.py @@ -0,0 +1,134 @@ +# Generated by Django 4.2.5 on 2023-11-24 16:53 + +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), + ), + ('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 + ), + ), + ( + 'client_secret', + models.CharField( + default=oidc4vp.models.set_client_secret, 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)), + ('code', models.CharField(max_length=250)), + ('result_verify', models.BooleanField(max_length=250)), + ('presentation_definition', models.CharField(max_length=250)), + ( + 'authorization', + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to='oidc4vp.authorization', + ), + ), + ( + 'organization', + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='vp_tokens', + to='oidc4vp.organization', + ), + ), + ( + 'user', + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='vp_tokens', + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.AddField( + model_name='authorization', + name='organization', + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='authorizations', + to='oidc4vp.organization', + ), + ), + migrations.AddField( + model_name='authorization', + name='user', + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/oidc4vp/models.py b/oidc4vp/models.py index ac6da6d..fa273b7 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -1,34 +1,80 @@ import requests +import secrets -from django.db import models +from django.conf import settings from django.http import QueryDict from django.utils.translation import gettext_lazy as _ from idhub_auth.models import User +from django.db import models + + +SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + + +def gen_salt(length: int) -> str: + """Generate a random string of SALT_CHARS with specified ``length``.""" + if length <= 0: + raise ValueError("Salt length must be positive") + + return "".join(secrets.choice(SALT_CHARS) for _ in range(length)) + + +def set_client_id(): + return gen_salt(24) + + +def set_client_secret(): + return gen_salt(48) + + +def set_code(): + return gen_salt(24) class Organization(models.Model): + """ + This class represent a member of one net trust or federated host + """ name = models.CharField(max_length=250) - client_id = models.CharField() - client_secret = models.CharField() + client_id = models.CharField(max_length=24, default=set_client_id) + client_secret = models.CharField(max_length=48, default=set_client_secret) response_uri = models.URLField( - help_text=_("Url where to send the presentation"), + help_text=_("Url where to send the verificable presentation"), max_length=250 ) + def send(self, vp): + """ + Send the verificable presentation to Verifier + """ + org = Organization.objects.get( + response_uri=settings.RESPONSE_URI + ) + auth = (org.client_id, org.client_secret) + return requests.post(self.url, data=vp, auth=auth) + def __str__(self): return self.name - def send(self, vcred): - return requests.post(self.url, data=vcred) + +################### +# Verifier clases # +################### class Authorization(models.Model): + """ + This class represent a query through browser the client to the wallet. + The Verifier need to do a redirection to the user to Wallet. + The code we use as a soft foreing key between Authorization and OAuth2VPToken. + """ + code = models.CharField(max_length=24, default=set_code) created = models.DateTimeField(auto_now=True) - presentation_definition = models.CharField() + presentation_definition = models.CharField(max_length=250) organization = models.ForeignKey( Organization, on_delete=models.CASCADE, - related_name='vp_tokens', + related_name='authorizations', null=True, ) user = models.ForeignKey( @@ -44,10 +90,10 @@ class Authorization(models.Model): data = { "response_type": "vp_token", "response_mode": "direct_post", - "client_id": "...", + "client_id": self.organization.client_id, "response_uri": response_uri, "presentation_definition": "...", - "nonce": "" + "nonce": gen_salt(5), } query_dict = QueryDict('', mutable=True) query_dict.update(data) @@ -56,12 +102,18 @@ class Authorization(models.Model): response_uri=self.organization.response_uri, params=query_dict.urlencode() ) + return url + class OAuth2VPToken(models.Model): + """ + This class represent the response of Wallet to Verifier + and the result of verify. + """ created = models.DateTimeField(auto_now=True) - response_code = models.CharField() - result_verify = models.BooleanField() - presentation_definition = models.CharField() + code = models.CharField(max_length=250) + result_verify = models.BooleanField(max_length=250) + presentation_definition = models.CharField(max_length=250) organization = models.ForeignKey( Organization, on_delete=models.CASCADE, @@ -74,4 +126,12 @@ class OAuth2VPToken(models.Model): related_name='vp_tokens', null=True, ) + authorization = models.ForeignKey( + Authorization, + on_delete=models.SET_NULL, + null=True, + ) + + def verifing(self): + pass From b182c86a622d10853931277b7ec5a8b5e8ed8f79 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 24 Nov 2023 18:10:43 +0100 Subject: [PATCH 03/70] fix nullable field --- oidc4vp/migrations/0001_initial.py | 4 ++-- oidc4vp/models.py | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/oidc4vp/migrations/0001_initial.py b/oidc4vp/migrations/0001_initial.py index cc0d8fd..c88d0f8 100644 --- a/oidc4vp/migrations/0001_initial.py +++ b/oidc4vp/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-11-24 16:53 +# Generated by Django 4.2.5 on 2023-11-24 17:10 from django.conf import settings from django.db import migrations, models @@ -50,7 +50,7 @@ class Migration(migrations.Migration): ( 'client_id', models.CharField( - default=oidc4vp.models.set_client_id, max_length=24 + default=oidc4vp.models.set_client_id, max_length=24, unique=True ), ), ( diff --git a/oidc4vp/models.py b/oidc4vp/models.py index fa273b7..e26464a 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -36,8 +36,15 @@ class Organization(models.Model): This class represent a member of one net trust or federated host """ name = models.CharField(max_length=250) - client_id = models.CharField(max_length=24, default=set_client_id) - client_secret = models.CharField(max_length=48, default=set_client_secret) + client_id = models.CharField( + max_length=24, + default=set_client_id, + unique=True + ) + client_secret = models.CharField( + max_length=48, + default=set_client_secret + ) response_uri = models.URLField( help_text=_("Url where to send the verificable presentation"), max_length=250 From 7e99c0ca9f0ce3cf288993c8ee15793280aa72a7 Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Mon, 27 Nov 2023 05:34:09 +0100 Subject: [PATCH 04/70] Testing branches --- utils/idhub_ssikit/README.md | 73 ++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/utils/idhub_ssikit/README.md b/utils/idhub_ssikit/README.md index e69de29..8443717 100644 --- a/utils/idhub_ssikit/README.md +++ b/utils/idhub_ssikit/README.md @@ -0,0 +1,73 @@ +# Helper routines to manage DIDs/VC/VPs + +This module is a wrapper around the functions exported by SpruceID's `DIDKit` framework. + +## DID generation and storage + +For now DIDs are of the kind `did:key`, with planned support for `did:web` in the near future. + +Creation of a DID involves two steps: +* Generate a unique DID controller key +* Derive a `did:key` type from the key + +Both must be stored in the IdHub database and linked to a `User` for later retrieval. + +```python +# Use case: generate and link a new DID for an existing user +user = request.user # ... + +controller_key = idhub_ssikit.generate_did_controller_key() +did_string = idhub_ssikit.keydid_from_controller_key(controller_key) + + +did = idhub.models.DID( + did = did_string, + user = user +) +did_controller_key = idhub.models.DIDControllerKey( + key_material = controller_key, + owner_did = did +) + +did.save() +did_controller_key.save() +``` + +## Verifiable Credential issuance + +Verifiable Credential templates are stored as Jinja2 (TBD) templates in `/schemas` folder. Please examine each template to see what data must be passed to it in order to render. + +The data passed to the template must at a minimum include: +* issuer_did +* subject_did +* vc_id + +For example, in order to render `/schemas/member-credential.json`: + +```python +from jinja2 import Environment, FileSystemLoader, select_autoescape +import idhub_ssikit + +env = Environment( + loader=FileSystemLoader("vc_templates"), + autoescape=select_autoescape() +) +unsigned_vc_template = env.get_template("member-credential.json") + +issuer_user = request.user +issuer_did = user.dids[0] # TODO: Django ORM pseudocode +issuer_did_controller_key = did.keys[0] # TODO: Django ORM pseudocode + +data = { + "vc_id": "http://pangea.org/credentials/3731", + "issuer_did": issuer_did, + "subject_did": "did:web:[...]", + "issuance_date": "2020-08-19T21:41:50Z", + "subject_is_member_of": "Pangea" +} +signed_credential = idhub_ssikit.render_and_sign_credential( + unsigned_vc_template, + issuer_did_controller_key, + data +) +``` \ No newline at end of file From fa5a2d172eef55c65af54cb8dc7763c3c13af44a Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Mon, 27 Nov 2023 05:36:58 +0100 Subject: [PATCH 05/70] Added some VC templates --- idhub/templates/credentials/exo.json | 30 +++++++++++++++++ idhub/templates/credentials/openarms.json | 33 +++++++++++++++++++ idhub/templates/credentials/paremanel.json | 30 +++++++++++++++++ .../credentials/verifiable_presentation.json | 11 +++++++ 4 files changed, 104 insertions(+) create mode 100644 idhub/templates/credentials/exo.json create mode 100644 idhub/templates/credentials/openarms.json create mode 100644 idhub/templates/credentials/paremanel.json create mode 100644 idhub/templates/credentials/verifiable_presentation.json diff --git a/idhub/templates/credentials/exo.json b/idhub/templates/credentials/exo.json new file mode 100644 index 0000000..1aee10b --- /dev/null +++ b/idhub/templates/credentials/exo.json @@ -0,0 +1,30 @@ +{ + "@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" + } + } +} diff --git a/idhub/templates/credentials/openarms.json b/idhub/templates/credentials/openarms.json new file mode 100644 index 0000000..e2ecbc6 --- /dev/null +++ b/idhub/templates/credentials/openarms.json @@ -0,0 +1,33 @@ +{ + "@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" + } + } +} diff --git a/idhub/templates/credentials/paremanel.json b/idhub/templates/credentials/paremanel.json new file mode 100644 index 0000000..a40396b --- /dev/null +++ b/idhub/templates/credentials/paremanel.json @@ -0,0 +1,30 @@ +{ + "@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" + } + } +} diff --git a/idhub/templates/credentials/verifiable_presentation.json b/idhub/templates/credentials/verifiable_presentation.json new file mode 100644 index 0000000..752affb --- /dev/null +++ b/idhub/templates/credentials/verifiable_presentation.json @@ -0,0 +1,11 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "id": "http://example.org/presentations/3731", + "type": [ + "VerifiablePresentation" + ], + "holder": "{{ holder_did }}", + "verifiableCredential": {{ verifiable_credential_list }} +} From 737d2a7dceb93e885775db96c2bfd318b8019ba8 Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Mon, 27 Nov 2023 07:04:30 +0100 Subject: [PATCH 06/70] Verifier portal backchannel endpoint --- idhub/urls.py | 4 +++ idhub/verification_portal/__init__.py | 0 idhub/verification_portal/models.py | 21 +++++++++++++ idhub/verification_portal/views.py | 43 +++++++++++++++++++++++++++ requirements.txt | 1 + 5 files changed, 69 insertions(+) create mode 100644 idhub/verification_portal/__init__.py create mode 100644 idhub/verification_portal/models.py create mode 100644 idhub/verification_portal/views.py diff --git a/idhub/urls.py b/idhub/urls.py index 785f4d1..80dd103 100644 --- a/idhub/urls.py +++ b/idhub/urls.py @@ -20,6 +20,7 @@ from django.urls import path, reverse_lazy from .views import LoginView from .admin import views as views_admin from .user import views as views_user +from .verification_portal import views as views_verification_portal app_name = 'idhub' @@ -171,4 +172,7 @@ urlpatterns = [ name='admin_import'), path('admin/import/new', views_admin.ImportAddView.as_view(), name='admin_import_add'), + + path('verification_portal/verify/', views_verification_portal.verify, + name="verification_portal_verify") ] diff --git a/idhub/verification_portal/__init__.py b/idhub/verification_portal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/idhub/verification_portal/models.py b/idhub/verification_portal/models.py new file mode 100644 index 0000000..c8e1615 --- /dev/null +++ b/idhub/verification_portal/models.py @@ -0,0 +1,21 @@ +from django.db import models + + +class VPVerifyRequest(models.Model): + """ + `nonce` is an opaque random string used to lookup verification requests + `expected_credentials` is a JSON list of credential types that must be present in this VP. + Example: ["FinancialSituationCredential", "HomeConnectivitySurveyCredential"] + `expected_contents` is a JSON object that places optional constraints on the contents of the + returned VP. + Example: [{"FinancialSituationCredential": {"financial_vulnerability_score": "7"}}] + `action` is (for now) a JSON object describing the next steps to take if this verification + is successful. For example "send mail to with and " + Example: {"action": "send_mail", "params": {"to": "orders@somconnexio.coop", "subject": "New client", "body": ...} + `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() + submitted_on = models.DateTimeField(auto_now=True) diff --git a/idhub/verification_portal/views.py b/idhub/verification_portal/views.py new file mode 100644 index 0000000..afc922b --- /dev/null +++ b/idhub/verification_portal/views.py @@ -0,0 +1,43 @@ +import json + +from django.core.mail import send_mail +from django.http import HttpResponse +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: use request.POST["presentation_submission"] + 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!") + return HttpResponse("OK! Your verifiable presentation was successfully presented.") + diff --git a/requirements.txt b/requirements.txt index 745c483..83aea96 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ didkit==0.3.2 jinja2==3.1.2 jsonref==1.1.0 pyld==2.0.3 +more-itertools==10.1.0 From 2c4dca40b7639665159a0e3bac0cb6d554be0a38 Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Mon, 27 Nov 2023 07:11:39 +0100 Subject: [PATCH 07/70] minor changes to verifier --- idhub/verification_portal/models.py | 5 ++++- idhub/verification_portal/views.py | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/idhub/verification_portal/models.py b/idhub/verification_portal/models.py index c8e1615..0bd203a 100644 --- a/idhub/verification_portal/models.py +++ b/idhub/verification_portal/models.py @@ -3,7 +3,8 @@ from django.db import models class VPVerifyRequest(models.Model): """ - `nonce` is an opaque random string used to lookup verification requests + `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 @@ -12,10 +13,12 @@ class VPVerifyRequest(models.Model): `action` is (for now) a JSON object describing the next steps to take if this verification is successful. For example "send mail to with and " Example: {"action": "send_mail", "params": {"to": "orders@somconnexio.coop", "subject": "New client", "body": ...} + `response` is a URL that the user's wallet will redirect the user to. `submitted_on` is used (by a cronjob) to purge old entries that didn't complete verification """ nonce = models.CharField(max_length=50) expected_credentials = models.CharField(max_length=255) expected_contents = models.TextField() action = models.TextField() + response_or_redirect = models.CharField(max_length=255) submitted_on = models.DateTimeField(auto_now=True) diff --git a/idhub/verification_portal/views.py b/idhub/verification_portal/views.py index afc922b..df7cab2 100644 --- a/idhub/verification_portal/views.py +++ b/idhub/verification_portal/views.py @@ -1,7 +1,7 @@ import json from django.core.mail import send_mail -from django.http import HttpResponse +from django.http import HttpResponse, HttpResponseRedirect from .models import VPVerifyRequest from django.shortcuts import get_object_or_404 from more_itertools import flatten, unique_everseen @@ -39,5 +39,6 @@ def verify(request): pass else: raise Exception("Unknown action!") - return HttpResponse("OK! Your verifiable presentation was successfully presented.") + # OK! Your verifiable presentation was successfully presented. + return HttpResponseRedirect(vr.response_or_redirect) From 8566098f2c36a919d32b7e430f4bc59d0533cd34 Mon Sep 17 00:00:00 2001 From: Daniel Armengod Date: Mon, 27 Nov 2023 07:42:12 +0100 Subject: [PATCH 08/70] Added verify_presentation bindings and use them in verification_portal backend --- idhub/verification_portal/views.py | 7 +++++- utils/idhub_ssikit/__init__.py | 37 ++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/idhub/verification_portal/views.py b/idhub/verification_portal/views.py index df7cab2..486f4f7 100644 --- a/idhub/verification_portal/views.py +++ b/idhub/verification_portal/views.py @@ -2,6 +2,8 @@ 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 @@ -9,7 +11,10 @@ from more_itertools import flatten, unique_everseen def verify(request): assert request.method == "POST" - # TODO: use request.POST["presentation_submission"] + # 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 diff --git a/utils/idhub_ssikit/__init__.py b/utils/idhub_ssikit/__init__.py index 18a5ff2..c4ac0e3 100644 --- a/utils/idhub_ssikit/__init__.py +++ b/utils/idhub_ssikit/__init__.py @@ -72,3 +72,40 @@ def verify_credential(vc, proof_options): return didkit.verify_credential(vc, proof_options) return asyncio.run(inner()) + + +def issue_verifiable_presentation(vc_list: list[str], jwk_holder: str, holder_did: str) -> str: + async def inner(): + unsigned_vp = unsigned_vp_template.render(data) + signed_vp = await didkit.issue_presentation( + unsigned_vp, + '{"proofFormat": "ldp"}', + jwk_holder + ) + return signed_vp + + # TODO: convert from Jinja2 -> django-templates + env = Environment( + loader=FileSystemLoader("vc_templates"), + autoescape=select_autoescape() + ) + unsigned_vp_template = env.get_template("verifiable_presentation.json") + data = { + "holder_did": holder_did, + "verifiable_credential_list": "[" + ",".join(vc_list) + "]" + } + + return asyncio.run(inner()) + + +def verify_presentation(vp): + """ + Returns a (bool, str) tuple indicating whether the credential is valid. + If the boolean is true, the credential is valid and the second argument can be ignored. + If it is false, the VC is invalid and the second argument contains a JSON object with further information. + """ + async def inner(): + proof_options = '{"proofFormat": "ldp"}' + return didkit.verify_presentation(vp, proof_options) + + return asyncio.run(inner()) From 545bf76f46051271da487241f6ad97ed22050052 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 27 Nov 2023 10:59:30 +0100 Subject: [PATCH 09/70] merge ssi --- oidc4vp/models.py | 28 +++++++++++++++++++++++ oidc4vp/views.py | 58 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 73 insertions(+), 13 deletions(-) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index e26464a..5432aa3 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -75,6 +75,12 @@ class Authorization(models.Model): The Verifier need to do a redirection to the user to Wallet. The code we use as a soft foreing key between Authorization and OAuth2VPToken. """ + 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) + code = models.CharField(max_length=24, default=set_code) created = models.DateTimeField(auto_now=True) presentation_definition = models.CharField(max_length=250) @@ -142,3 +148,25 @@ class OAuth2VPToken(models.Model): def verifing(self): pass + +class VPVerifyRequest(models.Model): + """ + `nonce` is an opaque random string used to lookup verification requests. URL-safe. + Example: "UPBQ3JE2DGJYHP5CPSCRIGTHRTCYXMQPNQ" + `expected_credentials` is a JSON list of credential types that must be present in this VP. + Example: ["FinancialSituationCredential", "HomeConnectivitySurveyCredential"] + `expected_contents` is a JSON object that places optional constraints on the contents of the + returned VP. + Example: [{"FinancialSituationCredential": {"financial_vulnerability_score": "7"}}] + `action` is (for now) a JSON object describing the next steps to take if this verification + is successful. For example "send mail to with and " + Example: {"action": "send_mail", "params": {"to": "orders@somconnexio.coop", "subject": "New client", "body": ...} + `response` is a URL that the user's wallet will redirect the user to. + `submitted_on` is used (by a cronjob) to purge old entries that didn't complete verification + """ + nonce = models.CharField(max_length=50) + expected_credentials = models.CharField(max_length=255) + expected_contents = models.TextField() + action = models.TextField() + response_or_redirect = models.CharField(max_length=255) + submitted_on = models.DateTimeField(auto_now=True) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 42e1a58..486f4f7 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -1,17 +1,49 @@ -from django.shortcuts import render +import json -class PeopleEditView(People, FormView): - template_name = "idhub/admin/user_edit.html" - form_class = ProfileForm - success_url = reverse_lazy('idhub:admin_people_list') +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 form_valid(self, form): - user = form.save() - messages.success(self.request, _('The credential was sended successfully')) - # Event.set_EV_USR_UPDATED_BY_ADMIN(user) - # Event.set_EV_USR_UPDATED(user) - - return super().form_valid(form) - +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) From 19e44cf52d6de6052c39f1bf389db3499a78778f Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 28 Nov 2023 09:39:02 +0100 Subject: [PATCH 10/70] demand authorization --- examples/organizations.csv | 2 + idhub/management/commands/initial_datas.py | 2 +- idhub/templates/idhub/base.html | 2 +- idhub/urls.py | 9 +- idhub/user/forms.py | 47 ++++++-- idhub/user/views.py | 30 ++++- oidc4vp/models.py | 29 +++-- .../credentials/verifiable_presentation.json | 0 oidc4vp/views.py | 105 ++++++++++++------ trustchain_idhub/settings.py | 1 + trustchain_idhub/urls.py | 1 + 11 files changed, 171 insertions(+), 57 deletions(-) rename {idhub => oidc4vp}/templates/credentials/verifiable_presentation.json (100%) diff --git a/examples/organizations.csv b/examples/organizations.csv index 61f0f82..e79edda 100644 --- a/examples/organizations.csv +++ b/examples/organizations.csv @@ -1,2 +1,4 @@ "ExO";"https://verify.exo.cat" "Somos Connexión";"https://verify.somosconexion.coop" +"test2";"http://localhost:9000/verify" +"test1";"http://localhost:8000/verify" diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py index 5de2603..2ce6640 100644 --- a/idhub/management/commands/initial_datas.py +++ b/idhub/management/commands/initial_datas.py @@ -41,4 +41,4 @@ class Command(BaseCommand): def create_organizations(self, name, url): - Organization.objects.create(name=name, url=url) + Organization.objects.create(name=name, response_uri=url) diff --git a/idhub/templates/idhub/base.html b/idhub/templates/idhub/base.html index c8d985b..3514c33 100644 --- a/idhub/templates/idhub/base.html +++ b/idhub/templates/idhub/base.html @@ -115,7 +115,7 @@ diff --git a/idhub/urls.py b/idhub/urls.py index 80dd103..48b9214 100644 --- a/idhub/urls.py +++ b/idhub/urls.py @@ -20,7 +20,7 @@ from django.urls import path, reverse_lazy from .views import LoginView from .admin import views as views_admin from .user import views as views_user -from .verification_portal import views as views_verification_portal +# from .verification_portal import views as views_verification_portal app_name = 'idhub' @@ -85,6 +85,9 @@ urlpatterns = [ 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/credentials_presentation/', views_user.CredentialsPresentationView.as_view(), name='user_credentials_presentation'), @@ -173,6 +176,6 @@ urlpatterns = [ path('admin/import/new', views_admin.ImportAddView.as_view(), name='admin_import_add'), - path('verification_portal/verify/', views_verification_portal.verify, - name="verification_portal_verify") + # path('verification_portal/verify/', views_verification_portal.verify, + # name="verification_portal_verify") ] diff --git a/idhub/user/forms.py b/idhub/user/forms.py index 57e3d36..8bacf89 100644 --- a/idhub/user/forms.py +++ b/idhub/user/forms.py @@ -1,4 +1,6 @@ +import requests from django import forms +from django.conf import settings from idhub_auth.models import User from idhub.models import DID, VerificableCredential from oidc4vp.models import Organization @@ -56,9 +58,40 @@ class RequestCredentialForm(forms.Form): return +class DemandAuthorizationForm(forms.Form): + organization = forms.ChoiceField(choices=[]) + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop('user', None) + super().__init__(*args, **kwargs) + self.fields['organization'].choices = [ + (x.id, x.name) for x in Organization.objects.filter() + if x.response_uri != settings.RESPONSE_URI + ] + + def save(self, commit=True): + self.org = Organization.objects.filter( + id=self.data['organization'] + ) + if not self.org.exists(): + return + + self.org = self.org[0] + + if commit: + url = self.org.demand_authorization() + auth = (self.org.client_id, self.org.client_secret) + # res = requests.get(url, auth=auth) + # import pdb; pdb.set_trace() + # if res.status == 200: + # return res.body + + return + + class CredentialPresentationForm(forms.Form): organization = forms.ChoiceField(choices=[]) - credential = forms.ChoiceField(choices=[]) + # credential = forms.ChoiceField(choices=[]) def __init__(self, *args, **kwargs): self.user = kwargs.pop('user', None) @@ -66,12 +99,12 @@ class CredentialPresentationForm(forms.Form): self.fields['organization'].choices = [ (x.id, x.name) for x in Organization.objects.filter() ] - self.fields['credential'].choices = [ - (x.id, x.type()) for x in VerificableCredential.objects.filter( - user=self.user, - status=VerificableCredential.Status.ISSUED - ) - ] + # self.fields['credential'].choices = [ + # (x.id, x.type()) for x in VerificableCredential.objects.filter( + # user=self.user, + # status=VerificableCredential.Status.ISSUED + # ) + # ] def save(self, commit=True): self.org = Organization.objects.filter( diff --git a/idhub/user/views.py b/idhub/user/views.py index 482b40e..f509016 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -12,7 +12,12 @@ from django.shortcuts import get_object_or_404, redirect from django.urls import reverse_lazy from django.http import HttpResponse from django.contrib import messages -from idhub.user.forms import ProfileForm, RequestCredentialForm, CredentialPresentationForm +from idhub.user.forms import ( + ProfileForm, + RequestCredentialForm, + CredentialPresentationForm, + DemandAuthorizationForm +) from idhub.mixins import UserView from idhub.models import DID, VerificableCredential, Event @@ -141,6 +146,28 @@ class CredentialsRequestView(MyWallet, FormView): return super().form_valid(form) +class DemandAuthorizationView(MyWallet, FormView): + template_name = "idhub/user/credentials_presentation.html" + subtitle = _('Credential presentation') + icon = 'bi bi-patch-check-fill' + form_class = DemandAuthorizationForm + success_url = reverse_lazy('idhub:user_demand_authorization') + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['user'] = self.request.user + return kwargs + + def form_valid(self, form): + authorization = form.save() + if authorization: + if authorization.get('redirect_uri'): + redirect(authorization.get('redirect_uri')) + else: + messages.error(self.request, _("Error sending credential!")) + return super().form_valid(form) + + class CredentialsPresentationView(MyWallet, FormView): template_name = "idhub/user/credentials_presentation.html" subtitle = _('Credential presentation') @@ -151,6 +178,7 @@ class CredentialsPresentationView(MyWallet, FormView): def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user + kwargs['authorize'] = self.request.GET.params.get("uri") return kwargs def form_valid(self, form): diff --git a/oidc4vp/models.py b/oidc4vp/models.py index 5432aa3..2d0d224 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -54,12 +54,27 @@ class Organization(models.Model): """ Send the verificable presentation to Verifier """ - org = Organization.objects.get( + org = self.__class__.objects.get( response_uri=settings.RESPONSE_URI ) auth = (org.client_id, org.client_secret) return requests.post(self.url, data=vp, auth=auth) + def demand_authorization(self): + """ + Send the a request for start a process of Verifier + """ + org = self.__class__.objects.get( + response_uri=settings.RESPONSE_URI + ) + # import pdb; pdb.set_trace() + url = "{url}/?demand_uri={redirect_uri}".format( + url=self.response_uri.strip("/"), + redirect_uri=settings.RESPONSE_URI + ) + auth = (org.client_id, org.client_secret) + return requests.get(url, auth=auth) + def __str__(self): return self.name @@ -75,11 +90,11 @@ class Authorization(models.Model): The Verifier need to do a redirection to the user to Wallet. The code we use as a soft foreing key between Authorization and OAuth2VPToken. """ - 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) + # 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) code = models.CharField(max_length=24, default=set_code) created = models.DateTimeField(auto_now=True) @@ -98,7 +113,7 @@ class Authorization(models.Model): def authorize(self): response_uri = self.__class__.objects.filter( - response_uri=settings.RESPONSE_URI + response_uri=settings.ALLOW_CODE_URI ) data = { "response_type": "vp_token", diff --git a/idhub/templates/credentials/verifiable_presentation.json b/oidc4vp/templates/credentials/verifiable_presentation.json similarity index 100% rename from idhub/templates/credentials/verifiable_presentation.json rename to oidc4vp/templates/credentials/verifiable_presentation.json diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 486f4f7..c217114 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -6,44 +6,75 @@ 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 +# from more_itertools import flatten, unique_everseen +from oidc4vp.models import Authorization + + +# class PeopleListView(People, TemplateView): +# template_name = "idhub/admin/people.html" +# subtitle = _('View users') +# icon = 'bi bi-person' + +# def get_context_data(self, **kwargs): +# context = super().get_context_data(**kwargs) +# context.update({ +# 'users': User.objects.filter(), +# }) +# return context + + +def DemandAuthorizationView(request): + assert request.method == "GET" + import pdb; pdb.set_trace() + params = request.GET.params + org = Organization.objects.filter( + url=params.get('redirect_uri') + ) + authorization = Authorization( + organization=org, + presentation_definition="MemberCredential" + ) + # authorization.save() + res = json.dumps({"uri": authorization.authorize()}) + return HttpResponse(res) 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) + import pdb; pdb.set_trace() +# assert request.method == "POST" +# # TODO: incorporate request.POST["presentation_submission"] as schema definition +# (presentation_valid, _) = verify_presentation(request.POST["vp_token"]) +# if not presentation_valid: +# raise Exception("Failed to verify signature on the given Verifiable Presentation.") +# vp = json.loads(request.POST["vp_token"]) +# nonce = vp["nonce"] +# # "vr" = verification_request +# vr = get_object_or_404(VPVerifyRequest, nonce=nonce) # TODO: return meaningful error, not 404 +# # Get a list of all included verifiable credential types +# included_credential_types = unique_everseen(flatten([ +# vc["type"] for vc in vp["verifiableCredential"] +# ])) +# # Check that it matches what we requested +# for requested_vc_type in json.loads(vr.expected_credentials): +# if requested_vc_type not in included_credential_types: +# raise Exception("You're missing some credentials we requested!") # TODO: return meaningful error +# # Perform whatever action we have to do +# action = json.loads(vr.action) +# if action["action"] == "send_mail": +# subject = action["params"]["subject"] +# to_email = action["params"]["to"] +# from_email = "noreply@verifier-portal" +# body = request.POST["vp-token"] +# send_mail( +# subject, +# body, +# from_email, +# [to_email] +# ) +# elif action["action"] == "something-else": +# pass +# else: +# raise Exception("Unknown action!") +# # OK! Your verifiable presentation was successfully presented. +# return HttpResponseRedirect(vr.response_or_redirect) diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index 4d90de7..ca45d06 100644 --- a/trustchain_idhub/settings.py +++ b/trustchain_idhub/settings.py @@ -186,3 +186,4 @@ USE_L10N = True AUTH_USER_MODEL = 'idhub_auth.User' RESPONSE_URI = config('RESPONSE_URI', default="") +ALLOW_CODE_URI= config('ALLOW_CODE_URI', default="") diff --git a/trustchain_idhub/urls.py b/trustchain_idhub/urls.py index f2fdc05..1668872 100644 --- a/trustchain_idhub/urls.py +++ b/trustchain_idhub/urls.py @@ -24,4 +24,5 @@ from django.contrib.auth import views as auth_views urlpatterns = [ # path('django-admin/', admin.site.urls), path('', include('idhub.urls')), + path('oidc4vp/', include('oidc4vp.urls')), ] From 47ea0bf067bd4eed1e50e83e90af51fa6eee4b86 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 28 Nov 2023 10:17:59 +0100 Subject: [PATCH 11/70] add translation --- locale/ca/LC_MESSAGES/django.mo | Bin 0 -> 51541 bytes locale/{ca_ES => ca}/LC_MESSAGES/django.po | 1233 +++++++++++--------- locale/es/LC_MESSAGES/django.mo | Bin 0 -> 380 bytes trustchain_idhub/settings.py | 1 + 4 files changed, 682 insertions(+), 552 deletions(-) create mode 100644 locale/ca/LC_MESSAGES/django.mo rename locale/{ca_ES => ca}/LC_MESSAGES/django.po (72%) create mode 100644 locale/es/LC_MESSAGES/django.mo diff --git a/locale/ca/LC_MESSAGES/django.mo b/locale/ca/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..965887f4db423c3dffd4e7096388d3e6cac521d2 GIT binary patch literal 51541 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, YEAR. # -#, fuzzy msgid "" msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" +"Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-11-21 14:37+0100\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" +"PO-Revision-Date: 2023-11-27 20:37+0100\n" +"Last-Translator: Leandro Navarro \n" +"Language-Team: \n" +"Language: ca\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.2.2\n" #: env/lib/python3.11/site-packages/bootstrap4/components.py:17 #: env/lib/python3.11/site-packages/bootstrap4/templates/bootstrap4/form_errors.html:3 #: env/lib/python3.11/site-packages/bootstrap4/templates/bootstrap4/messages.html:4 #: env/lib/python3.11/site-packages/django_bootstrap5/components.py:26 msgid "close" -msgstr "" +msgstr "tanca" #: env/lib/python3.11/site-packages/click/_termui_impl.py:518 #, python-brace-format msgid "{editor}: Editing failed" -msgstr "" +msgstr "{editor}: Error en l'edició" #: env/lib/python3.11/site-packages/click/_termui_impl.py:522 #, python-brace-format msgid "{editor}: Editing failed: {e}" -msgstr "" +msgstr "{editor}: Error en l'edició: {e}" #: env/lib/python3.11/site-packages/click/core.py:1120 msgid "Aborted!" -msgstr "" +msgstr "Interromput!" #: env/lib/python3.11/site-packages/click/core.py:1309 #: env/lib/python3.11/site-packages/click/decorators.py:559 msgid "Show this message and exit." -msgstr "" +msgstr "Mostra aquest missatge i surt." #: env/lib/python3.11/site-packages/click/core.py:1340 #: env/lib/python3.11/site-packages/click/core.py:1370 #, python-brace-format msgid "(Deprecated) {text}" -msgstr "" +msgstr "(Obsolet) {text}" #: env/lib/python3.11/site-packages/click/core.py:1387 msgid "Options" -msgstr "" +msgstr "Opcions" #: env/lib/python3.11/site-packages/click/core.py:1413 #, python-brace-format msgid "Got unexpected extra argument ({args})" msgid_plural "Got unexpected extra arguments ({args})" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "S'ha trobat un argument addicional inesperat ({args})" +msgstr[1] "S'han trobat uns arguments addicionals inesperats ({args})" #: env/lib/python3.11/site-packages/click/core.py:1429 msgid "DeprecationWarning: The command {name!r} is deprecated." -msgstr "" +msgstr "Advertència obsolescència: l'ordre {name!r} està obsoleta." #: env/lib/python3.11/site-packages/click/core.py:1636 msgid "Commands" -msgstr "" +msgstr "Ordres" #: env/lib/python3.11/site-packages/click/core.py:1668 msgid "Missing command." -msgstr "" +msgstr "Falta l'ordre." #: env/lib/python3.11/site-packages/click/core.py:1746 msgid "No such command {name!r}." -msgstr "" +msgstr "No hi ha aquesta comanda {name!r}." #: env/lib/python3.11/site-packages/click/core.py:2310 msgid "Value must be an iterable." -msgstr "" +msgstr "El valor ha de ser iterable." #: env/lib/python3.11/site-packages/click/core.py:2331 #, python-brace-format msgid "Takes {nargs} values but 1 was given." msgid_plural "Takes {nargs} values but {len} were given." -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Pren {nargs} valors però s'ha donat 1." +msgstr[1] "Pren {nargs} valors però s'han donat {len}." #: env/lib/python3.11/site-packages/click/core.py:2778 #, python-brace-format msgid "env var: {var}" -msgstr "" +msgstr "env var: {var}" #: env/lib/python3.11/site-packages/click/core.py:2808 msgid "(dynamic)" -msgstr "" +msgstr "(dinàmic)" #: env/lib/python3.11/site-packages/click/core.py:2821 #, python-brace-format msgid "default: {default}" -msgstr "" +msgstr "per defecte: {default}" #: env/lib/python3.11/site-packages/click/core.py:2834 msgid "required" -msgstr "" +msgstr "obligatori" #: env/lib/python3.11/site-packages/click/decorators.py:465 #, python-format msgid "%(prog)s, version %(version)s" -msgstr "" +msgstr "%(prog)s, versió %(version)s" #: env/lib/python3.11/site-packages/click/decorators.py:528 msgid "Show the version and exit." -msgstr "" +msgstr "Mostra la versió i surt." #: env/lib/python3.11/site-packages/click/exceptions.py:44 #: env/lib/python3.11/site-packages/click/exceptions.py:80 #, python-brace-format msgid "Error: {message}" -msgstr "" +msgstr "Error: {message}" #: env/lib/python3.11/site-packages/click/exceptions.py:72 #, python-brace-format msgid "Try '{command} {option}' for help." -msgstr "" +msgstr "Proveu ‘{command} {option}’ per obtenir ajuda." #: env/lib/python3.11/site-packages/click/exceptions.py:121 #, python-brace-format msgid "Invalid value: {message}" -msgstr "" +msgstr "Valor no vàlid: {message}" #: env/lib/python3.11/site-packages/click/exceptions.py:123 #, python-brace-format msgid "Invalid value for {param_hint}: {message}" -msgstr "" +msgstr "Valor no vàlid per a {param_hint}: {message}" #: env/lib/python3.11/site-packages/click/exceptions.py:179 msgid "Missing argument" -msgstr "" +msgstr "Manca l'argument" #: env/lib/python3.11/site-packages/click/exceptions.py:181 msgid "Missing option" -msgstr "" +msgstr "Falta opció" #: env/lib/python3.11/site-packages/click/exceptions.py:183 msgid "Missing parameter" -msgstr "" +msgstr "Falta un paràmetre" #: env/lib/python3.11/site-packages/click/exceptions.py:185 #, python-brace-format msgid "Missing {param_type}" -msgstr "" +msgstr "Falta {param_type}" #: env/lib/python3.11/site-packages/click/exceptions.py:192 #, python-brace-format msgid "Missing parameter: {param_name}" -msgstr "" +msgstr "Falta el paràmetre: {param_name}" #: env/lib/python3.11/site-packages/click/exceptions.py:212 #, python-brace-format msgid "No such option: {name}" -msgstr "" +msgstr "No hi ha aquesta opció: {name}" #: env/lib/python3.11/site-packages/click/exceptions.py:224 #, python-brace-format msgid "Did you mean {possibility}?" msgid_plural "(Possible options: {possibilities})" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Voleu dir {possibility}?" +msgstr[1] "(Opcions possibles: {possibilities})" #: env/lib/python3.11/site-packages/click/exceptions.py:262 msgid "unknown error" -msgstr "" +msgstr "error desconegut" #: env/lib/python3.11/site-packages/click/exceptions.py:269 msgid "Could not open file {filename!r}: {message}" -msgstr "" +msgstr "No s'ha pogut obrir el fitxer {filename!r}: {message}" #: env/lib/python3.11/site-packages/click/parser.py:231 msgid "Argument {name!r} takes {nargs} values." -msgstr "" +msgstr "L'argument {name!r} pren {nargs} valors." #: env/lib/python3.11/site-packages/click/parser.py:413 msgid "Option {name!r} does not take a value." -msgstr "" +msgstr "L'opció {name!r} no pren cap valor." #: env/lib/python3.11/site-packages/click/parser.py:474 msgid "Option {name!r} requires an argument." msgid_plural "Option {name!r} requires {nargs} arguments." -msgstr[0] "" -msgstr[1] "" +msgstr[0] "L'opció {name!r} requereix un argument." +msgstr[1] "L'opció {name!r} requereix {nargs} arguments." #: env/lib/python3.11/site-packages/click/shell_completion.py:319 msgid "Shell completion is not supported for Bash versions older than 4.4." msgstr "" +"La finalització de l'intèrpret d'ordres no és compatible amb les versions de " +"Bash anteriors a la 4.4." #: env/lib/python3.11/site-packages/click/shell_completion.py:326 msgid "Couldn't detect Bash version, shell completion is not supported." msgstr "" +"No s'ha pogut detectar la versió de Bash, la finalització de l'intèrpret " +"d'ordres no és compatible." #: env/lib/python3.11/site-packages/click/termui.py:158 msgid "Repeat for confirmation" -msgstr "" +msgstr "Repetiu per confirmar" #: env/lib/python3.11/site-packages/click/termui.py:174 msgid "Error: The value you entered was invalid." -msgstr "" +msgstr "Error: el valor que heu introduït no és vàlid." #: env/lib/python3.11/site-packages/click/termui.py:176 #, python-brace-format msgid "Error: {e.message}" -msgstr "" +msgstr "Error: {e.message}" #: env/lib/python3.11/site-packages/click/termui.py:187 msgid "Error: The two entered values do not match." -msgstr "" +msgstr "Error: els dos valors introduïts no coincideixen." #: env/lib/python3.11/site-packages/click/termui.py:243 msgid "Error: invalid input" -msgstr "" +msgstr "Error: entrada no vàlida" #: env/lib/python3.11/site-packages/click/termui.py:773 msgid "Press any key to continue..." -msgstr "" +msgstr "Premeu qualsevol tecla per continuar..." #: env/lib/python3.11/site-packages/click/types.py:266 #, python-brace-format @@ -231,182 +235,193 @@ msgid "" "Choose from:\n" "\t{choices}" msgstr "" +"Trieu entre:\n" +"\t{choices}" #: env/lib/python3.11/site-packages/click/types.py:298 msgid "{value!r} is not {choice}." msgid_plural "{value!r} is not one of {choices}." -msgstr[0] "" -msgstr[1] "" +msgstr[0] "{value!r} no és {choice}." +msgstr[1] "{value!r} no és un dels {choices}." #: env/lib/python3.11/site-packages/click/types.py:392 msgid "{value!r} does not match the format {format}." msgid_plural "{value!r} does not match the formats {formats}." -msgstr[0] "" -msgstr[1] "" +msgstr[0] "{value!r} no coincideix amb el format {format}." +msgstr[1] "{value!r} no coincideix amb els formats {formats}." #: env/lib/python3.11/site-packages/click/types.py:414 msgid "{value!r} is not a valid {number_type}." -msgstr "" +msgstr "{value!r} no és un {number_type} vàlid." #: env/lib/python3.11/site-packages/click/types.py:470 #, python-brace-format msgid "{value} is not in the range {range}." -msgstr "" +msgstr "{value} no es troba a l'interval {range}." #: env/lib/python3.11/site-packages/click/types.py:611 msgid "{value!r} is not a valid boolean." -msgstr "" +msgstr "{value!r} no és un booleà vàlid." #: env/lib/python3.11/site-packages/click/types.py:635 msgid "{value!r} is not a valid UUID." -msgstr "" +msgstr "{value!r} no és un UUID vàlid." #: env/lib/python3.11/site-packages/click/types.py:822 msgid "file" -msgstr "" +msgstr "arxiu" #: env/lib/python3.11/site-packages/click/types.py:824 msgid "directory" -msgstr "" +msgstr "directori" #: env/lib/python3.11/site-packages/click/types.py:826 msgid "path" -msgstr "" +msgstr "ruta" #: env/lib/python3.11/site-packages/click/types.py:877 msgid "{name} {filename!r} does not exist." -msgstr "" +msgstr "{name} {filename!r} no existeix." #: env/lib/python3.11/site-packages/click/types.py:886 msgid "{name} {filename!r} is a file." -msgstr "" +msgstr "{name} {filename!r} és un fitxer." #: env/lib/python3.11/site-packages/click/types.py:894 #, python-brace-format msgid "{name} '{filename}' is a directory." -msgstr "" +msgstr "{name} '{filename}' és un directori." #: env/lib/python3.11/site-packages/click/types.py:903 msgid "{name} {filename!r} is not readable." -msgstr "" +msgstr "{name} {filename!r} no es pot llegir." #: env/lib/python3.11/site-packages/click/types.py:912 msgid "{name} {filename!r} is not writable." -msgstr "" +msgstr "{name} {filename!r} no es pot escriure." #: env/lib/python3.11/site-packages/click/types.py:921 msgid "{name} {filename!r} is not executable." -msgstr "" +msgstr "{name} {filename!r} no és executable." #: env/lib/python3.11/site-packages/click/types.py:988 #, python-brace-format msgid "{len_type} values are required, but {len_value} was given." msgid_plural "{len_type} values are required, but {len_value} were given." -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Es requereixen {len_type} valors, però s'ha donat {len_value}." +msgstr[1] "Es requereixen {len_type} valors, però s'han donat {len_value}." #: env/lib/python3.11/site-packages/django/contrib/messages/apps.py:15 msgid "Messages" -msgstr "" +msgstr "Missatges" #: env/lib/python3.11/site-packages/django/contrib/sitemaps/apps.py:8 msgid "Site Maps" -msgstr "" +msgstr "Mapes del lloc" #: env/lib/python3.11/site-packages/django/contrib/staticfiles/apps.py:9 msgid "Static Files" -msgstr "" +msgstr "Fitxers estàtics" #: env/lib/python3.11/site-packages/django/contrib/syndication/apps.py:7 msgid "Syndication" -msgstr "" +msgstr "Sindicació" #. Translators: String used to replace omitted page numbers in elided page #. range generated by paginators, e.g. [1, 2, '…', 5, 6, 7, '…', 9, 10]. #: env/lib/python3.11/site-packages/django/core/paginator.py:30 msgid "…" -msgstr "" +msgstr "…" #: env/lib/python3.11/site-packages/django/core/paginator.py:50 msgid "That page number is not an integer" -msgstr "" +msgstr "Aquest número de pàgina no és un nombre enter" #: env/lib/python3.11/site-packages/django/core/paginator.py:52 msgid "That page number is less than 1" -msgstr "" +msgstr "Aquest número de pàgina és inferior a 1" #: env/lib/python3.11/site-packages/django/core/paginator.py:54 msgid "That page contains no results" -msgstr "" +msgstr "Aquesta pàgina no conté cap resultat" #: env/lib/python3.11/site-packages/django/core/validators.py:22 msgid "Enter a valid value." -msgstr "" +msgstr "Introduïu un valor vàlid." #: env/lib/python3.11/site-packages/django/core/validators.py:104 #: env/lib/python3.11/site-packages/django/forms/fields.py:752 msgid "Enter a valid URL." -msgstr "" +msgstr "Introduïu un URL vàlid." #: env/lib/python3.11/site-packages/django/core/validators.py:165 msgid "Enter a valid integer." -msgstr "" +msgstr "Introduïu un nombre enter vàlid." #: env/lib/python3.11/site-packages/django/core/validators.py:176 msgid "Enter a valid email address." -msgstr "" +msgstr "Introduïu una adreça electrònica vàlida." #. Translators: "letters" means latin letters: a-z and A-Z. #: env/lib/python3.11/site-packages/django/core/validators.py:259 msgid "" "Enter a valid “slug” consisting of letters, numbers, underscores or hyphens." msgstr "" +"Introduïu un \"slug\" vàlid format per lletres, números, guions baixos o " +"guions." #: env/lib/python3.11/site-packages/django/core/validators.py:267 msgid "" "Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or " "hyphens." msgstr "" +"Introduïu un \"slug\" vàlid format per lletres Unicode, números, guions " +"baixos o guions." #: env/lib/python3.11/site-packages/django/core/validators.py:279 #: env/lib/python3.11/site-packages/django/core/validators.py:287 #: env/lib/python3.11/site-packages/django/core/validators.py:316 msgid "Enter a valid IPv4 address." -msgstr "" +msgstr "Introduïu una adreça IPv4 vàlida." #: env/lib/python3.11/site-packages/django/core/validators.py:296 #: env/lib/python3.11/site-packages/django/core/validators.py:317 msgid "Enter a valid IPv6 address." -msgstr "" +msgstr "Introduïu una adreça IPv6 vàlida." #: env/lib/python3.11/site-packages/django/core/validators.py:308 #: env/lib/python3.11/site-packages/django/core/validators.py:315 msgid "Enter a valid IPv4 or IPv6 address." -msgstr "" +msgstr "Introduïu una adreça IPv4 o IPv6 vàlida." #: env/lib/python3.11/site-packages/django/core/validators.py:351 msgid "Enter only digits separated by commas." -msgstr "" +msgstr "Introduïu només els dígits separats per comes." #: env/lib/python3.11/site-packages/django/core/validators.py:357 #, python-format msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." msgstr "" +"Assegureu-vos que aquest valor sigui %(limit_value)s (és %(show_value)s)." #: env/lib/python3.11/site-packages/django/core/validators.py:392 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "" +"Assegureu-vos que aquest valor sigui inferior o igual a %(limit_value)s." #: env/lib/python3.11/site-packages/django/core/validators.py:401 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "" +"Assegureu-vos que aquest valor sigui superior o igual a %(limit_value)s." #: env/lib/python3.11/site-packages/django/core/validators.py:410 #, python-format msgid "Ensure this value is a multiple of step size %(limit_value)s." msgstr "" +"Assegureu-vos que aquest valor sigui un múltiple de la mida del pas " +"%(limit_value)s." #: env/lib/python3.11/site-packages/django/core/validators.py:420 #, python-format @@ -417,7 +432,11 @@ msgid_plural "" "Ensure this value has at least %(limit_value)d characters (it has " "%(show_value)d)." msgstr[0] "" +"Assegureu-vos que aquest valor tingui com a mínim %(limit_value)d caràcter " +"(té %(show_value)d)." msgstr[1] "" +"Assegureu-vos que aquest valor tingui com a mínim %(limit_value)d caràcters " +"(té %(show_value)d)." #: env/lib/python3.11/site-packages/django/core/validators.py:438 #, python-format @@ -428,27 +447,31 @@ msgid_plural "" "Ensure this value has at most %(limit_value)d characters (it has " "%(show_value)d)." msgstr[0] "" +"Assegureu-vos que aquest valor tingui com a màxim %(limit_value)d caràcters " +"(té %(show_value)d)." msgstr[1] "" +"Assegureu-vos que aquest valor tingui com a màxim %(limit_value)d caràcters " +"(té %(show_value)d)." #: env/lib/python3.11/site-packages/django/core/validators.py:461 #: env/lib/python3.11/site-packages/django/forms/fields.py:347 #: env/lib/python3.11/site-packages/django/forms/fields.py:386 msgid "Enter a number." -msgstr "" +msgstr "Introduïu una xifra." #: env/lib/python3.11/site-packages/django/core/validators.py:463 #, python-format msgid "Ensure that there are no more than %(max)s digit in total." msgid_plural "Ensure that there are no more than %(max)s digits in total." -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Assegureu-vos que no hi hagi més de %(max)s dígits en total." +msgstr[1] "Assegureu-vos que no hi hagi més de %(max)s dígits en total." #: env/lib/python3.11/site-packages/django/core/validators.py:468 #, python-format msgid "Ensure that there are no more than %(max)s decimal place." msgid_plural "Ensure that there are no more than %(max)s decimal places." -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Assegureu-vos que no hi hagi més de %(max)s decimals." +msgstr[1] "Assegureu-vos que no hi hagi més de %(max)s decimals." #: env/lib/python3.11/site-packages/django/core/validators.py:473 #, python-format @@ -457,7 +480,9 @@ msgid "" msgid_plural "" "Ensure that there are no more than %(max)s digits before the decimal point." msgstr[0] "" +"Assegureu-vos que no hi hagi més de %(max)s dígits abans del punt decimal." msgstr[1] "" +"Assegureu-vos que no hi hagi més de %(max)s dígits abans del punt decimal." #: env/lib/python3.11/site-packages/django/core/validators.py:544 #, python-format @@ -465,43 +490,45 @@ msgid "" "File extension “%(extension)s” is not allowed. Allowed extensions are: " "%(allowed_extensions)s." msgstr "" +"L'extensió de fitxer “%(extension)s” no està permesa. Les extensions " +"permeses són: %(allowed_extensions)s." #: env/lib/python3.11/site-packages/django/core/validators.py:605 msgid "Null characters are not allowed." -msgstr "" +msgstr "No es permeten caràcters nuls." #: env/lib/python3.11/site-packages/django/db/models/base.py:1423 #: env/lib/python3.11/site-packages/django/forms/models.py:893 msgid "and" -msgstr "" +msgstr "i" #: env/lib/python3.11/site-packages/django/db/models/base.py:1425 #, python-format msgid "%(model_name)s with this %(field_labels)s already exists." -msgstr "" +msgstr "%(model_name)s amb aquest %(field_labels)s ja existeix." #: env/lib/python3.11/site-packages/django/db/models/constraints.py:17 #, python-format msgid "Constraint “%(name)s” is violated." -msgstr "" +msgstr "S'ha incomplert la restricció \"%(name)s\"." #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:128 #, python-format msgid "Value %(value)r is not a valid choice." -msgstr "" +msgstr "El valor %(value)r no és una opció vàlida." #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:129 msgid "This field cannot be null." -msgstr "" +msgstr "Aquest camp no pot ser nul." #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:130 msgid "This field cannot be blank." -msgstr "" +msgstr "Aquest camp no pot estar en blanc." #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:131 #, python-format msgid "%(model_name)s with this %(field_label)s already exists." -msgstr "" +msgstr "%(model_name)s amb aquest %(field_label)s ja existeix." #. Translators: The 'lookup_type' is one of 'date', 'year' or #. 'month'. Eg: "Title must be unique for pub_date year" @@ -510,38 +537,39 @@ msgstr "" msgid "" "%(field_label)s must be unique for %(date_field_label)s %(lookup_type)s." msgstr "" +"%(field_label)s ha de ser únic per a %(date_field_label)s %(lookup_type)s." #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:173 #, python-format msgid "Field of type: %(field_type)s" -msgstr "" +msgstr "Camp de tipus: %(field_type)s" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1094 #, python-format msgid "“%(value)s” value must be either True or False." -msgstr "" +msgstr "El valor \"%(value)s\" ha de ser Cert o Fals." #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1095 #, python-format msgid "“%(value)s” value must be either True, False, or None." -msgstr "" +msgstr "El valor \"%(value)s\" ha de ser Cert, Fals o Cap." #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1097 msgid "Boolean (Either True or False)" -msgstr "" +msgstr "Booleà (cert o fals)" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1147 #, python-format msgid "String (up to %(max_length)s)" -msgstr "" +msgstr "Cadena (fins a %(max_length)s)" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1149 msgid "String (unlimited)" -msgstr "" +msgstr "Cadena (il·limitada)" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1253 msgid "Comma-separated integers" -msgstr "" +msgstr "Nombres enters separats per comes" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1354 #, python-format @@ -549,6 +577,8 @@ msgid "" "“%(value)s” value has an invalid date format. It must be in YYYY-MM-DD " "format." msgstr "" +"El valor \"%(value)s\" té un format de data no vàlid. Ha de tenir el format " +"AAAA-MM-DD." #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1358 #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1493 @@ -557,10 +587,12 @@ msgid "" "“%(value)s” value has the correct format (YYYY-MM-DD) but it is an invalid " "date." msgstr "" +"El valor \"%(value)s\" té el format correcte (AAAA-MM-DD), però no és una " +"data vàlida." #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1362 msgid "Date (without time)" -msgstr "" +msgstr "Data (sense hora)" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1489 #, python-format @@ -568,6 +600,8 @@ msgid "" "“%(value)s” value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[." "uuuuuu]][TZ] format." msgstr "" +"El valor \"%(value)s\" té un format no vàlid. Ha de tenir el format AAAA-MM-" +"DD HH:MM[:ss[.uuuuuu]][TZ]." #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1497 #, python-format @@ -575,19 +609,21 @@ msgid "" "“%(value)s” value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]]" "[TZ]) but it is an invalid date/time." msgstr "" +"El valor “%(value)s” té el format correcte (AAAA-MM-DD HH:MM[:ss[.uuuuuu]]" +"[TZ]), però no és una data/hora vàlida." #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1502 msgid "Date (with time)" -msgstr "" +msgstr "Data (amb hora)" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1626 #, python-format msgid "“%(value)s” value must be a decimal number." -msgstr "" +msgstr "El valor “%(value)s” ha de ser un nombre decimal." #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1628 msgid "Decimal number" -msgstr "" +msgstr "Nombre decimal" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1789 #, python-format @@ -595,83 +631,85 @@ msgid "" "“%(value)s” value has an invalid format. It must be in [DD] [[HH:]MM:]ss[." "uuuuuu] format." msgstr "" +"El valor \"%(value)s\" té un format no vàlid. Ha d'estar en format [DD] " +"[[HH:]MM:]ss[.uuuuuu]." #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1793 msgid "Duration" -msgstr "" +msgstr "Durada" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1845 msgid "Email address" -msgstr "" +msgstr "Adreça de correu electrònic" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1870 msgid "File path" -msgstr "" +msgstr "Ruta al document" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1948 #, python-format msgid "“%(value)s” value must be a float." -msgstr "" +msgstr "El valor “%(value)s” ha de ser un valor flotant." #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1950 msgid "Floating point number" -msgstr "" +msgstr "Nombre de coma flotant" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1990 #, python-format msgid "“%(value)s” value must be an integer." -msgstr "" +msgstr "El valor “%(value)s” ha de ser un nombre enter." #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1992 msgid "Integer" -msgstr "" +msgstr "Enter" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2088 msgid "Big (8 byte) integer" -msgstr "" +msgstr "Enter gran (8 bytes)" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2105 msgid "Small integer" -msgstr "" +msgstr "Enter petit" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2113 msgid "IPv4 address" -msgstr "" +msgstr "Adreça IPv4" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2144 msgid "IP address" -msgstr "" +msgstr "Adreça IP" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2237 #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2238 #, python-format msgid "“%(value)s” value must be either None, True or False." -msgstr "" +msgstr "El valor \"%(value)s\" ha de ser Cap, Cert o Fals." #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2240 msgid "Boolean (Either True, False or None)" -msgstr "" +msgstr "Booleà (cert, fals o cap)" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2291 msgid "Positive big integer" -msgstr "" +msgstr "Enter gran positiu" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2306 msgid "Positive integer" -msgstr "" +msgstr "Enter positiu" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2321 msgid "Positive small integer" -msgstr "" +msgstr "Enter petit positiu" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2337 #, python-format msgid "Slug (up to %(max_length)s)" -msgstr "" +msgstr "Slug (fins a %(max_length)s)" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2373 msgid "Text" -msgstr "" +msgstr "Text" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2448 #, python-format @@ -679,6 +717,8 @@ msgid "" "“%(value)s” value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] " "format." msgstr "" +"El valor \"%(value)s\" té un format no vàlid. Ha d'estar en format HH:MM[:" +"ss[.uuuuuu]]." #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2452 #, python-format @@ -686,120 +726,123 @@ msgid "" "“%(value)s” value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an " "invalid time." msgstr "" +"El valor \"%(value)s\" té el format correcte (HH:MM[:ss[.uuuuuu]]), però no " +"és una hora vàlida." #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2456 msgid "Time" -msgstr "" +msgstr "Hora" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2564 msgid "URL" -msgstr "" +msgstr "URL" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2588 msgid "Raw binary data" -msgstr "" +msgstr "Dades binàries en brut" #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2653 #, python-format msgid "“%(value)s” is not a valid UUID." -msgstr "" +msgstr "“%(value)s” no és un UUID vàlid." #: env/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2655 msgid "Universally unique identifier" -msgstr "" +msgstr "Identificador únic universal" #: env/lib/python3.11/site-packages/django/db/models/fields/files.py:232 #: idhub/templates/idhub/admin/import.html:16 msgid "File" -msgstr "" +msgstr "Fitxer" #: env/lib/python3.11/site-packages/django/db/models/fields/files.py:393 msgid "Image" -msgstr "" +msgstr "Imatge" #: env/lib/python3.11/site-packages/django/db/models/fields/json.py:26 msgid "A JSON object" -msgstr "" +msgstr "Un objecte JSON" #: env/lib/python3.11/site-packages/django/db/models/fields/json.py:28 msgid "Value must be valid JSON." -msgstr "" +msgstr "El valor ha de ser un JSON vàlid." #: env/lib/python3.11/site-packages/django/db/models/fields/related.py:919 #, python-format msgid "%(model)s instance with %(field)s %(value)r does not exist." -msgstr "" +msgstr "%(model)s instància amb %(field)s %(value)r no existeix." #: env/lib/python3.11/site-packages/django/db/models/fields/related.py:921 msgid "Foreign Key (type determined by related field)" -msgstr "" +msgstr "Clau externa (tipus determinat pel camp relacionat)" #: env/lib/python3.11/site-packages/django/db/models/fields/related.py:1212 msgid "One-to-one relationship" -msgstr "" +msgstr "Relació un a un" #: env/lib/python3.11/site-packages/django/db/models/fields/related.py:1269 #, python-format msgid "%(from)s-%(to)s relationship" -msgstr "" +msgstr "relació %(from)s-%(to)s" #: env/lib/python3.11/site-packages/django/db/models/fields/related.py:1271 #, python-format msgid "%(from)s-%(to)s relationships" -msgstr "" +msgstr "relacions %(from)s-%(to)s" #: env/lib/python3.11/site-packages/django/db/models/fields/related.py:1319 msgid "Many-to-many relationship" -msgstr "" +msgstr "Relació de molts a molts" #. Translators: If found as last label character, these punctuation #. characters will prevent the default label_suffix to be appended to the label #: env/lib/python3.11/site-packages/django/forms/boundfield.py:184 msgid ":?.!" -msgstr "" +msgstr ":?.!" #: env/lib/python3.11/site-packages/django/forms/fields.py:91 msgid "This field is required." -msgstr "" +msgstr "Aquest camp és obligatori." #: env/lib/python3.11/site-packages/django/forms/fields.py:298 msgid "Enter a whole number." -msgstr "" +msgstr "Introduïu un número sencer." #: env/lib/python3.11/site-packages/django/forms/fields.py:467 #: env/lib/python3.11/site-packages/django/forms/fields.py:1241 msgid "Enter a valid date." -msgstr "" +msgstr "Introduïu una data vàlida." #: env/lib/python3.11/site-packages/django/forms/fields.py:490 #: env/lib/python3.11/site-packages/django/forms/fields.py:1242 msgid "Enter a valid time." -msgstr "" +msgstr "Introduïu una hora vàlida." #: env/lib/python3.11/site-packages/django/forms/fields.py:517 msgid "Enter a valid date/time." -msgstr "" +msgstr "Introduïu una data/hora vàlida." #: env/lib/python3.11/site-packages/django/forms/fields.py:551 msgid "Enter a valid duration." -msgstr "" +msgstr "Introduïu una durada vàlida." #: env/lib/python3.11/site-packages/django/forms/fields.py:552 #, python-brace-format msgid "The number of days must be between {min_days} and {max_days}." -msgstr "" +msgstr "El nombre de dies ha d'estar entre {min_days} i {max_days}." #: env/lib/python3.11/site-packages/django/forms/fields.py:621 msgid "No file was submitted. Check the encoding type on the form." msgstr "" +"No s'ha enviat cap fitxer. Comproveu el tipus de codificació al formulari." #: env/lib/python3.11/site-packages/django/forms/fields.py:622 msgid "No file was submitted." -msgstr "" +msgstr "No s'ha enviat cap fitxer." #: env/lib/python3.11/site-packages/django/forms/fields.py:623 msgid "The submitted file is empty." -msgstr "" +msgstr "El fitxer enviat està buit." #: env/lib/python3.11/site-packages/django/forms/fields.py:625 #, python-format @@ -807,17 +850,23 @@ msgid "Ensure this filename has at most %(max)d character (it has %(length)d)." msgid_plural "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." msgstr[0] "" +"Assegureu-vos que aquest nom de fitxer tingui com a màxim %(max)d caràcter " +"(té %(length)d)." msgstr[1] "" +"Assegureu-vos que aquest nom de fitxer tingui com a màxim %(max)d caràcters " +"(té %(length)d)." #: env/lib/python3.11/site-packages/django/forms/fields.py:630 msgid "Please either submit a file or check the clear checkbox, not both." -msgstr "" +msgstr "Envieu un fitxer o marqueu la casella de selecció clara, no tots dos." #: env/lib/python3.11/site-packages/django/forms/fields.py:694 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" +"Carregueu una imatge vàlida. El fitxer que heu penjat no era una imatge o " +"una imatge malmesa." #: env/lib/python3.11/site-packages/django/forms/fields.py:857 #: env/lib/python3.11/site-packages/django/forms/fields.py:949 @@ -825,35 +874,36 @@ msgstr "" #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "" +"Seleccioneu una opció vàlida. %(value)s no és una de les opcions disponibles." #: env/lib/python3.11/site-packages/django/forms/fields.py:951 #: env/lib/python3.11/site-packages/django/forms/fields.py:1070 #: env/lib/python3.11/site-packages/django/forms/models.py:1564 msgid "Enter a list of values." -msgstr "" +msgstr "Introduïu una llista de valors." #: env/lib/python3.11/site-packages/django/forms/fields.py:1071 msgid "Enter a complete value." -msgstr "" +msgstr "Introduïu un valor complet." #: env/lib/python3.11/site-packages/django/forms/fields.py:1310 msgid "Enter a valid UUID." -msgstr "" +msgstr "Introduïu un UUID vàlid." #: env/lib/python3.11/site-packages/django/forms/fields.py:1340 msgid "Enter a valid JSON." -msgstr "" +msgstr "Introduïu un JSON vàlid." #. Translators: This is the default suffix added to form field labels #: env/lib/python3.11/site-packages/django/forms/forms.py:98 msgid ":" -msgstr "" +msgstr ":" #: env/lib/python3.11/site-packages/django/forms/forms.py:244 #: env/lib/python3.11/site-packages/django/forms/forms.py:328 #, python-format msgid "(Hidden field %(name)s) %(error)s" -msgstr "" +msgstr "(Camp ocult %(name)s) %(error)s" #: env/lib/python3.11/site-packages/django/forms/formsets.py:63 #, python-format @@ -861,25 +911,28 @@ msgid "" "ManagementForm data is missing or has been tampered with. Missing fields: " "%(field_names)s. You may need to file a bug report if the issue persists." msgstr "" +"Les dades del Fomulario de Gestión falten o s'han manipulat. Camps que " +"falten: %(field_names)s. És possible que hàgiu de presentar un informe " +"d'error si el problema persisteix." #: env/lib/python3.11/site-packages/django/forms/formsets.py:67 #, python-format msgid "Please submit at most %(num)d form." msgid_plural "Please submit at most %(num)d forms." -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Envieu com a màxim %(num)d formulari." +msgstr[1] "Envieu com a màxim %(num)d formularis." #: env/lib/python3.11/site-packages/django/forms/formsets.py:72 #, python-format msgid "Please submit at least %(num)d form." msgid_plural "Please submit at least %(num)d forms." -msgstr[0] "" -msgstr[1] "" +msgstr[0] "Envieu almenys %(num)d formulari." +msgstr[1] "Envieu almenys %(num)d formularis." #: env/lib/python3.11/site-packages/django/forms/formsets.py:484 #: env/lib/python3.11/site-packages/django/forms/formsets.py:491 msgid "Order" -msgstr "" +msgstr "Ordre" #: env/lib/python3.11/site-packages/django/forms/formsets.py:499 #: idhub/templates/idhub/admin/dids.html:54 @@ -899,17 +952,17 @@ msgstr "" #: idhub/templates/templates/musician/mailbox_check_delete.html:12 #: idhub/templates/templates/musician/mailbox_form.html:25 msgid "Delete" -msgstr "" +msgstr "Esborrar" #: env/lib/python3.11/site-packages/django/forms/models.py:886 #, python-format msgid "Please correct the duplicate data for %(field)s." -msgstr "" +msgstr "Corregiu les dades duplicades de %(field)s." #: env/lib/python3.11/site-packages/django/forms/models.py:891 #, python-format msgid "Please correct the duplicate data for %(field)s, which must be unique." -msgstr "" +msgstr "Corregiu les dades duplicades de %(field)s, que han de ser úniques." #: env/lib/python3.11/site-packages/django/forms/models.py:898 #, python-format @@ -917,23 +970,27 @@ msgid "" "Please correct the duplicate data for %(field_name)s which must be unique " "for the %(lookup)s in %(date_field)s." msgstr "" +"Corregiu les dades duplicades per a %(field_name)s que han de ser úniques " +"per a %(lookup)s a %(date_field)s." #: env/lib/python3.11/site-packages/django/forms/models.py:907 msgid "Please correct the duplicate values below." -msgstr "" +msgstr "Corregiu els valors duplicats a continuació." #: env/lib/python3.11/site-packages/django/forms/models.py:1338 msgid "The inline value did not match the parent instance." -msgstr "" +msgstr "El valor en línia no coincideix amb la instància principal." #: env/lib/python3.11/site-packages/django/forms/models.py:1429 msgid "Select a valid choice. That choice is not one of the available choices." msgstr "" +"Seleccioneu una opció vàlida. Aquesta elecció no és una de les opcions " +"disponibles." #: env/lib/python3.11/site-packages/django/forms/models.py:1568 #, python-format msgid "“%(pk)s” is not a valid value." -msgstr "" +msgstr "\"%(pk)s\" no és un valor vàlid." #: env/lib/python3.11/site-packages/django/forms/utils.py:226 #, python-format @@ -941,434 +998,436 @@ msgid "" "%(datetime)s couldn’t be interpreted in time zone %(current_timezone)s; it " "may be ambiguous or it may not exist." msgstr "" +"%(datetime)s no s'ha pogut interpretar a la zona horària " +"%(current_timezone)s; pot ser ambigu o pot no existir." #: env/lib/python3.11/site-packages/django/forms/widgets.py:463 msgid "Clear" -msgstr "" +msgstr "Neteja" #: env/lib/python3.11/site-packages/django/forms/widgets.py:464 msgid "Currently" -msgstr "" +msgstr "Actualment" #: env/lib/python3.11/site-packages/django/forms/widgets.py:465 msgid "Change" -msgstr "" +msgstr "Canviar" #: env/lib/python3.11/site-packages/django/forms/widgets.py:794 msgid "Unknown" -msgstr "" +msgstr "Desconeguda" #: env/lib/python3.11/site-packages/django/forms/widgets.py:795 msgid "Yes" -msgstr "" +msgstr "Yes" #: env/lib/python3.11/site-packages/django/forms/widgets.py:796 msgid "No" -msgstr "" +msgstr "No" #. Translators: Please do not add spaces around commas. #: env/lib/python3.11/site-packages/django/template/defaultfilters.py:861 msgid "yes,no,maybe" -msgstr "" +msgstr "sí,no,potser" #: env/lib/python3.11/site-packages/django/template/defaultfilters.py:891 #: env/lib/python3.11/site-packages/django/template/defaultfilters.py:908 #, python-format msgid "%(size)d byte" msgid_plural "%(size)d bytes" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%(size)d byte" +msgstr[1] "%(size)d bytes" #: env/lib/python3.11/site-packages/django/template/defaultfilters.py:910 #, python-format msgid "%s KB" -msgstr "" +msgstr "%s KB" #: env/lib/python3.11/site-packages/django/template/defaultfilters.py:912 #, python-format msgid "%s MB" -msgstr "" +msgstr "%s MB" #: env/lib/python3.11/site-packages/django/template/defaultfilters.py:914 #, python-format msgid "%s GB" -msgstr "" +msgstr "%s GB" #: env/lib/python3.11/site-packages/django/template/defaultfilters.py:916 #, python-format msgid "%s TB" -msgstr "" +msgstr "%s TB" #: env/lib/python3.11/site-packages/django/template/defaultfilters.py:918 #, python-format msgid "%s PB" -msgstr "" +msgstr "%s PB" #: env/lib/python3.11/site-packages/django/utils/dateformat.py:73 msgid "p.m." -msgstr "" +msgstr "p.m." #: env/lib/python3.11/site-packages/django/utils/dateformat.py:74 msgid "a.m." -msgstr "" +msgstr "a.m." #: env/lib/python3.11/site-packages/django/utils/dateformat.py:79 msgid "PM" -msgstr "" +msgstr "PM" #: env/lib/python3.11/site-packages/django/utils/dateformat.py:80 msgid "AM" -msgstr "" +msgstr "AM" #: env/lib/python3.11/site-packages/django/utils/dateformat.py:152 msgid "midnight" -msgstr "" +msgstr "mitjanit" #: env/lib/python3.11/site-packages/django/utils/dateformat.py:154 msgid "noon" -msgstr "" +msgstr "migdia" #: env/lib/python3.11/site-packages/django/utils/dates.py:7 msgid "Monday" -msgstr "" +msgstr "Dilluns" #: env/lib/python3.11/site-packages/django/utils/dates.py:8 msgid "Tuesday" -msgstr "" +msgstr "Dimarts" #: env/lib/python3.11/site-packages/django/utils/dates.py:9 msgid "Wednesday" -msgstr "" +msgstr "Dimecres" #: env/lib/python3.11/site-packages/django/utils/dates.py:10 msgid "Thursday" -msgstr "" +msgstr "Dijous" #: env/lib/python3.11/site-packages/django/utils/dates.py:11 msgid "Friday" -msgstr "" +msgstr "Divendres" #: env/lib/python3.11/site-packages/django/utils/dates.py:12 msgid "Saturday" -msgstr "" +msgstr "Dissabte" #: env/lib/python3.11/site-packages/django/utils/dates.py:13 msgid "Sunday" -msgstr "" +msgstr "Diumenge" #: env/lib/python3.11/site-packages/django/utils/dates.py:16 msgid "Mon" -msgstr "" +msgstr "Dl" #: env/lib/python3.11/site-packages/django/utils/dates.py:17 msgid "Tue" -msgstr "" +msgstr "Dt" #: env/lib/python3.11/site-packages/django/utils/dates.py:18 msgid "Wed" -msgstr "" +msgstr "Dx" #: env/lib/python3.11/site-packages/django/utils/dates.py:19 msgid "Thu" -msgstr "" +msgstr "Dj" #: env/lib/python3.11/site-packages/django/utils/dates.py:20 msgid "Fri" -msgstr "" +msgstr "Dv" #: env/lib/python3.11/site-packages/django/utils/dates.py:21 msgid "Sat" -msgstr "" +msgstr "Ds" #: env/lib/python3.11/site-packages/django/utils/dates.py:22 msgid "Sun" -msgstr "" +msgstr "Dg" #: env/lib/python3.11/site-packages/django/utils/dates.py:25 msgid "January" -msgstr "" +msgstr "Gener" #: env/lib/python3.11/site-packages/django/utils/dates.py:26 msgid "February" -msgstr "" +msgstr "Febrer" #: env/lib/python3.11/site-packages/django/utils/dates.py:27 msgid "March" -msgstr "" +msgstr "Març" #: env/lib/python3.11/site-packages/django/utils/dates.py:28 msgid "April" -msgstr "" +msgstr "Abril" #: env/lib/python3.11/site-packages/django/utils/dates.py:29 msgid "May" -msgstr "" +msgstr "Maig" #: env/lib/python3.11/site-packages/django/utils/dates.py:30 msgid "June" -msgstr "" +msgstr "Juny" #: env/lib/python3.11/site-packages/django/utils/dates.py:31 msgid "July" -msgstr "" +msgstr "Juliol" #: env/lib/python3.11/site-packages/django/utils/dates.py:32 msgid "August" -msgstr "" +msgstr "Agost" #: env/lib/python3.11/site-packages/django/utils/dates.py:33 msgid "September" -msgstr "" +msgstr "Setembre" #: env/lib/python3.11/site-packages/django/utils/dates.py:34 msgid "October" -msgstr "" +msgstr "Octubre" #: env/lib/python3.11/site-packages/django/utils/dates.py:35 msgid "November" -msgstr "" +msgstr "Novembre" #: env/lib/python3.11/site-packages/django/utils/dates.py:36 msgid "December" -msgstr "" +msgstr "Desembre" #: env/lib/python3.11/site-packages/django/utils/dates.py:39 msgid "jan" -msgstr "" +msgstr "gen" #: env/lib/python3.11/site-packages/django/utils/dates.py:40 msgid "feb" -msgstr "" +msgstr "feb" #: env/lib/python3.11/site-packages/django/utils/dates.py:41 msgid "mar" -msgstr "" +msgstr "mar" #: env/lib/python3.11/site-packages/django/utils/dates.py:42 msgid "apr" -msgstr "" +msgstr "apr" #: env/lib/python3.11/site-packages/django/utils/dates.py:43 msgid "may" -msgstr "" +msgstr "maig" #: env/lib/python3.11/site-packages/django/utils/dates.py:44 msgid "jun" -msgstr "" +msgstr "jun" #: env/lib/python3.11/site-packages/django/utils/dates.py:45 msgid "jul" -msgstr "" +msgstr "jul" #: env/lib/python3.11/site-packages/django/utils/dates.py:46 msgid "aug" -msgstr "" +msgstr "ago" #: env/lib/python3.11/site-packages/django/utils/dates.py:47 msgid "sep" -msgstr "" +msgstr "set" #: env/lib/python3.11/site-packages/django/utils/dates.py:48 msgid "oct" -msgstr "" +msgstr "oct" #: env/lib/python3.11/site-packages/django/utils/dates.py:49 msgid "nov" -msgstr "" +msgstr "nov" #: env/lib/python3.11/site-packages/django/utils/dates.py:50 msgid "dec" -msgstr "" +msgstr "des" #: env/lib/python3.11/site-packages/django/utils/dates.py:53 msgctxt "abbrev. month" msgid "Jan." -msgstr "" +msgstr "Gen." #: env/lib/python3.11/site-packages/django/utils/dates.py:54 msgctxt "abbrev. month" msgid "Feb." -msgstr "" +msgstr "Febr." #: env/lib/python3.11/site-packages/django/utils/dates.py:55 msgctxt "abbrev. month" msgid "March" -msgstr "" +msgstr "Març" #: env/lib/python3.11/site-packages/django/utils/dates.py:56 msgctxt "abbrev. month" msgid "April" -msgstr "" +msgstr "Abril" #: env/lib/python3.11/site-packages/django/utils/dates.py:57 msgctxt "abbrev. month" msgid "May" -msgstr "" +msgstr "Maig" #: env/lib/python3.11/site-packages/django/utils/dates.py:58 msgctxt "abbrev. month" msgid "June" -msgstr "" +msgstr "Juny" #: env/lib/python3.11/site-packages/django/utils/dates.py:59 msgctxt "abbrev. month" msgid "July" -msgstr "" +msgstr "Juliol" #: env/lib/python3.11/site-packages/django/utils/dates.py:60 msgctxt "abbrev. month" msgid "Aug." -msgstr "" +msgstr "Ago." #: env/lib/python3.11/site-packages/django/utils/dates.py:61 msgctxt "abbrev. month" msgid "Sept." -msgstr "" +msgstr "Sept." #: env/lib/python3.11/site-packages/django/utils/dates.py:62 msgctxt "abbrev. month" msgid "Oct." -msgstr "" +msgstr "Oct." #: env/lib/python3.11/site-packages/django/utils/dates.py:63 msgctxt "abbrev. month" msgid "Nov." -msgstr "" +msgstr "Nov." #: env/lib/python3.11/site-packages/django/utils/dates.py:64 msgctxt "abbrev. month" msgid "Dec." -msgstr "" +msgstr "Des." #: env/lib/python3.11/site-packages/django/utils/dates.py:67 msgctxt "alt. month" msgid "January" -msgstr "" +msgstr "Gener" #: env/lib/python3.11/site-packages/django/utils/dates.py:68 msgctxt "alt. month" msgid "February" -msgstr "" +msgstr "Febrer" #: env/lib/python3.11/site-packages/django/utils/dates.py:69 msgctxt "alt. month" msgid "March" -msgstr "" +msgstr "Març" #: env/lib/python3.11/site-packages/django/utils/dates.py:70 msgctxt "alt. month" msgid "April" -msgstr "" +msgstr "Abril" #: env/lib/python3.11/site-packages/django/utils/dates.py:71 msgctxt "alt. month" msgid "May" -msgstr "" +msgstr "Maig" #: env/lib/python3.11/site-packages/django/utils/dates.py:72 msgctxt "alt. month" msgid "June" -msgstr "" +msgstr "Juny" #: env/lib/python3.11/site-packages/django/utils/dates.py:73 msgctxt "alt. month" msgid "July" -msgstr "" +msgstr "Juliol" #: env/lib/python3.11/site-packages/django/utils/dates.py:74 msgctxt "alt. month" msgid "August" -msgstr "" +msgstr "Agost" #: env/lib/python3.11/site-packages/django/utils/dates.py:75 msgctxt "alt. month" msgid "September" -msgstr "" +msgstr "Setembre" #: env/lib/python3.11/site-packages/django/utils/dates.py:76 msgctxt "alt. month" msgid "October" -msgstr "" +msgstr "Octubre" #: env/lib/python3.11/site-packages/django/utils/dates.py:77 msgctxt "alt. month" msgid "November" -msgstr "" +msgstr "Novembre" #: env/lib/python3.11/site-packages/django/utils/dates.py:78 msgctxt "alt. month" msgid "December" -msgstr "" +msgstr "Desembre" #: env/lib/python3.11/site-packages/django/utils/ipv6.py:8 msgid "This is not a valid IPv6 address." -msgstr "" +msgstr "Aquesta adreça IPv6 no és vàlida." #: env/lib/python3.11/site-packages/django/utils/text.py:78 #, python-format msgctxt "String to return when truncating text" msgid "%(truncated_text)s…" -msgstr "" +msgstr "%(truncated_text)s…" #: env/lib/python3.11/site-packages/django/utils/text.py:254 msgid "or" -msgstr "" +msgstr "o" #. Translators: This string is used as a separator between list elements #: env/lib/python3.11/site-packages/django/utils/text.py:273 #: env/lib/python3.11/site-packages/django/utils/timesince.py:135 msgid ", " -msgstr "" +msgstr ", " #: env/lib/python3.11/site-packages/django/utils/timesince.py:8 #, python-format msgid "%(num)d year" msgid_plural "%(num)d years" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%(num)d any" +msgstr[1] "%(num)d anys" #: env/lib/python3.11/site-packages/django/utils/timesince.py:9 #, python-format msgid "%(num)d month" msgid_plural "%(num)d months" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%(num)d mes" +msgstr[1] "%(num)d mesos" #: env/lib/python3.11/site-packages/django/utils/timesince.py:10 #, python-format msgid "%(num)d week" msgid_plural "%(num)d weeks" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%(num)d setmana" +msgstr[1] "%(num)d setmanes" #: env/lib/python3.11/site-packages/django/utils/timesince.py:11 #, python-format msgid "%(num)d day" msgid_plural "%(num)d days" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%(num)d dia" +msgstr[1] "%(num)d dies" #: env/lib/python3.11/site-packages/django/utils/timesince.py:12 #, python-format msgid "%(num)d hour" msgid_plural "%(num)d hours" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%(num)d hora" +msgstr[1] "%(num)d hores" #: env/lib/python3.11/site-packages/django/utils/timesince.py:13 #, python-format msgid "%(num)d minute" msgid_plural "%(num)d minutes" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%(num)d minut" +msgstr[1] "%(num)d minuts" #: env/lib/python3.11/site-packages/django/views/csrf.py:111 msgid "Forbidden" -msgstr "" +msgstr "Prohibit" #: env/lib/python3.11/site-packages/django/views/csrf.py:112 msgid "CSRF verification failed. Request aborted." -msgstr "" +msgstr "La verificació CSRF ha fallat. Sol·licitud avortada." #: env/lib/python3.11/site-packages/django/views/csrf.py:116 msgid "" @@ -1377,6 +1436,10 @@ msgid "" "required for security reasons, to ensure that your browser is not being " "hijacked by third parties." msgstr "" +"Esteu veient aquest missatge perquè aquest lloc HTTPS requereix que el " +"vostre navegador web enviï una “Referer header”, però no se n'ha enviat cap. " +"Aquesta capçalera és necessària per motius de seguretat, per garantir que el " +"vostre navegador no sigui segrestat per tercers." #: env/lib/python3.11/site-packages/django/views/csrf.py:122 msgid "" @@ -1384,6 +1447,9 @@ msgid "" "enable them, at least for this site, or for HTTPS connections, or for “same-" "origin” requests." msgstr "" +"Si heu configurat el vostre navegador per desactivar les capçaleres " +"\"Referer\", si us plau, torneu a habilitar-les, almenys per a aquest lloc, " +"o per a connexions HTTPS, o per a sol·licituds del \"mateix origen\"." #: env/lib/python3.11/site-packages/django/views/csrf.py:127 msgid "" @@ -1393,6 +1459,11 @@ msgid "" "If you’re concerned about privacy, use alternatives like for links to third-party sites." msgstr "" +"Si utilitzeu el etiqueteu o " +"inclogueu la capçalera \"Política de referència: no-referrer\", traieu-les. " +"La protecció CSRF requereix que la capçalera \"Referer\" faci una " +"comprovació estricta de referència. Si us preocupa la privadesa, utilitzeu " +"alternatives, com ara enllaços a llocs de tercers." #: env/lib/python3.11/site-packages/django/views/csrf.py:136 msgid "" @@ -1400,44 +1471,51 @@ msgid "" "submitting forms. This cookie is required for security reasons, to ensure " "that your browser is not being hijacked by third parties." msgstr "" +"Esteu veient aquest missatge perquè aquest lloc requereix una galeta CSRF a " +"l'hora d'enviar formularis. Aquesta galeta és necessària per motius de " +"seguretat, per garantir que el vostre navegador no sigui segrestat per " +"tercers." #: env/lib/python3.11/site-packages/django/views/csrf.py:142 msgid "" "If you have configured your browser to disable cookies, please re-enable " "them, at least for this site, or for “same-origin” requests." msgstr "" +"Si heu configurat el vostre navegador per desactivar les galetes, torneu-les " +"a habilitar, almenys per a aquest lloc, o per a sol·licituds del \"mateix " +"origen\"." #: env/lib/python3.11/site-packages/django/views/csrf.py:148 msgid "More information is available with DEBUG=True." -msgstr "" +msgstr "Hi ha més informació disponible amb DEBUG=True." #: env/lib/python3.11/site-packages/django/views/generic/dates.py:44 msgid "No year specified" -msgstr "" +msgstr "Cap any especificat" #: env/lib/python3.11/site-packages/django/views/generic/dates.py:64 #: env/lib/python3.11/site-packages/django/views/generic/dates.py:115 #: env/lib/python3.11/site-packages/django/views/generic/dates.py:214 msgid "Date out of range" -msgstr "" +msgstr "Data fora d'interval" #: env/lib/python3.11/site-packages/django/views/generic/dates.py:94 msgid "No month specified" -msgstr "" +msgstr "Sense mes especificat" #: env/lib/python3.11/site-packages/django/views/generic/dates.py:147 msgid "No day specified" -msgstr "" +msgstr "Sense dia especificat" #: env/lib/python3.11/site-packages/django/views/generic/dates.py:194 msgid "No week specified" -msgstr "" +msgstr "Cap setmana especificada" #: env/lib/python3.11/site-packages/django/views/generic/dates.py:349 #: env/lib/python3.11/site-packages/django/views/generic/dates.py:380 #, python-format msgid "No %(verbose_name_plural)s available" -msgstr "" +msgstr "No hi ha %(verbose_name_plural)s disponible" #: env/lib/python3.11/site-packages/django/views/generic/dates.py:652 #, python-format @@ -1445,49 +1523,52 @@ msgid "" "Future %(verbose_name_plural)s not available because %(class_name)s." "allow_future is False." msgstr "" +"Futur %(verbose_name_plural)s no disponible perquè %(class_name)s." +"allow_future és fals." #: env/lib/python3.11/site-packages/django/views/generic/dates.py:692 #, python-format msgid "Invalid date string “%(datestr)s” given format “%(format)s”" msgstr "" +"La cadena de data \"%(datestr)s\" no és vàlida amb el format \"%(format)s\"" #: env/lib/python3.11/site-packages/django/views/generic/detail.py:56 #, python-format msgid "No %(verbose_name)s found matching the query" -msgstr "" +msgstr "No s'ha trobat cap %(verbose_name)s que coincideixi amb la consulta" #: env/lib/python3.11/site-packages/django/views/generic/list.py:70 msgid "Page is not “last”, nor can it be converted to an int." -msgstr "" +msgstr "La pàgina no és \"última\", ni es pot convertir a int." #: env/lib/python3.11/site-packages/django/views/generic/list.py:77 #, python-format msgid "Invalid page (%(page_number)s): %(message)s" -msgstr "" +msgstr "Pàgina no vàlida (%(page_number)s): %(message)s" #: env/lib/python3.11/site-packages/django/views/generic/list.py:169 #, python-format msgid "Empty list and “%(class_name)s.allow_empty” is False." -msgstr "" +msgstr "Llista buida i \"%(class_name)s.allow_empty\" és Fals." #: env/lib/python3.11/site-packages/django/views/static.py:38 msgid "Directory indexes are not allowed here." -msgstr "" +msgstr "Els índexs de directoris no es permeten aquí." #: env/lib/python3.11/site-packages/django/views/static.py:40 #, python-format msgid "“%(path)s” does not exist" -msgstr "" +msgstr "“%(path)s” no existeix" #: env/lib/python3.11/site-packages/django/views/static.py:79 #, python-format msgid "Index of %(directory)s" -msgstr "" +msgstr "Índex de %(directory)s" #: env/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:7 #: env/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:220 msgid "The install worked successfully! Congratulations!" -msgstr "" +msgstr "La instal·lació ha funcionat correctament! Felicitats!" #: env/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:206 #, python-format @@ -1495,6 +1576,9 @@ msgid "" "View release notes for Django %(version)s" msgstr "" +"Consulta les notes de la versió de Django " +"%(version)s" #: env/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:221 #, python-format @@ -1504,275 +1588,279 @@ msgid "" "rel=\"noopener\">DEBUG=True is in your settings file and you have not " "configured any URLs." msgstr "" +"Esteu veient aquesta pàgina perquè DEBUG=True és al vostre fitxer de configuració i no heu " +"configurat cap URL." #: env/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:229 msgid "Django Documentation" -msgstr "" +msgstr "Documentació de Django" #: env/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:230 msgid "Topics, references, & how-to’s" -msgstr "" +msgstr "Temes, referències, & com fer-ho" #: env/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:238 msgid "Tutorial: A Polling App" -msgstr "" +msgstr "Tutorial: una aplicació de votació" #: env/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:239 msgid "Get started with Django" -msgstr "" +msgstr "Comença amb Django" #: env/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:247 msgid "Django Community" -msgstr "" +msgstr "Comunitat Django" #: env/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:248 msgid "Connect, get help, or contribute" -msgstr "" +msgstr "Connecteu-vos, obteniu ajuda o contribuïu" #: env/lib/python3.11/site-packages/isort/main.py:158 msgid "show this help message and exit" -msgstr "" +msgstr "mostra aquest missatge d'ajuda i surt" #: idhub/admin/forms.py:112 msgid "The user does not exist!" -msgstr "" +msgstr "L'usuari no existeix!" #: idhub/admin/forms.py:154 idhub/admin/forms.py:168 idhub/admin/forms.py:177 msgid "This membership already exists!" -msgstr "" +msgstr "Aquesta subscripció ja existeix!" #: idhub/admin/forms.py:159 msgid "The end date is less than the start date" -msgstr "" +msgstr "La data de finalització és inferior a la data d'inici" #: idhub/admin/forms.py:206 msgid "Is not possible to have a duplicate role" -msgstr "" +msgstr "No és possible tenir una funció duplicada" #: idhub/admin/views.py:48 idhub/templates/idhub/base.html:69 #: idhub/templates/idhub/base_admin.html:69 idhub/user/views.py:32 msgid "Dashboard" -msgstr "" +msgstr "Panell de control" #: idhub/admin/views.py:49 idhub/user/views.py:33 msgid "Events" -msgstr "" +msgstr "Esdeveniments" #: idhub/admin/views.py:61 msgid "User management" -msgstr "" +msgstr "Configuració de gestió d'usuaris" #: idhub/admin/views.py:66 msgid "Access control management" -msgstr "" +msgstr "Gestió del control d'accés" #: idhub/admin/views.py:71 idhub/user/views.py:74 msgid "Credential management" -msgstr "" +msgstr "Gestió de credencials" #: idhub/admin/views.py:76 msgid "Template management" -msgstr "" +msgstr "Gestió de plantilles" #: idhub/admin/views.py:81 msgid "Data file management" -msgstr "" +msgstr "Gestió de fitxers de dades" #: idhub/admin/views.py:87 idhub/templates/idhub/base_admin.html:81 msgid "View users" -msgstr "" +msgstr "Veure usuaris" #: idhub/admin/views.py:100 msgid "User personal information" -msgstr "" +msgstr "Informació personal de l'usuari" #: idhub/admin/views.py:124 msgid "It is not possible deactivate your account!" -msgstr "" +msgstr "No és possible desactivar el teu compte!" #: idhub/admin/views.py:148 msgid "It is not possible delete your account!" -msgstr "" +msgstr "No és possible esborrar el teu compte!" #: idhub/admin/views.py:155 msgid "Update user" -msgstr "" +msgstr "Actualitza l'usuari" #: idhub/admin/views.py:184 msgid "The account was updated successfully" -msgstr "" +msgstr "El compte s'ha actualitzat correctament" #: idhub/admin/views.py:193 idhub/templates/idhub/base_admin.html:86 msgid "Add user" -msgstr "" +msgstr "Afegeix usuari" #: idhub/admin/views.py:207 msgid "The account was created successfully" -msgstr "" +msgstr "El compte s'ha creat correctament" #: idhub/admin/views.py:221 idhub/admin/views.py:265 msgid "Associate a membership to the user" -msgstr "" +msgstr "Associar una pertinença a l'usuari" #: idhub/admin/views.py:252 idhub/admin/views.py:342 msgid "Membership created successfully" -msgstr "" +msgstr "Membre creat amb èxit" #: idhub/admin/views.py:297 idhub/admin/views.py:377 msgid "Membership updated successfully" -msgstr "" +msgstr "Membre s'ha actualitzat correctament" #: idhub/admin/views.py:312 msgid "Is not possible delete your account!" -msgstr "" +msgstr "No és possible eliminar el teu compte!" #: idhub/admin/views.py:319 msgid "Add a user role to access a service" -msgstr "" +msgstr "Afegiu un rol d'usuari per accedir a un servei" #: idhub/admin/views.py:355 msgid "Modify a user role to access a service" -msgstr "" +msgstr "Modificar un rol d'usuari per accedir a un servei" #: idhub/admin/views.py:403 idhub/templates/idhub/base_admin.html:99 msgid "Manage roles" -msgstr "" +msgstr "Administrar rols" #: idhub/admin/views.py:415 msgid "Add role" -msgstr "" +msgstr "Afegeix un rol" #: idhub/admin/views.py:424 msgid "Role created successfully" -msgstr "" +msgstr "Rol creat correctament" #: idhub/admin/views.py:431 msgid "Edit role" -msgstr "" +msgstr "Edita rol" #: idhub/admin/views.py:446 msgid "Role updated successfully" -msgstr "" +msgstr "El rol s'ha actualitzat correctament" #: idhub/admin/views.py:459 msgid "Role deleted successfully" -msgstr "" +msgstr "El rol s'ha suprimit correctament" #: idhub/admin/views.py:466 idhub/templates/idhub/base_admin.html:104 msgid "Manage services" -msgstr "" +msgstr "Gestionar els serveis" #: idhub/admin/views.py:478 idhub/templates/idhub/admin/services.html:35 msgid "Add service" -msgstr "" +msgstr "Afegeix un servei" #: idhub/admin/views.py:492 msgid "Service created successfully" -msgstr "" +msgstr "Servei creat correctament" #: idhub/admin/views.py:499 msgid "Modify service" -msgstr "" +msgstr "Modificar servei" #: idhub/admin/views.py:519 msgid "Service updated successfully" -msgstr "" +msgstr "Servei actualitzat correctament" #: idhub/admin/views.py:532 msgid "Service deleted successfully" -msgstr "" +msgstr "El servei s'ha suprimit correctament" #: idhub/admin/views.py:539 idhub/templates/idhub/base_admin.html:117 msgid "View credentials" -msgstr "" +msgstr "Veure credencials" #: idhub/admin/views.py:552 msgid "Change credential status" -msgstr "" +msgstr "Canvia l'estat de la credencial" #: idhub/admin/views.py:594 msgid "Credential revoked successfully" -msgstr "" +msgstr "La credencial s'ha revocat correctament" #: idhub/admin/views.py:616 msgid "Credential deleted successfully" -msgstr "" +msgstr "La credencial s'ha suprimit correctament" #: idhub/admin/views.py:625 msgid "Manage identities (DID)" -msgstr "" +msgstr "Gestiona les identitats (DID)" #: idhub/admin/views.py:638 msgid "Add a new organizational identity (DID)" -msgstr "" +msgstr "Afegeix una identitat organitzativa nova (DID)" #: idhub/admin/views.py:650 idhub/user/views.py:194 msgid "DID created successfully" -msgstr "" +msgstr "DID creat correctament" #: idhub/admin/views.py:657 idhub/admin/views.py:676 msgid "Organization Identities (DID)" -msgstr "" +msgstr "Identitats de l'organització (DID)" #: idhub/admin/views.py:671 idhub/user/views.py:217 msgid "DID updated successfully" -msgstr "" +msgstr "DID s'ha actualitzat correctament" #: idhub/admin/views.py:687 idhub/user/views.py:233 msgid "DID delete successfully" -msgstr "" +msgstr "DID s’ha suprimit correctament" #: idhub/admin/views.py:693 idhub/templates/idhub/base_admin.html:132 msgid "View org. credentials" -msgstr "" +msgstr "Veure org. credencials" #: idhub/admin/views.py:700 idhub/templates/idhub/base_admin.html:137 msgid "Configure credential issuance" -msgstr "" +msgstr "Configura l'emissió de credencials" #: idhub/admin/views.py:707 msgid "View credential templates" -msgstr "" +msgstr "Veure plantilles de credencials" #: idhub/admin/views.py:741 msgid "Upload template" -msgstr "" +msgstr "Carrega la plantilla" #: idhub/admin/views.py:757 msgid "There are some errors in the file" -msgstr "" +msgstr "Hi ha alguns errors al fitxer" #: idhub/admin/views.py:771 msgid "This template already exists!" -msgstr "" +msgstr "Aquesta plantilla ja existeix!" #: idhub/admin/views.py:777 idhub/admin/views.py:822 msgid "This is not a valid schema!" -msgstr "" +msgstr "Aquest no és un esquema vàlid!" #: idhub/admin/views.py:786 msgid "Import template" -msgstr "" +msgstr "Importa plantilla" #: idhub/admin/views.py:814 msgid "The schema was added sucessfully" -msgstr "" +msgstr "L'esquema s'ha afegit correctament" #: idhub/admin/views.py:839 idhub/templates/idhub/admin/import.html:31 msgid "Import data" -msgstr "" +msgstr "Importa les dades" #: idhub/admin/views.py:852 idhub/admin/views.py:865 msgid "Import" -msgstr "" +msgstr "Importa" #: idhub/admin/views.py:878 msgid "The file was imported successfully!" -msgstr "" +msgstr "El fitxer s'ha importat correctament!" #: idhub/admin/views.py:883 msgid "Error importing the file!" -msgstr "" +msgstr "S'ha produït un error en importar el fitxer!" #: idhub/models.py:67 #, python-brace-format @@ -1780,60 +1868,69 @@ msgid "" "The user {username} was registered: name: {first_name}, last name: " "{last_name}" msgstr "" +"L'usuari {username} estava registrat: nom: {first_name}, cognom: {last_name}" #: idhub/models.py:79 #, python-brace-format msgid "" "Welcome. You has been registered: name: {first_name}, last name: {last_name}" -msgstr "" +msgstr "Benvingut. Has estat registrat: nom: {first_name}, cognom: {last_name}" #: idhub/models.py:92 #, python-brace-format msgid "" "The user '{username}' has request the update of the following information: " msgstr "" +"L'usuari '{username}' ha sol·licitat l'actualització de la informació " +"següent: " #: idhub/models.py:104 msgid "You have requested the update of the following information: " -msgstr "" +msgstr "Heu sol·licitat l'actualització de la informació següent: " #: idhub/models.py:141 #, python-brace-format msgid "The admin has deleted the user: username: {username}" -msgstr "" +msgstr "L'administrador ha suprimit l'usuari: nom d'usuari: {username}" #: idhub/models.py:151 #, python-brace-format msgid "New DID with DID-ID: '{did}' created by user '{username}'" -msgstr "" +msgstr "DID nou amb DID-ID: '{did}' creat per l'usuari '{username}'" #: idhub/models.py:162 #, python-brace-format msgid "New DID with label: '{label}' and DID-ID: '{did}' was created'" -msgstr "" +msgstr "S'ha creat un DID nou amb l'etiqueta: '{label}' i l'ID-DID: '{did}''" #: idhub/models.py:174 #, python-brace-format msgid "" "The DID with label '{label}' and DID-ID: '{did}' was deleted from your wallet" msgstr "" +"El DID amb l'etiqueta \"{label}\" i el DID-ID: \"{did}\" s'ha suprimit de la " +"vostra cartera" #: idhub/models.py:186 #, python-brace-format msgid "The credential of type '{type}' and ID: '{id}' was deleted" -msgstr "" +msgstr "S'ha suprimit la credencial del tipus \"{type}\" i ID: \"{id}\"" #: idhub/models.py:197 #, python-brace-format msgid "" "The credential of type '{type}' and ID: '{id}' was deleted from your wallet" msgstr "" +"La credencial del tipus \"{type}\" i ID: \"{id}\" s'han suprimit de la " +"vostra cartera" #: idhub/models.py:209 #, python-brace-format msgid "" "The credential of type '{type}' and ID: '{id}' was issued for user {username}" msgstr "" +"S'ha emès la credencial del tipus \"{type}\" i ID: \"{id}\" per a l'usuari " +"{username}" #: idhub/models.py:221 #, python-brace-format @@ -1841,115 +1938,123 @@ msgid "" "The credential of type '{type}' and ID: '{id}' was issued and stored in your " "wallet" msgstr "" +"La credencial del tipus \"{type}\" i ID: \"{id}\" s'ha emès i s'ha " +"emmagatzemat a la vostra cartera" #: idhub/models.py:263 #, python-brace-format msgid "The credential of type '{type}' was enabled for user {username}" msgstr "" +"La credencial del tipus \"{type}\" s'ha habilitat per a l'usuari {username}" #: idhub/models.py:274 #, python-brace-format msgid "You can request the '{type}' credential" -msgstr "" +msgstr "Podeu sol·licitar la credencial '{type}'" #: idhub/models.py:285 #, python-brace-format msgid "The credential of type '{type}' and ID: '{id}' was revoked for " -msgstr "" +msgstr "S'ha revocat la credencial del tipus \"{type}\" i ID: \"{id}\" per " #: idhub/models.py:296 #, python-brace-format msgid "The credential of type '{type}' and ID: '{id}' was revoked by admin" msgstr "" +"L'administrador ha revocat la credencial del tipus \"{type}\" i ID: \"{id}\"" #: idhub/models.py:308 msgid "A new role was created by admin" -msgstr "" +msgstr "Un nou rol va ser creat per l'administrador" #: idhub/models.py:316 msgid "The role was modified by admin" -msgstr "" +msgstr "El rol va ser modificat per l'administrador" #: idhub/models.py:324 msgid "The role was removed by admin" -msgstr "" +msgstr "L'administrador ha eliminat el rol" #: idhub/models.py:332 msgid "A new service was created by admin" -msgstr "" +msgstr "Un nou servei ha estat creat per l'administrador" #: idhub/models.py:340 msgid "The service was modified by admin" -msgstr "" +msgstr "El servei ha estat modificat per l'administrador" #: idhub/models.py:348 msgid "The service was removed by admin" -msgstr "" +msgstr "El servei va ser eliminat per l'administrador" #: idhub/models.py:356 #, python-brace-format msgid "" "New Organisational DID with label: '{label}' and DID-ID: '{did}' was created" msgstr "" +"S'ha creat un DID organitzatiu nou amb l'etiqueta: '{label}' i l'ID-DID: " +"'{did}'" #: idhub/models.py:367 #, python-brace-format msgid "" "Organisational DID with label: '{label}' and DID-ID: '{did}' was removed" msgstr "" +"S'ha eliminat el DID de l'organització amb l'etiqueta \"{label}\" i l'ID-" +"DID: \"{did}\"" #: idhub/models.py:457 msgid "Enabled" -msgstr "" +msgstr "Habilitat" #: idhub/models.py:458 idhub/templates/idhub/admin/credentials.html:17 #: idhub/templates/idhub/user/credentials.html:17 msgid "Issued" -msgstr "" +msgstr "Emès" #: idhub/models.py:459 msgid "Revoked" -msgstr "" +msgstr "Revocat" #: idhub/models.py:460 msgid "Expired" -msgstr "" +msgstr "Caducat" #: idhub/models.py:576 msgid "Beneficiary" -msgstr "" +msgstr "Beneficiari" #: idhub/models.py:577 msgid "Employee" -msgstr "" +msgstr "Empleat" #: idhub/models.py:578 msgid "Member" -msgstr "" +msgstr "Membre" #: idhub/models.py:580 msgid "Type of membership" -msgstr "" +msgstr "Tipus de pertinença" #: idhub/models.py:582 msgid "Start date" -msgstr "" +msgstr "Data d'inici" #: idhub/models.py:583 msgid "What date did the membership start?" -msgstr "" +msgstr "Quina data va començar la subscripció?" #: idhub/models.py:588 msgid "End date" -msgstr "" +msgstr "Data final" #: idhub/models.py:589 msgid "What date will the membership end?" -msgstr "" +msgstr "Quina data finalitzarà la subscripció?" #: idhub/models.py:605 msgid "name" -msgstr "" +msgstr "nom" #: idhub/models.py:606 idhub/models.py:614 #: idhub/templates/idhub/admin/dashboard.html:14 @@ -1961,31 +2066,31 @@ msgstr "" #: idhub/templates/idhub/user/dashboard.html:14 #: idhub/templates/idhub/user/roles.html:16 msgid "Description" -msgstr "" +msgstr "Descripción" #: idhub/models.py:613 idhub/templates/templates/musician/addresses.html:16 msgid "Domain" -msgstr "" +msgstr "Domini" #: idhub/models.py:622 msgid "None" -msgstr "" +msgstr "Sense" #: idhub/models.py:647 msgid "Url where to send the presentation" -msgstr "" +msgstr "URL on enviar la presentació" #: idhub/templates/auth/login.html:47 msgid "Log in" -msgstr "" +msgstr "Entra" #: idhub/templates/auth/login.html:52 msgid "Forgot your password? Click here to recover" -msgstr "" +msgstr "Has oblidat la teva contrasenya? Feu clic aquí per recuperar-la" #: idhub/templates/auth/login_base.html:91 msgid "Forgot your password?" -msgstr "" +msgstr "Heu oblidat la contrasenya?" #: idhub/templates/auth/login_base.html:97 #, python-format @@ -1993,76 +2098,91 @@ msgid "" "Send an email to %(support_email)s " "including your username and we will provide instructions." msgstr "" +"Envieu un correu electrònic a %(support_email)s amb el vostre nom d'usuari i us " +"proporcionarem instruccions." #: idhub/templates/auth/password_reset.html:8 msgid "Password reset" -msgstr "" +msgstr "Restableix contrasenya" #: idhub/templates/auth/password_reset.html:9 msgid "" "Forgotten your password? Enter your email address below, and we'll email " "instructions for setting a new one." msgstr "" +"Has oblidat la teva contrassenya? Introduïu la vostra adreça de correu " +"electrònic a continuació i us enviarem les instruccions per configurar-ne " +"una de nova." #: idhub/templates/auth/password_reset.html:21 msgid "Reset my password" -msgstr "" +msgstr "Reinicia la meva contrasenya" #: idhub/templates/auth/password_reset_complete.html:9 msgid "Password reset complete" -msgstr "" +msgstr "S'ha completat la reinicialització de la contrasenya" #: idhub/templates/auth/password_reset_complete.html:10 msgid "Your password has been set. You may go ahead and log in now." -msgstr "" +msgstr "S'ha establert la vostra contrasenya. Ara podeu entrar." #: idhub/templates/auth/password_reset_complete.html:11 idhub/views.py:9 msgid "Login" -msgstr "" +msgstr "Inicia la sessió" #: idhub/templates/auth/password_reset_confirm.html:9 msgid "Enter new password" -msgstr "" +msgstr "Introduïu la contrasenya nova" #: idhub/templates/auth/password_reset_confirm.html:10 msgid "" "Please enter your new password twice so we can verify you typed it in " "correctly." msgstr "" +"Introduïu la contrasenya nova dues vegades, de manera que ens assegurem que " +"l'heu escrita correctament." #: idhub/templates/auth/password_reset_confirm.html:21 msgid "Change my password" -msgstr "" +msgstr "Canvia la contrasenya" #: idhub/templates/auth/password_reset_confirm.html:29 msgid "Password reset unsuccessful" -msgstr "" +msgstr "No s'ha pogut recuperar la contrasenya" #: idhub/templates/auth/password_reset_confirm.html:30 msgid "" "The password reset link was invalid, possibly because it has already been " "used." msgstr "" +"L'enllaç de restabliment de la contrasenya no era vàlid, possiblement perquè " +"ja s'ha utilitzat." #: idhub/templates/auth/password_reset_confirm.html:31 msgid "Please request a new password reset." -msgstr "" +msgstr "Sol·liciteu un restabliment de la contrasenya nova." #: idhub/templates/auth/password_reset_done.html:7 msgid "Password reset sent" -msgstr "" +msgstr "S'ha enviat el restabliment de la contrasenya" #: idhub/templates/auth/password_reset_done.html:9 msgid "" "We've emailed you instructions for setting your password, if an account " "exists with the email you entered. You should receive them shortly." msgstr "" +"Us hem enviat per correu electrònic instruccions per configurar la vostra " +"contrasenya, si hi ha un compte amb el correu electrònic que heu introduït. " +"Les hauríeu de rebre en breu." #: idhub/templates/auth/password_reset_done.html:11 msgid "" "If you don't receive an email, please make sure you've entered the address " "you registered with, and check your spam folder." msgstr "" +"Si no rebeu cap correu electrònic, assegureu-vos que heu introduït l'adreça " +"amb la qual us heu registrat i comproveu la vostra carpeta de correu brossa." #: idhub/templates/auth/password_reset_email.html:3 #: idhub/templates/auth/password_reset_email.txt:2 @@ -2071,34 +2191,36 @@ msgid "" "You're receiving this email because you requested a password reset for your " "user account at %(site_name)s." msgstr "" +"Heu rebut aquest correu electrònic perquè heu sol·licitat un restabliment de " +"la contrasenya per al vostre compte d'usuari a %(site_name)s." #: idhub/templates/auth/password_reset_email.html:7 #: idhub/templates/auth/password_reset_email.txt:4 msgid "Please go to the following page and choose a new password:" -msgstr "" +msgstr "Aneu a la pàgina següent i trieu una nova contrasenya:" #: idhub/templates/auth/password_reset_email.html:19 #: idhub/templates/auth/password_reset_email.txt:8 msgid "Your username, in case you've forgotten:" -msgstr "" +msgstr "El vostre nom d'usuari, en cas que l'heu oblidat:" #: idhub/templates/auth/password_reset_email.html:23 #: idhub/templates/auth/password_reset_email.txt:10 #: idhub/templates/idhub/admin/registration/activate_user_email.html:24 #: idhub/templates/idhub/admin/registration/activate_user_email.txt:15 msgid "Thanks for using our site!" -msgstr "" +msgstr "Gràcies per utilitzar el nostre lloc!" #: idhub/templates/auth/password_reset_email.html:27 #: idhub/templates/auth/password_reset_email.txt:12 #, python-format msgid "The %(site_name)s team" -msgstr "" +msgstr "L'equip de %(site_name)s" #: idhub/templates/auth/password_reset_subject.txt:2 #, python-format msgid "Password reset on %(site_name)s" -msgstr "" +msgstr "Restabliment de la contrasenya a %(site_name)s" #: idhub/templates/idhub/admin/credentials.html:15 #: idhub/templates/idhub/user/credentials.html:15 @@ -2107,38 +2229,38 @@ msgstr "" #: idhub/templates/templates/musician/databases.html:17 #: idhub/templates/templates/musician/domain_detail.html:17 msgid "Type" -msgstr "" +msgstr "Tipus" #: idhub/templates/idhub/admin/credentials.html:16 #: idhub/templates/idhub/user/credentials.html:16 msgid "Details" -msgstr "" +msgstr "Detalls" #: idhub/templates/idhub/admin/credentials.html:18 #: idhub/templates/idhub/user/credentials.html:18 msgid "Status" -msgstr "" +msgstr "Status" #: idhub/templates/idhub/admin/credentials.html:19 msgid "User" -msgstr "" +msgstr "Usuaris" #: idhub/templates/idhub/admin/credentials.html:31 #: idhub/templates/idhub/admin/people.html:33 #: idhub/templates/idhub/admin/schemas.html:30 msgid "View" -msgstr "" +msgstr "Vista" #: idhub/templates/idhub/admin/dashboard.html:13 msgid "Event" -msgstr "" +msgstr "Esdeveniment" #: idhub/templates/idhub/admin/dashboard.html:15 #: idhub/templates/idhub/admin/dids.html:15 #: idhub/templates/idhub/user/dashboard.html:15 #: idhub/templates/idhub/user/dids.html:15 msgid "Date" -msgstr "" +msgstr "Data" #: idhub/templates/idhub/admin/did_register.html:29 #: idhub/templates/idhub/admin/import_add.html:27 @@ -2158,7 +2280,7 @@ msgstr "" #: idhub/templates/templates/musician/mailbox_check_delete.html:13 #: idhub/templates/templates/musician/mailbox_form.html:20 msgid "Cancel" -msgstr "" +msgstr "Cancel·lar" #: idhub/templates/idhub/admin/did_register.html:30 #: idhub/templates/idhub/admin/import_add.html:28 @@ -2174,12 +2296,12 @@ msgstr "" #: idhub/templates/templates/musician/mailbox_change_password.html:12 #: idhub/templates/templates/musician/mailbox_form.html:21 msgid "Save" -msgstr "" +msgstr "Desar" #: idhub/templates/idhub/admin/dids.html:16 #: idhub/templates/idhub/user/dids.html:16 msgid "Label" -msgstr "" +msgstr "Etiqueta" #: idhub/templates/idhub/admin/dids.html:28 #: idhub/templates/idhub/admin/roles.html:26 @@ -2188,77 +2310,77 @@ msgstr "" #: idhub/templates/idhub/admin/user_edit.html:86 #: idhub/templates/idhub/user/dids.html:28 msgid "Edit" -msgstr "" +msgstr "Editar" #: idhub/templates/idhub/admin/dids.html:29 #: idhub/templates/idhub/admin/schemas.html:31 #: idhub/templates/idhub/user/dids.html:29 msgid "Remove" -msgstr "" +msgstr "Esborra" #: idhub/templates/idhub/admin/dids.html:35 msgid "Add identity" -msgstr "" +msgstr "Afegeix identitat" #: idhub/templates/idhub/admin/dids.html:46 #: idhub/templates/idhub/user/dids.html:46 msgid "Delete DID" -msgstr "" +msgstr "Suprimeix DID" #: idhub/templates/idhub/admin/dids.html:50 #: idhub/templates/idhub/user/dids.html:50 msgid "Are you sure that you want delete this DID?" -msgstr "" +msgstr "Esteu segur que voleu suprimir aquest DID?" #: idhub/templates/idhub/admin/import.html:15 #: idhub/templates/idhub/admin/schemas.html:15 msgid "Created" -msgstr "" +msgstr "Creat" #: idhub/templates/idhub/admin/import.html:17 msgid "Success" -msgstr "" +msgstr "Ha tingut èxit" #: idhub/templates/idhub/admin/issue_credentials.html:14 #: idhub/templates/idhub/admin/issue_credentials.html:72 msgid "Revoke" -msgstr "" +msgstr "Revoca" #: idhub/templates/idhub/admin/issue_credentials.html:54 #: idhub/templates/idhub/user/credential.html:41 msgid "View in JSON format" -msgstr "" +msgstr "Visualitza en format JSON" #: idhub/templates/idhub/admin/issue_credentials.html:64 msgid "Revoke credential" -msgstr "" +msgstr "Revoca la credencial" #: idhub/templates/idhub/admin/issue_credentials.html:68 msgid "Are you sure that you want revoke this credential?" -msgstr "" +msgstr "Esteu segur que voleu revocar aquesta credencial?" #: idhub/templates/idhub/admin/issue_credentials.html:82 msgid "Delete credential" -msgstr "" +msgstr "Suprimeix la credencial" #: idhub/templates/idhub/admin/issue_credentials.html:86 msgid "Are you sure that you want delete this Credential?" -msgstr "" +msgstr "Esteu segur que voleu suprimir aquesta credencial?" #: idhub/templates/idhub/admin/people.html:13 msgid "Last name" -msgstr "" +msgstr "Cognoms" #: idhub/templates/idhub/admin/people.html:14 msgid "First name" -msgstr "" +msgstr "Nom" #: idhub/templates/idhub/admin/people.html:16 #: idhub/templates/idhub/admin/user.html:62 #: idhub/templates/idhub/admin/user_edit.html:41 #: idhub/templates/idhub/user/profile.html:43 msgid "Membership" -msgstr "" +msgstr "Afiliació" #: idhub/templates/idhub/admin/people.html:17 #: idhub/templates/idhub/admin/roles.html:15 @@ -2267,12 +2389,12 @@ msgstr "" #: idhub/templates/idhub/admin/user_edit.html:73 #: idhub/templates/idhub/user/roles.html:15 msgid "Role" -msgstr "" +msgstr "Càrrec" #: idhub/templates/idhub/admin/registration/activate_user_email.html:2 #: idhub/templates/idhub/admin/registration/activate_user_subject.txt:2 msgid "IdHub" -msgstr "" +msgstr "IdHub" #: idhub/templates/idhub/admin/registration/activate_user_email.html:4 #: idhub/templates/idhub/admin/registration/activate_user_email.txt:5 @@ -2281,529 +2403,536 @@ msgid "" "You're receiving this email because your user account at %(site)s has been " "activated." msgstr "" +"Heu rebut aquest correu electrònic perquè el vostre compte d'usuari a " +"%(site)s s'ha activat." #: idhub/templates/idhub/admin/registration/activate_user_email.html:8 #: idhub/templates/idhub/admin/registration/activate_user_email.txt:7 msgid "Your username is:" -msgstr "" +msgstr "El teu nom d'usuari és:" #: idhub/templates/idhub/admin/registration/activate_user_email.html:12 #: idhub/templates/idhub/admin/registration/activate_user_email.txt:9 msgid "Please go to the following page and choose a password:" -msgstr "" +msgstr "Aneu a la pàgina següent i trieu una contrasenya:" #: idhub/templates/idhub/admin/registration/activate_user_email.html:28 #: idhub/templates/idhub/admin/registration/activate_user_email.txt:17 #, python-format msgid "The %(site)s team" -msgstr "" +msgstr "L'equip de %(site)s" #: idhub/templates/idhub/admin/registration/activate_user_email.txt:3 msgid "Idhub" -msgstr "" +msgstr "Idhub" #: idhub/templates/idhub/admin/registration/activate_user_subject.txt:3 #, python-format msgid "User activation on %(site)s" -msgstr "" +msgstr "Activació d'usuari a %(site)s" #: idhub/templates/idhub/admin/roles.html:33 #: idhub/templates/idhub/admin/user_edit.html:93 msgid "Add Role" -msgstr "" +msgstr "Afegeix un rol" #: idhub/templates/idhub/admin/schemas.html:16 msgid "Template file" -msgstr "" +msgstr "Fitxer de plantilla" #: idhub/templates/idhub/admin/schemas.html:17 #: idhub/templates/templates/musician/mailboxes.html:14 msgid "Name" -msgstr "" +msgstr "Name" #: idhub/templates/idhub/admin/schemas.html:37 #: idhub/templates/idhub/admin/schemas_import.html:29 msgid "Add template" -msgstr "" +msgstr "Afegeix plantilla" #: idhub/templates/idhub/admin/schemas.html:48 msgid "Delete template" -msgstr "" +msgstr "Suprimeix la plantilla" #: idhub/templates/idhub/admin/schemas.html:52 msgid "Are you sure that you want delete this template?" -msgstr "" +msgstr "Segur que voleu suprimir aquesta plantilla?" #: idhub/templates/idhub/admin/schemas_import.html:15 msgid "Available templates" -msgstr "" +msgstr "Plantilles disponibles" #: idhub/templates/idhub/admin/schemas_import.html:23 msgid "Add" -msgstr "" +msgstr "Afegeix" #: idhub/templates/idhub/admin/services.html:15 #: idhub/templates/idhub/admin/user.html:89 #: idhub/templates/idhub/admin/user_edit.html:75 #: idhub/templates/idhub/user/roles.html:17 msgid "Service" -msgstr "" +msgstr "Servei" #: idhub/templates/idhub/admin/user.html:13 msgid "Modify" -msgstr "" +msgstr "Modifica" #: idhub/templates/idhub/admin/user.html:14 msgid "Deactivate" -msgstr "" +msgstr "Desactiva" #: idhub/templates/idhub/admin/user.html:14 msgid "Activate" -msgstr "" +msgstr "Activa" #: idhub/templates/idhub/admin/user.html:63 #: idhub/templates/idhub/admin/user_edit.html:42 #: idhub/templates/idhub/user/profile.html:44 msgid "From" -msgstr "" +msgstr "Remitent" #: idhub/templates/idhub/admin/user.html:64 #: idhub/templates/idhub/admin/user_edit.html:43 #: idhub/templates/idhub/user/profile.html:45 msgid "To" -msgstr "" +msgstr "Destinatari" #: idhub/templates/idhub/admin/user.html:112 msgid "Delete user" -msgstr "" +msgstr "Suprimeix l'usuari" #: idhub/templates/idhub/admin/user.html:116 msgid "Are you sure that you want delete this user?" -msgstr "" +msgstr "Esteu segur que voleu suprimir aquest usuari?" #: idhub/templates/idhub/admin/user_edit.html:61 msgid "Add membership" -msgstr "" +msgstr "Afegeix membres" #: idhub/templates/idhub/base.html:76 msgid "My information" -msgstr "" +msgstr "La meva informació" #: idhub/templates/idhub/base.html:81 msgid "My personal information" -msgstr "" +msgstr "La meva informació personal" #: idhub/templates/idhub/base.html:86 idhub/user/views.py:62 msgid "My roles" -msgstr "" +msgstr "Els meus rols" #: idhub/templates/idhub/base.html:91 idhub/user/views.py:68 msgid "GDPR info" -msgstr "" +msgstr "Informació GDPR" #: idhub/templates/idhub/base.html:99 idhub/user/views.py:26 msgid "My wallet" -msgstr "" +msgstr "La meva cartera" #: idhub/templates/idhub/base.html:104 idhub/user/views.py:169 #: idhub/user/views.py:203 idhub/user/views.py:222 msgid "Identities (DIDs)" -msgstr "" +msgstr "Identitats (DID)" #: idhub/templates/idhub/base.html:109 msgid "My credentials" -msgstr "" +msgstr "Les meves credencials" #: idhub/templates/idhub/base.html:114 msgid "Request a credential" -msgstr "" +msgstr "Sol·licitar una credencial" #: idhub/templates/idhub/base.html:119 msgid "Present a credential" -msgstr "" +msgstr "Presentar una credencial" #: idhub/templates/idhub/base_admin.html:76 msgid "Users" -msgstr "" +msgstr "Usuaris" #: idhub/templates/idhub/base_admin.html:94 msgid "Roles" -msgstr "" +msgstr "Rols" #: idhub/templates/idhub/base_admin.html:112 #: idhub/templates/idhub/user/profile.html:46 msgid "Credentials" -msgstr "" +msgstr "Credencials" #: idhub/templates/idhub/base_admin.html:122 msgid "Organization's wallet" -msgstr "" +msgstr "Cartera de l'organització" #: idhub/templates/idhub/base_admin.html:127 msgid "Manage Identities (DIDs)" -msgstr "" +msgstr "Gestionar identitats (DID)" #: idhub/templates/idhub/base_admin.html:147 msgid "Templates" -msgstr "" +msgstr "Plantilles" #: idhub/templates/idhub/base_admin.html:153 msgid "Data" -msgstr "" +msgstr "Dades" #: idhub/templates/idhub/user/credentials_presentation.html:30 msgid "Send" -msgstr "" +msgstr "Enviar" #: idhub/templates/idhub/user/credentials_request.html:30 msgid "Request" -msgstr "" +msgstr "Sol·licitud" #: idhub/templates/idhub/user/dids.html:35 msgid "Add Identity" -msgstr "" +msgstr "Afegeix identitat" #: idhub/templates/idhub/user/profile.html:13 msgid "ARCO Forms" -msgstr "" +msgstr "Formularis ARCO" #: idhub/templates/idhub/user/profile.html:14 msgid "Notice of Privacy" -msgstr "" +msgstr "Avís de Privacitat" #: idhub/templates/templates/musician/address_check_delete.html:7 #, python-format msgid "Are you sure that you want remove the address: \"%(address_name)s\"?" -msgstr "" +msgstr "Esteu segur que voleu eliminar l'adreça: \"%(address_name)s\"?" #: idhub/templates/templates/musician/address_check_delete.html:8 #: idhub/templates/templates/musician/mailbox_check_delete.html:11 msgid "WARNING: This action cannot be undone." -msgstr "" +msgstr "ADVERTIMENT: Aquesta acció no es pot desfer." #: idhub/templates/templates/musician/addresses.html:15 msgid "Email" -msgstr "" +msgstr "Adreça electrònica" #: idhub/templates/templates/musician/addresses.html:17 #: idhub/templates/templates/musician/mail_base.html:22 msgid "Mailboxes" -msgstr "" +msgstr "Bústies" #: idhub/templates/templates/musician/addresses.html:18 msgid "Forward" -msgstr "" +msgstr "Reenvia" #: idhub/templates/templates/musician/addresses.html:38 msgid "New mail address" -msgstr "" +msgstr "Nova adreça de correu" #: idhub/templates/templates/musician/billing.html:6 msgid "Billing" -msgstr "" +msgstr "Facturació" #: idhub/templates/templates/musician/billing.html:7 msgid "Billing page description." -msgstr "" +msgstr "Descripció de la pàgina de facturació." #: idhub/templates/templates/musician/billing.html:19 msgid "Number" -msgstr "" +msgstr "Número" #: idhub/templates/templates/musician/billing.html:20 msgid "Bill date" -msgstr "" +msgstr "Data de facturació" #: idhub/templates/templates/musician/billing.html:22 msgid "Total" -msgstr "" +msgstr "Total" #: idhub/templates/templates/musician/billing.html:23 msgid "Download PDF" -msgstr "" +msgstr "Descarregar PDF" #: idhub/templates/templates/musician/components/table_paginator.html:15 msgid "Previous" -msgstr "" +msgstr "Anterior" #: idhub/templates/templates/musician/components/table_paginator.html:29 msgid "Next" -msgstr "" +msgstr "Següent" #: idhub/templates/templates/musician/dashboard.html:6 msgid "Welcome back" -msgstr "" +msgstr "Benvingut de nou" #: idhub/templates/templates/musician/dashboard.html:8 #, python-format msgid "Last time you logged in was: %(last_login)s" -msgstr "" +msgstr "La darrera vegada que vas iniciar sessió va ser: %(last_login)s" #: idhub/templates/templates/musician/dashboard.html:10 msgid "It's the first time you log into the system, welcome on board!" -msgstr "" +msgstr "És la primera vegada que inicieu sessió al sistema, benvingut a bord!" #: idhub/templates/templates/musician/dashboard.html:29 msgid "Notifications" -msgstr "" +msgstr "Notificacions" #: idhub/templates/templates/musician/dashboard.html:33 msgid "There is no notifications at this time." -msgstr "" +msgstr "No hi ha notificacions en aquest moment." #: idhub/templates/templates/musician/dashboard.html:40 msgid "Your domains and websites" -msgstr "" +msgstr "Els vostres dominis i llocs web" #: idhub/templates/templates/musician/dashboard.html:41 msgid "Dashboard page description." -msgstr "" +msgstr "Descripció de la pàgina del tauler." #: idhub/templates/templates/musician/dashboard.html:56 msgid "view configuration" -msgstr "" +msgstr "veure la configuració" #: idhub/templates/templates/musician/dashboard.html:63 msgid "Expiration date" -msgstr "" +msgstr "Data de caducitat" #: idhub/templates/templates/musician/dashboard.html:70 msgid "Mail" -msgstr "" +msgstr "Correu" #: idhub/templates/templates/musician/dashboard.html:73 msgid "mail addresses created" -msgstr "" +msgstr "adreces de correu creades" #: idhub/templates/templates/musician/dashboard.html:78 msgid "Mail list" -msgstr "" +msgstr "Llista de correu" #: idhub/templates/templates/musician/dashboard.html:83 msgid "Software as a Service" -msgstr "" +msgstr "Programari com a servei" #: idhub/templates/templates/musician/dashboard.html:85 msgid "Nothing installed" -msgstr "" +msgstr "No hi ha res instal·lat" #: idhub/templates/templates/musician/dashboard.html:90 msgid "Disk usage" -msgstr "" +msgstr "Ús del disc" #: idhub/templates/templates/musician/dashboard.html:107 msgid "Configuration details" -msgstr "" +msgstr "Detalls de configuració" #: idhub/templates/templates/musician/dashboard.html:114 msgid "FTP access:" -msgstr "" +msgstr "Accés FTP:" #. Translators: domain configuration detail modal #: idhub/templates/templates/musician/dashboard.html:116 msgid "Contact with the support team to get details concerning FTP access." msgstr "" +"Contacta amb l'equip d'assistència per obtenir detalls sobre l'accés FTP." #: idhub/templates/templates/musician/dashboard.html:125 msgid "No website configured." -msgstr "" +msgstr "No s'ha configurat cap lloc web." #: idhub/templates/templates/musician/dashboard.html:127 msgid "Root directory:" -msgstr "" +msgstr "Directori arrel:" #: idhub/templates/templates/musician/dashboard.html:128 msgid "Type:" -msgstr "" +msgstr "Tipus:" #: idhub/templates/templates/musician/dashboard.html:133 msgid "View DNS records" -msgstr "" +msgstr "Veure registres DNS" #: idhub/templates/templates/musician/databases.html:21 msgid "associated to" -msgstr "" +msgstr "associada a" #: idhub/templates/templates/musician/databases.html:34 msgid "No users for this database." -msgstr "" +msgstr "No hi ha usuaris per a aquesta base de dades." #: idhub/templates/templates/musician/databases.html:45 msgid "Open database manager" -msgstr "" +msgstr "Obriu el gestor de bases de dades" #. Translators: database page when there isn't any database. #. Translators: saas page when there isn't any saas. #: idhub/templates/templates/musician/databases.html:58 #: idhub/templates/templates/musician/saas.html:49 msgid "Ooops! Looks like there is nothing here!" -msgstr "" +msgstr "Vaja! Sembla que aquí no hi ha res!" #: idhub/templates/templates/musician/domain_detail.html:5 msgid "Go back" -msgstr "" +msgstr "Torna enrere" #: idhub/templates/templates/musician/domain_detail.html:7 msgid "DNS settings for" -msgstr "" +msgstr "Configuració de DNS per a" #: idhub/templates/templates/musician/domain_detail.html:8 msgid "DNS settings page description." -msgstr "" +msgstr "Descripció de la pàgina de configuració de DNS." #: idhub/templates/templates/musician/domain_detail.html:18 msgid "Value" -msgstr "" +msgstr "Valor" #: idhub/templates/templates/musician/mail_base.html:6 #: idhub/templates/templates/musician/mailinglists.html:6 msgid "Go to global" -msgstr "" +msgstr "Anar a global" #: idhub/templates/templates/musician/mail_base.html:10 #: idhub/templates/templates/musician/mailinglists.html:9 msgid "for" -msgstr "" +msgstr "per" #: idhub/templates/templates/musician/mail_base.html:18 #: idhub/templates/templates/musician/mailboxes.html:16 msgid "Addresses" -msgstr "" +msgstr "Adreces" #: idhub/templates/templates/musician/mailbox_change_password.html:5 #: idhub/templates/templates/musician/mailbox_form.html:24 msgid "Change password" -msgstr "" +msgstr "Canviar la contrasenya" #: idhub/templates/templates/musician/mailbox_check_delete.html:7 #, python-format msgid "Are you sure that you want remove the mailbox: \"%(name)s\"?" -msgstr "" +msgstr "Esteu segur que voleu eliminar la bústia de correu: \"%(name)s\"?" #: idhub/templates/templates/musician/mailbox_check_delete.html:9 msgid "" "All mailbox's messages will be deleted and cannot be recovered." msgstr "" +"Tots els missatges de la bústia se suprimiran i no es podran " +"recuperar ." #: idhub/templates/templates/musician/mailbox_form.html:9 msgid "Warning!" -msgstr "" +msgstr "Atenció!" #: idhub/templates/templates/musician/mailbox_form.html:9 msgid "" "You have reached the limit of mailboxes of your subscription so " "extra fees may apply." msgstr "" +"Heu arribat al límit de bústies de la vostra subscripció, de manera que es " +"poden aplicar càrrecs addicionals." #: idhub/templates/templates/musician/mailbox_form.html:10 msgid "Close" -msgstr "" +msgstr "Tanca" #: idhub/templates/templates/musician/mailboxes.html:15 msgid "Filtering" -msgstr "" +msgstr "Filtratge" #: idhub/templates/templates/musician/mailboxes.html:27 msgid "Update password" -msgstr "" +msgstr "Actualitzar la contrasenya" #: idhub/templates/templates/musician/mailboxes.html:43 msgid "New mailbox" -msgstr "" +msgstr "Nova bústia" #: idhub/templates/templates/musician/mailinglists.html:34 msgid "Active" -msgstr "" +msgstr "Actiu" #: idhub/templates/templates/musician/mailinglists.html:36 msgid "Inactive" -msgstr "" +msgstr "Inactiu" #: idhub/templates/templates/musician/profile.html:6 msgid "Profile" -msgstr "" +msgstr "Perfil" #: idhub/templates/templates/musician/profile.html:7 msgid "Little description on profile page." -msgstr "" +msgstr "Petita descripció a la pàgina de perfil." #: idhub/templates/templates/musician/profile.html:11 msgid "User information" -msgstr "" +msgstr "Informació d'usuari" #: idhub/templates/templates/musician/profile.html:21 msgid "Preferred language:" -msgstr "" +msgstr "Idioma preferit:" #: idhub/templates/templates/musician/profile.html:35 msgid "Billing information" -msgstr "" +msgstr "Dades de facturació" #: idhub/templates/templates/musician/profile.html:49 msgid "payment method:" -msgstr "" +msgstr "mètode de pagament:" #: idhub/templates/templates/musician/profile.html:60 msgid "Check your last bills" -msgstr "" +msgstr "Comproveu les vostres últimes factures" #: idhub/templates/templates/musician/saas.html:18 msgid "Installed on" -msgstr "" +msgstr "Instal·lat a" #: idhub/templates/templates/musician/saas.html:29 msgid "Service info" -msgstr "" +msgstr "Informació del servei" #: idhub/templates/templates/musician/saas.html:30 msgid "active" -msgstr "" +msgstr "actiu" #: idhub/templates/templates/musician/saas.html:37 msgid "Open service admin panel" -msgstr "" +msgstr "Obriu el tauler d'administració del servei" #: idhub/user/views.py:21 msgid "My profile" -msgstr "" +msgstr "El meu perfil" #: idhub/user/views.py:40 msgid "My personal data" -msgstr "" +msgstr "Les meves dades personals" #: idhub/user/views.py:87 msgid "Credential" -msgstr "" +msgstr "Credencial" #: idhub/user/views.py:123 msgid "Credential request" -msgstr "" +msgstr "Sol·licitar una credencial" #: idhub/user/views.py:136 msgid "The credential was issued successfully!" -msgstr "" +msgstr "La credencial s'ha emès correctament!" #: idhub/user/views.py:140 msgid "The credential does not exist!" -msgstr "" +msgstr "La credencial no existeix!" #: idhub/user/views.py:146 msgid "Credential presentation" -msgstr "" +msgstr "Presentació de credencials" #: idhub/user/views.py:161 msgid "The credential was presented successfully!" -msgstr "" +msgstr "La credencial s'ha presentat correctament!" #: idhub/user/views.py:163 msgid "Error sending credential!" -msgstr "" +msgstr "S'ha produït un error en enviar la credencial!" #: idhub/user/views.py:182 msgid "Add a new Identity (DID)" -msgstr "" +msgstr "Afegeix una nova identitat (DID)" #: idhub_auth/forms.py:19 idhub_auth/forms.py:27 msgid "The string must contain only characters and spaces" -msgstr "" +msgstr "La cadena ha de contenir només caràcters i espais" diff --git a/locale/es/LC_MESSAGES/django.mo b/locale/es/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..71cbdf3e9d8d54be31066ec4ad8628bc2c1f2845 GIT binary patch literal 380 zcmYL@K~KUk7=|%=+R?Lz&%}d9i{c3jGZa>EvE7z2Nc2{r&Y96JZ6W$Y{CoZuJ5A(G zp7i_Dx9RhJeDu}vIq;l#&OC>nD^HugXY4QU{MmN?lNtRkR}RH%w3NnHT4Bh@vF%H^(V-=Ii1iQ$Qo9Pt!I1Rhe%oml#`f^NEGFCKEL->Rc=KoQ6a?!10%_7(V7ey8`V`;n{war z20Z3;uifk31QV^CRQ|iq#``$=;jWunRB8aLH({)F;i8zL{=V00y-I_qTIqGAN(}v% i$^}`yHKImSZ8jEzYJOK6-VWez49^vuhS0kh1f3tbb!oc* literal 0 HcmV?d00001 diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index 61d9637..c09c41b 100644 --- a/trustchain_idhub/settings.py +++ b/trustchain_idhub/settings.py @@ -79,6 +79,7 @@ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', + 'django.middleware.locale.LocaleMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', From a976b5bd16669ef72ad6891fab9322e7c7465fc6 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 28 Nov 2023 10:48:57 +0100 Subject: [PATCH 12/70] view demand --- oidc4vp/urls.py | 12 ++++++++++++ oidc4vp/views.py | 31 +++++++++++++++---------------- 2 files changed, 27 insertions(+), 16 deletions(-) create mode 100644 oidc4vp/urls.py diff --git a/oidc4vp/urls.py b/oidc4vp/urls.py new file mode 100644 index 0000000..d06949c --- /dev/null +++ b/oidc4vp/urls.py @@ -0,0 +1,12 @@ +from django.urls import path, reverse_lazy + +from oidc4vp import views + + +app_name = 'oidc4vp' + + +urlpatterns = [ + path('verify/', views.VerifyView.as_view(), + name="verification_portal_verify") +] diff --git a/oidc4vp/views.py b/oidc4vp/views.py index c217114..4a39fba 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -1,26 +1,25 @@ import json -from django.core.mail import send_mail -from django.http import HttpResponse, HttpResponseRedirect +from django.views.generic.edit import View -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 from oidc4vp.models import Authorization +from django.http import HttpResponse -# class PeopleListView(People, TemplateView): -# template_name = "idhub/admin/people.html" -# subtitle = _('View users') -# icon = 'bi bi-person' +# from django.core.mail import send_mail +# from django.http import HttpResponse, HttpResponseRedirect -# def get_context_data(self, **kwargs): -# context = super().get_context_data(**kwargs) -# context.update({ -# 'users': User.objects.filter(), -# }) -# return context +# from utils.idhub_ssikit import verify_presentation +# from oidc4vp.models import VPVerifyRequest +# from django.shortcuts import get_object_or_404 +# from more_itertools import flatten, unique_everseen + + +class VerifyView(View): + def get(self, request, *args, **kwargs): + import pdb; pdb.set_trace() + res = json.dumps({"uri": "http://localhost:10000"}) + return HttpResponse(res) def DemandAuthorizationView(request): From cfbbaf491eb9909265533f661900b7db4647223d Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 28 Nov 2023 12:48:50 +0100 Subject: [PATCH 13/70] fix organization example --- examples/organizations.csv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/organizations.csv b/examples/organizations.csv index e79edda..70e1af0 100644 --- a/examples/organizations.csv +++ b/examples/organizations.csv @@ -1,4 +1,4 @@ "ExO";"https://verify.exo.cat" "Somos Connexión";"https://verify.somosconexion.coop" -"test2";"http://localhost:9000/verify" -"test1";"http://localhost:8000/verify" +"test2";"http://localhost:9000/oidc4vp/verify" +"test1";"http://localhost:8000/oidc4vp/verify" From 5e95d6b15c96cfb152eb4311e9fbff18dd4c8e96 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 28 Nov 2023 12:49:28 +0100 Subject: [PATCH 14/70] demand authorization get to verifier --- oidc4vp/views.py | 105 +++++++++++++++++++++-------------------------- 1 file changed, 46 insertions(+), 59 deletions(-) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 4a39fba..6fe4623 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -2,7 +2,7 @@ import json from django.views.generic.edit import View -from oidc4vp.models import Authorization +from oidc4vp.models import Authorization, Organization from django.http import HttpResponse @@ -11,69 +11,56 @@ from django.http import HttpResponse # from utils.idhub_ssikit import verify_presentation # from oidc4vp.models import VPVerifyRequest -# from django.shortcuts import get_object_or_404 +from django.shortcuts import get_object_or_404 # from more_itertools import flatten, unique_everseen class VerifyView(View): def get(self, request, *args, **kwargs): + org_url = request.GET.get('demand_uri') + org = get_object_or_404(Organization, response_uri=org_url) + authorization = Authorization( + organization=org, + presentation_definition="MemberCredential" + ) import pdb; pdb.set_trace() - res = json.dumps({"uri": "http://localhost:10000"}) + res = json.dumps({"redirect_uri": authorization.authorize()}) return HttpResponse(res) - -def DemandAuthorizationView(request): - assert request.method == "GET" - import pdb; pdb.set_trace() - params = request.GET.params - org = Organization.objects.filter( - url=params.get('redirect_uri') - ) - authorization = Authorization( - organization=org, - presentation_definition="MemberCredential" - ) - # authorization.save() - res = json.dumps({"uri": authorization.authorize()}) - return HttpResponse(res) - - -def verify(request): - import pdb; pdb.set_trace() -# 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) - + def post(self, request, *args, **kwargs): + import pdb; pdb.set_trace() + # # 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) From d84ad8f47000901bec3ef6bcf8011f9a47cd0aa1 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 28 Nov 2023 17:33:24 +0100 Subject: [PATCH 15/70] first step of oidc --- idhub/user/forms.py | 6 ++---- idhub/user/views.py | 4 ++-- oidc4vp/models.py | 6 +----- oidc4vp/views.py | 29 +++++++++++++++++++++++------ 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/idhub/user/forms.py b/idhub/user/forms.py index 8bacf89..cce197c 100644 --- a/idhub/user/forms.py +++ b/idhub/user/forms.py @@ -81,10 +81,8 @@ class DemandAuthorizationForm(forms.Form): if commit: url = self.org.demand_authorization() auth = (self.org.client_id, self.org.client_secret) - # res = requests.get(url, auth=auth) - # import pdb; pdb.set_trace() - # if res.status == 200: - # return res.body + if url.status_code == 200: + return url.json().get('redirect_uri') return diff --git a/idhub/user/views.py b/idhub/user/views.py index f509016..09eae35 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -160,9 +160,9 @@ class DemandAuthorizationView(MyWallet, FormView): def form_valid(self, form): authorization = form.save() + # import pdb; pdb.set_trace() if authorization: - if authorization.get('redirect_uri'): - redirect(authorization.get('redirect_uri')) + redirect(authorization) else: messages.error(self.request, _("Error sending credential!")) return super().form_valid(form) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index 2d0d224..83cdef1 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -112,15 +112,11 @@ class Authorization(models.Model): ) def authorize(self): - response_uri = self.__class__.objects.filter( - response_uri=settings.ALLOW_CODE_URI - ) data = { "response_type": "vp_token", "response_mode": "direct_post", "client_id": self.organization.client_id, - "response_uri": response_uri, - "presentation_definition": "...", + "presentation_definition": self.presentation_definition, "nonce": gen_salt(5), } query_dict = QueryDict('', mutable=True) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 6fe4623..77a97b6 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -1,9 +1,11 @@ import json +import base64 from django.views.generic.edit import View from oidc4vp.models import Authorization, Organization -from django.http import HttpResponse +from django.http import HttpResponse, Http404 +from django.shortcuts import get_object_or_404 # from django.core.mail import send_mail @@ -11,22 +13,37 @@ from django.http import HttpResponse # from utils.idhub_ssikit import verify_presentation # from oidc4vp.models import VPVerifyRequest -from django.shortcuts import get_object_or_404 # from more_itertools import flatten, unique_everseen class VerifyView(View): def get(self, request, *args, **kwargs): - org_url = request.GET.get('demand_uri') - org = get_object_or_404(Organization, response_uri=org_url) + org = self.validate(request) + if not org: + raise Http404("Page not Found!") + authorization = Authorization( organization=org, presentation_definition="MemberCredential" ) - import pdb; pdb.set_trace() - res = json.dumps({"redirect_uri": authorization.authorize()}) return HttpResponse(res) + def validate(self, request): + auth_header = request.headers.get('Authorization', b'') + auth_data = auth_header.split() + + if len(auth_data) == 2 and auth_data[0].lower() == b'basic': + decoded_auth = base64.b64decode(auth_data[1]).decode('utf-8') + client_id, client_secret = decoded_auth.split(':', 1) + org_url = request.GET.get('demand_uri') + org = get_object_or_404( + Organization, + response_uri=org_url, + client_id=client_id, + client_secret=client_secret + ) + return org + def post(self, request, *args, **kwargs): import pdb; pdb.set_trace() # # TODO: incorporate request.POST["presentation_submission"] as schema definition From 4a3cf5c5df86be6b662d369bb83738392e20683d Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 10:54:05 +0100 Subject: [PATCH 16/70] fix models --- oidc4vp/migrations/0001_initial.py | 34 +++++++++++++++++++++++++++++- oidc4vp/models.py | 27 ++++++++++++++++++------ 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/oidc4vp/migrations/0001_initial.py b/oidc4vp/migrations/0001_initial.py index c88d0f8..917553d 100644 --- a/oidc4vp/migrations/0001_initial.py +++ b/oidc4vp/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-11-24 17:10 +# Generated by Django 4.2.5 on 2023-11-29 09:52 from django.conf import settings from django.db import migrations, models @@ -59,6 +59,18 @@ class Migration(migrations.Migration): default=oidc4vp.models.set_client_secret, max_length=48 ), ), + ( + 'my_client_id', + models.CharField( + default=oidc4vp.models.set_client_id, max_length=24, unique=True + ), + ), + ( + 'my_client_secret', + models.CharField( + default=oidc4vp.models.set_client_secret, max_length=48 + ), + ), ( 'response_uri', models.URLField( @@ -68,6 +80,26 @@ class Migration(migrations.Migration): ), ], ), + migrations.CreateModel( + name='VPVerifyRequest', + fields=[ + ( + 'id', + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ('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)), + ], + ), migrations.CreateModel( name='OAuth2VPToken', fields=[ diff --git a/oidc4vp/models.py b/oidc4vp/models.py index 83cdef1..3a711bf 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -33,7 +33,14 @@ def set_code(): class Organization(models.Model): """ - This class represent a member of one net trust or federated host + This class represent a member of one net trust or federated host. + Client_id and client_secret are the credentials of this organization + get a connection to my. (receive a request) + My_client_id and my_client_secret are my credentials than to use if I + want to connect to this organization. (send a request) + For use the packages requests we need use my_client_id + For use in the get or post method of a View, then we need use client_id + and secret_id """ name = models.CharField(max_length=250) client_id = models.CharField( @@ -45,6 +52,15 @@ class Organization(models.Model): max_length=48, default=set_client_secret ) + my_client_id = models.CharField( + max_length=24, + default=set_client_id, + unique=True + ) + my_client_secret = models.CharField( + max_length=48, + default=set_client_secret + ) response_uri = models.URLField( help_text=_("Url where to send the verificable presentation"), max_length=250 @@ -54,11 +70,8 @@ class Organization(models.Model): """ Send the verificable presentation to Verifier """ - org = self.__class__.objects.get( - response_uri=settings.RESPONSE_URI - ) - auth = (org.client_id, org.client_secret) - return requests.post(self.url, data=vp, auth=auth) + auth = (self.my_client_id, self.client_secret) + return requests.post(self.response_uri, data=vp, auth=auth) def demand_authorization(self): """ @@ -72,7 +85,7 @@ class Organization(models.Model): url=self.response_uri.strip("/"), redirect_uri=settings.RESPONSE_URI ) - auth = (org.client_id, org.client_secret) + auth = (self.my_client_id, self.client_secret) return requests.get(url, auth=auth) def __str__(self): From 37d3430680d0cf540cf4018a24e6f6b2627d241c Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 11:18:12 +0100 Subject: [PATCH 17/70] fix credentials --- oidc4vp/models.py | 5 +---- oidc4vp/views.py | 9 +++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index 3a711bf..94c30c5 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -54,12 +54,9 @@ class Organization(models.Model): ) my_client_id = models.CharField( max_length=24, - default=set_client_id, - unique=True ) my_client_secret = models.CharField( max_length=48, - default=set_client_secret ) response_uri = models.URLField( help_text=_("Url where to send the verificable presentation"), @@ -128,7 +125,7 @@ class Authorization(models.Model): data = { "response_type": "vp_token", "response_mode": "direct_post", - "client_id": self.organization.client_id, + "client_id": self.organization.my_client_id, "presentation_definition": self.presentation_definition, "nonce": gen_salt(5), } diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 77a97b6..7e8ce0e 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -19,13 +19,13 @@ from django.shortcuts import get_object_or_404 class VerifyView(View): def get(self, request, *args, **kwargs): org = self.validate(request) - if not org: - raise Http404("Page not Found!") - + # TODO Not hardcode the list of types of presentation_definition + presentation_definition = json.dumps(['MemberCredential']) authorization = Authorization( organization=org, - presentation_definition="MemberCredential" + presentation_definition=presentation_definition ) + res = json.dumps({"redirect_uri": authorization.authorize()}) return HttpResponse(res) def validate(self, request): @@ -45,6 +45,7 @@ class VerifyView(View): return org def post(self, request, *args, **kwargs): + org = self.validate(request) import pdb; pdb.set_trace() # # TODO: incorporate request.POST["presentation_submission"] as schema definition # (presentation_valid, _) = verify_presentation(request.POST["vp_token"]) From c4fa16107c6c56dcdffcf043a2e7b740f1939757 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 11:19:17 +0100 Subject: [PATCH 18/70] fix migrations --- oidc4vp/migrations/0001_initial.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/oidc4vp/migrations/0001_initial.py b/oidc4vp/migrations/0001_initial.py index 917553d..230817d 100644 --- a/oidc4vp/migrations/0001_initial.py +++ b/oidc4vp/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-11-29 09:52 +# Generated by Django 4.2.5 on 2023-11-29 10:18 from django.conf import settings from django.db import migrations, models @@ -59,18 +59,8 @@ class Migration(migrations.Migration): default=oidc4vp.models.set_client_secret, max_length=48 ), ), - ( - 'my_client_id', - models.CharField( - default=oidc4vp.models.set_client_id, max_length=24, unique=True - ), - ), - ( - 'my_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( From f0bb7182bb2b1c352b5743aeb7f165ab2c2dba8a Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 11:42:20 +0100 Subject: [PATCH 19/70] fix validate credentials --- oidc4vp/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 7e8ce0e..667b4cb 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -32,7 +32,7 @@ class VerifyView(View): auth_header = request.headers.get('Authorization', b'') auth_data = auth_header.split() - if len(auth_data) == 2 and auth_data[0].lower() == b'basic': + if len(auth_data) == 2 and auth_data[0].lower() == 'basic': decoded_auth = base64.b64decode(auth_data[1]).decode('utf-8') client_id, client_secret = decoded_auth.split(':', 1) org_url = request.GET.get('demand_uri') From c6d0cd2b5b2a0b94a12e29fb6804c360e5086ca0 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 11:50:24 +0100 Subject: [PATCH 20/70] fix demand authorization --- oidc4vp/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index 94c30c5..37a18f9 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -82,7 +82,7 @@ class Organization(models.Model): url=self.response_uri.strip("/"), redirect_uri=settings.RESPONSE_URI ) - auth = (self.my_client_id, self.client_secret) + auth = (self.my_client_id, self.my_client_secret) return requests.get(url, auth=auth) def __str__(self): From 7d477a079b2735a69357a6a1b7e0ac876c730132 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 11:53:30 +0100 Subject: [PATCH 21/70] fix view demand authorization --- idhub/user/forms.py | 1 - idhub/user/views.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/idhub/user/forms.py b/idhub/user/forms.py index cce197c..16151bb 100644 --- a/idhub/user/forms.py +++ b/idhub/user/forms.py @@ -80,7 +80,6 @@ class DemandAuthorizationForm(forms.Form): if commit: url = self.org.demand_authorization() - auth = (self.org.client_id, self.org.client_secret) if url.status_code == 200: return url.json().get('redirect_uri') diff --git a/idhub/user/views.py b/idhub/user/views.py index 09eae35..0d0cb35 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -160,9 +160,8 @@ class DemandAuthorizationView(MyWallet, FormView): def form_valid(self, form): authorization = form.save() - # import pdb; pdb.set_trace() if authorization: - redirect(authorization) + return redirect(authorization) else: messages.error(self.request, _("Error sending credential!")) return super().form_valid(form) From 222509c72c7a0fb5f119e80738a1f465995df932 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 12:06:53 +0100 Subject: [PATCH 22/70] suport credentials defined in settings --- oidc4vp/views.py | 4 ++-- trustchain_idhub/settings.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 667b4cb..045731f 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -1,6 +1,7 @@ import json import base64 +from django.conf import settings from django.views.generic.edit import View from oidc4vp.models import Authorization, Organization @@ -19,8 +20,7 @@ from django.shortcuts import get_object_or_404 class VerifyView(View): def get(self, request, *args, **kwargs): org = self.validate(request) - # TODO Not hardcode the list of types of presentation_definition - presentation_definition = json.dumps(['MemberCredential']) + presentation_definition = json.dumps(settings.SUPPORTED_CREDENTIALS) authorization = Authorization( organization=org, presentation_definition=presentation_definition diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index ca45d06..e25b340 100644 --- a/trustchain_idhub/settings.py +++ b/trustchain_idhub/settings.py @@ -187,3 +187,8 @@ USE_L10N = True AUTH_USER_MODEL = 'idhub_auth.User' RESPONSE_URI = config('RESPONSE_URI', default="") ALLOW_CODE_URI= config('ALLOW_CODE_URI', default="") +SUPPORTED_CREDENTIALS = config( + 'SUPPORTED_CREDENTIALS', + default='[]', + cast=literal_eval +) From 5e76b4631f693c2b36d3deb780c777821002e10b Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 12:27:20 +0100 Subject: [PATCH 23/70] add authorization endpoint --- oidc4vp/models.py | 10 +++++----- oidc4vp/urls.py | 6 ++++-- oidc4vp/views.py | 4 ++++ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index 37a18f9..f004494 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -67,18 +67,18 @@ class Organization(models.Model): """ Send the verificable presentation to Verifier """ + url = "{url}/verify".format( + url=self.response_uri.strip("/"), + ) auth = (self.my_client_id, self.client_secret) - return requests.post(self.response_uri, data=vp, auth=auth) + return requests.post(url, data=vp, auth=auth) def demand_authorization(self): """ Send the a request for start a process of Verifier """ - org = self.__class__.objects.get( - response_uri=settings.RESPONSE_URI - ) # import pdb; pdb.set_trace() - url = "{url}/?demand_uri={redirect_uri}".format( + url = "{url}/verify?demand_uri={redirect_uri}".format( url=self.response_uri.strip("/"), redirect_uri=settings.RESPONSE_URI ) diff --git a/oidc4vp/urls.py b/oidc4vp/urls.py index d06949c..03010e8 100644 --- a/oidc4vp/urls.py +++ b/oidc4vp/urls.py @@ -7,6 +7,8 @@ app_name = 'oidc4vp' urlpatterns = [ - path('verify/', views.VerifyView.as_view(), - name="verification_portal_verify") + path('verify', views.VerifyView.as_view(), + name="verify"), + path('authorization', views.AuthorizationView.as_view(), + name="authorization"), ] diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 045731f..7b1666d 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -17,6 +17,10 @@ from django.shortcuts import get_object_or_404 # from more_itertools import flatten, unique_everseen +class AuthorizationView(View): + pass + + class VerifyView(View): def get(self, request, *args, **kwargs): org = self.validate(request) From 3ba9a7ab88170f49f45f9a7670121209a26ec6e8 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 12:32:58 +0100 Subject: [PATCH 24/70] fix not validate --- oidc4vp/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 7b1666d..8ad53a4 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -24,6 +24,8 @@ class AuthorizationView(View): class VerifyView(View): def get(self, request, *args, **kwargs): org = self.validate(request) + if not org: + return Http404("Organization not found!") presentation_definition = json.dumps(settings.SUPPORTED_CREDENTIALS) authorization = Authorization( organization=org, From adfc4df13d868f7906e6065d1b51e13b00f3dde7 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 12:33:55 +0100 Subject: [PATCH 25/70] fix not validate --- oidc4vp/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 8ad53a4..8abbf66 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -25,7 +25,7 @@ class VerifyView(View): def get(self, request, *args, **kwargs): org = self.validate(request) if not org: - return Http404("Organization not found!") + raise Http404("Organization not found!") presentation_definition = json.dumps(settings.SUPPORTED_CREDENTIALS) authorization = Authorization( organization=org, From a1700a9d6eda519f3f59806d1b6d589d6bdeeebc Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 12:37:17 +0100 Subject: [PATCH 26/70] fix slash --- oidc4vp/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index f004494..ee4e44a 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -133,7 +133,7 @@ class Authorization(models.Model): query_dict.update(data) url = '{response_uri}/authorize?{params}'.format( - response_uri=self.organization.response_uri, + response_uri=self.organization.response_uri.strip("/"), params=query_dict.urlencode() ) return url From 4f1a5afea72a8f3aa8eca593a37feca90d1d6385 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 13:21:49 +0100 Subject: [PATCH 27/70] first step authorize --- idhub/templates/idhub/base.html | 2 +- oidc4vp/urls.py | 4 ++-- oidc4vp/views.py | 39 +++++++++++++++++++++++++++------ 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/idhub/templates/idhub/base.html b/idhub/templates/idhub/base.html index 3514c33..834b7ef 100644 --- a/idhub/templates/idhub/base.html +++ b/idhub/templates/idhub/base.html @@ -115,7 +115,7 @@ diff --git a/oidc4vp/urls.py b/oidc4vp/urls.py index 03010e8..b151a85 100644 --- a/oidc4vp/urls.py +++ b/oidc4vp/urls.py @@ -9,6 +9,6 @@ app_name = 'oidc4vp' urlpatterns = [ path('verify', views.VerifyView.as_view(), name="verify"), - path('authorization', views.AuthorizationView.as_view(), - name="authorization"), + path('authorize', views.AuthorizeView.as_view(), + name="authorize"), ] diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 8abbf66..699f926 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -2,12 +2,18 @@ import json import base64 from django.conf import settings -from django.views.generic.edit import View +from django.views.generic.edit import View, FormView +from django.http import HttpResponse, Http404 +from django.shortcuts import get_object_or_404, redirect +from django.utils.translation import gettext_lazy as _ +from django.urls import reverse_lazy from oidc4vp.models import Authorization, Organization -from django.http import HttpResponse, Http404 -from django.shortcuts import get_object_or_404 +from idhub.mixins import UserView +from idhub.user.forms import ( + DemandAuthorizationForm +) # from django.core.mail import send_mail # from django.http import HttpResponse, HttpResponseRedirect @@ -17,15 +23,32 @@ from django.shortcuts import get_object_or_404 # from more_itertools import flatten, unique_everseen -class AuthorizationView(View): - pass +class AuthorizeView(UserView, FormView): + title = _("My wallet") + section = "MyWallet" + template_name = "credentials_presentation.html" + subtitle = _('Credential presentation') + icon = 'bi bi-patch-check-fill' + form_class = DemandAuthorizationForm + success_url = reverse_lazy('idhub:user_demand_authorization') + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['user'] = self.request.user + return kwargs + + def form_valid(self, form): + authorization = form.save() + if authorization: + return redirect(authorization) + else: + messages.error(self.request, _("Error sending credential!")) + return super().form_valid(form) class VerifyView(View): def get(self, request, *args, **kwargs): org = self.validate(request) - if not org: - raise Http404("Organization not found!") presentation_definition = json.dumps(settings.SUPPORTED_CREDENTIALS) authorization = Authorization( organization=org, @@ -50,6 +73,8 @@ class VerifyView(View): ) return org + raise Http404("Organization not found!") + def post(self, request, *args, **kwargs): org = self.validate(request) import pdb; pdb.set_trace() From 89f1668c5c84adcc58560b800482d0c09fd6c230 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 13:22:13 +0100 Subject: [PATCH 28/70] fix response with problems in connections --- idhub/user/views.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/idhub/user/views.py b/idhub/user/views.py index 0d0cb35..0a3bfde 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -159,7 +159,15 @@ class DemandAuthorizationView(MyWallet, FormView): return kwargs def form_valid(self, form): - authorization = form.save() + try: + authorization = form.save() + except Exception: + txt = _("Problems connecting with {url}").format( + url=form.org.response_uri + ) + messages.error(self.request, txt) + return super().form_valid(form) + if authorization: return redirect(authorization) else: From 8da426ef345d51afc35823ee08d5e49bec94ee2f Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 29 Nov 2023 17:29:31 +0100 Subject: [PATCH 29/70] add type in schemas --- idhub/admin/views.py | 13 ++++- idhub/migrations/0001_initial.py | 87 ++++++++++++---------------- idhub/models.py | 8 +-- oidc4vp/forms.py | 97 ++++++++++++++++++++++---------- oidc4vp/views.py | 12 ++-- 5 files changed, 123 insertions(+), 94 deletions(-) diff --git a/idhub/admin/views.py b/idhub/admin/views.py index f8fd6d0..08e0052 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -772,11 +772,14 @@ class SchemasNewView(SchemasMix): return try: data = f.read().decode('utf-8') - assert credtools.validate_schema(json.loads(data)) + ldata = json.loads(data) + assert credtools.validate_schema(ldata) + name = ldata.get('name') + assert name except Exception: messages.error(self.request, _('This is not a valid schema!')) return - schema = Schemas.objects.create(file_schema=file_name, data=data) + schema = Schemas.objects.create(file_schema=file_name, data=data, type=name) schema.save() return schema @@ -818,10 +821,14 @@ class SchemasImportAddView(SchemasMix): data = self.open_file(file_name) try: json.loads(data) + ldata = json.loads(data) + assert credtools.validate_schema(ldata) + name = ldata.get('name') + assert name except Exception: messages.error(self.request, _('This is not a valid schema!')) return - schema = Schemas.objects.create(file_schema=file_name, data=data) + schema = Schemas.objects.create(file_schema=file_name, data=data, type=name) schema.save() return schema diff --git a/idhub/migrations/0001_initial.py b/idhub/migrations/0001_initial.py index b4d6ac7..b163ccc 100644 --- a/idhub/migrations/0001_initial.py +++ b/idhub/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-11-15 09:58 +# Generated by Django 4.2.5 on 2023-11-29 16:14 from django.conf import settings from django.db import migrations, models @@ -57,27 +57,6 @@ class Migration(migrations.Migration): ('created_at', models.DateTimeField(auto_now=True)), ], ), - 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)), - ( - 'url', - models.CharField( - help_text='Url where to send the presentation', max_length=250 - ), - ), - ], - ), migrations.CreateModel( name='Rol', fields=[ @@ -111,6 +90,7 @@ class Migration(migrations.Migration): 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)), @@ -274,36 +254,39 @@ class Migration(migrations.Migration): 'type', models.PositiveSmallIntegerField( choices=[ - (1, 'EV_USR_REGISTERED'), - (2, 'EV_USR_WELCOME'), - (3, 'EV_DATA_UPDATE_REQUESTED_BY_USER'), - (4, 'EV_DATA_UPDATE_REQUESTED'), - (5, 'EV_USR_UPDATED_BY_ADMIN'), - (6, 'EV_USR_UPDATED'), - (7, 'EV_USR_DELETED_BY_ADMIN'), - (8, 'EV_DID_CREATED_BY_USER'), - (9, 'EV_DID_CREATED'), - (10, 'EV_DID_DELETED'), - (11, 'EV_CREDENTIAL_DELETED_BY_ADMIN'), - (12, 'EV_CREDENTIAL_DELETED'), - (13, 'EV_CREDENTIAL_ISSUED_FOR_USER'), - (14, 'EV_CREDENTIAL_ISSUED'), - (15, 'EV_CREDENTIAL_PRESENTED_BY_USER'), - (16, 'EV_CREDENTIAL_PRESENTED'), - (17, 'EV_CREDENTIAL_ENABLED'), - (18, 'EV_CREDENTIAL_CAN_BE_REQUESTED'), - (19, 'EV_CREDENTIAL_REVOKED_BY_ADMIN'), - (20, 'EV_CREDENTIAL_REVOKED'), - (21, 'EV_ROLE_CREATED_BY_ADMIN'), - (22, 'EV_ROLE_MODIFIED_BY_ADMIN'), - (23, 'EV_ROLE_DELETED_BY_ADMIN'), - (24, 'EV_SERVICE_CREATED_BY_ADMIN'), - (25, 'EV_SERVICE_MODIFIED_BY_ADMIN'), - (26, 'EV_SERVICE_DELETED_BY_ADMIN'), - (27, 'EV_ORG_DID_CREATED_BY_ADMIN'), - (28, 'EV_ORG_DID_DELETED_BY_ADMIN'), - (29, 'EV_USR_DEACTIVATED_BY_ADMIN'), - (30, 'EV_USR_ACTIVATED_BY_ADMIN'), + (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'), ] ), ), diff --git a/idhub/models.py b/idhub/models.py index 09ba43b..384d3ed 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -431,6 +431,7 @@ class DID(models.Model): class Schemas(models.Model): + type = models.CharField(max_length=250) file_schema = models.CharField(max_length=250) data = models.TextField() created_at = models.DateTimeField(auto_now=True) @@ -492,10 +493,9 @@ class VerificableCredential(models.Model): return json.loads(self.data) def type(self): - if self.data: - return self.get_schema.get('type')[-1] - - return self.schema.name() + # if self.data and: + # return self.get_schema.get('type')[-1] + return self.schema.type def description(self): if not self.data: diff --git a/oidc4vp/forms.py b/oidc4vp/forms.py index f4f56d5..cdef22e 100644 --- a/oidc4vp/forms.py +++ b/oidc4vp/forms.py @@ -1,41 +1,78 @@ from django import forms +from django.conf import settings + +from oidc4vp.models import Organization -class Organization(forms.Form): - wallet = forms.ChoiceField( - "Wallet", - choices=[(x.id, x.name) for x in Organization.objects.all()] - ) +# class OrganizationForm(forms.Form): +# wallet = forms.ChoiceField( +# "Wallet", +# choices=[(x.id, x.name) for x in Organization.objects.all()] +# ) - def clean_wallet(self): - data = self.cleaned_data["wallet"] - organization = Organization.objects.filter( - id=data - ) +# def clean_wallet(self): +# data = self.cleaned_data["wallet"] +# organization = Organization.objects.filter( +# id=data +# ) - if not organization.exists(): - raise ValidationError("organization is not valid!") +# if not organization.exists(): +# raise ValidationError("organization is not valid!") - self.organization = organization.first() +# self.organization = organization.first() - return data +# return data - def authorize(self): - data = { - "response_type": "vp_token", - "response_mode": "direct_post", - "client_id": self.organization.client_id, - "response_uri": settings.RESPONSE_URI, - "presentation_definition": self.pv_definition(), - "nonce": "" - } - query_dict = QueryDict('', mutable=True) - query_dict.update(data) +# def authorize(self): +# data = { +# "response_type": "vp_token", +# "response_mode": "direct_post", +# "client_id": self.organization.client_id, +# "response_uri": settings.RESPONSE_URI, +# "presentation_definition": self.pv_definition(), +# "nonce": "" +# } +# query_dict = QueryDict('', mutable=True) +# query_dict.update(data) - url = '{response_uri}/authorize?{params}'.format( - response_uri=self.organization.response_uri, - params=query_dict.urlencode() +# url = '{response_uri}/authorize?{params}'.format( +# response_uri=self.organization.response_uri, +# params=query_dict.urlencode() +# ) + +# def pv_definition(self): +# return "" + + +class AuthorizeForm(forms.Form): + organization = forms.ChoiceField(choices=[]) + + def __init__(self, *args, **kwargs): + # import pdb; pdb.set_trace() + self.user = kwargs.pop('user', None) + self.presentation_definition = kwargs.pop('presentation_definition', []) + self.credentials = self.user.vcredentials.filter( + schema__type__in=self.presentation_definition ) + super().__init__(*args, **kwargs) + self.fields['organization'].choices = [ + (x.id, x.name) for x in Organization.objects.filter() + if x.response_uri != settings.RESPONSE_URI + ] + + def save(self, commit=True): + self.org = Organization.objects.filter( + id=self.data['organization'] + ) + if not self.org.exists(): + return + + self.org = self.org[0] + + if commit: + url = self.org.demand_authorization() + if url.status_code == 200: + return url.json().get('redirect_uri') + + return - def pv_definition(self): - return "" diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 699f926..5a5427e 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -11,9 +11,8 @@ from django.urls import reverse_lazy from oidc4vp.models import Authorization, Organization from idhub.mixins import UserView -from idhub.user.forms import ( - DemandAuthorizationForm -) +from oidc4vp.forms import AuthorizeForm + # from django.core.mail import send_mail # from django.http import HttpResponse, HttpResponseRedirect @@ -29,12 +28,15 @@ class AuthorizeView(UserView, FormView): template_name = "credentials_presentation.html" subtitle = _('Credential presentation') icon = 'bi bi-patch-check-fill' - form_class = DemandAuthorizationForm + form_class = AuthorizeForm success_url = reverse_lazy('idhub:user_demand_authorization') def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user + vps = self.request.GET.get('presentation_definition') + # import pdb; pdb.set_trace() + kwargs['presentation_definition'] = json.loads(vps) return kwargs def form_valid(self, form): @@ -77,7 +79,7 @@ class VerifyView(View): def post(self, request, *args, **kwargs): org = self.validate(request) - import pdb; pdb.set_trace() + # import pdb; pdb.set_trace() # # TODO: incorporate request.POST["presentation_submission"] as schema definition # (presentation_valid, _) = verify_presentation(request.POST["vp_token"]) # if not presentation_valid: From d8c9e0b8b3880c2934cff192e8bfe83fdb048db4 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 1 Dec 2023 17:10:21 +0100 Subject: [PATCH 30/70] get the credentials from form --- oidc4vp/forms.py | 42 ++++++++++++++++++++++++++---------------- oidc4vp/views.py | 1 + 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/oidc4vp/forms.py b/oidc4vp/forms.py index cdef22e..9a8633e 100644 --- a/oidc4vp/forms.py +++ b/oidc4vp/forms.py @@ -45,34 +45,44 @@ from oidc4vp.models import Organization class AuthorizeForm(forms.Form): - organization = forms.ChoiceField(choices=[]) + # organization = forms.ChoiceField(choices=[]) def __init__(self, *args, **kwargs): # import pdb; pdb.set_trace() + self.data = kwargs.get('data', {}).copy() self.user = kwargs.pop('user', None) self.presentation_definition = kwargs.pop('presentation_definition', []) + + reg = r'({})'.format('|'.join(self.presentation_definition)) + self.credentials = self.user.vcredentials.filter( - schema__type__in=self.presentation_definition + schema__type__iregex=reg ) super().__init__(*args, **kwargs) - self.fields['organization'].choices = [ - (x.id, x.name) for x in Organization.objects.filter() - if x.response_uri != settings.RESPONSE_URI - ] + for vp in self.presentation_definition: + vp = vp.lower() + choices = [ + (str(x.id), x.schema.type.lower()) for x in self.credentials.filter( + schema__type__iexact=vp) + ] + self.fields[vp.lower()] = forms.ChoiceField( + widget=forms.RadioSelect, + choices=choices + ) def save(self, commit=True): - self.org = Organization.objects.filter( - id=self.data['organization'] - ) - if not self.org.exists(): - return + # self.org = Organization.objects.filter( + # id=self.data['organization'] + # ) + # if not self.org.exists(): + # return - self.org = self.org[0] + # self.org = self.org[0] - if commit: - url = self.org.demand_authorization() - if url.status_code == 200: - return url.json().get('redirect_uri') + # if commit: + # url = self.org.demand_authorization() + # if url.status_code == 200: + # return url.json().get('redirect_uri') return diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 5a5427e..48ea8b7 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -7,6 +7,7 @@ from django.http import HttpResponse, Http404 from django.shortcuts import get_object_or_404, redirect from django.utils.translation import gettext_lazy as _ from django.urls import reverse_lazy +from django.contrib import messages from oidc4vp.models import Authorization, Organization from idhub.mixins import UserView From 98aecad77bfe0e351e457e34692f9deada187e90 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 1 Dec 2023 17:50:30 +0100 Subject: [PATCH 31/70] add type in schema model and fix description in credential --- idhub/admin/views.py | 15 ++++-- idhub/migrations/0001_initial.py | 66 ++++++++++++++------------- idhub/models.py | 23 +++------- idhub_auth/migrations/0001_initial.py | 2 +- 4 files changed, 54 insertions(+), 52 deletions(-) diff --git a/idhub/admin/views.py b/idhub/admin/views.py index f8fd6d0..0693fb2 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -772,11 +772,14 @@ class SchemasNewView(SchemasMix): return try: data = f.read().decode('utf-8') - assert credtools.validate_schema(json.loads(data)) + ldata = json.loads(data) + assert credtools.validate_schema(ldata) + name = ldata.get('name') + assert name except Exception: messages.error(self.request, _('This is not a valid schema!')) return - schema = Schemas.objects.create(file_schema=file_name, data=data) + schema = Schemas.objects.create(file_schema=file_name, data=data, type=name) schema.save() return schema @@ -817,11 +820,15 @@ class SchemasImportAddView(SchemasMix): def create_schema(self, file_name): data = self.open_file(file_name) try: - json.loads(data) + ldata = json.loads(data) + assert credtools.validate_schema(ldata) + name = ldata.get('name') + assert name + except Exception: messages.error(self.request, _('This is not a valid schema!')) return - schema = Schemas.objects.create(file_schema=file_name, data=data) + schema = Schemas.objects.create(file_schema=file_name, data=data, type=name) schema.save() return schema diff --git a/idhub/migrations/0001_initial.py b/idhub/migrations/0001_initial.py index b4d6ac7..a39a956 100644 --- a/idhub/migrations/0001_initial.py +++ b/idhub/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-11-15 09:58 +# Generated by Django 4.2.5 on 2023-12-01 16:40 from django.conf import settings from django.db import migrations, models @@ -111,6 +111,7 @@ class Migration(migrations.Migration): 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)), @@ -274,36 +275,39 @@ class Migration(migrations.Migration): 'type', models.PositiveSmallIntegerField( choices=[ - (1, 'EV_USR_REGISTERED'), - (2, 'EV_USR_WELCOME'), - (3, 'EV_DATA_UPDATE_REQUESTED_BY_USER'), - (4, 'EV_DATA_UPDATE_REQUESTED'), - (5, 'EV_USR_UPDATED_BY_ADMIN'), - (6, 'EV_USR_UPDATED'), - (7, 'EV_USR_DELETED_BY_ADMIN'), - (8, 'EV_DID_CREATED_BY_USER'), - (9, 'EV_DID_CREATED'), - (10, 'EV_DID_DELETED'), - (11, 'EV_CREDENTIAL_DELETED_BY_ADMIN'), - (12, 'EV_CREDENTIAL_DELETED'), - (13, 'EV_CREDENTIAL_ISSUED_FOR_USER'), - (14, 'EV_CREDENTIAL_ISSUED'), - (15, 'EV_CREDENTIAL_PRESENTED_BY_USER'), - (16, 'EV_CREDENTIAL_PRESENTED'), - (17, 'EV_CREDENTIAL_ENABLED'), - (18, 'EV_CREDENTIAL_CAN_BE_REQUESTED'), - (19, 'EV_CREDENTIAL_REVOKED_BY_ADMIN'), - (20, 'EV_CREDENTIAL_REVOKED'), - (21, 'EV_ROLE_CREATED_BY_ADMIN'), - (22, 'EV_ROLE_MODIFIED_BY_ADMIN'), - (23, 'EV_ROLE_DELETED_BY_ADMIN'), - (24, 'EV_SERVICE_CREATED_BY_ADMIN'), - (25, 'EV_SERVICE_MODIFIED_BY_ADMIN'), - (26, 'EV_SERVICE_DELETED_BY_ADMIN'), - (27, 'EV_ORG_DID_CREATED_BY_ADMIN'), - (28, 'EV_ORG_DID_DELETED_BY_ADMIN'), - (29, 'EV_USR_DEACTIVATED_BY_ADMIN'), - (30, 'EV_USR_ACTIVATED_BY_ADMIN'), + (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'), ] ), ), diff --git a/idhub/models.py b/idhub/models.py index 53f8186..50deb54 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -432,6 +432,7 @@ class DID(models.Model): class Schemas(models.Model): + type = models.CharField(max_length=250) file_schema = models.CharField(max_length=250) data = models.TextField() created_at = models.DateTimeField(auto_now=True) @@ -486,23 +487,11 @@ class VerificableCredential(models.Model): related_name='vcredentials', ) - @property - def get_schema(self): - if not self.data: - return {} - return json.loads(self.data) - def type(self): - if self.data: - return self.get_schema.get('type')[-1] - - return self.schema.name() + return self.schema.type def description(self): - if not self.data: - return self.schema.description() - - for des in self.get_schema.get('description', []): + for des in json.loads(self.render()).get('description', []): if settings.LANGUAGE_CODE == des.get('lang'): return des.get('value', '') return '' @@ -528,8 +517,10 @@ class VerificableCredential(models.Model): def get_context(self): d = json.loads(self.csv_data) - format = "%Y-%m-%dT%H:%M:%SZ" - issuance_date = self.issued_on.strftime(format) + issuance_date = '' + if self.issued_on: + format = "%Y-%m-%dT%H:%M:%SZ" + issuance_date = self.issued_on.strftime(format) context = { 'vc_id': self.id, diff --git a/idhub_auth/migrations/0001_initial.py b/idhub_auth/migrations/0001_initial.py index d40f0a4..26dbc6c 100644 --- a/idhub_auth/migrations/0001_initial.py +++ b/idhub_auth/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-11-15 09:58 +# Generated by Django 4.2.5 on 2023-12-01 16:40 from django.db import migrations, models From 2c97bf8d36aab6f63fd6c6720461dfca4bfd62fd Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 1 Dec 2023 18:27:11 +0100 Subject: [PATCH 32/70] fix url organizations --- examples/organizations.csv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/organizations.csv b/examples/organizations.csv index 70e1af0..45029bc 100644 --- a/examples/organizations.csv +++ b/examples/organizations.csv @@ -1,4 +1,4 @@ "ExO";"https://verify.exo.cat" "Somos Connexión";"https://verify.somosconexion.coop" -"test2";"http://localhost:9000/oidc4vp/verify" -"test1";"http://localhost:8000/oidc4vp/verify" +"test2";"http://localhost:9000/oidc4vp/" +"test1";"http://localhost:8000/oidc4vp/" From 37908ba1e70d9761a6dbe454fe7c258d8a7c1eb3 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 1 Dec 2023 19:31:09 +0100 Subject: [PATCH 33/70] generate presentation, step1 --- idhub/migrations/0001_initial.py | 11 ++- idhub/models.py | 6 +- idhub_auth/migrations/0001_initial.py | 2 +- oidc4vp/forms.py | 83 ++++++---------- .../templates/credentials_presentation.html | 96 +++++++++++++++++++ utils/idhub_ssikit/__init__.py | 33 +++---- 6 files changed, 155 insertions(+), 76 deletions(-) create mode 100644 oidc4vp/templates/credentials_presentation.html diff --git a/idhub/migrations/0001_initial.py b/idhub/migrations/0001_initial.py index 4fbf53d..0a49195 100644 --- a/idhub/migrations/0001_initial.py +++ b/idhub/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-12-01 17:19 +# Generated by Django 4.2.5 on 2023-12-01 18:29 from django.conf import settings from django.db import migrations, models @@ -148,7 +148,6 @@ class Migration(migrations.Migration): ('verified', models.BooleanField()), ('created_on', models.DateTimeField(auto_now=True)), ('issued_on', models.DateTimeField(null=True)), - ('subject_did', models.CharField(max_length=250)), ('data', models.TextField()), ('csv_data', models.TextField()), ( @@ -179,6 +178,14 @@ class Migration(migrations.Migration): to='idhub.schemas', ), ), + ( + 'subject_did', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='subject_credentials', + to='idhub.did', + ), + ), ( 'user', models.ForeignKey( diff --git a/idhub/models.py b/idhub/models.py index 262f186..915929c 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -463,7 +463,6 @@ class VerificableCredential(models.Model): verified = models.BooleanField() created_on = models.DateTimeField(auto_now=True) issued_on = models.DateTimeField(null=True) - subject_did = models.CharField(max_length=250) data = models.TextField() csv_data = models.TextField() status = models.PositiveSmallIntegerField( @@ -475,6 +474,11 @@ class VerificableCredential(models.Model): on_delete=models.CASCADE, related_name='vcredentials', ) + subject_did = models.ForeignKey( + DID, + on_delete=models.CASCADE, + related_name='subject_credentials', + ) issuer_did = models.ForeignKey( DID, on_delete=models.CASCADE, diff --git a/idhub_auth/migrations/0001_initial.py b/idhub_auth/migrations/0001_initial.py index 9091472..741a8b9 100644 --- a/idhub_auth/migrations/0001_initial.py +++ b/idhub_auth/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-12-01 17:19 +# Generated by Django 4.2.5 on 2023-12-01 18:29 from django.db import migrations, models diff --git a/oidc4vp/forms.py b/oidc4vp/forms.py index 9a8633e..3d4b16f 100644 --- a/oidc4vp/forms.py +++ b/oidc4vp/forms.py @@ -1,54 +1,13 @@ from django import forms from django.conf import settings +from utils.idhub_ssikit import issue_verifiable_presentation from oidc4vp.models import Organization -# class OrganizationForm(forms.Form): -# wallet = forms.ChoiceField( -# "Wallet", -# choices=[(x.id, x.name) for x in Organization.objects.all()] -# ) - -# def clean_wallet(self): -# data = self.cleaned_data["wallet"] -# organization = Organization.objects.filter( -# id=data -# ) - -# if not organization.exists(): -# raise ValidationError("organization is not valid!") - -# self.organization = organization.first() - -# return data - -# def authorize(self): -# data = { -# "response_type": "vp_token", -# "response_mode": "direct_post", -# "client_id": self.organization.client_id, -# "response_uri": settings.RESPONSE_URI, -# "presentation_definition": self.pv_definition(), -# "nonce": "" -# } -# query_dict = QueryDict('', mutable=True) -# query_dict.update(data) - -# url = '{response_uri}/authorize?{params}'.format( -# response_uri=self.organization.response_uri, -# params=query_dict.urlencode() -# ) - -# def pv_definition(self): -# return "" - - class AuthorizeForm(forms.Form): - # organization = forms.ChoiceField(choices=[]) def __init__(self, *args, **kwargs): - # import pdb; pdb.set_trace() self.data = kwargs.get('data', {}).copy() self.user = kwargs.pop('user', None) self.presentation_definition = kwargs.pop('presentation_definition', []) @@ -69,20 +28,36 @@ class AuthorizeForm(forms.Form): widget=forms.RadioSelect, choices=choices ) + def clean(self): + data = super().clean() + import pdb; pdb.set_trace() + self.list_credentials = [] + for c in self.credentials: + if str(c.id) == data.get(c.schema.type.lower()): + self.list_credentials.append(c) + return data def save(self, commit=True): - # self.org = Organization.objects.filter( - # id=self.data['organization'] - # ) - # if not self.org.exists(): - # return + if not self.list_credentials: + return - # self.org = self.org[0] + did = self.list_credentials[0].subject_did - # if commit: - # url = self.org.demand_authorization() - # if url.status_code == 200: - # return url.json().get('redirect_uri') - - return + self.vp = issue_verifiable_presentation( + vp_template: Template, + vc_list: list[str], + jwk_holder: str, + holder_did: str) + + self.vp = issue_verifiable_presentation( + vp_template: Template, + self.list_credentials, + did.key_material, + did.did) + + if commit: + result = requests.post(self.vp) + return result + + return diff --git a/oidc4vp/templates/credentials_presentation.html b/oidc4vp/templates/credentials_presentation.html new file mode 100644 index 0000000..63f54ba --- /dev/null +++ b/oidc4vp/templates/credentials_presentation.html @@ -0,0 +1,96 @@ +{% extends "idhub/base.html" %} +{% load i18n %} + +{% block content %} +

+ + {{ subtitle }} +

+{% load django_bootstrap5 %} +
+{% csrf_token %} +{% if form.errors %} + +{% endif %} +{% if form.credentials.all %} +{% for presentation in form.presentation_definition %} +
+
+

{{ presentation|capfirst }} +

+
+ +
+
+
+ + + + + + + + + + + + {% for f in form.credentials.all %} + {% if f.schema.type.lower == presentation.lower %} + + + + + + + + {% endif %} + {% endfor %} + +
{{ f.type }}{{ f.description }}{{ f.get_issued_on }}
+
+
+
+{% endfor %} + +
+ {% trans 'I read and understood the' %} + {% trans 'data sharing notice' %} +
+ + +{% else %} +
+
+

{% trans 'There are not credentials for present' %}

+
+
+{% endif %} +
+ + + +{% endblock %} diff --git a/utils/idhub_ssikit/__init__.py b/utils/idhub_ssikit/__init__.py index c4ac0e3..35464b3 100644 --- a/utils/idhub_ssikit/__init__.py +++ b/utils/idhub_ssikit/__init__.py @@ -4,6 +4,7 @@ import didkit import json import jinja2 from django.template.backends.django import Template +from django.template.loader import get_template def generate_did_controller_key(): @@ -49,34 +50,35 @@ def render_and_sign_credential(vc_template: jinja2.Template, jwk_issuer, vc_data def sign_credential(unsigned_vc: str, jwk_issuer): """ - Signs the and unsigned credential with the provided key. + Signs the unsigned credential with the provided key. + The credential template must be rendered with all user data. """ async def inner(): - signed_vc = await didkit.issue_credential( - unsigned_vc, - '{"proofFormat": "ldp"}', - jwk_issuer - ) - return signed_vc + signed_vc = await didkit.issue_credential( + unsigned_vc, + '{"proofFormat": "ldp"}', + jwk_issuer + ) + return signed_vc return asyncio.run(inner()) -def verify_credential(vc, proof_options): +def verify_credential(vc): """ Returns a (bool, str) tuple indicating whether the credential is valid. If the boolean is true, the credential is valid and the second argument can be ignored. If it is false, the VC is invalid and the second argument contains a JSON object with further information. """ async def inner(): - return didkit.verify_credential(vc, proof_options) + return await didkit.verify_credential(vc, '{"proofFormat": "ldp"}') return asyncio.run(inner()) -def issue_verifiable_presentation(vc_list: list[str], jwk_holder: str, holder_did: str) -> str: +def issue_verifiable_presentation(vp_template: Template, vc_list: list[str], jwk_holder: str, holder_did: str) -> str: async def inner(): - unsigned_vp = unsigned_vp_template.render(data) + unsigned_vp = vp_template.render(data) signed_vp = await didkit.issue_presentation( unsigned_vp, '{"proofFormat": "ldp"}', @@ -84,12 +86,6 @@ def issue_verifiable_presentation(vc_list: list[str], jwk_holder: str, holder_di ) return signed_vp - # TODO: convert from Jinja2 -> django-templates - env = Environment( - loader=FileSystemLoader("vc_templates"), - autoescape=select_autoescape() - ) - unsigned_vp_template = env.get_template("verifiable_presentation.json") data = { "holder_did": holder_did, "verifiable_credential_list": "[" + ",".join(vc_list) + "]" @@ -106,6 +102,7 @@ def verify_presentation(vp): """ async def inner(): proof_options = '{"proofFormat": "ldp"}' - return didkit.verify_presentation(vp, proof_options) + return await didkit.verify_presentation(vp, proof_options) return asyncio.run(inner()) + From 501d2b2894259c67d8d4f3926dc4728dc8c8d2b6 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 4 Dec 2023 09:51:08 +0100 Subject: [PATCH 34/70] send a verificable presentation --- idhub/migrations/0001_initial.py | 3 ++- idhub/models.py | 3 ++- idhub/user/forms.py | 2 +- idhub_auth/migrations/0001_initial.py | 2 +- oidc4vp/forms.py | 21 ++++++++++++--------- oidc4vp/models.py | 2 +- oidc4vp/views.py | 12 ++++++++++++ 7 files changed, 31 insertions(+), 14 deletions(-) diff --git a/idhub/migrations/0001_initial.py b/idhub/migrations/0001_initial.py index 0a49195..d841426 100644 --- a/idhub/migrations/0001_initial.py +++ b/idhub/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-12-01 18:29 +# Generated by Django 4.2.5 on 2023-12-04 08:44 from django.conf import settings from django.db import migrations, models @@ -181,6 +181,7 @@ class Migration(migrations.Migration): ( 'subject_did', models.ForeignKey( + null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subject_credentials', to='idhub.did', diff --git a/idhub/models.py b/idhub/models.py index 915929c..b7c26fa 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -478,6 +478,7 @@ class VerificableCredential(models.Model): DID, on_delete=models.CASCADE, related_name='subject_credentials', + null=True ) issuer_did = models.ForeignKey( DID, @@ -528,7 +529,7 @@ class VerificableCredential(models.Model): context = { 'vc_id': self.id, 'issuer_did': self.issuer_did.did, - 'subject_did': self.subject_did, + 'subject_did': self.subject_did.did, 'issuance_date': issuance_date, 'first_name': self.user.first_name, 'last_name': self.user.last_name, diff --git a/idhub/user/forms.py b/idhub/user/forms.py index 16151bb..5b6f9ab 100644 --- a/idhub/user/forms.py +++ b/idhub/user/forms.py @@ -44,7 +44,7 @@ class RequestCredentialForm(forms.Form): if not all([cred.exists(), did.exists()]): return - did = did[0].did + did = did[0] cred = cred[0] try: cred.issue(did) diff --git a/idhub_auth/migrations/0001_initial.py b/idhub_auth/migrations/0001_initial.py index 741a8b9..3ee6d7a 100644 --- a/idhub_auth/migrations/0001_initial.py +++ b/idhub_auth/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-12-01 18:29 +# Generated by Django 4.2.5 on 2023-12-04 08:44 from django.db import migrations, models diff --git a/oidc4vp/forms.py b/oidc4vp/forms.py index 3d4b16f..18ca7ac 100644 --- a/oidc4vp/forms.py +++ b/oidc4vp/forms.py @@ -1,5 +1,7 @@ +import requests from django import forms from django.conf import settings +from django.template.loader import get_template from utils.idhub_ssikit import issue_verifiable_presentation from oidc4vp.models import Organization @@ -10,6 +12,7 @@ class AuthorizeForm(forms.Form): def __init__(self, *args, **kwargs): self.data = kwargs.get('data', {}).copy() self.user = kwargs.pop('user', None) + self.org = kwargs.pop('org', None) self.presentation_definition = kwargs.pop('presentation_definition', []) reg = r'({})'.format('|'.join(self.presentation_definition)) @@ -42,22 +45,22 @@ class AuthorizeForm(forms.Form): return did = self.list_credentials[0].subject_did + vp_template = get_template('credentials/verifiable_presentation.json') + + # self.vp = issue_verifiable_presentation( + # vp_template: Template, + # vc_list: list[str], + # jwk_holder: str, + # holder_did: str) self.vp = issue_verifiable_presentation( - vp_template: Template, - vc_list: list[str], - jwk_holder: str, - holder_did: str) - - self.vp = issue_verifiable_presentation( - vp_template: Template, + vp_template, self.list_credentials, did.key_material, did.did) if commit: - result = requests.post(self.vp) - return result + return org.send(self.vp) return diff --git a/oidc4vp/models.py b/oidc4vp/models.py index ee4e44a..5dae13e 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -70,7 +70,7 @@ class Organization(models.Model): url = "{url}/verify".format( url=self.response_uri.strip("/"), ) - auth = (self.my_client_id, self.client_secret) + auth = (self.my_client_id, self.my_client_secret) return requests.post(url, data=vp, auth=auth) def demand_authorization(self): diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 48ea8b7..5a28741 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -38,6 +38,7 @@ class AuthorizeView(UserView, FormView): vps = self.request.GET.get('presentation_definition') # import pdb; pdb.set_trace() kwargs['presentation_definition'] = json.loads(vps) + kwargs["org"] = self.get_org() return kwargs def form_valid(self, form): @@ -48,6 +49,17 @@ class AuthorizeView(UserView, FormView): messages.error(self.request, _("Error sending credential!")) return super().form_valid(form) + def get_org(self): + client_id = self.request.GET.get("client_id") + if not client_id: + raise Http404("Organization not found!") + + org = get_object_or_404( + Organization, + client_id=client_id, + ) + return org + class VerifyView(View): def get(self, request, *args, **kwargs): From b7dfb6dcfbad30050f5cfa974a1a99492fe52a78 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 4 Dec 2023 10:56:22 +0100 Subject: [PATCH 35/70] send vp_token --- idhub/models.py | 2 +- oidc4vp/forms.py | 40 +++++++++++-------- oidc4vp/models.py | 2 +- .../credentials/verifiable_presentation.json | 2 +- utils/idhub_ssikit/__init__.py | 12 ++++++ 5 files changed, 38 insertions(+), 20 deletions(-) diff --git a/idhub/models.py b/idhub/models.py index b7c26fa..ee7ebed 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -529,7 +529,7 @@ class VerificableCredential(models.Model): context = { 'vc_id': self.id, 'issuer_did': self.issuer_did.did, - 'subject_did': self.subject_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, diff --git a/oidc4vp/forms.py b/oidc4vp/forms.py index 18ca7ac..7c04255 100644 --- a/oidc4vp/forms.py +++ b/oidc4vp/forms.py @@ -1,9 +1,13 @@ +import json import requests + from django import forms from django.conf import settings from django.template.loader import get_template +from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ValidationError -from utils.idhub_ssikit import issue_verifiable_presentation +from utils.idhub_ssikit import create_verifiable_presentation from oidc4vp.models import Organization @@ -33,34 +37,36 @@ class AuthorizeForm(forms.Form): ) def clean(self): data = super().clean() - import pdb; pdb.set_trace() self.list_credentials = [] for c in self.credentials: if str(c.id) == data.get(c.schema.type.lower()): + if c.status is not c.Status.ISSUED.value or not c.data: + txt = _('There are some problems with this credentials') + raise ValidationError(txt) + self.list_credentials.append(c) + return data def save(self, commit=True): if not self.list_credentials: return - did = self.list_credentials[0].subject_did - vp_template = get_template('credentials/verifiable_presentation.json') - - # self.vp = issue_verifiable_presentation( - # vp_template: Template, - # vc_list: list[str], - # jwk_holder: str, - # holder_did: str) - - self.vp = issue_verifiable_presentation( - vp_template, - self.list_credentials, - did.key_material, - did.did) + self.get_verificable_presentation() if commit: - return org.send(self.vp) + return self.org.send(self.vp) return + def get_verificable_presentation(self): + did = self.list_credentials[0].subject_did + vp_template = get_template('credentials/verifiable_presentation.json') + vc_list = json.dumps([json.loads(x.data) 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) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index 5dae13e..4111c18 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -71,13 +71,13 @@ class Organization(models.Model): url=self.response_uri.strip("/"), ) auth = (self.my_client_id, self.my_client_secret) + # import pdb; pdb.set_trace() return requests.post(url, data=vp, auth=auth) def demand_authorization(self): """ Send the a request for start a process of Verifier """ - # import pdb; pdb.set_trace() url = "{url}/verify?demand_uri={redirect_uri}".format( url=self.response_uri.strip("/"), redirect_uri=settings.RESPONSE_URI diff --git a/oidc4vp/templates/credentials/verifiable_presentation.json b/oidc4vp/templates/credentials/verifiable_presentation.json index 752affb..a55b769 100644 --- a/oidc4vp/templates/credentials/verifiable_presentation.json +++ b/oidc4vp/templates/credentials/verifiable_presentation.json @@ -7,5 +7,5 @@ "VerifiablePresentation" ], "holder": "{{ holder_did }}", - "verifiableCredential": {{ verifiable_credential_list }} + "verifiableCredential": {{ verifiable_credential_list|safe }} } diff --git a/utils/idhub_ssikit/__init__.py b/utils/idhub_ssikit/__init__.py index 35464b3..cc3e9b4 100644 --- a/utils/idhub_ssikit/__init__.py +++ b/utils/idhub_ssikit/__init__.py @@ -94,6 +94,18 @@ def issue_verifiable_presentation(vp_template: Template, vc_list: list[str], jwk return asyncio.run(inner()) +def create_verifiable_presentation(jwk_holder: str, unsigned_vp: str) -> str: + async def inner(): + signed_vp = await didkit.issue_presentation( + unsigned_vp, + '{"proofFormat": "ldp"}', + jwk_holder + ) + return signed_vp + + return asyncio.run(inner()) + + def verify_presentation(vp): """ Returns a (bool, str) tuple indicating whether the credential is valid. From e511393fee57cb36de65a3a154c98431da1497c6 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 4 Dec 2023 15:47:48 +0100 Subject: [PATCH 36/70] verify --- oidc4vp/models.py | 4 +-- oidc4vp/views.py | 63 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index 4111c18..55fb327 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -71,8 +71,8 @@ class Organization(models.Model): url=self.response_uri.strip("/"), ) auth = (self.my_client_id, self.my_client_secret) - # import pdb; pdb.set_trace() - return requests.post(url, data=vp, auth=auth) + data = {"vp_token": vp} + return requests.post(url, data=data, auth=auth) def demand_authorization(self): """ diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 5a28741..77598b4 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -5,6 +5,8 @@ from django.conf import settings from django.views.generic.edit import View, FormView from django.http import HttpResponse, Http404 from django.shortcuts import get_object_or_404, redirect +from django.views.decorators.csrf import csrf_exempt +from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ from django.urls import reverse_lazy from django.contrib import messages @@ -13,12 +15,12 @@ from oidc4vp.models import Authorization, Organization from idhub.mixins import UserView from oidc4vp.forms import AuthorizeForm +from utils.idhub_ssikit import verify_presentation # from django.core.mail import send_mail # from django.http import HttpResponse, HttpResponseRedirect -# from utils.idhub_ssikit import verify_presentation # from oidc4vp.models import VPVerifyRequest # from more_itertools import flatten, unique_everseen @@ -43,10 +45,29 @@ class AuthorizeView(UserView, FormView): def form_valid(self, form): authorization = form.save() - if authorization: - return redirect(authorization) - else: + import pdb; pdb.set_trace() + if not authorization or authorization.status_code != 200: messages.error(self.request, _("Error sending credential!")) + return super().form_valid(form) + try: + authorization = json.loads(authorization.text) + except: + messages.error(self.request, _("Error sending credential!")) + return super().form_valid(form) + + verify = authorization.get('verify') + result, msg = verify.split(",") + if 'error' in result.lower(): + messages.error(self.request, msg) + if 'ok' in result.lower(): + messages.success(self.request, msg) + + if authorization.get('redirect_uri'): + return redirect(authorization.get('redirect_uri')) + elif authorization.get('response'): + txt = authorization.get('response') + messages.success(self.request, txt) + return super().form_valid(form) def get_org(self): @@ -61,6 +82,7 @@ class AuthorizeView(UserView, FormView): return org +@method_decorator(csrf_exempt, name='dispatch') class VerifyView(View): def get(self, request, *args, **kwargs): org = self.validate(request) @@ -73,6 +95,7 @@ class VerifyView(View): return HttpResponse(res) def validate(self, request): + # import pdb; pdb.set_trace() auth_header = request.headers.get('Authorization', b'') auth_data = auth_header.split() @@ -81,17 +104,37 @@ class VerifyView(View): client_id, client_secret = decoded_auth.split(':', 1) org_url = request.GET.get('demand_uri') org = get_object_or_404( - Organization, - response_uri=org_url, - client_id=client_id, - client_secret=client_secret - ) + Organization, + client_id=client_id, + client_secret=client_secret + ) return org - raise Http404("Organization not found!") + raise Http404("Page not Found!") def post(self, request, *args, **kwargs): org = self.validate(request) + vp_token = self.request.POST.get("vp_token") + if not vp_token: + raise Http404("Page not Found!") + + response = self.get_response_verify() + result = verify_presentation(request.POST["vp_token"]) + verification = json.loads(result) + if verification.get('errors') or verification.get('warnings'): + response["verify"] = "Error, Verification Failed" + return HttpResponse(response) + + response["verify"] = "Ok, Verification correct" + response["response"] = "Validation Code 255255255" + return HttpResponse(json.dumps(response)) + + def get_response_verify(self): + return { + "verify": ',', + "redirect_uri": "", + "response": "", + } # import pdb; pdb.set_trace() # # TODO: incorporate request.POST["presentation_submission"] as schema definition # (presentation_valid, _) = verify_presentation(request.POST["vp_token"]) From 39b89bebe0c7d04b7b0c8ca2ab62aec0ede4e408 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 4 Dec 2023 17:12:12 +0100 Subject: [PATCH 37/70] fix json request --- oidc4vp/views.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 77598b4..fef0f50 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -3,7 +3,7 @@ import base64 from django.conf import settings from django.views.generic.edit import View, FormView -from django.http import HttpResponse, Http404 +from django.http import HttpResponse, Http404, JsonResponse from django.shortcuts import get_object_or_404, redirect from django.views.decorators.csrf import csrf_exempt from django.utils.decorators import method_decorator @@ -45,12 +45,11 @@ class AuthorizeView(UserView, FormView): def form_valid(self, form): authorization = form.save() - import pdb; pdb.set_trace() if not authorization or authorization.status_code != 200: messages.error(self.request, _("Error sending credential!")) return super().form_valid(form) try: - authorization = json.loads(authorization.text) + authorization = authorization.json() except: messages.error(self.request, _("Error sending credential!")) return super().form_valid(form) @@ -127,7 +126,7 @@ class VerifyView(View): response["verify"] = "Ok, Verification correct" response["response"] = "Validation Code 255255255" - return HttpResponse(json.dumps(response)) + return JsonResponse(response) def get_response_verify(self): return { From 753e1f6d1a1929c91f7309c1ef79202d4171e593 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 4 Dec 2023 17:12:39 +0100 Subject: [PATCH 38/70] promotion page --- promotion/__init__.py | 0 promotion/admin.py | 3 + promotion/apps.py | 6 + promotion/forms.py | 59 + promotion/migrations/__init__.py | 0 promotion/models.py | 3 + promotion/templates/select_wallet.html | 1100 +++++++++++++++ .../templates/somconnexio.tarifes-mobil.html | 1218 +++++++++++++++++ promotion/tests.py | 3 + promotion/urls.py | 14 + promotion/views.py | 30 + trustchain_idhub/settings.py | 3 +- trustchain_idhub/urls.py | 1 + 13 files changed, 2439 insertions(+), 1 deletion(-) create mode 100644 promotion/__init__.py create mode 100644 promotion/admin.py create mode 100644 promotion/apps.py create mode 100644 promotion/forms.py create mode 100644 promotion/migrations/__init__.py create mode 100644 promotion/models.py create mode 100644 promotion/templates/select_wallet.html create mode 100644 promotion/templates/somconnexio.tarifes-mobil.html create mode 100644 promotion/tests.py create mode 100644 promotion/urls.py create mode 100644 promotion/views.py diff --git a/promotion/__init__.py b/promotion/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/promotion/admin.py b/promotion/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/promotion/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/promotion/apps.py b/promotion/apps.py new file mode 100644 index 0000000..baccc02 --- /dev/null +++ b/promotion/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PromotionConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'promotion' diff --git a/promotion/forms.py b/promotion/forms.py new file mode 100644 index 0000000..a6f6d78 --- /dev/null +++ b/promotion/forms.py @@ -0,0 +1,59 @@ + +import json +import requests + +from django import forms +from django.conf import settings +from django.template.loader import get_template +from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ValidationError + +from utils.idhub_ssikit import create_verifiable_presentation +from oidc4vp.models import Organization + + +class WalletForm(forms.Form): + + def __init__(self, *args, **kwargs): + self.presentation_definition = kwargs.pop('presentation_definition', []) + + reg = r'({})'.format('|'.join(self.presentation_definition)) + + self.credentials = self.user.vcredentials.filter( + schema__type__iregex=reg + ) + super().__init__(*args, **kwargs) + for vp in self.presentation_definition: + vp = vp.lower() + choices = [ + (str(x.id), x.schema.type.lower()) for x in self.credentials.filter( + schema__type__iexact=vp) + ] + self.fields[vp.lower()] = forms.ChoiceField( + widget=forms.RadioSelect, + choices=choices + ) + def clean(self): + data = super().clean() + self.list_credentials = [] + for c in self.credentials: + if str(c.id) == data.get(c.schema.type.lower()): + if c.status is not c.Status.ISSUED.value or not c.data: + txt = _('There are some problems with this credentials') + raise ValidationError(txt) + + self.list_credentials.append(c) + + return data + + def save(self, commit=True): + if not self.list_credentials: + return + + self.get_verificable_presentation() + + if commit: + return self.org.send(self.vp) + + return + diff --git a/promotion/migrations/__init__.py b/promotion/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/promotion/models.py b/promotion/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/promotion/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/promotion/templates/select_wallet.html b/promotion/templates/select_wallet.html new file mode 100644 index 0000000..254b0fb --- /dev/null +++ b/promotion/templates/select_wallet.html @@ -0,0 +1,1100 @@ + + + + + + + + + + + + + + + Escull la teva tarifa mòbil - Som Connexió + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + Tarifes
+ +
+
+
+ Persones sòcies 9.257 + Contractes 22.303 +
+ +
+ Blog  |  + Contacte +
+ + + Vols que et truquem? +
+
+skip to Main Content
+
+ + + + + + +
+ +
+ + +
+ + +
+ + + + + +
+ + +
+ + +
+ + + +
+ + + + + + + + + + +
+ + +
+ + + + +Back To Top + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/promotion/templates/somconnexio.tarifes-mobil.html b/promotion/templates/somconnexio.tarifes-mobil.html new file mode 100644 index 0000000..e65b856 --- /dev/null +++ b/promotion/templates/somconnexio.tarifes-mobil.html @@ -0,0 +1,1218 @@ + + + + + + + + + + + + + + + Escull la teva tarifa mòbil - Som Connexió + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + Tarifes
+ +
+
+
+ Persones sòcies 9.257 + Contractes 22.303 +
+ +
+ Blog  |  + Contacte +
+ + + Vols que et truquem? +
+
+skip to Main Content
+
+ + + + + + +
+ +
+ + +
+ + +
+ + + +
+ + +
+ +
+
El més preguntat
+
    +
  • + Mòbil: tarifes i detalls +
    +

    Pots consultar els preus i detalls de les tarifes de mòbil en aquest enllaç.

    +

     

    +
    +
  • +
  • + Quina cobertura de mòbil té Som Connexió? +
    +

    La cobertura del servei és Yoigo/MásMóvil, Orange (4G) i Movistar (3G) i el nostre proveïdor de serveis és MásMóvil.

    +
    +
  • +
  • + Trucades gratuïtes entre mòbils de Som Connexió +
    +

    Les tarifes de mòbil de 0 minuts i 150 minuts inclouen 1.000 minuts de trucades gratuïtes a d’altres telèfons mòbils de Som Connexió (a la tarifa de minuts il·limitats ja estan incloses totes les trucades a mòbils de qualsevol operadora dins l’estat espanyol).

    +

    Això vol dir que si truques a un telèfon mòbil de Som Connexió, els minuts que utilitzis no es descomptaran del teu abonament sinó d’aquests 1.000 minuts.

    +

    Aquesta prestació només s’aplica a la telefonia mòbil de Som Connexió. La telefonia fixa de Som Connexió no inclou aquests minuts, ni en el servei de Fibra ni en el d’ADSL.

    +

    Aquest servei és automàtic. No s’ha de fer res per activar-lo i no es pot desactivar.

    +
    +
  • +
  • + Costos d’alta (mòbil) +
    +

    Quan es fa una alta de servei de telefonia mòbil, afegim a la factura els costos de l’alta del servei: 2,05€ (IVA inclòs)

    +
    +
  • +
  • + Puc mantenir el meu número de mòbil? +
    +

    Sí, sense cap dubte! Se’n diu portabilitat. Però si vols, també es pot canviar.

    +
    +
  • +
  • + Contractar dades addicionals +
    +

    Si esgotes dades abans d’acabar el mes, tens l’opció de contractar dades addicionals que podràs utilitzar fins a acabar el mes:

    +
      +
    • 500 MB (2 €)*
    • +
    • 1 GB (3 €)*
    • +
    • 3 GB (5 €)*
    • +
    • 5 GB (6 €)*
    • +
    • 10 GB (9€)*
    • +
    +

    * IVA inclòs

    +

    Aquí t’expliquem com afegir dades extra.

    +

    Contractar dades addicionals en les tarifes compartides

    +

    Si els mòbils que estan compartint dades esgoten els 50 GB abans que acabi el més, es pot comprar una ampliació de 10 GB per 9€ o de 20 GB per 12€ que podran consumir els diferents mòbils. Aquí t’expliquem com afegir dades extra a les tarifes compartides.

    +
    +
  • +
  • + Preus SMS +
    +

    Els SMS tenen un preu de 0,10€/sms

    +

    IMPORTANT: els SMS no estan inclosos a cap de les tarifes de Som Connexió. Tampoc a la tarifa de minuts il·limitats.

    +
    +
  • +
  • + Preus de trucades fora de tarifa des del mòbil +
    +

    Aquests són els preus de les trucades un cop has superat els minuts que tens contractats a la teva tarifa:

    +

    0,18€ per l’establiment de la trucada, que inclou els 5 primers minuts de la conversa.

    +

    A partir del minut 6, es paga 0,036€/min.

    +

    Consulta les tarifes de roaming AQUÍ.

    +
    +
  • +
  • + Tarifa bàsica mòbil: 0 min i 0 Gb +
    +

    Amb un cost base de 2€ mensuals, es tracta d’un consum “a granel”, és a dir, que es paga per minut i per unitat de dades consumides.

    +

    Aquesta tarifa és adequada per a persones que utilitzen el mòbil per estar localitzables i fer una trucada molt puntualment (només surt a compte si es fan menys de 6 trucades al mes de curta durada) i no utilitza el mòbil per connectar-se a Internet o ho fa de una manera molt esporàdica.

    +

    El preu per trucada en aquest abonament és de 0,18€ per l’establiment de la trucada, que inclou els 5 primers minuts de la conversa. A partir del minut 6, es paga 0,036€/min

    +

    El preu del servei de dades quan no s’ha contractat cap abonament és de 0,036€/Mb

    +

    Si vols desactivar el servei de dades, ens ho has de sol·licitar a serveis@somconnexio.coop.

    +
    +
  • +
  • + NÚMEROS DE TARIFACIÓ ESPECIAL +
    +

    Els preus estàndard de veu o missatges exclouen:

    +
      +
    • Els serveis de tarifació especial (per exemple: 80x, 90x o SMS Premium). Mira la taula de tarifes AQUÍ
    • +
    • Trànsit internacional
    • +
    • Trànsit en itinerància
    • +
    +

    Alguns dels serveis de tarifació especial de cost elevat estan bloquejats per defecte. Per tenir més informació sobre l’opció de bloqueig dels números de cost especial, consulta aquesta TAULA.

    +

    En aquest enllaç de la OCU ens donen més informació sobre els números de tarificació especial.

    +
    +
  • +
+
+
+
+
+ + +
+
+ Vols que et truquem? +
+
+
+
+ +
+
+
+
+ + +
+ +
+ + +
+ + +
+ + + +
+ + + + + + + + + + +
+ + +
+ + + + +Back To Top + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/promotion/tests.py b/promotion/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/promotion/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/promotion/urls.py b/promotion/urls.py new file mode 100644 index 0000000..8fd5eb7 --- /dev/null +++ b/promotion/urls.py @@ -0,0 +1,14 @@ +from django.urls import path, reverse_lazy + +from promotion import views + + +app_name = 'promotion' + + +urlpatterns = [ + path('', views.PromotionView.as_view(), + name="show_promotion"), + path('select_wallet', views.SelectWalletView.as_view(), + name="select_wallet"), +] diff --git a/promotion/views.py b/promotion/views.py new file mode 100644 index 0000000..6988ac0 --- /dev/null +++ b/promotion/views.py @@ -0,0 +1,30 @@ +from django.views.generic.edit import View, FormView +from django.template.loader import get_template +from django.urls import reverse_lazy +from django.http import HttpResponse + +from promotion.forms import WalletForm + + +class PromotionView(View): + template_name = "somconnexio.tarifes-mobil.html" + def get(self, request, *args, **kwargs): + self.context = {} + template = get_template( + self.template_name, + ).render() + return HttpResponse(template) + + +class SelectWalletView(FormView): + template_name = "select_wallet.html" + form_class = WalletForm + success_url = reverse_lazy('promotion:select_wallet') + def get(self, request, *args, **kwargs): + self.context = {} + template = get_template( + self.template_name, + # context + ).render() + return HttpResponse(template) + diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index e25b340..274416d 100644 --- a/trustchain_idhub/settings.py +++ b/trustchain_idhub/settings.py @@ -73,7 +73,8 @@ INSTALLED_APPS = [ 'django_tables2', 'idhub_auth', 'oidc4vp', - 'idhub' + 'idhub', + 'promotion' ] MIDDLEWARE = [ diff --git a/trustchain_idhub/urls.py b/trustchain_idhub/urls.py index 1668872..e8468a3 100644 --- a/trustchain_idhub/urls.py +++ b/trustchain_idhub/urls.py @@ -25,4 +25,5 @@ urlpatterns = [ # path('django-admin/', admin.site.urls), path('', include('idhub.urls')), path('oidc4vp/', include('oidc4vp.urls')), + path('promotion/', include('promotion.urls')), ] From 914d408dedca6accdd6497454f7ae0eb1e578a1f Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Thu, 7 Dec 2023 18:10:04 +0100 Subject: [PATCH 39/70] allow code --- oidc4vp/forms.py | 11 +- oidc4vp/models.py | 95 +++++++++------ oidc4vp/views.py | 112 +++++++----------- promotion/forms.py | 70 +++++------ promotion/models.py | 25 +++- promotion/templates/select_wallet.html | 26 ++++ promotion/templates/somconnexio_contract.html | 1 + promotion/views.py | 81 +++++++++++-- 8 files changed, 277 insertions(+), 144 deletions(-) create mode 100644 promotion/templates/somconnexio_contract.html diff --git a/oidc4vp/forms.py b/oidc4vp/forms.py index 7c04255..5f6f6df 100644 --- a/oidc4vp/forms.py +++ b/oidc4vp/forms.py @@ -9,6 +9,7 @@ from django.core.exceptions import ValidationError from utils.idhub_ssikit import create_verifiable_presentation from oidc4vp.models import Organization +from idhub.models import VerificableCredential class AuthorizeForm(forms.Form): @@ -17,12 +18,14 @@ class AuthorizeForm(forms.Form): self.data = kwargs.get('data', {}).copy() self.user = kwargs.pop('user', None) self.org = kwargs.pop('org', None) + self.code = kwargs.pop('code', None) self.presentation_definition = kwargs.pop('presentation_definition', []) reg = r'({})'.format('|'.join(self.presentation_definition)) self.credentials = self.user.vcredentials.filter( - schema__type__iregex=reg + schema__type__iregex=reg, + status=VerificableCredential.Status.ISSUED.value ) super().__init__(*args, **kwargs) for vp in self.presentation_definition: @@ -46,6 +49,10 @@ class AuthorizeForm(forms.Form): self.list_credentials.append(c) + if not self.code: + txt = _("There isn't code in request") + raise ValidationError(txt) + return data def save(self, commit=True): @@ -55,7 +62,7 @@ class AuthorizeForm(forms.Form): self.get_verificable_presentation() if commit: - return self.org.send(self.vp) + return self.org.send(self.vp, self.code) return diff --git a/oidc4vp/models.py b/oidc4vp/models.py index 55fb327..b29349c 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -4,8 +4,10 @@ import secrets from django.conf import settings from django.http import QueryDict from django.utils.translation import gettext_lazy as _ +from django.shortcuts import get_object_or_404 from idhub_auth.models import User from django.db import models +from utils.idhub_ssikit import verify_presentation SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" @@ -63,7 +65,7 @@ class Organization(models.Model): max_length=250 ) - def send(self, vp): + def send(self, vp, code): """ Send the verificable presentation to Verifier """ @@ -72,6 +74,9 @@ class Organization(models.Model): ) auth = (self.my_client_id, self.my_client_secret) data = {"vp_token": vp} + if code: + data["code"] = code + return requests.post(url, data=data, auth=auth) def demand_authorization(self): @@ -100,13 +105,8 @@ class Authorization(models.Model): The Verifier need to do a redirection to the user to Wallet. The code we use as a soft foreing key between Authorization and OAuth2VPToken. """ - # 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) - code = models.CharField(max_length=24, default=set_code) + code_used = models.BooleanField() created = models.DateTimeField(auto_now=True) presentation_definition = models.CharField(max_length=250) organization = models.ForeignKey( @@ -121,19 +121,24 @@ class Authorization(models.Model): null=True, ) - def authorize(self): + def authorize(self, path=None): data = { "response_type": "vp_token", "response_mode": "direct_post", "client_id": self.organization.my_client_id, "presentation_definition": self.presentation_definition, + "code": self.code, "nonce": gen_salt(5), } query_dict = QueryDict('', mutable=True) query_dict.update(data) + response_uri = self.organization.response_uri.strip("/") + if path: + response_uri = "{}/{}".format(response_uri, path.strip("/")) + url = '{response_uri}/authorize?{params}'.format( - response_uri=self.organization.response_uri.strip("/"), + response_uri=response_uri, params=query_dict.urlencode() ) return url @@ -145,9 +150,8 @@ class OAuth2VPToken(models.Model): and the result of verify. """ created = models.DateTimeField(auto_now=True) - code = models.CharField(max_length=250) - result_verify = models.BooleanField(max_length=250) - presentation_definition = models.CharField(max_length=250) + result_verify = models.CharField(max_length=255) + vp_token = models.TextField() organization = models.ForeignKey( Organization, on_delete=models.CASCADE, @@ -163,31 +167,54 @@ class OAuth2VPToken(models.Model): authorization = models.ForeignKey( Authorization, on_delete=models.SET_NULL, + related_name='oauth2vptoken', null=True, ) + def __init__(self, *args, **kwargs): + code = kwargs.pop("code", None) + super().__init__(*args, **kwargs) + + self.authorization = get_object_or_404( + Authorization, + code=code + ) + def verifing(self): - pass + self.result_verify = verify_presentation(self.vp_token) + def get_response_verify(self): + response = { + "verify": ',', + "redirect_uri": "", + "response": "", + } + verification = json.loads(self.result_verify) + if verification.get('errors') or verification.get('warnings'): + response["verify"] = "Error, Verification Failed" + return response + + response["verify"] = "Ok, Verification correct" + response["redirect_uri"] = self.get_redirect_url() + return response -class VPVerifyRequest(models.Model): - """ - `nonce` is an opaque random string used to lookup verification requests. URL-safe. - Example: "UPBQ3JE2DGJYHP5CPSCRIGTHRTCYXMQPNQ" - `expected_credentials` is a JSON list of credential types that must be present in this VP. - Example: ["FinancialSituationCredential", "HomeConnectivitySurveyCredential"] - `expected_contents` is a JSON object that places optional constraints on the contents of the - returned VP. - Example: [{"FinancialSituationCredential": {"financial_vulnerability_score": "7"}}] - `action` is (for now) a JSON object describing the next steps to take if this verification - is successful. For example "send mail to with and " - Example: {"action": "send_mail", "params": {"to": "orders@somconnexio.coop", "subject": "New client", "body": ...} - `response` is a URL that the user's wallet will redirect the user to. - `submitted_on` is used (by a cronjob) to purge old entries that didn't complete verification - """ - nonce = models.CharField(max_length=50) - expected_credentials = models.CharField(max_length=255) - expected_contents = models.TextField() - action = models.TextField() - response_or_redirect = models.CharField(max_length=255) - submitted_on = models.DateTimeField(auto_now=True) + def get_redirect_url(self): + data = { + "code": self.authorization.code, + } + query_dict = QueryDict('', mutable=True) + query_dict.update(data) + + response_uri = settings.ALLOW_CODE_URI + + url = '{response_uri}?{params}'.format( + response_uri=response_uri, + params=query_dict.urlencode() + ) + return url + + def get_user_info(self): + tk = json.loads(self.vp_token) + self.user_info = tk.get( + "verifiableCredential", [{}] + )[-1].get("credentialSubject") diff --git a/oidc4vp/views.py b/oidc4vp/views.py index fef0f50..dbdac7d 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -18,13 +18,6 @@ from oidc4vp.forms import AuthorizeForm from utils.idhub_ssikit import verify_presentation -# from django.core.mail import send_mail -# from django.http import HttpResponse, HttpResponseRedirect - -# from oidc4vp.models import VPVerifyRequest -# from more_itertools import flatten, unique_everseen - - class AuthorizeView(UserView, FormView): title = _("My wallet") section = "MyWallet" @@ -37,10 +30,14 @@ class AuthorizeView(UserView, FormView): def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user - vps = self.request.GET.get('presentation_definition') + try: + vps = json.loads(self.request.GET.get('presentation_definition')) + except: + vps = [] # import pdb; pdb.set_trace() - kwargs['presentation_definition'] = json.loads(vps) + kwargs['presentation_definition'] = vps kwargs["org"] = self.get_org() + kwargs["code"] = self.request.GET.get('code') return kwargs def form_valid(self, form): @@ -90,11 +87,35 @@ class VerifyView(View): organization=org, presentation_definition=presentation_definition ) + authorization.save() res = json.dumps({"redirect_uri": authorization.authorize()}) return HttpResponse(res) - def validate(self, request): + def post(self, request, *args, **kwargs): # import pdb; pdb.set_trace() + code = self.request.POST.get("code") + vp_tk = self.request.POST.get("vp_token") + + if not vp_tk or not code: + raise Http404("Page not Found!") + + org = self.validate(request) + + vp_token = OAuth2VPToken( + vp_token = vp_tk, + organization=org, + code=code + ) + + vp_token.verifing() + response = vp_token.get_response_verify() + vp_token.save() + if response["redirect_uri"]: + response["response"] = "Validation Code 255255255" + + return JsonResponse(response) + + def validate(self, request): auth_header = request.headers.get('Authorization', b'') auth_data = auth_header.split() @@ -111,62 +132,21 @@ class VerifyView(View): raise Http404("Page not Found!") - def post(self, request, *args, **kwargs): - org = self.validate(request) - vp_token = self.request.POST.get("vp_token") - if not vp_token: + +class AllowCodeView(View): + def get(self, request, *args, **kwargs): + code = self.request.GET.get("code") + + if not code: + raise Http404("Page not Found!") + self.authorization = get_object_or_404( + Authorization, + code=code, + code_used=False + ) + if not self.authorization.promotions: raise Http404("Page not Found!") - response = self.get_response_verify() - result = verify_presentation(request.POST["vp_token"]) - verification = json.loads(result) - if verification.get('errors') or verification.get('warnings'): - response["verify"] = "Error, Verification Failed" - return HttpResponse(response) - - response["verify"] = "Ok, Verification correct" - response["response"] = "Validation Code 255255255" - return JsonResponse(response) + promotion = self.authorization.promotions[0] + return redirect(promotion.get_url(code)) - def get_response_verify(self): - return { - "verify": ',', - "redirect_uri": "", - "response": "", - } - # import pdb; pdb.set_trace() - # # TODO: incorporate request.POST["presentation_submission"] as schema definition - # (presentation_valid, _) = verify_presentation(request.POST["vp_token"]) - # if not presentation_valid: - # raise Exception("Failed to verify signature on the given Verifiable Presentation.") - # vp = json.loads(request.POST["vp_token"]) - # nonce = vp["nonce"] - # # "vr" = verification_request - # vr = get_object_or_404(VPVerifyRequest, nonce=nonce) # TODO: return meaningful error, not 404 - # # Get a list of all included verifiable credential types - # included_credential_types = unique_everseen(flatten([ - # vc["type"] for vc in vp["verifiableCredential"] - # ])) - # # Check that it matches what we requested - # for requested_vc_type in json.loads(vr.expected_credentials): - # if requested_vc_type not in included_credential_types: - # raise Exception("You're missing some credentials we requested!") # TODO: return meaningful error - # # Perform whatever action we have to do - # action = json.loads(vr.action) - # if action["action"] == "send_mail": - # subject = action["params"]["subject"] - # to_email = action["params"]["to"] - # from_email = "noreply@verifier-portal" - # body = request.POST["vp-token"] - # send_mail( - # subject, - # body, - # from_email, - # [to_email] - # ) - # elif action["action"] == "something-else": - # pass - # else: - # raise Exception("Unknown action!") - # # OK! Your verifiable presentation was successfully presented. - # return HttpResponseRedirect(vr.response_or_redirect) diff --git a/promotion/forms.py b/promotion/forms.py index a6f6d78..d7af389 100644 --- a/promotion/forms.py +++ b/promotion/forms.py @@ -9,51 +9,55 @@ from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError from utils.idhub_ssikit import create_verifiable_presentation -from oidc4vp.models import Organization +from oidc4vp.models import Organization, Authorization class WalletForm(forms.Form): + organization = forms.ChoiceField(choices=[]) def __init__(self, *args, **kwargs): self.presentation_definition = kwargs.pop('presentation_definition', []) - - reg = r'({})'.format('|'.join(self.presentation_definition)) - - self.credentials = self.user.vcredentials.filter( - schema__type__iregex=reg - ) super().__init__(*args, **kwargs) - for vp in self.presentation_definition: - vp = vp.lower() - choices = [ - (str(x.id), x.schema.type.lower()) for x in self.credentials.filter( - schema__type__iexact=vp) - ] - self.fields[vp.lower()] = forms.ChoiceField( - widget=forms.RadioSelect, - choices=choices - ) - def clean(self): - data = super().clean() - self.list_credentials = [] - for c in self.credentials: - if str(c.id) == data.get(c.schema.type.lower()): - if c.status is not c.Status.ISSUED.value or not c.data: - txt = _('There are some problems with this credentials') - raise ValidationError(txt) - - self.list_credentials.append(c) - - return data + self.fields['organization'].choices = [ + (x.id, x.name) for x in Organization.objects.filter() + if x.response_uri != settings.RESPONSE_URI + ] def save(self, commit=True): - if not self.list_credentials: + self.org = Organization.objects.filter( + id=self.data['organization'] + ) + if not self.org.exists(): return - self.get_verificable_presentation() + self.org = self.org[0] + + self.authorization = Authorization( + organization=self.org, + presentation_definition=self.presentation_definition, + ) + self.promotion = Promotion( + discount = Promotion.Types.VULNERABLE.value, + authorize = self.authorization + ) if commit: - return self.org.send(self.vp) + self.authorization.save() + self.promotion.save() - return + return self.authorization.authorize() + + return + +class ContractForm(forms.Form): + nif = forms.CharField() + name = forms.CharField() + first_last_name = forms.CharField() + second_last_name = forms.CharField() + email = forms.CharField() + email_repeat = forms.CharField() + telephone = forms.CharField() + birthday = forms.CharField() + gen = forms.CharField() + lang = forms.CharField() diff --git a/promotion/models.py b/promotion/models.py index 71a8362..bbf7bbc 100644 --- a/promotion/models.py +++ b/promotion/models.py @@ -1,3 +1,26 @@ from django.db import models +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ +from oidc4vp.models import Authorization -# Create your models here. + +class Promotion(models.Model): + class Types(models.IntegerChoices): + VULNERABLE = 1, _("Financial vulnerability") + + name = models.CharField(max_length=250) + discount = models.PositiveSmallIntegerField( + choices=Types.choices, + ) + authorize = models.ForeignKey( + Authorization, + on_delete=models.CASCADE, + related_name='promotions', + null=True, + ) + + def get_url(self, code): + url = "{}?code={}".format( + reverse_lazy("promotion:show_promotion"), + code + ) \ No newline at end of file diff --git a/promotion/templates/select_wallet.html b/promotion/templates/select_wallet.html index 254b0fb..ba813ee 100644 --- a/promotion/templates/select_wallet.html +++ b/promotion/templates/select_wallet.html @@ -528,6 +528,32 @@ bt_experiments["19676"] = {"name":"A\/B Test Home 3 variants (oct. 2013)","conve
+{% load i18n %} +{% load django_bootstrap5 %} +
+{% csrf_token %} +{% if form.errors %} + +{% endif %} +
+
+ {% bootstrap_form form %} +
+
+ + +
diff --git a/promotion/templates/somconnexio_contract.html b/promotion/templates/somconnexio_contract.html new file mode 100644 index 0000000..251e821 --- /dev/null +++ b/promotion/templates/somconnexio_contract.html @@ -0,0 +1 @@ +Som Connexió
diff --git a/promotion/views.py b/promotion/views.py index 6988ac0..dcde3f0 100644 --- a/promotion/views.py +++ b/promotion/views.py @@ -1,9 +1,12 @@ +import json + from django.views.generic.edit import View, FormView +from django.shortcuts import redirect from django.template.loader import get_template from django.urls import reverse_lazy from django.http import HttpResponse -from promotion.forms import WalletForm +from promotion.forms import WalletForm, ContractForm class PromotionView(View): @@ -16,15 +19,77 @@ class PromotionView(View): return HttpResponse(template) +class PromotionMobile1View(FormView): + template_name = "somconnexio_contract.html" + promotion = None + vp_tokens = None + authorization = None + form_class = ContractForm + def get(self, request, *args, **kwargs): + code = self.request.GET.get("code") + self.get_discount(code) + self.context = { + "promotion": self.promotion, + "verificable_presentation": self.vp_token + } + template = get_template( + self.template_name, + ).render() + return HttpResponse(template) + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + self.vp_token.get_user_info() + kwargs['verificable_presentation'] = self.vp_token + kwargs["nif"] = self.vp_token.user_info.get("nif", '') + kwargs["name"] = self.vp_token.user_info.get("name", '') + kwargs["first_last_name"] = self.vp_token.user_info.get("first_last_name", '') + kwargs["second_last_name"] = self.vp_token.user_info.get("second_last_name", '') + kwargs["email"] = self.vp_token.user_info.get("email", '') + kwargs["email_repeat"] = self.vp_token.user_info.get("email", '') + kwargs["telephone"] = self.vp_token.user_info.get("telephone", '') + kwargs["birthday"] = self.vp_token.user_info.get("birthday", '') + kwargs["gen"] = self.vp_token.user_info.get("gen", '') + kwargs["lang"] = self.vp_token.user_info.get("lang", '') + return kwargs + + def form_valid(self, form): + url = form.save() + return redirect(url) + + def get_discount(self, code): + self.authorization = Authorization.objects.filter( + code=code, + code_unused=False + ).first() + if self.authorization: + if self.authorization.promotions: + self.promotion = self.authorization.promotionsp[-1] + if self.authorization.vp_tokens: + self.vp_tokens = self.authorization.vp_tokens[-1] + + class SelectWalletView(FormView): template_name = "select_wallet.html" form_class = WalletForm success_url = reverse_lazy('promotion:select_wallet') - def get(self, request, *args, **kwargs): - self.context = {} - template = get_template( - self.template_name, - # context - ).render() - return HttpResponse(template) + # def get(self, request, *args, **kwargs): + # self.context = {'form': fo} + # template = get_template( + # self.template_name, + # # context + # ).render() + # return HttpResponse(template) + + # def post(self, request, *args, **kwargs): + # super().post(request, *args, **kwargs) + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['presentation_definition'] = json.dumps(["MemberShipCard"]) + return kwargs + + def form_valid(self, form): + url = form.save() + return redirect(url) From 93557fe2776f5f2dcac46fa6c8ce5ded582bd1b9 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 12:11:18 +0100 Subject: [PATCH 40/70] form of contract --- idhub/migrations/0001_initial.py | 2 +- idhub_auth/migrations/0001_initial.py | 2 +- oidc4vp/migrations/0001_initial.py | 29 +- promotion/migrations/0001_initial.py | 45 + promotion/models.py | 7 +- promotion/templates/somconnexio_contract.html | 1172 +++++++++++++++- ...il.html => somconnexio_tarifes_mobil.html} | 0 promotion/templates/somconnexio_thanks.html | 1223 +++++++++++++++++ promotion/urls.py | 4 + promotion/views.py | 49 +- 10 files changed, 2492 insertions(+), 41 deletions(-) create mode 100644 promotion/migrations/0001_initial.py rename promotion/templates/{somconnexio.tarifes-mobil.html => somconnexio_tarifes_mobil.html} (100%) create mode 100644 promotion/templates/somconnexio_thanks.html diff --git a/idhub/migrations/0001_initial.py b/idhub/migrations/0001_initial.py index d841426..6d87ae7 100644 --- a/idhub/migrations/0001_initial.py +++ b/idhub/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-12-04 08:44 +# Generated by Django 4.2.5 on 2023-12-11 08:35 from django.conf import settings from django.db import migrations, models diff --git a/idhub_auth/migrations/0001_initial.py b/idhub_auth/migrations/0001_initial.py index 3ee6d7a..f460a62 100644 --- a/idhub_auth/migrations/0001_initial.py +++ b/idhub_auth/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-12-04 08:44 +# Generated by Django 4.2.5 on 2023-12-11 08:35 from django.db import migrations, models diff --git a/oidc4vp/migrations/0001_initial.py b/oidc4vp/migrations/0001_initial.py index 230817d..700c4e8 100644 --- a/oidc4vp/migrations/0001_initial.py +++ b/oidc4vp/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.5 on 2023-11-29 10:18 +# Generated by Django 4.2.5 on 2023-12-11 08:35 from django.conf import settings from django.db import migrations, models @@ -30,6 +30,7 @@ class Migration(migrations.Migration): 'code', models.CharField(default=oidc4vp.models.set_code, max_length=24), ), + ('code_used', models.BooleanField()), ('created', models.DateTimeField(auto_now=True)), ('presentation_definition', models.CharField(max_length=250)), ], @@ -70,26 +71,6 @@ class Migration(migrations.Migration): ), ], ), - migrations.CreateModel( - name='VPVerifyRequest', - fields=[ - ( - 'id', - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID', - ), - ), - ('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)), - ], - ), migrations.CreateModel( name='OAuth2VPToken', fields=[ @@ -103,14 +84,14 @@ class Migration(migrations.Migration): ), ), ('created', models.DateTimeField(auto_now=True)), - ('code', models.CharField(max_length=250)), - ('result_verify', models.BooleanField(max_length=250)), - ('presentation_definition', models.CharField(max_length=250)), + ('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='oauth2vptoken', to='oidc4vp.authorization', ), ), diff --git a/promotion/migrations/0001_initial.py b/promotion/migrations/0001_initial.py new file mode 100644 index 0000000..cbc1f17 --- /dev/null +++ b/promotion/migrations/0001_initial.py @@ -0,0 +1,45 @@ +# Generated by Django 4.2.5 on 2023-12-11 08:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ('oidc4vp', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Promotion', + fields=[ + ( + 'id', + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), + ('name', models.CharField(max_length=250)), + ( + 'discount', + models.PositiveSmallIntegerField( + choices=[(1, 'Financial vulnerability')] + ), + ), + ( + 'authorize', + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='promotions', + to='oidc4vp.authorization', + ), + ), + ], + ), + ] diff --git a/promotion/models.py b/promotion/models.py index bbf7bbc..2750b79 100644 --- a/promotion/models.py +++ b/promotion/models.py @@ -21,6 +21,9 @@ class Promotion(models.Model): def get_url(self, code): url = "{}?code={}".format( - reverse_lazy("promotion:show_promotion"), + reverse_lazy("promotion:contract"), code - ) \ No newline at end of file + ) + + def get_discount(self, price): + return price - price*0.25 \ No newline at end of file diff --git a/promotion/templates/somconnexio_contract.html b/promotion/templates/somconnexio_contract.html index 251e821..71d0cb2 100644 --- a/promotion/templates/somconnexio_contract.html +++ b/promotion/templates/somconnexio_contract.html @@ -1 +1,1171 @@ -Som Connexió
+ + + + + + + + + + + + + + + Escull la teva tarifa mòbil - Som Connexió + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + Tarifes
+ +
+
+
+ Persones sòcies 9.257 + Contractes 22.303 +
+ +
+ Blog  |  + Contacte +
+ + + Vols que et truquem? +
+
+skip to Main Content
+
+ + + + + + +
+ +
+ + +
+ + +
+ + + +
+

HOLA HOLA

+ + +
+
+{% load i18n %} +{% load django_bootstrap5 %} +
+{% csrf_token %} +{% if form.errors %} + +{% endif %} +{% bootstrap_form form %} + + +
+
+
+ +
+
    + + Resum + +
  • + Imports inicials +
  • +
  • + Tarjeta SIM {{ sim }}€ +
  • +
  • + Factura mensual +
  • +
  • + {{ mensual }}€ +
  • + Total {{ total }}€ (IVA inclòs) +
+
+
+
+
+ + +
+
+ Vols que et truquem? +
+
+
+
+ +
+
+
+
+ + +
+ +
+ + +
+ + +
+ + + +
+ + + + + + + + + + +
+ + +
+ + + + +Back To Top + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/promotion/templates/somconnexio.tarifes-mobil.html b/promotion/templates/somconnexio_tarifes_mobil.html similarity index 100% rename from promotion/templates/somconnexio.tarifes-mobil.html rename to promotion/templates/somconnexio_tarifes_mobil.html diff --git a/promotion/templates/somconnexio_thanks.html b/promotion/templates/somconnexio_thanks.html new file mode 100644 index 0000000..b3c694f --- /dev/null +++ b/promotion/templates/somconnexio_thanks.html @@ -0,0 +1,1223 @@ + + + + + + + + + + + + + + + Escull la teva tarifa mòbil - Som Connexió + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + Tarifes
+ +
+
+
+ Persones sòcies 9.257 + Contractes 22.303 +
+ +
+ Blog  |  + Contacte +
+ + + Vols que et truquem? +
+
+skip to Main Content
+
+ + + + + + +
+ +
+ + +
+ + +
+ + + +
+

Contrato

+ + +
+
+Contrato realizado correctamente. Te hemos enviado la información por email. +
+
+ +
+
El més preguntat
+
    +
  • + Mòbil: tarifes i detalls +
    +

    Pots consultar els preus i detalls de les tarifes de mòbil en aquest enllaç.

    +

     

    +
    +
  • +
  • + Quina cobertura de mòbil té Som Connexió? +
    +

    La cobertura del servei és Yoigo/MásMóvil, Orange (4G) i Movistar (3G) i el nostre proveïdor de serveis és MásMóvil.

    +
    +
  • +
  • + Trucades gratuïtes entre mòbils de Som Connexió +
    +

    Les tarifes de mòbil de 0 minuts i 150 minuts inclouen 1.000 minuts de trucades gratuïtes a d’altres telèfons mòbils de Som Connexió (a la tarifa de minuts il·limitats ja estan incloses totes les trucades a mòbils de qualsevol operadora dins l’estat espanyol).

    +

    Això vol dir que si truques a un telèfon mòbil de Som Connexió, els minuts que utilitzis no es descomptaran del teu abonament sinó d’aquests 1.000 minuts.

    +

    Aquesta prestació només s’aplica a la telefonia mòbil de Som Connexió. La telefonia fixa de Som Connexió no inclou aquests minuts, ni en el servei de Fibra ni en el d’ADSL.

    +

    Aquest servei és automàtic. No s’ha de fer res per activar-lo i no es pot desactivar.

    +
    +
  • +
  • + Costos d’alta (mòbil) +
    +

    Quan es fa una alta de servei de telefonia mòbil, afegim a la factura els costos de l’alta del servei: 2,05€ (IVA inclòs)

    +
    +
  • +
  • + Puc mantenir el meu número de mòbil? +
    +

    Sí, sense cap dubte! Se’n diu portabilitat. Però si vols, també es pot canviar.

    +
    +
  • +
  • + Contractar dades addicionals +
    +

    Si esgotes dades abans d’acabar el mes, tens l’opció de contractar dades addicionals que podràs utilitzar fins a acabar el mes:

    +
      +
    • 500 MB (2 €)*
    • +
    • 1 GB (3 €)*
    • +
    • 3 GB (5 €)*
    • +
    • 5 GB (6 €)*
    • +
    • 10 GB (9€)*
    • +
    +

    * IVA inclòs

    +

    Aquí t’expliquem com afegir dades extra.

    +

    Contractar dades addicionals en les tarifes compartides

    +

    Si els mòbils que estan compartint dades esgoten els 50 GB abans que acabi el més, es pot comprar una ampliació de 10 GB per 9€ o de 20 GB per 12€ que podran consumir els diferents mòbils. Aquí t’expliquem com afegir dades extra a les tarifes compartides.

    +
    +
  • +
  • + Preus SMS +
    +

    Els SMS tenen un preu de 0,10€/sms

    +

    IMPORTANT: els SMS no estan inclosos a cap de les tarifes de Som Connexió. Tampoc a la tarifa de minuts il·limitats.

    +
    +
  • +
  • + Preus de trucades fora de tarifa des del mòbil +
    +

    Aquests són els preus de les trucades un cop has superat els minuts que tens contractats a la teva tarifa:

    +

    0,18€ per l’establiment de la trucada, que inclou els 5 primers minuts de la conversa.

    +

    A partir del minut 6, es paga 0,036€/min.

    +

    Consulta les tarifes de roaming AQUÍ.

    +
    +
  • +
  • + Tarifa bàsica mòbil: 0 min i 0 Gb +
    +

    Amb un cost base de 2€ mensuals, es tracta d’un consum “a granel”, és a dir, que es paga per minut i per unitat de dades consumides.

    +

    Aquesta tarifa és adequada per a persones que utilitzen el mòbil per estar localitzables i fer una trucada molt puntualment (només surt a compte si es fan menys de 6 trucades al mes de curta durada) i no utilitza el mòbil per connectar-se a Internet o ho fa de una manera molt esporàdica.

    +

    El preu per trucada en aquest abonament és de 0,18€ per l’establiment de la trucada, que inclou els 5 primers minuts de la conversa. A partir del minut 6, es paga 0,036€/min

    +

    El preu del servei de dades quan no s’ha contractat cap abonament és de 0,036€/Mb

    +

    Si vols desactivar el servei de dades, ens ho has de sol·licitar a serveis@somconnexio.coop.

    +
    +
  • +
  • + NÚMEROS DE TARIFACIÓ ESPECIAL +
    +

    Els preus estàndard de veu o missatges exclouen:

    +
      +
    • Els serveis de tarifació especial (per exemple: 80x, 90x o SMS Premium). Mira la taula de tarifes AQUÍ
    • +
    • Trànsit internacional
    • +
    • Trànsit en itinerància
    • +
    +

    Alguns dels serveis de tarifació especial de cost elevat estan bloquejats per defecte. Per tenir més informació sobre l’opció de bloqueig dels números de cost especial, consulta aquesta TAULA.

    +

    En aquest enllaç de la OCU ens donen més informació sobre els números de tarificació especial.

    +
    +
  • +
+
+
+
+
+ + +
+
+ Vols que et truquem? +
+
+
+
+ +
+
+
+
+ + +
+ +
+ + +
+ + +
+ + + +
+ + + + + + + + + + +
+ + +
+ + + + +Back To Top + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/promotion/urls.py b/promotion/urls.py index 8fd5eb7..2925480 100644 --- a/promotion/urls.py +++ b/promotion/urls.py @@ -11,4 +11,8 @@ urlpatterns = [ name="show_promotion"), path('select_wallet', views.SelectWalletView.as_view(), name="select_wallet"), + path('contract', views.ContractView.as_view(), + name="contract"), + path('contract/1', views.ThanksView.as_view(), + name="thanks"), ] diff --git a/promotion/views.py b/promotion/views.py index dcde3f0..0ed4455 100644 --- a/promotion/views.py +++ b/promotion/views.py @@ -6,11 +6,21 @@ from django.template.loader import get_template from django.urls import reverse_lazy from django.http import HttpResponse +from oidc4vp.models import Authorization from promotion.forms import WalletForm, ContractForm class PromotionView(View): - template_name = "somconnexio.tarifes-mobil.html" + template_name = "somconnexio_tarifes-mobil.html" + def get(self, request, *args, **kwargs): + self.context = {} + template = get_template( + self.template_name, + ).render() + return HttpResponse(template) + +class ThanksView(View): + template_name = "somconnexio_thanks.html" def get(self, request, *args, **kwargs): self.context = {} template = get_template( @@ -19,26 +29,38 @@ class PromotionView(View): return HttpResponse(template) -class PromotionMobile1View(FormView): +class ContractView(FormView): template_name = "somconnexio_contract.html" promotion = None - vp_tokens = None + vp_token = None authorization = None form_class = ContractForm - def get(self, request, *args, **kwargs): + success_url = reverse_lazy('promotion:thanks') + + def get_context_data(self, **kwargs): + # import pdb; pdb.set_trace() + self.context = super().get_context_data(**kwargs) code = self.request.GET.get("code") self.get_discount(code) - self.context = { + self.context.update({ "promotion": self.promotion, - "verificable_presentation": self.vp_token - } - template = get_template( - self.template_name, - ).render() - return HttpResponse(template) - + "verificable_presentation": self.vp_token, + "sim": 10.0, + "mensual": 15.0, + "total": 25.0 + }) + if self.promotion: + self.context['sim'] = self.context.get_discount(self.context["sim"]) + self.context['mensual'] = self.context.get_discount(self.context["mensual"]) + self.context['total'] = self.context.get_discount(self.context["total"]) + return self.context + + def get_form_kwargs(self): kwargs = super().get_form_kwargs() + if not self.vp_token: + return kwargs + self.vp_token.get_user_info() kwargs['verificable_presentation'] = self.vp_token kwargs["nif"] = self.vp_token.user_info.get("nif", '') @@ -58,6 +80,9 @@ class PromotionMobile1View(FormView): return redirect(url) def get_discount(self, code): + if not code: + return + self.authorization = Authorization.objects.filter( code=code, code_unused=False From f81394e1f339a8204ae6220ea0a5b16492cc1e67 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 12:19:57 +0100 Subject: [PATCH 41/70] fix link to normal contract --- promotion/forms.py | 1 + promotion/templates/somconnexio_tarifes_mobil.html | 1 + promotion/views.py | 5 ++--- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/promotion/forms.py b/promotion/forms.py index d7af389..9ef48dc 100644 --- a/promotion/forms.py +++ b/promotion/forms.py @@ -61,3 +61,4 @@ class ContractForm(forms.Form): birthday = forms.CharField() gen = forms.CharField() lang = forms.CharField() + diff --git a/promotion/templates/somconnexio_tarifes_mobil.html b/promotion/templates/somconnexio_tarifes_mobil.html index e65b856..3016e59 100644 --- a/promotion/templates/somconnexio_tarifes_mobil.html +++ b/promotion/templates/somconnexio_tarifes_mobil.html @@ -529,6 +529,7 @@ bt_experiments["19676"] = {"name":"A\/B Test Home 3 variants (oct. 2013)","conve
+
El més preguntat
diff --git a/promotion/views.py b/promotion/views.py index 0ed4455..fbc6343 100644 --- a/promotion/views.py +++ b/promotion/views.py @@ -11,7 +11,7 @@ from promotion.forms import WalletForm, ContractForm class PromotionView(View): - template_name = "somconnexio_tarifes-mobil.html" + template_name = "somconnexio_tarifes_mobil.html" def get(self, request, *args, **kwargs): self.context = {} template = get_template( @@ -76,8 +76,7 @@ class ContractView(FormView): return kwargs def form_valid(self, form): - url = form.save() - return redirect(url) + return super().form_valid(form) def get_discount(self, code): if not code: From c65a8639c1bd108494fad7b3a5cf993a7a857331 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 13:41:19 +0100 Subject: [PATCH 42/70] fix form --- promotion/forms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/promotion/forms.py b/promotion/forms.py index 9ef48dc..5b96423 100644 --- a/promotion/forms.py +++ b/promotion/forms.py @@ -10,6 +10,7 @@ from django.core.exceptions import ValidationError from utils.idhub_ssikit import create_verifiable_presentation from oidc4vp.models import Organization, Authorization +from promotion.models import Promotion class WalletForm(forms.Form): From 17faf5fff4741eda5d995d891263d2afb8f6ff97 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 13:55:56 +0100 Subject: [PATCH 43/70] sync command --- idhub/management/commands/initial_datas.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py index 2ce6640..fa9feb3 100644 --- a/idhub/management/commands/initial_datas.py +++ b/idhub/management/commands/initial_datas.py @@ -6,6 +6,7 @@ from django.core.management.base import BaseCommand, CommandError from django.contrib.auth import get_user_model from decouple import config from oidc4vp.models import Organization +from promotion.models import Promotion User = get_user_model() @@ -29,6 +30,7 @@ class Command(BaseCommand): f = csv.reader(csvfile, delimiter=';', quotechar='"') for r in f: self.create_organizations(r[0].strip(), r[1].strip()) + self.sync_credentials_organizations() def create_admin_users(self, email, password): User.objects.create_superuser(email=email, password=password) @@ -42,3 +44,11 @@ class Command(BaseCommand): def create_organizations(self, name, url): Organization.objects.create(name=name, response_uri=url) + + def sync_credentials_organizations(self): + org1 = Organization.objects.get(name="test1") + org2 = Organization.objects.get(name="test2") + org2.my_client_id = org1.client_id + org2.my_client_secret = org1.client_secret + org1.my_client_id = org2.client_id + org1.my_client_secret = org2.client_secret From 1be7a1829337e055a0e9600408926990e964961b Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 14:06:43 +0100 Subject: [PATCH 44/70] fix bollean --- oidc4vp/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index b29349c..ec3667d 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -106,7 +106,7 @@ class Authorization(models.Model): The code we use as a soft foreing key between Authorization and OAuth2VPToken. """ code = models.CharField(max_length=24, default=set_code) - code_used = models.BooleanField() + code_used = models.BooleanField(default=False) created = models.DateTimeField(auto_now=True) presentation_definition = models.CharField(max_length=250) organization = models.ForeignKey( From 6cf77c55db79eabbaa52554684e363cbabe2a659 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 14:07:59 +0100 Subject: [PATCH 45/70] . --- promotion/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/promotion/models.py b/promotion/models.py index 2750b79..76425b8 100644 --- a/promotion/models.py +++ b/promotion/models.py @@ -26,4 +26,5 @@ class Promotion(models.Model): ) def get_discount(self, price): - return price - price*0.25 \ No newline at end of file + return price - price*0.25 + From de3819b2347912c94a2f8691e4207264412ba931 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 15:38:16 +0100 Subject: [PATCH 46/70] fixing --- idhub/management/commands/initial_datas.py | 2 ++ oidc4vp/views.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py index fa9feb3..cc45eec 100644 --- a/idhub/management/commands/initial_datas.py +++ b/idhub/management/commands/initial_datas.py @@ -52,3 +52,5 @@ class Command(BaseCommand): org2.my_client_secret = org1.client_secret org1.my_client_id = org2.client_id org1.my_client_secret = org2.client_secret + org1.save() + org2.save() diff --git a/oidc4vp/views.py b/oidc4vp/views.py index dbdac7d..ea592d4 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _ from django.urls import reverse_lazy from django.contrib import messages -from oidc4vp.models import Authorization, Organization +from oidc4vp.models import Authorization, Organization, OAuth2VPToken from idhub.mixins import UserView from oidc4vp.forms import AuthorizeForm @@ -34,13 +34,13 @@ class AuthorizeView(UserView, FormView): vps = json.loads(self.request.GET.get('presentation_definition')) except: vps = [] - # import pdb; pdb.set_trace() kwargs['presentation_definition'] = vps kwargs["org"] = self.get_org() kwargs["code"] = self.request.GET.get('code') return kwargs def form_valid(self, form): + # import pdb; pdb.set_trace() authorization = form.save() if not authorization or authorization.status_code != 200: messages.error(self.request, _("Error sending credential!")) @@ -67,6 +67,7 @@ class AuthorizeView(UserView, FormView): return super().form_valid(form) def get_org(self): + # import pdb; pdb.set_trace() client_id = self.request.GET.get("client_id") if not client_id: raise Http404("Organization not found!") From c84bea8d06a5ee4d0b6d13591c6858f265435911 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 15:39:50 +0100 Subject: [PATCH 47/70] fixing --- oidc4vp/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index ec3667d..ca449c7 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -1,3 +1,4 @@ +import json import requests import secrets From 0a8690659fff3fd0ec335801b1f57c9ccccd286b Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 15:49:39 +0100 Subject: [PATCH 48/70] fix --- promotion/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/promotion/models.py b/promotion/models.py index 76425b8..4401faf 100644 --- a/promotion/models.py +++ b/promotion/models.py @@ -24,6 +24,7 @@ class Promotion(models.Model): reverse_lazy("promotion:contract"), code ) + return url def get_discount(self, price): return price - price*0.25 From 7b46501289d5fa9db2203ada918424b30ab887cf Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 15:50:33 +0100 Subject: [PATCH 49/70] fix --- oidc4vp/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index ea592d4..fd164bf 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -145,9 +145,9 @@ class AllowCodeView(View): code=code, code_used=False ) - if not self.authorization.promotions: + if not self.authorization.promotions.exists(): raise Http404("Page not Found!") - promotion = self.authorization.promotions[0] + promotion = self.authorization.promotions.all()[0] return redirect(promotion.get_url(code)) From 69f1b003bf38c5825180a9bba48d32ab5de7e4fc Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 15:52:54 +0100 Subject: [PATCH 50/70] add endpoint allow_code --- oidc4vp/urls.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/oidc4vp/urls.py b/oidc4vp/urls.py index b151a85..d7b79be 100644 --- a/oidc4vp/urls.py +++ b/oidc4vp/urls.py @@ -11,4 +11,6 @@ urlpatterns = [ name="verify"), path('authorize', views.AuthorizeView.as_view(), name="authorize"), + path('allow_code', views.AllowCodeView.as_view(), + name="allow_code"), ] From be49195833ca50f66cfb15d8abdaa720cc5b1e6c Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 16:26:20 +0100 Subject: [PATCH 51/70] . --- examples/organizations.csv | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/organizations.csv b/examples/organizations.csv index 45029bc..a2f02c7 100644 --- a/examples/organizations.csv +++ b/examples/organizations.csv @@ -1,4 +1,6 @@ "ExO";"https://verify.exo.cat" "Somos Connexión";"https://verify.somosconexion.coop" -"test2";"http://localhost:9000/oidc4vp/" -"test1";"http://localhost:8000/oidc4vp/" +"test1";"https://idhub1.demo.pangea.org:9000/oidc4vp/" +"test2";"https://idhub2.demo.pangea.org:9000/oidc4vp/" +"test3";"http://localhost:9000/oidc4vp/" +"test4";"http://localhost:8000/oidc4vp/" From 6e6b8c46461fe9acb5c3368f5fdcccfe4aba98b2 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 16:27:43 +0100 Subject: [PATCH 52/70] . --- examples/organizations.csv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/organizations.csv b/examples/organizations.csv index a2f02c7..af74c61 100644 --- a/examples/organizations.csv +++ b/examples/organizations.csv @@ -1,6 +1,6 @@ "ExO";"https://verify.exo.cat" "Somos Connexión";"https://verify.somosconexion.coop" -"test1";"https://idhub1.demo.pangea.org:9000/oidc4vp/" -"test2";"https://idhub2.demo.pangea.org:9000/oidc4vp/" +"test1";"https://idhub1.demo.pangea.org/oidc4vp/" +"test2";"https://idhub2.demo.pangea.org/oidc4vp/" "test3";"http://localhost:9000/oidc4vp/" "test4";"http://localhost:8000/oidc4vp/" From f296cd020ce463ca86d897a6ec0d5ec6ab9269c0 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 18:40:37 +0100 Subject: [PATCH 53/70] fix render user info --- oidc4vp/models.py | 7 ++----- oidc4vp/views.py | 5 ++--- promotion/views.py | 46 ++++++++++++++++++++++++++-------------------- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/oidc4vp/models.py b/oidc4vp/models.py index ca449c7..66ceff1 100644 --- a/oidc4vp/models.py +++ b/oidc4vp/models.py @@ -168,7 +168,7 @@ class OAuth2VPToken(models.Model): authorization = models.ForeignKey( Authorization, on_delete=models.SET_NULL, - related_name='oauth2vptoken', + related_name='vp_tokens', null=True, ) @@ -176,10 +176,7 @@ class OAuth2VPToken(models.Model): code = kwargs.pop("code", None) super().__init__(*args, **kwargs) - self.authorization = get_object_or_404( - Authorization, - code=code - ) + self.authorization = Authorization.objects.filter(code=code).first() def verifing(self): self.result_verify = verify_presentation(self.vp_token) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index fd164bf..3f34bfd 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -40,7 +40,6 @@ class AuthorizeView(UserView, FormView): return kwargs def form_valid(self, form): - # import pdb; pdb.set_trace() authorization = form.save() if not authorization or authorization.status_code != 200: messages.error(self.request, _("Error sending credential!")) @@ -67,7 +66,6 @@ class AuthorizeView(UserView, FormView): return super().form_valid(form) def get_org(self): - # import pdb; pdb.set_trace() client_id = self.request.GET.get("client_id") if not client_id: raise Http404("Organization not found!") @@ -93,7 +91,6 @@ class VerifyView(View): return HttpResponse(res) def post(self, request, *args, **kwargs): - # import pdb; pdb.set_trace() code = self.request.POST.get("code") vp_tk = self.request.POST.get("vp_token") @@ -107,6 +104,8 @@ class VerifyView(View): organization=org, code=code ) + if not vp_token.authorization: + raise Http404("Page not Found!") vp_token.verifing() response = vp_token.get_response_verify() diff --git a/promotion/views.py b/promotion/views.py index fbc6343..79b47ea 100644 --- a/promotion/views.py +++ b/promotion/views.py @@ -38,7 +38,6 @@ class ContractView(FormView): success_url = reverse_lazy('promotion:thanks') def get_context_data(self, **kwargs): - # import pdb; pdb.set_trace() self.context = super().get_context_data(**kwargs) code = self.request.GET.get("code") self.get_discount(code) @@ -50,29 +49,34 @@ class ContractView(FormView): "total": 25.0 }) if self.promotion: - self.context['sim'] = self.context.get_discount(self.context["sim"]) - self.context['mensual'] = self.context.get_discount(self.context["mensual"]) - self.context['total'] = self.context.get_discount(self.context["total"]) + self.context['sim'] = self.promotion.get_discount(self.context["sim"]) + self.context['mensual'] = self.promotion.get_discount(self.context["mensual"]) + self.context['total'] = self.promotion.get_discount(self.context["total"]) + + if self.vp_token: + self.context['verificable_presentation'] = self.vp_token + return self.context def get_form_kwargs(self): kwargs = super().get_form_kwargs() + code = self.request.GET.get("code") + self.get_discount(code) if not self.vp_token: return kwargs self.vp_token.get_user_info() - kwargs['verificable_presentation'] = self.vp_token - kwargs["nif"] = self.vp_token.user_info.get("nif", '') - kwargs["name"] = self.vp_token.user_info.get("name", '') - kwargs["first_last_name"] = self.vp_token.user_info.get("first_last_name", '') - kwargs["second_last_name"] = self.vp_token.user_info.get("second_last_name", '') - kwargs["email"] = self.vp_token.user_info.get("email", '') - kwargs["email_repeat"] = self.vp_token.user_info.get("email", '') - kwargs["telephone"] = self.vp_token.user_info.get("telephone", '') - kwargs["birthday"] = self.vp_token.user_info.get("birthday", '') - kwargs["gen"] = self.vp_token.user_info.get("gen", '') - kwargs["lang"] = self.vp_token.user_info.get("lang", '') + kwargs['initial']["nif"] = self.vp_token.user_info.get("nif", '') + kwargs['initial']["name"] = self.vp_token.user_info.get("name", '') + kwargs['initial']["first_last_name"] = self.vp_token.user_info.get("first_last_name", '') + kwargs['initial']["second_last_name"] = self.vp_token.user_info.get("second_last_name", '') + kwargs['initial']["email"] = self.vp_token.user_info.get("email", '') + kwargs['initial']["email_repeat"] = self.vp_token.user_info.get("email", '') + kwargs['initial']["telephone"] = self.vp_token.user_info.get("telephone", '') + kwargs['initial']["birthday"] = self.vp_token.user_info.get("birthday", '') + kwargs['initial']["gen"] = self.vp_token.user_info.get("gen", '') + kwargs['initial']["lang"] = self.vp_token.user_info.get("lang", '') return kwargs def form_valid(self, form): @@ -81,16 +85,18 @@ class ContractView(FormView): def get_discount(self, code): if not code: return + if self.authorization: + return self.authorization = Authorization.objects.filter( code=code, - code_unused=False + code_used=False ).first() if self.authorization: - if self.authorization.promotions: - self.promotion = self.authorization.promotionsp[-1] - if self.authorization.vp_tokens: - self.vp_tokens = self.authorization.vp_tokens[-1] + if self.authorization.promotions.exists(): + self.promotion = self.authorization.promotions.all()[0] + if self.authorization.vp_tokens.exists(): + self.vp_token = self.authorization.vp_tokens.all()[0] class SelectWalletView(FormView): From 267336658233c7546b31c31a8c61b1b933fe60af Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 19:10:14 +0100 Subject: [PATCH 54/70] . --- promotion/templates/somconnexio_contract.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/promotion/templates/somconnexio_contract.html b/promotion/templates/somconnexio_contract.html index 71d0cb2..70627f7 100644 --- a/promotion/templates/somconnexio_contract.html +++ b/promotion/templates/somconnexio_contract.html @@ -526,8 +526,6 @@ bt_experiments["19676"] = {"name":"A\/B Test Home 3 variants (oct. 2013)","conve
-

HOLA HOLA

-
From cfe577ae726e4b2f4a3fd6b95b67d57276384a58 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 19:11:59 +0100 Subject: [PATCH 55/70] . --- promotion/templates/somconnexio_contract.html | 1 + 1 file changed, 1 insertion(+) diff --git a/promotion/templates/somconnexio_contract.html b/promotion/templates/somconnexio_contract.html index 70627f7..7b7779e 100644 --- a/promotion/templates/somconnexio_contract.html +++ b/promotion/templates/somconnexio_contract.html @@ -527,6 +527,7 @@ bt_experiments["19676"] = {"name":"A\/B Test Home 3 variants (oct. 2013)","conve
+

Tarifes Mòbil - Contractar

{% load i18n %} From efcc794c3e7b1adc9adeb1a8f2dcd7d1924f8de3 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 19:16:06 +0100 Subject: [PATCH 56/70] fix return code as response --- oidc4vp/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 3f34bfd..3e6f0f3 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -110,8 +110,9 @@ class VerifyView(View): vp_token.verifing() response = vp_token.get_response_verify() vp_token.save() - if response["redirect_uri"]: - response["response"] = "Validation Code 255255255" + if not vp_token.authorization.promotions.exists(): + response["redirect_uri"] = "" + response["response"] = "Validation Code {}".format(code) return JsonResponse(response) From 5bb02bc37524166d8e9cdfeb0943c57831bfbb43 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Mon, 11 Dec 2023 20:06:53 +0100 Subject: [PATCH 57/70] integrate more organizations --- idhub/management/commands/initial_datas.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py index cc45eec..982bb97 100644 --- a/idhub/management/commands/initial_datas.py +++ b/idhub/management/commands/initial_datas.py @@ -30,7 +30,8 @@ class Command(BaseCommand): f = csv.reader(csvfile, delimiter=';', quotechar='"') for r in f: self.create_organizations(r[0].strip(), r[1].strip()) - self.sync_credentials_organizations() + self.sync_credentials_organizations("test1", "test2") + self.sync_credentials_organizations("test3", "test4") def create_admin_users(self, email, password): User.objects.create_superuser(email=email, password=password) @@ -45,9 +46,9 @@ class Command(BaseCommand): def create_organizations(self, name, url): Organization.objects.create(name=name, response_uri=url) - def sync_credentials_organizations(self): - org1 = Organization.objects.get(name="test1") - org2 = Organization.objects.get(name="test2") + def sync_credentials_organizations(self, test1, test2): + org1 = Organization.objects.get(name=test1) + org2 = Organization.objects.get(name=test2) org2.my_client_id = org1.client_id org2.my_client_secret = org1.client_secret org1.my_client_id = org2.client_id From f7a5ce52ee3dcc84b8623426ca949cf938143aaf Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 12 Dec 2023 13:22:43 +0100 Subject: [PATCH 58/70] postgres as a option --- requirements.txt | 1 + trustchain_idhub/settings.py | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index f009565..9b19238 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,4 @@ jinja2==3.1.2 jsonref==1.1.0 pyld==2.0.3 more-itertools==10.1.0 +dj-database-url==2.1.0 diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index 274416d..7a3e043 100644 --- a/trustchain_idhub/settings.py +++ b/trustchain_idhub/settings.py @@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/4.2/ref/settings/ import os from ast import literal_eval +from dj_database_url import parse as db_url from pathlib import Path from django.contrib.messages import constants as messages @@ -112,10 +113,15 @@ WSGI_APPLICATION = 'trustchain_idhub.wsgi.application' # https://docs.djangoproject.com/en/4.2/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', - } + # 'default': { + # 'ENGINE': 'django.db.backends.sqlite3', + # 'NAME': BASE_DIR / 'db.sqlite3', + # } + 'default': config( + 'DATABASE_URL', + default='sqlite:///' + os.path.join(BASE_DIR, 'db.sqlite3'), + cast=db_url + ) # 'default': config( # 'DATABASE_URL', # default='sqlite:///' + os.path.join(BASE_DIR, 'db.sqlite3'), From db058178d1a34a9573abc9b81523d1300ead16d2 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 12 Dec 2023 13:23:10 +0100 Subject: [PATCH 59/70] change names of organizations --- examples/organizations.csv | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/organizations.csv b/examples/organizations.csv index af74c61..2f447ef 100644 --- a/examples/organizations.csv +++ b/examples/organizations.csv @@ -1,6 +1,5 @@ -"ExO";"https://verify.exo.cat" -"Somos Connexión";"https://verify.somosconexion.coop" -"test1";"https://idhub1.demo.pangea.org/oidc4vp/" -"test2";"https://idhub2.demo.pangea.org/oidc4vp/" -"test3";"http://localhost:9000/oidc4vp/" -"test4";"http://localhost:8000/oidc4vp/" +"pangea.org";"https://idhub1.demo.pangea.org/oidc4vp/" +"somconnexio.coop";"https://idhub2.demo.pangea.org/oidc4vp/" +"exo.cat";"https://verify.exo.cat" +"local 9000";"http://localhost:9000/oidc4vp/" +"local 8000";"http://localhost:8000/oidc4vp/" From 79dcb42c237c8ebf9415ae84a35c4c4f90d846b5 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 12 Dec 2023 13:26:17 +0100 Subject: [PATCH 60/70] fix command initial_datas --- idhub/management/commands/initial_datas.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py index 982bb97..1d44558 100644 --- a/idhub/management/commands/initial_datas.py +++ b/idhub/management/commands/initial_datas.py @@ -30,8 +30,8 @@ class Command(BaseCommand): f = csv.reader(csvfile, delimiter=';', quotechar='"') for r in f: self.create_organizations(r[0].strip(), r[1].strip()) - self.sync_credentials_organizations("test1", "test2") - self.sync_credentials_organizations("test3", "test4") + self.sync_credentials_organizations("pangea.org", "somconnexio.coop") + self.sync_credentials_organizations("local 8000", "local 9000") def create_admin_users(self, email, password): User.objects.create_superuser(email=email, password=password) From 40d662f62e0e50652ca036e9f8af1b81ed33faac Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 12 Dec 2023 18:00:04 +0100 Subject: [PATCH 61/70] fix dids and credentials from the user --- idhub/admin/views.py | 2 +- idhub/user/views.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/idhub/admin/views.py b/idhub/admin/views.py index 99b6885..b6dcbc8 100644 --- a/idhub/admin/views.py +++ b/idhub/admin/views.py @@ -629,7 +629,7 @@ class DidsView(Credentials): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ - 'dids': DID.objects, + 'dids': DID.objects.filter(user=self.request.user), }) return context diff --git a/idhub/user/views.py b/idhub/user/views.py index 0a3bfde..77ca3ec 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -81,8 +81,11 @@ class CredentialsView(MyWallet, TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) + creds = VerificableCredential.objects.filter( + user=self.request.user + ) context.update({ - 'credentials': VerificableCredential.objects, + 'credentials': creds, }) return context From b16cc85cd3dec15df3a01a452132c492e9ee8506 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 12 Dec 2023 18:00:21 +0100 Subject: [PATCH 62/70] add more initial datas --- idhub/management/commands/initial_datas.py | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/idhub/management/commands/initial_datas.py b/idhub/management/commands/initial_datas.py index 1d44558..034bc68 100644 --- a/idhub/management/commands/initial_datas.py +++ b/idhub/management/commands/initial_datas.py @@ -1,10 +1,14 @@ import os import csv +import json from pathlib import Path +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 decouple import config +from idhub.models import DID, Schemas from oidc4vp.models import Organization from promotion.models import Promotion @@ -32,6 +36,8 @@ 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) @@ -55,3 +61,37 @@ 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_schemas(self): + schemas_files = os.listdir(settings.SCHEMAS_DIR) + schemas = [x for x in schemas_files + if not Schemas.objects.filter(file_schema=x).exists()] + for x in schemas_files: + if Schemas.objects.filter(file_schema=x).exists(): + continue + self._create_schemas(x) + + def _create_schemas(self, file_name): + data = self.open_file(file_name) + try: + ldata = json.loads(data) + assert credtools.validate_schema(ldata) + name = ldata.get('name') + assert name + except Exception: + return + Schemas.objects.create(file_schema=file_name, data=data, type=name) + + def open_file(self, file_name): + data = '' + filename = Path(settings.SCHEMAS_DIR).joinpath(file_name) + with filename.open() as schema_file: + data = schema_file.read() + + return data From 8abb43f95d4823ae4b07dd7a75a29ffc7ddaffdc Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Tue, 12 Dec 2023 18:00:48 +0100 Subject: [PATCH 63/70] remove schema --- schemas/member.json | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 schemas/member.json diff --git a/schemas/member.json b/schemas/member.json deleted file mode 100644 index 38199be..0000000 --- a/schemas/member.json +++ /dev/null @@ -1,21 +0,0 @@ - { - "$id": "https://pangea.org/schemas/member-credential-schema.json", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "name": "MemberCredential", - "description": "MemberCredential using JsonSchemaCredential", - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "email": { - "type": "string", - "format": "email" - }, - "membershipType": { - "type": "string", - "enum": ["individual", "organization"] - } - }, - "required": ["name", "email", "membershipType"] - } From cb1b7de4f63260338ca8a8252670837c78844382 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 13 Dec 2023 17:52:18 +0100 Subject: [PATCH 64/70] simplify flow from promotion web --- idhub/user/views.py | 3 +++ oidc4vp/forms.py | 4 +++- oidc4vp/views.py | 12 ++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/idhub/user/views.py b/idhub/user/views.py index 77ca3ec..d7b1cb3 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -144,6 +144,9 @@ class CredentialsRequestView(MyWallet, FormView): messages.success(self.request, _("The credential was issued successfully!")) Event.set_EV_CREDENTIAL_ISSUED_FOR_USER(cred) Event.set_EV_CREDENTIAL_ISSUED(cred) + url = self.request.session.pop('next_url', None) + if url: + return redirect(url) else: messages.error(self.request, _("The credential does not exist!")) return super().form_valid(form) diff --git a/oidc4vp/forms.py b/oidc4vp/forms.py index 5f6f6df..b3c4c9e 100644 --- a/oidc4vp/forms.py +++ b/oidc4vp/forms.py @@ -23,8 +23,10 @@ class AuthorizeForm(forms.Form): reg = r'({})'.format('|'.join(self.presentation_definition)) - self.credentials = self.user.vcredentials.filter( + self.all_credentials = self.user.vcredentials.filter( schema__type__iregex=reg, + ) + self.credentials = self.all_credentials.filter( status=VerificableCredential.Status.ISSUED.value ) super().__init__(*args, **kwargs) diff --git a/oidc4vp/views.py b/oidc4vp/views.py index 3e6f0f3..a62e462 100644 --- a/oidc4vp/views.py +++ b/oidc4vp/views.py @@ -27,6 +27,12 @@ class AuthorizeView(UserView, FormView): form_class = AuthorizeForm success_url = reverse_lazy('idhub:user_demand_authorization') + def get(self, request, *args, **kwargs): + response = super().get(request, *args, **kwargs) + if self.request.session.get('next_url'): + return redirect(reverse_lazy('idhub:user_credentials_request')) + return response + def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user @@ -38,6 +44,12 @@ class AuthorizeView(UserView, FormView): kwargs["org"] = self.get_org() kwargs["code"] = self.request.GET.get('code') return kwargs + + def get_form(self, form_class=None): + form = super().get_form(form_class=form_class) + if form.all_credentials.exists() and not form.credentials.exists(): + self.request.session['next_url'] = self.request.get_full_path() + return form def form_valid(self, form): authorization = form.save() From d0dd59e098dabac89fc654724cf0634a88646178 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Wed, 13 Dec 2023 17:53:11 +0100 Subject: [PATCH 65/70] catalan as default --- trustchain_idhub/settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index 7a3e043..a798491 100644 --- a/trustchain_idhub/settings.py +++ b/trustchain_idhub/settings.py @@ -186,8 +186,9 @@ MESSAGE_TAGS = { LOCALE_PATHS = [ os.path.join(BASE_DIR, 'locale'), ] -LANGUAGE_CODE="en" +# LANGUAGE_CODE="en" # LANGUAGE_CODE="es" +LANGUAGE_CODE="ca" USE_I18N = True USE_L10N = True From 1a4bb0845d23a045210f321182e9915b359d69ac Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Thu, 14 Dec 2023 12:13:58 +0100 Subject: [PATCH 66/70] list of langs --- trustchain_idhub/settings.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index 87a5661..7601739 100644 --- a/trustchain_idhub/settings.py +++ b/trustchain_idhub/settings.py @@ -190,6 +190,12 @@ LOCALE_PATHS = [ # LANGUAGE_CODE="en" # LANGUAGE_CODE="es" LANGUAGE_CODE="ca" +gettext = lambda s: s +LANGUAGES = ( + ('de', gettext('German')), + ('en', gettext('English')), + ('ca', gettext('Catalan')), +) USE_I18N = True USE_L10N = True From a66d9d1b91135ef3fbb2dc68c6cf4e9218834e38 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Thu, 14 Dec 2023 12:14:14 +0100 Subject: [PATCH 67/70] fix trans in table --- idhub/admin/tables.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/idhub/admin/tables.py b/idhub/admin/tables.py index 7ce7d43..078217c 100644 --- a/idhub/admin/tables.py +++ b/idhub/admin/tables.py @@ -1,4 +1,5 @@ import django_tables2 as tables +from django.utils.translation import gettext_lazy as _ from idhub.models import Rol, Event from idhub_auth.models import User @@ -18,9 +19,9 @@ class RolesTable(tables.Table): class DashboardTable(tables.Table): - type = tables.Column(verbose_name="Event") - message = tables.Column(verbose_name="Description") - created = tables.Column(verbose_name="Date") + type = tables.Column(verbose_name=_("Event")) + message = tables.Column(verbose_name=_("Description")) + created = tables.Column(verbose_name=_("Date")) class Meta: model = Event From 9ae75b00ab0528df0809bf808da8d1d0bb18cc1a Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Thu, 14 Dec 2023 17:15:22 +0100 Subject: [PATCH 68/70] fix translate --- idhub/admin/forms.py | 6 +++--- idhub/admin/tables.py | 4 ---- idhub/models.py | 7 ++++--- idhub/templates/idhub/admin/roles.html | 2 +- idhub/templates/idhub/admin/services.html | 2 +- idhub/templates/idhub/admin/user.html | 6 +++--- idhub/templates/idhub/admin/user_edit.html | 4 ++-- idhub_auth/forms.py | 11 +++++++---- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/idhub/admin/forms.py b/idhub/admin/forms.py index 4682579..1919a62 100644 --- a/idhub/admin/forms.py +++ b/idhub/admin/forms.py @@ -19,9 +19,9 @@ from idhub_auth.models import User class ImportForm(forms.Form): - did = forms.ChoiceField(choices=[]) - schema = forms.ChoiceField(choices=[]) - file_import = forms.FileField() + did = forms.ChoiceField(label=_("Did"), choices=[]) + schema = forms.ChoiceField(label=_("Schema"), choices=[]) + file_import = forms.FileField(label=_("File import")) def __init__(self, *args, **kwargs): self._schema = None diff --git a/idhub/admin/tables.py b/idhub/admin/tables.py index 078217c..79c22ba 100644 --- a/idhub/admin/tables.py +++ b/idhub/admin/tables.py @@ -19,10 +19,6 @@ class RolesTable(tables.Table): class DashboardTable(tables.Table): - type = tables.Column(verbose_name=_("Event")) - message = tables.Column(verbose_name=_("Description")) - created = tables.Column(verbose_name=_("Date")) - class Meta: model = Event template_name = "idhub/custom_table.html" diff --git a/idhub/models.py b/idhub/models.py index ee7ebed..3aa6ec8 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -46,9 +46,10 @@ class Event(models.Model): EV_USR_DEACTIVATED_BY_ADMIN = 29, "User deactivated" EV_USR_ACTIVATED_BY_ADMIN = 30, "User activated" - created = models.DateTimeField(auto_now=True) - message = models.CharField(max_length=350) + created = models.DateTimeField(_("Date"), auto_now=True) + message = models.CharField(_("Description"), max_length=350) type = models.PositiveSmallIntegerField( + _("Event"), choices=Types.choices, ) user = models.ForeignKey( @@ -403,7 +404,7 @@ class Event(models.Model): class DID(models.Model): created_at = models.DateTimeField(auto_now=True) - label = models.CharField(max_length=50) + 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: diff --git a/idhub/templates/idhub/admin/roles.html b/idhub/templates/idhub/admin/roles.html index 25bfd93..d3d6593 100644 --- a/idhub/templates/idhub/admin/roles.html +++ b/idhub/templates/idhub/admin/roles.html @@ -24,7 +24,7 @@ {{ rol.name }} {{ rol.description|default:""}} - + {% endfor %} diff --git a/idhub/templates/idhub/admin/services.html b/idhub/templates/idhub/admin/services.html index cea981d..639d1c1 100644 --- a/idhub/templates/idhub/admin/services.html +++ b/idhub/templates/idhub/admin/services.html @@ -26,7 +26,7 @@ {{ service.description }} {{ service.get_roles }} - + {% endfor %} diff --git a/idhub/templates/idhub/admin/user.html b/idhub/templates/idhub/admin/user.html index aacd509..fbb04d1 100644 --- a/idhub/templates/idhub/admin/user.html +++ b/idhub/templates/idhub/admin/user.html @@ -21,7 +21,7 @@
- First Name: + {% trans "First name" %}:
{{ object.first_name|default:'' }} @@ -29,7 +29,7 @@
- Last Name: + {% trans "Last name" %}:
{{ object.last_name|default:'' }} @@ -37,7 +37,7 @@
- Email: + {% trans "Email address" %}:
{{ object.email }} diff --git a/idhub/templates/idhub/admin/user_edit.html b/idhub/templates/idhub/admin/user_edit.html index 9bc0a48..5198cca 100644 --- a/idhub/templates/idhub/admin/user_edit.html +++ b/idhub/templates/idhub/admin/user_edit.html @@ -52,7 +52,7 @@ {{ membership.start_date|default:'' }} {{ membership.end_date|default:'' }} - + {% endfor %} @@ -84,7 +84,7 @@ {{ rol.service.description }} {{ rol.service.domain }} - + {% endfor %} diff --git a/idhub_auth/forms.py b/idhub_auth/forms.py index f292182..f4297b7 100644 --- a/idhub_auth/forms.py +++ b/idhub_auth/forms.py @@ -6,17 +6,20 @@ from idhub_auth.models import User class ProfileForm(forms.ModelForm): - first_name = forms.CharField(required=True) - last_name = forms.CharField(required=True) + first_name = forms.CharField(label=_("First name"), required=True) + last_name = forms.CharField(label=_("Last name"), required=True) class Meta: model = User fields = ['first_name', 'last_name', 'email'] + labels = { + 'email': _('Email address'), + } def clean_first_name(self): first_name = super().clean()['first_name'] match = r'^[a-zA-ZäöüßÄÖÜáéíóúàèìòùÀÈÌÒÙÁÉÍÓÚßñÑçÇ\s\'-]+' - if not re.match(match, first_name): + if not re.fullmatch(match, first_name): txt = _("The string must contain only characters and spaces") raise forms.ValidationError(txt) @@ -25,7 +28,7 @@ class ProfileForm(forms.ModelForm): def clean_last_name(self): last_name = super().clean()['last_name'] match = r'^[a-zA-ZäöüßÄÖÜáéíóúàèìòùÀÈÌÒÙÁÉÍÓÚßñÑçÇ\s\'-]+' - if not re.match(match, last_name): + if not re.fullmatch(match, last_name): txt = _("The string must contain only characters and spaces") raise forms.ValidationError(txt) From b4b55a811d55ffeb810385fa048539df13157729 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Thu, 14 Dec 2023 17:59:40 +0100 Subject: [PATCH 69/70] add more translate sentens --- idhub/admin/forms.py | 2 +- idhub/models.py | 1 + idhub/templates/idhub/base.html | 2 +- idhub/templates/idhub/base_admin.html | 2 +- idhub/urls.py | 3 -- idhub/user/forms.py | 47 +++------------------------ idhub/user/views.py | 25 -------------- idhub_auth/forms.py | 7 ++-- idhub_auth/models.py | 7 ++-- 9 files changed, 15 insertions(+), 81 deletions(-) diff --git a/idhub/admin/forms.py b/idhub/admin/forms.py index 1919a62..2649f4b 100644 --- a/idhub/admin/forms.py +++ b/idhub/admin/forms.py @@ -139,7 +139,7 @@ class ImportForm(forms.Form): class SchemaForm(forms.Form): - file_template = forms.FileField() + file_template = forms.FileField(label=_("File template")) class MembershipForm(forms.ModelForm): diff --git a/idhub/models.py b/idhub/models.py index 3aa6ec8..7d1ef6c 100644 --- a/idhub/models.py +++ b/idhub/models.py @@ -629,6 +629,7 @@ class UserRol(models.Model): ) service = models.ForeignKey( Service, + verbose_name=_("Service"), on_delete=models.CASCADE, related_name='users', ) diff --git a/idhub/templates/idhub/base.html b/idhub/templates/idhub/base.html index 834b7ef..390cb33 100644 --- a/idhub/templates/idhub/base.html +++ b/idhub/templates/idhub/base.html @@ -139,7 +139,7 @@

{{ title }}

- +
diff --git a/idhub/templates/idhub/base_admin.html b/idhub/templates/idhub/base_admin.html index 1253806..c97f165 100644 --- a/idhub/templates/idhub/base_admin.html +++ b/idhub/templates/idhub/base_admin.html @@ -171,7 +171,7 @@

{{ title }}

- +
diff --git a/idhub/urls.py b/idhub/urls.py index 48b9214..d139c32 100644 --- a/idhub/urls.py +++ b/idhub/urls.py @@ -88,9 +88,6 @@ urlpatterns = [ path('user/credentials_presentation/demand', views_user.DemandAuthorizationView.as_view(), name='user_demand_authorization'), - path('user/credentials_presentation/', - views_user.CredentialsPresentationView.as_view(), - name='user_credentials_presentation'), # Admin path('admin/dashboard/', views_admin.DashboardView.as_view(), diff --git a/idhub/user/forms.py b/idhub/user/forms.py index 5b6f9ab..5ac04ad 100644 --- a/idhub/user/forms.py +++ b/idhub/user/forms.py @@ -1,6 +1,8 @@ import requests from django import forms from django.conf import settings +from django.utils.translation import gettext_lazy as _ + from idhub_auth.models import User from idhub.models import DID, VerificableCredential from oidc4vp.models import Organization @@ -15,8 +17,8 @@ class ProfileForm(forms.ModelForm): class RequestCredentialForm(forms.Form): - did = forms.ChoiceField(choices=[]) - credential = forms.ChoiceField(choices=[]) + did = forms.ChoiceField(label=_("Did"), choices=[]) + credential = forms.ChoiceField(label=_("Credential"), choices=[]) def __init__(self, *args, **kwargs): self.user = kwargs.pop('user', None) @@ -59,7 +61,7 @@ class RequestCredentialForm(forms.Form): class DemandAuthorizationForm(forms.Form): - organization = forms.ChoiceField(choices=[]) + organization = forms.ChoiceField(label=_("Organization"), choices=[]) def __init__(self, *args, **kwargs): self.user = kwargs.pop('user', None) @@ -85,42 +87,3 @@ class DemandAuthorizationForm(forms.Form): return - -class CredentialPresentationForm(forms.Form): - organization = forms.ChoiceField(choices=[]) - # credential = forms.ChoiceField(choices=[]) - - def __init__(self, *args, **kwargs): - self.user = kwargs.pop('user', None) - super().__init__(*args, **kwargs) - self.fields['organization'].choices = [ - (x.id, x.name) for x in Organization.objects.filter() - ] - # self.fields['credential'].choices = [ - # (x.id, x.type()) for x in VerificableCredential.objects.filter( - # user=self.user, - # status=VerificableCredential.Status.ISSUED - # ) - # ] - - def save(self, commit=True): - self.org = Organization.objects.filter( - id=self.data['organization'] - ) - self.cred = VerificableCredential.objects.filter( - user=self.user, - id=self.data['credential'], - status=VerificableCredential.Status.ISSUED - ) - if not all([self.org.exists(), self.cred.exists()]): - return - - self.org = self.org[0] - self.cred = self.cred[0] - - if commit: - self.org.send(self.cred) - return self.cred - - return - diff --git a/idhub/user/views.py b/idhub/user/views.py index d7b1cb3..e6e28dc 100644 --- a/idhub/user/views.py +++ b/idhub/user/views.py @@ -15,7 +15,6 @@ from django.contrib import messages from idhub.user.forms import ( ProfileForm, RequestCredentialForm, - CredentialPresentationForm, DemandAuthorizationForm ) from idhub.mixins import UserView @@ -181,30 +180,6 @@ class DemandAuthorizationView(MyWallet, FormView): return super().form_valid(form) -class CredentialsPresentationView(MyWallet, FormView): - template_name = "idhub/user/credentials_presentation.html" - subtitle = _('Credential presentation') - icon = 'bi bi-patch-check-fill' - form_class = CredentialPresentationForm - success_url = reverse_lazy('idhub:user_credentials') - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs['user'] = self.request.user - kwargs['authorize'] = self.request.GET.params.get("uri") - return kwargs - - def form_valid(self, form): - cred = form.save() - if cred: - Event.set_EV_CREDENTIAL_PRESENTED_BY_USER(cred, form.org) - Event.set_EV_CREDENTIAL_PRESENTED(cred, form.org) - messages.success(self.request, _("The credential was presented successfully!")) - else: - messages.error(self.request, _("Error sending credential!")) - return super().form_valid(form) - - class DidsView(MyWallet, TemplateView): template_name = "idhub/user/dids.html" subtitle = _('Identities (DIDs)') diff --git a/idhub_auth/forms.py b/idhub_auth/forms.py index f4297b7..f4279b7 100644 --- a/idhub_auth/forms.py +++ b/idhub_auth/forms.py @@ -6,15 +6,12 @@ from idhub_auth.models import User class ProfileForm(forms.ModelForm): - first_name = forms.CharField(label=_("First name"), required=True) - last_name = forms.CharField(label=_("Last name"), required=True) + first_name = forms.CharField(required=True) + last_name = forms.CharField(required=True) class Meta: model = User fields = ['first_name', 'last_name', 'email'] - labels = { - 'email': _('Email address'), - } def clean_first_name(self): first_name = super().clean()['first_name'] diff --git a/idhub_auth/models.py b/idhub_auth/models.py index ccda94c..07a7896 100644 --- a/idhub_auth/models.py +++ b/idhub_auth/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.utils.translation import gettext_lazy as _ from django.contrib.auth.models import BaseUserManager, AbstractBaseUser @@ -35,14 +36,14 @@ class UserManager(BaseUserManager): class User(AbstractBaseUser): email = models.EmailField( - verbose_name="email address", + _('Email address'), max_length=255, unique=True, ) is_active = models.BooleanField(default=True) is_admin = models.BooleanField(default=False) - first_name = models.CharField(max_length=255, blank=True, null=True) - last_name = models.CharField(max_length=255, blank=True, null=True) + first_name = models.CharField(_("First name"), max_length=255, blank=True, null=True) + last_name = models.CharField(_("Last name"), max_length=255, blank=True, null=True) objects = UserManager() From 8ee3f561a4300e1b39b0ef90ac9e0d27111ed602 Mon Sep 17 00:00:00 2001 From: Cayo Puigdefabregas Date: Fri, 15 Dec 2023 14:08:03 +0100 Subject: [PATCH 70/70] use logging correctly --- idhub/email/views.py | 12 +++++++++--- trustchain_idhub/settings.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/idhub/email/views.py b/idhub/email/views.py index 83d10cc..72e0daa 100644 --- a/idhub/email/views.py +++ b/idhub/email/views.py @@ -1,3 +1,5 @@ +import logging + from django.conf import settings from django.template import loader from django.core.mail import EmailMultiAlternatives @@ -7,6 +9,9 @@ from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode +logger = logging.getLogger(__name__) + + class NotifyActivateUserByEmail: def get_email_context(self, user): """ @@ -49,10 +54,11 @@ class NotifyActivateUserByEmail: email_message.attach_alternative(html_email, 'text/html') try: if settings.DEVELOPMENT: - print(to_email) - print(body) + logger.warning(to_email) + logger.warning(body) return email_message.send() - except Exception: + except Exception as err: + logger.error(err) return diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py index 7601739..b9eecc5 100644 --- a/trustchain_idhub/settings.py +++ b/trustchain_idhub/settings.py @@ -207,3 +207,18 @@ SUPPORTED_CREDENTIALS = config( default='[]', cast=literal_eval ) + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "console": {"class": "logging.StreamHandler"}, + }, + "loggers": { + "django": { + "handlers": ["console"], + "level": "INFO", + }, + } +} +