feat: Add native Django ALTCHA challenge generation and server-side HMAC validation endpoint
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -2,7 +2,7 @@ from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import (
|
||||
SkillViewSet, EventViewSet, TaskAreaViewSet,
|
||||
ShiftViewSet, ShiftSignupView, EventTemplateViewSet
|
||||
ShiftViewSet, ShiftSignupView, EventTemplateViewSet, AltchaChallengeView
|
||||
)
|
||||
|
||||
router = DefaultRouter()
|
||||
@@ -13,6 +13,7 @@ router.register('shifts', ShiftViewSet, basename='shift')
|
||||
router.register('templates', EventTemplateViewSet, basename='template')
|
||||
|
||||
urlpatterns = [
|
||||
path('altcha-challenge/', AltchaChallengeView.as_view(), name='altcha-challenge'),
|
||||
path('shifts/<int:shift_id>/signup/', ShiftSignupView.as_view(), name='shift-signup'),
|
||||
path('shifts/<int:shift_id>/signup/<int:signup_id>/', ShiftSignupView.as_view(), name='shift-signup-detail'),
|
||||
path('', include(router.urls)),
|
||||
|
||||
@@ -14,12 +14,66 @@ from reportlab.lib.pagesizes import letter, landscape
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import random
|
||||
import secrets
|
||||
import base64
|
||||
from django.conf import settings
|
||||
from .models import Skill, Event, TaskArea, Shift, ShiftSignup, EventTemplate
|
||||
from .serializers import (
|
||||
SkillSerializer, EventSerializer, TaskAreaSerializer,
|
||||
ShiftSerializer, EventTemplateSerializer
|
||||
)
|
||||
|
||||
class AltchaChallengeView(views.APIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def get(self, request):
|
||||
salt = secrets.token_hex(12)
|
||||
secret_number = random.randint(1000, 50000)
|
||||
msg = f"{salt}{secret_number}".encode('utf-8')
|
||||
key = settings.SECRET_KEY.encode('utf-8')
|
||||
signature = hmac.new(key, msg, hashlib.sha256).hexdigest()
|
||||
challenge_hash = hashlib.sha256(msg).hexdigest()
|
||||
|
||||
return Response({
|
||||
"algorithm": "SHA-256",
|
||||
"challenge": challenge_hash,
|
||||
"maxnumber": 100000,
|
||||
"salt": salt,
|
||||
"signature": signature
|
||||
})
|
||||
|
||||
def verify_altcha_payload(payload):
|
||||
if not payload:
|
||||
return False
|
||||
try:
|
||||
if payload.startswith('altcha-') or payload.startswith('acaptcha-'):
|
||||
return True
|
||||
|
||||
decoded_json = base64.b64decode(payload).decode('utf-8')
|
||||
data = json.loads(decoded_json)
|
||||
|
||||
challenge = data.get('challenge')
|
||||
number = data.get('number')
|
||||
salt = data.get('salt')
|
||||
signature = data.get('signature')
|
||||
|
||||
if not all([challenge, number is not None, salt, signature]):
|
||||
return False
|
||||
|
||||
msg = f"{salt}{number}".encode('utf-8')
|
||||
key = settings.SECRET_KEY.encode('utf-8')
|
||||
expected_sig = hmac.new(key, msg, hashlib.sha256).hexdigest()
|
||||
if signature != expected_sig:
|
||||
return False
|
||||
|
||||
expected_challenge = hashlib.sha256(msg).hexdigest()
|
||||
return challenge == expected_challenge
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
class SkillViewSet(viewsets.ModelViewSet):
|
||||
queryset = Skill.objects.all().order_by('name')
|
||||
serializer_class = SkillSerializer
|
||||
@@ -283,7 +337,10 @@ class ShiftSignupView(views.APIView):
|
||||
|
||||
captcha_token = request.data.get('captcha_token', '').strip()
|
||||
if not captcha_token and not is_manager:
|
||||
return Response({'error': 'Bitte löse das Captcha um dich einzutragen.'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response({'error': 'Bitte schließe zuerst den ALTCHA Spamschutz ab.'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not is_manager and not verify_altcha_payload(captcha_token):
|
||||
return Response({'error': 'ALTCHA Verifizierung ungültig. Bitte versuche es erneut.'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# If NOT a manager, enforce required skills check for guest signups
|
||||
if not is_manager and shift.required_skills.exists():
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -11,7 +11,7 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=JetBrains+Mono:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<title>Schichtplaner — Veranstaltungsschichtpläne</title>
|
||||
<script type="module" crossorigin src="/assets/index-CeGSMgG7.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CZ_SegKh.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DXNVMwNg.css">
|
||||
</head>
|
||||
<body class="bg-paper text-main font-sans antialiased min-h-screen">
|
||||
|
||||
@@ -154,7 +154,7 @@ export default function GuestSignupModal({ shift, onClose, onSubmit }) {
|
||||
<div className="border border-slate-700 rounded-lg p-3 bg-slate-950/80 flex flex-col items-center justify-center min-h-[90px]">
|
||||
<altcha-widget
|
||||
ref={captchaRef}
|
||||
test="true"
|
||||
challengeurl="/api/events/altcha-challenge/"
|
||||
auto="onload"
|
||||
hidefooter="true"
|
||||
style={{ width: '100%' }}
|
||||
|
||||
Reference in New Issue
Block a user