Skip to content

Commit

Permalink
feat: add API (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
sverben authored Jun 12, 2024
1 parent b9f1811 commit d8e343f
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 47 deletions.
1 change: 1 addition & 0 deletions corvee/settings.py.example
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ USE_TZ = True


# IDP Settings
OPENID_CONFIGURATION = 'https://leden.djoamersfoot.nl/o/.well-known/openid-configuration'
LOGIN_REDIRECT_URL='/main/'
IDP_CLIENT_ID = 'henk'
IDP_CLIENT_SECRET = 'secret'
Expand Down
56 changes: 55 additions & 1 deletion corvee/src/api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from django.http.response import JsonResponse
from django.utils import timezone
from django.views import View

from corvee.src.models import Persoon
from corvee.src.corvee import Corvee
from corvee.src.mixins import TokenRequiredMixin
from corvee.src.models import Persoon
from corvee.src.mixins import AuthenticatedMixin
from corvee.src.utils import acknowledge, insufficient, punishment, absent


class SelectedV1(TokenRequiredMixin):
Expand All @@ -11,3 +16,52 @@ def get(self, request, *args, **kwargs):
names = [person.short_name for person in selected]
present_names = [person.short_name for person in present]
return JsonResponse({"selected": names, "present": present_names})


class StatusV1(AuthenticatedMixin, View):
def get(self, request, *args, **kwargs):
weekday = timezone.now().weekday()
day = "fri" if weekday == 4 else "sat"
pod = Corvee.get_pod()
if weekday not in [4, 5]:
return JsonResponse({"ok": False, "error": "Vandaag is er geen corvee"})

return JsonResponse({
"current": list(Persoon.objects.filter(selected=True).order_by('latest').values()),
"day": day,
"pod": pod
})


class RenewV1(AuthenticatedMixin, View):
def get(self, request, *args, **kwargs):
Corvee.renew_list()
return JsonResponse({"ok": True})


class AcknowledgeV1(AuthenticatedMixin, View):
def get(self, request, *args, **kwargs):
acknowledge(request, self.kwargs.get('pk'))

return JsonResponse({"ok": True})


class InsufficientV1(AuthenticatedMixin, View):
def get(self, request, *args, **kwargs):
insufficient(request, self.kwargs.get('pk'))

return JsonResponse({"ok": True})


class PunishmentV1(AuthenticatedMixin, View):
def get(self, request, *args, **kwargs):
punishment(request, self.kwargs.get('pk'))

return JsonResponse({"ok": True})


class AbsentV1(AuthenticatedMixin, View):
def get(self, request, *args, **kwargs):
absent(request, self.kwargs.get('pk'))

return JsonResponse({"ok": True})
4 changes: 2 additions & 2 deletions corvee/src/corvee.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def update_members(access_token):
Persoon.objects.filter(marked_for_deletion=True).delete()

@staticmethod
def _get_pod():
def get_pod():
pod = 'm'
hour = timezone.now().hour
if hour >= 18:
Expand All @@ -74,7 +74,7 @@ def renew_list(requery_present_members=True):
day = 'fri' if weekday == 4 else 'sat'
if weekday not in [4, 5]:
return
pod = Corvee._get_pod()
pod = Corvee.get_pod()
if requery_present_members:
# Get list of present members from 'Aanmelden' API
presence = PresenceApiClient(client_id=settings.BACKEND_CLIENT_ID,
Expand Down
51 changes: 49 additions & 2 deletions corvee/src/mixins.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from django.contrib.auth.mixins import UserPassesTestMixin
from django.http import HttpResponse
import jwt
from django.conf import settings
from django.contrib.auth.mixins import UserPassesTestMixin
from django.contrib.auth.models import User
from django.http import HttpResponse, HttpResponseForbidden
from django.views import View

from corvee.src.utils import get_access_token, get_openid_configuration, get_jwks_client


class PermissionRequiredMixin(UserPassesTestMixin):
required_permission = 'corvee.view_persoon'
Expand All @@ -15,6 +19,7 @@ def check_user(self, user):
def test_func(self):
return self.check_user(self.request.user)


class TokenRequiredMixin(View):
def dispatch(self, request, *args, **kwargs):
given_token = request.headers.get("Authorization", "").lower()
Expand All @@ -24,3 +29,45 @@ def dispatch(self, request, *args, **kwargs):
return super(TokenRequiredMixin, self).dispatch(request, *args, **kwargs)

return HttpResponse("401 JOCH!", status=401, content_type="text/plain")


class AuthenticatedMixin:
def dispatch(self, request, *args, **kwargs):
token = get_access_token(request)
if not token:
return HttpResponseForbidden()

openid_configuration = get_openid_configuration()
jwks_client = get_jwks_client()

signing_key = jwks_client.get_signing_key_from_jwt(token)
decoded_jwt = jwt.decode(
token,
key=signing_key.key,
algorithms=openid_configuration['id_token_signing_alg_values_supported'],
options={'verify_aud': False}
)
if 'corvee' not in decoded_jwt:
return HttpResponseForbidden()

username = f"idp-{decoded_jwt['sub']}"
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
user = User(username=username)
user.set_unusable_password()
user.first_name = decoded_jwt['given_name']
user.last_name = decoded_jwt['family_name']
user.is_superuser = True
user.save()

if not user.is_staff:
for required_role in settings.IDP_REQUIRED_ROLES:
if required_role in decoded_jwt['account_type'].lower():
break
else:
return HttpResponseForbidden()

request.user = user

return super().dispatch(request, *args, **kwargs)
82 changes: 82 additions & 0 deletions corvee/src/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from datetime import datetime
from functools import lru_cache

import requests
from django.conf import settings
from django.http import HttpRequest
from django.utils import timezone
from jwt import PyJWKClient

from corvee.src.corvee import Corvee
from corvee.src.models import AuditLog, Persoon


class Auditor:

@staticmethod
def audit(first_name, last_name, action, user):
audit = AuditLog()
audit.first_name = first_name
audit.last_name = last_name
audit.datetime = timezone.now().date()
audit.action = action
audit.performed_by = f"{user.first_name} {user.last_name}"
audit.save()


@lru_cache()
def get_openid_configuration():
return requests.get(settings.OPENID_CONFIGURATION, timeout=10).json()


@lru_cache()
def get_jwks_client():
return PyJWKClient(uri=get_openid_configuration()['jwks_uri'])


def get_access_token(request) -> (str, None):
token = request.GET.get('access_token', '').strip()
if token == "":
parts = request.headers.get('authorization', '').split()
if len(parts) == 2 and parts[0].lower() == 'bearer':
token = parts[1]
if token == "":
return None
return token


def acknowledge(request: HttpRequest, pk: str):
persoon = Persoon.objects.get(pk=pk)
persoon.latest = timezone.now()
persoon.selected = False
persoon.save()

Auditor.audit(persoon.first_name, persoon.last_name, 'acknowledged', request.user)


def insufficient(request: HttpRequest, pk: str):
persoon = Persoon.objects.get(pk=pk)
persoon.selected = False
persoon.save()

Auditor.audit(persoon.first_name, persoon.last_name, 'insufficient', request.user)


def absent(request: HttpRequest, pk: str):
persoon = Persoon.objects.get(pk=pk)
persoon.selected = False
persoon.absent = True
persoon.save()

Auditor.audit(persoon.first_name, persoon.last_name, 'absent', request.user)

Corvee.renew_list(requery_present_members=False)


def punishment(request: HttpRequest, pk: str):
persoon = Persoon.objects.get(pk=pk)
persoon.latest = timezone.make_aware(datetime(1900, 1, 1, 0, 0, 0))
persoon.selected = False
persoon.save()

Auditor.audit(persoon.first_name, persoon.last_name, 'punishment', request.user)
47 changes: 6 additions & 41 deletions corvee/src/views.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,18 @@
import uuid
from datetime import datetime

from django.conf import settings
from django.contrib.auth import logout, login as auth_login
from django.contrib.auth.models import User
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden
from django.shortcuts import reverse
from django.utils import timezone
from django.views.generic.edit import View
from django.views.generic.list import ListView
from requests_oauthlib import OAuth2Session

from corvee.src.corvee import Corvee
from corvee.src.mixins import PermissionRequiredMixin
from corvee.src.models import Persoon, AuditLog


class Auditor:

@staticmethod
def audit(first_name, last_name, action, user):
audit = AuditLog()
audit.first_name = first_name
audit.last_name = last_name
audit.datetime = timezone.now().date()
audit.action = action
audit.performed_by = f"{user.first_name} {user.last_name}"
audit.save()
from corvee.src.models import Persoon
from corvee.src.utils import acknowledge, insufficient, punishment, absent


class LoginView(View):
Expand Down Expand Up @@ -117,50 +103,29 @@ def get_context_data(self, *, object_list=None, **kwargs):

class Acknowledge(PermissionRequiredMixin, View):
def get(self, request, *args, **kwargs):
persoon = Persoon.objects.get(pk=self.kwargs.get('pk'))
persoon.latest = timezone.now()
persoon.selected = False
persoon.save()

Auditor.audit(persoon.first_name, persoon.last_name, 'acknowledged', request.user)
acknowledge(request, self.kwargs.get('pk'))

return HttpResponseRedirect(reverse('main'))


class Insufficient(PermissionRequiredMixin, View):
def get(self, request, *args, **kwargs):
persoon = Persoon.objects.get(pk=self.kwargs.get('pk'))
persoon.selected = False
persoon.save()

Auditor.audit(persoon.first_name, persoon.last_name, 'insufficient', request.user)
insufficient(request, self.kwargs.get('pk'))

return HttpResponseRedirect(reverse('main'))


class Punishment(PermissionRequiredMixin, View):
def get(self, request, *args, **kwargs):
persoon = Persoon.objects.get(pk=self.kwargs.get('pk'))
persoon.latest = timezone.make_aware(datetime(1900, 1, 1, 0, 0, 0))
persoon.selected = False
persoon.save()

Auditor.audit(persoon.first_name, persoon.last_name, 'punishment', request.user)
punishment(request, self.kwargs.get('pk'))

return HttpResponseRedirect(reverse('main'))


class Absent(PermissionRequiredMixin, View):

def get(self, request, *args, **kwargs):
persoon = Persoon.objects.get(pk=self.kwargs.get('pk'))
persoon.selected = False
persoon.absent = True
persoon.save()

Auditor.audit(persoon.first_name, persoon.last_name, 'absent', request.user)

Corvee.renew_list(requery_present_members=False)
absent(request, self.kwargs.get('pk'))

return HttpResponseRedirect(reverse('main'))

Expand Down
8 changes: 7 additions & 1 deletion corvee/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@
path('absent/<int:pk>/', views.Absent.as_view(), name='absent'),
path('renew/', views.Renew.as_view(), name='renew'),
path('logoff/', views.LogoffView.as_view(), name='logoff'),
path('api/v1/selected', api.SelectedV1.as_view(), name='selected'),
path('api/v1/selected', api.SelectedV1.as_view(), name='selected_api'),
path('api/v1/status', api.StatusV1.as_view(), name='status_api'),
path('api/v1/renew', api.RenewV1.as_view(), name='renew_api'),
path('api/v1/ack/<int:pk>', api.AcknowledgeV1.as_view(), name='acknowledge_api'),
path('api/v1/insuff/<int:pk>', api.InsufficientV1.as_view(), name='insufficient_api'),
path('api/v1/punish/<int:pk>', api.PunishmentV1.as_view(), name='punishment_api'),
path('api/v1/absent/<int:pk>', api.AbsentV1.as_view(), name='absent_api'),
re_path(r'oauth/.*', views.LoginResponseView.as_view()),
path('', views.LoginView.as_view(), name='login')
]
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ oauthlib==3.2.2
pytz==2024.1
requests==2.32.3
requests-oauthlib==2.0.0
pyjwt==2.8.0
cryptography==42.0.8

0 comments on commit d8e343f

Please sign in to comment.