diff --git a/website/age/apps.py b/website/age/apps.py index 2c9ce9a9..cbbc3652 100644 --- a/website/age/apps.py +++ b/website/age/apps.py @@ -18,6 +18,8 @@ def ready(self): AgeOverviewView, ) + from age import signals # noqa + def filter_user_page(user_page_list: list): """Add age overview tab on accounts page.""" user_page_list.append( diff --git a/website/age/migrations/0001_initial.py b/website/age/migrations/0001_initial.py index e15a692d..9076ab39 100644 --- a/website/age/migrations/0001_initial.py +++ b/website/age/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.4 on 2023-09-18 05:28 +# Generated by Django 4.2.4 on 2023-09-18 20:45 from django.conf import settings from django.db import migrations, models @@ -21,7 +21,11 @@ class Migration(migrations.Migration): ("created_at", models.DateTimeField(auto_now_add=True)), ( "user", - models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="is_18_years_old", + to=settings.AUTH_USER_MODEL, + ), ), ], ), diff --git a/website/age/migrations/0002_alter_is18yearsold_user.py b/website/age/migrations/0002_alter_is18yearsold_user.py deleted file mode 100644 index bc1c3ad4..00000000 --- a/website/age/migrations/0002_alter_is18yearsold_user.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 4.2.4 on 2023-09-18 05:45 - -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), - ("age", "0001_initial"), - ] - - operations = [ - migrations.AlterField( - model_name="is18yearsold", - name="user", - field=models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="is_18_years_old", - to=settings.AUTH_USER_MODEL, - ), - ), - ] diff --git a/website/age/migrations/0003_sessionmapping.py b/website/age/migrations/0003_sessionmapping.py deleted file mode 100644 index c2de7edc..00000000 --- a/website/age/migrations/0003_sessionmapping.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.2.4 on 2023-09-18 14:53 - -from django.db import migrations, models -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ("age", "0002_alter_is18yearsold_user"), - ] - - operations = [ - migrations.CreateModel( - name="SessionMapping", - fields=[ - ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ("session_token", models.CharField(max_length=20)), - ], - ), - ] diff --git a/website/age/migrations/0004_alter_sessionmapping_session_token.py b/website/age/migrations/0004_alter_sessionmapping_session_token.py deleted file mode 100644 index ed9c1683..00000000 --- a/website/age/migrations/0004_alter_sessionmapping_session_token.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.4 on 2023-09-18 14:54 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("age", "0003_sessionmapping"), - ] - - operations = [ - migrations.AlterField( - model_name="sessionmapping", - name="session_token", - field=models.CharField(max_length=20, unique=True), - ), - ] diff --git a/website/age/models.py b/website/age/models.py index de62e272..560a06a0 100644 --- a/website/age/models.py +++ b/website/age/models.py @@ -12,10 +12,3 @@ class Is18YearsOld(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="is_18_years_old") created_at = models.DateTimeField(auto_now_add=True) - - -class SessionMapping(models.Model): - """Session mapping class for Yivi.""" - - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - session_token = models.CharField(max_length=20, unique=True) diff --git a/website/age/signals.py b/website/age/signals.py new file mode 100644 index 00000000..434ccbe2 --- /dev/null +++ b/website/age/signals.py @@ -0,0 +1,23 @@ +from django.conf import settings +from django.dispatch import receiver + +from age import models +from yivi.models import Session +from yivi.signals import attributes_verified + + +@receiver(attributes_verified) +def update_is_over_18(sender, **kwargs): + session: Session = kwargs.get("session") + if session.user is None or models.Is18YearsOld.objects.filter(user=session.user).exists(): + return + + attributes = kwargs.get("attributes") + for attribute_conjuction_clause in attributes: + for attribute_disjunction_clause in attribute_conjuction_clause: + attribute_id = attribute_disjunction_clause["id"] + if ( + attribute_id == settings.AGE_VERIFICATION_DISCLOSE_ATTRIBUTE + and attribute_disjunction_clause["status"] == "PRESENT" + ): + models.Is18YearsOld.objects.create(user=session.user) diff --git a/website/age/templates/age/age_overview.html b/website/age/templates/age/age_overview.html index 5482eceb..b128f5b7 100644 --- a/website/age/templates/age/age_overview.html +++ b/website/age/templates/age/age_overview.html @@ -22,14 +22,14 @@ - - + {% endif %} diff --git a/website/age/urls.py b/website/age/urls.py deleted file mode 100644 index 9fcd9927..00000000 --- a/website/age/urls.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.urls import path - -from age import views - - -urlpatterns = [] diff --git a/website/age/views.py b/website/age/views.py index 468b1727..13a3c7ae 100644 --- a/website/age/views.py +++ b/website/age/views.py @@ -1,6 +1,10 @@ +import json + +from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin from django.shortcuts import render from django.template.loader import render_to_string +from django.utils.safestring import mark_safe from django.views.generic import TemplateView from age import models @@ -15,10 +19,20 @@ def get(self, request, **kwargs): """Get Age Overview View.""" is_18_years_old = models.Is18YearsOld.objects.filter(user=request.user).exists() - rendered_tab = render_to_string("age/age_overview.html", context={"is_over_18": is_18_years_old}) + rendered_tab = render_to_string( + "age/age_overview.html", + context={ + "is_over_18": is_18_years_old, + "disclose": mark_safe(json.dumps({"disclose": [[[settings.AGE_VERIFICATION_DISCLOSE_ATTRIBUTE]]]})), + }, + ) return render( request, self.template_name, - {"active": kwargs.get("active"), "tabs": kwargs.get("tabs"), "rendered_tab": rendered_tab}, + { + "active": kwargs.get("active"), + "tabs": kwargs.get("tabs"), + "rendered_tab": rendered_tab, + }, ) diff --git a/website/tosti/api/v1/urls.py b/website/tosti/api/v1/urls.py index e6fc0a1d..110a3c5c 100644 --- a/website/tosti/api/v1/urls.py +++ b/website/tosti/api/v1/urls.py @@ -12,7 +12,7 @@ path("associations/", include("associations.api.v1.urls")), path("transactions/", include("transactions.api.v1.urls")), path("users/", include("users.api.v1.urls")), - path("age/", include("age.api.v1.urls")), + path("yivi/", include("yivi.api.v1.urls")), path("fridges/", include("fridges.api.v1.urls")), path( "schema", diff --git a/website/tosti/settings/base.py b/website/tosti/settings/base.py index 3a4a0672..c68d8c72 100644 --- a/website/tosti/settings/base.py +++ b/website/tosti/settings/base.py @@ -41,6 +41,7 @@ "silvasoft", "oauth2_provider", "corsheaders", + "yivi", "age", "fridges", ] @@ -260,3 +261,5 @@ ] DJANGO_CRON_DELETE_LOGS_OLDER_THAN = 14 + +AGE_VERIFICATION_DISCLOSE_ATTRIBUTE = "irma-demo.MijnOverheid.ageLower.over18" diff --git a/website/tosti/urls.py b/website/tosti/urls.py index 2c411bf3..5514de71 100644 --- a/website/tosti/urls.py +++ b/website/tosti/urls.py @@ -46,10 +46,6 @@ "thaliedje/", include(("thaliedje.urls", "thaliedje"), namespace="thaliedje"), ), - path( - "age/", - include(("age.urls", "age"), namespace="age"), - ), path( "fridges/", include(("fridges.urls", "fridges"), namespace="fridges"), diff --git a/website/age/api/__init__.py b/website/yivi/__init__.py similarity index 100% rename from website/age/api/__init__.py rename to website/yivi/__init__.py diff --git a/website/age/api/v1/__init__.py b/website/yivi/api/__init__.py similarity index 100% rename from website/age/api/v1/__init__.py rename to website/yivi/api/__init__.py diff --git a/website/yivi/api/v1/__init__.py b/website/yivi/api/v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/website/age/api/v1/serializers.py b/website/yivi/api/v1/serializers.py similarity index 100% rename from website/age/api/v1/serializers.py rename to website/yivi/api/v1/serializers.py diff --git a/website/age/api/v1/urls.py b/website/yivi/api/v1/urls.py similarity index 75% rename from website/age/api/v1/urls.py rename to website/yivi/api/v1/urls.py index f3292186..5d45e4d1 100644 --- a/website/age/api/v1/urls.py +++ b/website/yivi/api/v1/urls.py @@ -1,5 +1,5 @@ from django.urls import path -from age.api.v1.views import YiviStartAPIView, YiviResultAPIView +from yivi.api.v1.views import YiviStartAPIView, YiviResultAPIView urlpatterns = [ diff --git a/website/age/api/v1/views.py b/website/yivi/api/v1/views.py similarity index 63% rename from website/age/api/v1/views.py rename to website/yivi/api/v1/views.py index ad71ae9a..8e344797 100644 --- a/website/age/api/v1/views.py +++ b/website/yivi/api/v1/views.py @@ -2,9 +2,10 @@ from rest_framework.response import Response from rest_framework.views import APIView -from age.models import SessionMapping -from age.services import get_yivi_client -from age.yivi import YiviException +from yivi import signals +from yivi.models import Session +from yivi.services import get_yivi_client +from yivi.yivi import YiviException from tosti.api.permissions import IsAuthenticatedOrTokenHasScopeForMethod @@ -23,19 +24,23 @@ class YiviStartAPIView(APIView): def post(self, request, **kwargs): """Start a Yivi request.""" yivi_client = get_yivi_client() + disclose = request.data.get("disclose", None) + if disclose is None: + return Response(status=400, data="Parameter 'disclose' must be specified.") + try: response = yivi_client.start_session( { "@context": "https://irma.app/ld/request/disclosure/v2", - "disclose": [[["irma-demo.MijnOverheid.ageLower.over18"]]], + "disclose": disclose, } ) except YiviException as e: return Response(status=e.http_status, data=e.msg) token = response["token"] - session_mapping = SessionMapping.objects.create(session_token=token) - response["token"] = session_mapping.id + session = Session.objects.create(session_token=token, user=request.user) + response["token"] = session.id return Response(data=response) @@ -55,8 +60,15 @@ def get(self, request, **kwargs): """Get the result of a Yivi session.""" yivi_client = get_yivi_client() session_uuid = kwargs.get("pk") - session = get_object_or_404(SessionMapping, pk=session_uuid) + session = get_object_or_404(Session, pk=session_uuid, user=request.user) try: - return Response(data=yivi_client.session_result(session.session_token)) + response = yivi_client.session_result(session.session_token) except YiviException as e: return Response(status=e.http_status, data=e.msg) + + response["token"] = session.id + + if response.get("proofStatus") == "VALID": + signals.attributes_verified.send_robust(self.__class__, session=session, attributes=response["disclosed"]) + + return Response(data=response) diff --git a/website/yivi/apps.py b/website/yivi/apps.py new file mode 100644 index 00000000..85209b54 --- /dev/null +++ b/website/yivi/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class YiviConfig(AppConfig): + """Yivi App Config.""" + + default_auto_field = "django.db.models.BigAutoField" + name = "yivi" diff --git a/website/yivi/migrations/0001_initial.py b/website/yivi/migrations/0001_initial.py new file mode 100644 index 00000000..e75fc643 --- /dev/null +++ b/website/yivi/migrations/0001_initial.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.4 on 2023-09-18 20:29 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Session", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ("session_token", models.CharField(max_length=20, unique=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "user", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/website/yivi/migrations/__init__.py b/website/yivi/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/website/yivi/models.py b/website/yivi/models.py new file mode 100644 index 00000000..2be25402 --- /dev/null +++ b/website/yivi/models.py @@ -0,0 +1,16 @@ +import uuid + +from django.contrib.auth import get_user_model +from django.db import models + + +User = get_user_model() + + +class Session(models.Model): + """Session mapping class for Yivi.""" + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + session_token = models.CharField(max_length=20, unique=True) + user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) diff --git a/website/age/services.py b/website/yivi/services.py similarity index 85% rename from website/age/services.py rename to website/yivi/services.py index c9039783..d0e88fb5 100644 --- a/website/age/services.py +++ b/website/yivi/services.py @@ -1,6 +1,6 @@ from django.conf import settings -from age.yivi import Yivi +from yivi.yivi import Yivi def get_yivi_client() -> Yivi: diff --git a/website/yivi/signals.py b/website/yivi/signals.py new file mode 100644 index 00000000..1f634e7a --- /dev/null +++ b/website/yivi/signals.py @@ -0,0 +1,3 @@ +import django.dispatch + +attributes_verified = django.dispatch.Signal() diff --git a/website/age/static/age/js/yivi.js b/website/yivi/static/yivi/js/yivi.js similarity index 100% rename from website/age/static/age/js/yivi.js rename to website/yivi/static/yivi/js/yivi.js diff --git a/website/age/yivi.py b/website/yivi/yivi.py similarity index 100% rename from website/age/yivi.py rename to website/yivi/yivi.py