diff --git a/README.md b/README.md
index 353f7b4..829ad44 100644
--- a/README.md
+++ b/README.md
@@ -174,6 +174,35 @@ IdHub's repository is organized into several directories, each serving a specifi
- **utils**: A utility folder containing various helper scripts and tools developed by us but that are independent of idHub. Even so, IdHub uses them and needs them (examples of this are the validation system for the data that is loades by excel, or the system that manages the sskit)
+## Webhook
+You need define a token un the admin section "/webhool/tokens"
+For define one query here there are a python example:
+```
+ import requests
+ import json
+
+ url = "https://api.example.com/webhook/verify/"
+ data = {
+ "type": "credential",
+ "data": {
+ '@context': ['https://www.....
+ }
+
+ headers = {
+ "Authorization": f"Bearer {token}",
+ "Content-Type": "application/json"
+ }
+
+ response = requests.post(url, headers=headers, data=json.dumps(data))
+
+ response.status_code == 200
+ response.json()
+
+```
+ The response of verification can be ```{'status': 'success'}``` or ```{'status': 'fail'}```
+ If no there are *type* in data or this is not a *credential* then, the verification proccess hope a *presentation*
+ The field *data* have the credential or presentation.
+
## Documentation
For detailed documentation, visit [Documentation Link](http://idhub.pangea.org/help/).
diff --git a/idhub/migrations/0005_alter_file_datas_created_at_and_more.py b/idhub/migrations/0005_alter_file_datas_created_at_and_more.py
new file mode 100644
index 0000000..42ba9f8
--- /dev/null
+++ b/idhub/migrations/0005_alter_file_datas_created_at_and_more.py
@@ -0,0 +1,84 @@
+# Generated by Django 4.2.5 on 2024-06-13 08:08
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('idhub', '0004_alter_event_type'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='file_datas',
+ name='created_at',
+ field=models.DateTimeField(auto_now=True, verbose_name='Date'),
+ ),
+ migrations.AlterField(
+ model_name='file_datas',
+ name='file_name',
+ field=models.CharField(max_length=250, verbose_name='File'),
+ ),
+ migrations.AlterField(
+ model_name='file_datas',
+ name='success',
+ field=models.BooleanField(default=True, verbose_name='Success'),
+ ),
+ migrations.AlterField(
+ model_name='schemas',
+ name='_description',
+ field=models.CharField(
+ db_column='description',
+ max_length=250,
+ null=True,
+ verbose_name='Description',
+ ),
+ ),
+ migrations.AlterField(
+ model_name='schemas',
+ name='_name',
+ field=models.TextField(db_column='name', null=True, verbose_name='Name'),
+ ),
+ migrations.AlterField(
+ model_name='schemas',
+ name='created_at',
+ field=models.DateTimeField(auto_now=True, verbose_name='Date'),
+ ),
+ migrations.AlterField(
+ model_name='schemas',
+ name='file_schema',
+ field=models.CharField(max_length=250, verbose_name='Schema'),
+ ),
+ migrations.AlterField(
+ model_name='verificablecredential',
+ name='issued_on',
+ field=models.DateTimeField(null=True, verbose_name='Issued on'),
+ ),
+ migrations.AlterField(
+ model_name='verificablecredential',
+ name='status',
+ field=models.PositiveSmallIntegerField(
+ choices=[(1, 'Enabled'), (2, 'Issued'), (3, 'Revoked'), (4, 'Expired')],
+ default=1,
+ verbose_name='Status',
+ ),
+ ),
+ migrations.AlterField(
+ model_name='verificablecredential',
+ name='type',
+ field=models.CharField(max_length=250, verbose_name='Type'),
+ ),
+ migrations.AlterField(
+ model_name='verificablecredential',
+ name='user',
+ field=models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name='vcredentials',
+ to=settings.AUTH_USER_MODEL,
+ verbose_name='User',
+ ),
+ ),
+ ]
diff --git a/idhub/templates/idhub/base_admin.html b/idhub/templates/idhub/base_admin.html
index db8354e..79066ed 100644
--- a/idhub/templates/idhub/base_admin.html
+++ b/idhub/templates/idhub/base_admin.html
@@ -155,6 +155,11 @@
+
+
+ {% trans "Webhook Tokens" %}
+
+
diff --git a/idhub_auth/migrations/0002_alter_user_is_active_alter_user_is_admin.py b/idhub_auth/migrations/0002_alter_user_is_active_alter_user_is_admin.py
new file mode 100644
index 0000000..3d50023
--- /dev/null
+++ b/idhub_auth/migrations/0002_alter_user_is_active_alter_user_is_admin.py
@@ -0,0 +1,22 @@
+# Generated by Django 4.2.5 on 2024-06-13 08:08
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('idhub_auth', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='user',
+ name='is_active',
+ field=models.BooleanField(default=True, verbose_name='is active'),
+ ),
+ migrations.AlterField(
+ model_name='user',
+ name='is_admin',
+ field=models.BooleanField(default=False, verbose_name='is admin'),
+ ),
+ ]
diff --git a/trustchain_idhub/settings.py b/trustchain_idhub/settings.py
index 18b9a4d..473fa24 100644
--- a/trustchain_idhub/settings.py
+++ b/trustchain_idhub/settings.py
@@ -82,7 +82,8 @@ INSTALLED_APPS = [
'idhub_auth',
'oidc4vp',
'idhub',
- 'promotion'
+ 'promotion',
+ 'webhook'
]
MIDDLEWARE = [
diff --git a/trustchain_idhub/urls.py b/trustchain_idhub/urls.py
index e8468a3..a4bc0b1 100644
--- a/trustchain_idhub/urls.py
+++ b/trustchain_idhub/urls.py
@@ -26,4 +26,5 @@ urlpatterns = [
path('', include('idhub.urls')),
path('oidc4vp/', include('oidc4vp.urls')),
path('promotion/', include('promotion.urls')),
+ path('webhook/', include('webhook.urls')),
]
diff --git a/webhook/__init__.py b/webhook/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/webhook/admin.py b/webhook/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/webhook/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/webhook/apps.py b/webhook/apps.py
new file mode 100644
index 0000000..60ee235
--- /dev/null
+++ b/webhook/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class WebhookConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'webhook'
diff --git a/webhook/forms.py b/webhook/forms.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/webhook/forms.py
@@ -0,0 +1 @@
+
diff --git a/webhook/migrations/0001_initial.py b/webhook/migrations/0001_initial.py
new file mode 100644
index 0000000..7bad033
--- /dev/null
+++ b/webhook/migrations/0001_initial.py
@@ -0,0 +1,27 @@
+# Generated by Django 4.2.5 on 2024-06-13 08:08
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ initial = True
+
+ dependencies = []
+
+ operations = [
+ migrations.CreateModel(
+ name='Token',
+ fields=[
+ (
+ 'id',
+ models.BigAutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name='ID',
+ ),
+ ),
+ ('token', models.UUIDField()),
+ ],
+ ),
+ ]
diff --git a/webhook/migrations/__init__.py b/webhook/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/webhook/models.py b/webhook/models.py
new file mode 100644
index 0000000..54218c2
--- /dev/null
+++ b/webhook/models.py
@@ -0,0 +1,7 @@
+from django.db import models
+
+# Create your models here.
+
+
+class Token(models.Model):
+ token = models.UUIDField()
diff --git a/webhook/tables.py b/webhook/tables.py
new file mode 100644
index 0000000..498a38e
--- /dev/null
+++ b/webhook/tables.py
@@ -0,0 +1,67 @@
+import django_tables2 as tables
+from django.utils.html import format_html
+from django.utils.translation import gettext_lazy as _
+
+from webhook.models import Token
+
+
+class ButtonColumn(tables.Column):
+ attrs = {
+ "a": {
+ "type": "button",
+ "class": "text-danger",
+ "title": "Remove",
+ }
+ }
+ # it makes no sense to order a column of buttons
+ orderable = False
+ # django_tables will only call the render function if it doesn't find
+ # any empty values in the data, so we stop it from matching the data
+ # to any value considered empty
+ empty_values = ()
+
+ def render(self):
+ return format_html('')
+
+
+class TokensTable(tables.Table):
+ delete = ButtonColumn(
+ verbose_name=_("Delete"),
+ linkify={
+ "viewname": "webhook:delete_token",
+ "args": [tables.A("pk")]
+ },
+ orderable=False
+ )
+
+ token = tables.Column(verbose_name=_("Token"), empty_values=())
+
+ def render_view_user(self):
+ return format_html('')
+
+ # def render_token(self, record):
+ # return record.get_memberships()
+
+ # def order_membership(self, queryset, is_descending):
+ # # TODO: Test that this doesn't return more rows than it should
+ # queryset = queryset.order_by(
+ # ("-" if is_descending else "") + "memberships__type"
+ # )
+
+ # return (queryset, True)
+
+ # def render_role(self, record):
+ # return record.get_roles()
+
+ # def order_role(self, queryset, is_descending):
+ # queryset = queryset.order_by(
+ # ("-" if is_descending else "") + "roles"
+ # )
+
+ # return (queryset, True)
+
+ class Meta:
+ model = Token
+ template_name = "idhub/custom_table.html"
+ fields = ("token", "view_user")
+
diff --git a/webhook/templates/token.html b/webhook/templates/token.html
new file mode 100644
index 0000000..7ab829b
--- /dev/null
+++ b/webhook/templates/token.html
@@ -0,0 +1,14 @@
+{% extends "idhub/base_admin.html" %}
+{% load i18n %}
+{% load render_table from django_tables2 %}
+
+{% block content %}
+
+
+ {{ subtitle }}
+
+{% render_table table %}
+
+{% endblock %}
diff --git a/webhook/tests.py b/webhook/tests.py
new file mode 100644
index 0000000..35b923f
--- /dev/null
+++ b/webhook/tests.py
@@ -0,0 +1,76 @@
+import json
+
+from uuid import uuid4
+from django.test import TestCase
+from django.core.cache import cache
+from django.urls import reverse
+from django.conf import settings
+
+from idhub_auth.models import User
+from oidc4vp.models import Organization
+from webhook.models import Token
+
+
+class AdminDashboardViewTest(TestCase):
+
+ def setUp(self):
+ cache.set("KEY_DIDS", '1234', None)
+ self.user = User.objects.create_user(
+ email='normaluser@example.org',
+ password='testpass12',
+ )
+ self.user.accept_gdpr=True
+ self.user.save()
+
+ self.admin_user = User.objects.create_superuser(
+ email='adminuser@example.org',
+ password='adminpass12')
+ self.admin_user.accept_gdpr=True
+ self.admin_user.save()
+ self.org = Organization.objects.create(name="testserver", main=True)
+
+ settings.DOMAIN = self.org.name
+ settings.ENABLE_EMAIL = False
+ self.client.login(email='adminuser@example.org', password='adminpass12')
+
+ def get_credential(self):
+ return {'@context': ['https://www.w3.org/2018/credentials/v1', 'https://idhub.pangea.org/context/base.jsonld', 'https://idhub.pangea.org/context/membership-card.jsonld'], 'type': ['VerifiableCredential', 'VerifiableAttestation', 'MembershipCard'], 'id': 'http://localhost/credentials/1', 'issuer': {'id': 'did:web:idhub.pangea.org:dids:z6Mko7ParpJZzBopW48DY8125Tcz9hMCaH7YzteWKnFcVzhu', 'name': 'Pangea'}, 'issuanceDate': '2024-06-11T14:41:12Z', 'issued': '2024-06-11T14:41:12Z', 'validFrom': '2024-06-11T14:41:12Z', 'name': [{'value': 'Membership Card', 'lang': 'en'}, {'value': 'Carnet de soci/a', 'lang': 'ca_ES'}, {'value': 'Carnet de socio/a', 'lang': 'es'}], 'description': [{'value': "The membership card specifies an individual's subscription or enrollment in specific services or benefits issued by an organization.", 'lang': 'en'}, {'value': "El carnet de soci especifica la subscripció o la inscripció d'un individu en serveis o beneficis específics emesos per una organització.", 'lang': 'ca_ES'}, {'value': 'El carnet de socio especifica la suscripción o inscripción de un individuo en servicios o beneficios específicos emitidos por uns organización.', 'lang': 'es'}], 'credentialSubject': {'id': 'did:web:localhost:did-registry:z6MkoAQJ96ppDQFw1idhvcR9NssPJQLFBoVDD2L62r7fh5yS', 'firstName': 'Pedro', 'lastName': 'Lagasta', 'email': 'user1@example.org', 'typeOfPerson': 'natural', 'organisation': 'Pangea', 'membershipType': 'Employee', 'membershipId': '1a', 'affiliatedSince': '2023-01-01'}, 'credentialSchema': {'id': 'https://idhub.pangea.org/vc_schemas/membership-card.json', 'type': 'FullJsonSchemaValidator2021'}, 'proof': {'type': 'Ed25519Signature2018', 'proofPurpose': 'assertionMethod', 'verificationMethod': 'did:web:idhub.pangea.org:dids:z6Mko7ParpJZzBopW48DY8125Tcz9hMCaH7YzteWKnFcVzhu#z6Mko7ParpJZzBopW48DY8125Tcz9hMCaH7YzteWKnFcVzhu', 'created': '2024-06-11T14:59:37Z', 'jws': 'eyJhbGciOiJFZERTQSIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9..kC2_QSkWRQ_fZ6C0_lRWvf4xuuxOueeCMb6dQFPKyn1h3gAHg_tb98RETIZeaQD6759wjJuH-IPJpvo4vzCDDA'}}
+
+ def test_render_tokens_page(self):
+ response = self.client.get('/webhook/tokens/', follow=True)
+
+ self.assertEqual(response.status_code, 200)
+
+ def test_new_token(self):
+ response = self.client.get('/webhook/tokens/new', follow=True)
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(Token.objects.count(), 1)
+
+ tk = Token.objects.first()
+ url = "/webhook/tokens/{}/del".format(tk.id)
+ response = self.client.get(url, follow=True)
+
+ self.assertEqual(Token.objects.count(), 0)
+
+ def test_verify(self):
+ token = uuid4()
+ Token.objects.create(token=token)
+ data = {
+ "type": "credential",
+ "data": self.get_credential()
+ }
+ headers = {
+ "Authorization": f"Bearer {token}",
+ "Content-Type": "application/json"
+ }
+ response = self.client.post(
+ reverse('webhook:verify'),
+ content_type='application/json',
+ data=data,
+ headers=headers
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.json(), {'status': 'success'})
+ self.client.headers = None
diff --git a/webhook/urls.py b/webhook/urls.py
new file mode 100644
index 0000000..c0c863a
--- /dev/null
+++ b/webhook/urls.py
@@ -0,0 +1,13 @@
+from webhook import views
+
+from django.urls import path
+
+
+app_name = 'webhook'
+
+urlpatterns = [
+ path('verify/', views.webhook_verify, name='verify'),
+ path('tokens/', views.WebHookTokenView.as_view(), name='tokens'),
+ path('tokens/new', views.TokenNewView.as_view(), name='new_token'),
+ path('tokens//del', views.TokenDeleteView.as_view(), name='delete_token'),
+]
diff --git a/webhook/views.py b/webhook/views.py
new file mode 100644
index 0000000..2536eea
--- /dev/null
+++ b/webhook/views.py
@@ -0,0 +1,96 @@
+import json
+
+from django.shortcuts import get_object_or_404, redirect
+from django.utils.translation import gettext_lazy as _
+from django.views.decorators.csrf import csrf_exempt
+from django.views.generic.edit import DeleteView
+from django.views.generic.base import View
+from django.http import JsonResponse
+from django_tables2 import SingleTableView
+from pyvckit.verify import verify_vp, verify_vc
+from uuid import uuid4
+
+from idhub.mixins import AdminView
+from webhook.models import Token
+from webhook.tables import TokensTable
+
+
+@csrf_exempt
+def webhook_verify(request):
+ if request.method == 'POST':
+ auth_header = request.headers.get('Authorization')
+ if not auth_header or not auth_header.startswith('Bearer '):
+ return JsonResponse({'error': 'Invalid or missing token'}, status=401)
+
+ token = auth_header.split(' ')[1]
+ tk = Token.objects.filter(token=token).first()
+ if not tk:
+ return JsonResponse({'error': 'Invalid or missing token'}, status=401)
+
+ try:
+ data = json.loads(request.body)
+ except json.JSONDecodeError:
+ return JsonResponse({'error': 'Invalid JSON'}, status=400)
+
+ typ = data.get("type")
+ vc = data.get("data")
+ try:
+ vc = json.dumps(vc)
+ except Exception:
+ return JsonResponse({'error': 'Invalid JSON'}, status=400)
+
+ func = verify_vp
+ if typ == "credential":
+ func = verify_vc
+
+ if func(vc):
+ return JsonResponse({'status': 'success'}, status=200)
+
+ return JsonResponse({'status': 'fail'}, status=200)
+
+ return JsonResponse({'error': 'Invalid request method'}, status=400)
+
+
+class WebHookTokenView(AdminView, SingleTableView):
+ template_name = "token.html"
+ title = _("Credential management")
+ section = "Credential"
+ subtitle = _('Managament Tokens')
+ icon = 'bi bi-key'
+ model = Token
+ table_class = TokensTable
+
+ def get_queryset(self):
+ """
+ Override the get_queryset method to filter events based on the user type.
+ """
+ return Token.objects.filter().order_by("-id")
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context.update({
+ 'tokens': Token.objects,
+ })
+ return context
+
+
+class TokenDeleteView(AdminView, DeleteView):
+ model = Token
+
+ def get(self, request, *args, **kwargs):
+ self.check_valid_user()
+ self.pk = kwargs['pk']
+ self.object = get_object_or_404(self.model, pk=self.pk)
+ self.object.delete()
+
+ return redirect('webhook:tokens')
+
+
+class TokenNewView(AdminView, View):
+
+ def get(self, request, *args, **kwargs):
+ self.check_valid_user()
+ Token.objects.create(token=uuid4())
+
+ return redirect('webhook:tokens')
+