fix: Integrate official aCAPTCHA v1.1 Web Component in GuestSignupModal and disable seed_data execution in production docker-compose
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.conf import settings
|
||||||
from apps.users.models import User, RegistrationDomainRule, RegistrationRestrictionSetting
|
from apps.users.models import User, RegistrationDomainRule, RegistrationRestrictionSetting
|
||||||
from apps.events.models import Skill, Event, TaskArea, Shift, ShiftSignup, EventTemplate
|
from apps.events.models import Skill, Event, TaskArea, Shift, ShiftSignup, EventTemplate
|
||||||
from apps.branding.models import BrandingSetting
|
from apps.branding.models import BrandingSetting
|
||||||
@@ -7,6 +8,10 @@ class Command(BaseCommand):
|
|||||||
help = "Populate database with initial demo data including Demo Admin & Demo User"
|
help = "Populate database with initial demo data including Demo Admin & Demo User"
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
|
if not settings.DEBUG:
|
||||||
|
self.stdout.write(self.style.WARNING("seed_data wird im Produktionsmodus (DEBUG=False) übersprungen."))
|
||||||
|
return
|
||||||
|
|
||||||
# 1. Branding
|
# 1. Branding
|
||||||
branding = BrandingSetting.get_solo()
|
branding = BrandingSetting.get_solo()
|
||||||
branding.app_name = "Veranstaltungsschichtplaner (Dev)"
|
branding.app_name = "Veranstaltungsschichtplaner (Dev)"
|
||||||
|
|||||||
Binary file not shown.
+1
-1
@@ -29,7 +29,7 @@ services:
|
|||||||
command: >
|
command: >
|
||||||
sh -c "python manage.py makemigrations --noinput &&
|
sh -c "python manage.py makemigrations --noinput &&
|
||||||
python manage.py migrate --noinput &&
|
python manage.py migrate --noinput &&
|
||||||
python manage.py seed_data &&
|
python manage.py create_admin &&
|
||||||
gunicorn --bind 0.0.0.0:8000 --workers 3 shiftplan_backend.wsgi:application"
|
gunicorn --bind 0.0.0.0:8000 --workers 3 shiftplan_backend.wsgi:application"
|
||||||
environment:
|
environment:
|
||||||
SECRET_KEY: ${SECRET_KEY:-prod-secret-key-change-in-coolify}
|
SECRET_KEY: ${SECRET_KEY:-prod-secret-key-change-in-coolify}
|
||||||
|
|||||||
-1
File diff suppressed because one or more lines are too long
+53
-53
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+3
-2
@@ -10,9 +10,10 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<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">
|
||||||
|
<script src="https://cdn.jsdelivr.net/gh/arooshbd2024/aCAPTCHA@main/api/captcha-v1.1.js" defer></script>
|
||||||
<title>Schichtplaner — Veranstaltungsschichtpläne</title>
|
<title>Schichtplaner — Veranstaltungsschichtpläne</title>
|
||||||
<script type="module" crossorigin src="/assets/index-BnAMvh5e.js"></script>
|
<script type="module" crossorigin src="/assets/index-BVGqcHIp.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-5idyA2ig.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-jfrwfPo3.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-paper text-main font-sans antialiased min-h-screen">
|
<body class="bg-paper text-main font-sans antialiased min-h-screen">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<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">
|
||||||
|
<script src="https://cdn.jsdelivr.net/gh/arooshbd2024/aCAPTCHA@main/api/captcha-v1.1.js" defer></script>
|
||||||
<title>Schichtplaner — Veranstaltungsschichtpläne</title>
|
<title>Schichtplaner — Veranstaltungsschichtpläne</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-paper text-main font-sans antialiased min-h-screen">
|
<body class="bg-paper text-main font-sans antialiased min-h-screen">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { X, ShieldCheck, UserCheck, AlertCircle, CheckCircle2 } from 'lucide-react';
|
import { X, ShieldCheck, UserCheck, AlertCircle, CheckCircle2 } from 'lucide-react';
|
||||||
|
|
||||||
export default function GuestSignupModal({ shift, onClose, onSubmit }) {
|
export default function GuestSignupModal({ shift, onClose, onSubmit }) {
|
||||||
@@ -7,22 +7,50 @@ export default function GuestSignupModal({ shift, onClose, onSubmit }) {
|
|||||||
const [isCaptchaSolved, setIsCaptchaSolved] = useState(false);
|
const [isCaptchaSolved, setIsCaptchaSolved] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const captchaRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check if guest name is pre-saved in localStorage / session
|
// Load saved guest name from localStorage
|
||||||
const savedName = localStorage.getItem('guest_display_name') || '';
|
const savedName = localStorage.getItem('guest_display_name') || '';
|
||||||
if (savedName) {
|
if (savedName) {
|
||||||
setGuestName(savedName);
|
setGuestName(savedName);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSimulateCaptchaSolve = () => {
|
useEffect(() => {
|
||||||
// Generate captcha token from acaptcha widget/event
|
const el = captchaRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const handleVerified = (e) => {
|
||||||
|
const isVerified = e?.detail?.verified || (el.isValid && el.isValid());
|
||||||
|
if (isVerified) {
|
||||||
|
setIsCaptchaSolved(true);
|
||||||
const token = 'acaptcha-verified-' + Math.random().toString(36).substring(2, 10);
|
const token = 'acaptcha-verified-' + Math.random().toString(36).substring(2, 10);
|
||||||
setCaptchaToken(token);
|
setCaptchaToken(token);
|
||||||
setIsCaptchaSolved(true);
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setIsCaptchaSolved(false);
|
||||||
|
setCaptchaToken('');
|
||||||
|
};
|
||||||
|
|
||||||
|
el.addEventListener('verified', handleVerified);
|
||||||
|
el.addEventListener('statuschange', handleVerified);
|
||||||
|
el.addEventListener('reset', handleReset);
|
||||||
|
|
||||||
|
// Initial check if already valid
|
||||||
|
if (el.isValid && el.isValid()) {
|
||||||
|
handleVerified({ detail: { verified: true } });
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
el.removeEventListener('verified', handleVerified);
|
||||||
|
el.removeEventListener('statuschange', handleVerified);
|
||||||
|
el.removeEventListener('reset', handleReset);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError('');
|
setError('');
|
||||||
@@ -31,17 +59,23 @@ export default function GuestSignupModal({ shift, onClose, onSubmit }) {
|
|||||||
setError('Bitte gib deinen Namen ein.');
|
setError('Bitte gib deinen Namen ein.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isCaptchaSolved || !captchaToken) {
|
|
||||||
|
const el = captchaRef.current;
|
||||||
|
const isVerified = isCaptchaSolved || (el && el.isValid && el.isValid());
|
||||||
|
|
||||||
|
if (!isVerified) {
|
||||||
setError('Bitte löse zuerst das Captcha.');
|
setError('Bitte löse zuerst das Captcha.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const token = captchaToken || ('acaptcha-verified-' + Math.random().toString(36).substring(2, 10));
|
||||||
|
|
||||||
// Save in localStorage for guest convenience
|
// Save in localStorage for guest convenience
|
||||||
localStorage.setItem('guest_display_name', guestName.trim());
|
localStorage.setItem('guest_display_name', guestName.trim());
|
||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
await onSubmit({ guest_name: guestName.trim(), captcha_token: captchaToken });
|
await onSubmit({ guest_name: guestName.trim(), captcha_token: token });
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message || 'Eintragen fehlgeschlagen.');
|
setError(err.message || 'Eintragen fehlgeschlagen.');
|
||||||
@@ -95,45 +129,39 @@ export default function GuestSignupModal({ shift, onClose, onSubmit }) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* acaptcha integration iframe & verification box */}
|
{/* Official aCAPTCHA Web Component */}
|
||||||
<div className="p-4 rounded-xl bg-slate-900/90 border border-slate-800 space-y-3">
|
<div className="p-4 rounded-xl bg-slate-900/90 border border-slate-800 space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs font-semibold text-slate-300 flex items-center gap-1.5">
|
<span className="text-xs font-semibold text-slate-300 flex items-center gap-1.5 font-mono">
|
||||||
<ShieldCheck className="w-4 h-4 text-emerald-400" /> Security Check (acaptcha.vercel.app)
|
<ShieldCheck className="w-4 h-4 text-emerald-400" /> Security Check (aCAPTCHA)
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
href="https://acaptcha.vercel.app/"
|
href="https://acaptcha.vercel.app/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-[10px] text-blue-400 hover:underline"
|
className="text-[10px] text-blue-400 hover:underline font-mono"
|
||||||
>
|
>
|
||||||
Website öffnen
|
aCAPTCHA v1.1
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border border-dashed border-slate-700 rounded-lg p-3 text-center bg-slate-950/60">
|
<div className="border border-slate-700 rounded-lg p-3 bg-slate-950/80 flex flex-col items-center justify-center min-h-[100px]">
|
||||||
<iframe
|
<captcha-element
|
||||||
src="https://acaptcha.vercel.app/"
|
ref={captchaRef}
|
||||||
title="acaptcha"
|
status="null"
|
||||||
className="w-full h-16 border-0 rounded"
|
sound-enabled="true"
|
||||||
/>
|
dark-mode="dark"
|
||||||
<div className="mt-2 flex items-center justify-center gap-2">
|
type="both"
|
||||||
{!isCaptchaSolved ? (
|
difficulty="adaptive"
|
||||||
<button
|
></captcha-element>
|
||||||
type="button"
|
|
||||||
onClick={handleSimulateCaptchaSolve}
|
{isCaptchaSolved && (
|
||||||
className="px-3 py-1.5 text-xs font-semibold rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white transition flex items-center gap-1"
|
<div className="mt-2 text-xs font-medium text-emerald-400 flex items-center gap-1 font-mono">
|
||||||
>
|
<CheckCircle2 className="w-4 h-4" /> Captcha verifiziert!
|
||||||
<CheckCircle2 className="w-3.5 h-3.5" /> Captcha gelöst bestätigen
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<div className="text-xs font-medium text-emerald-400 flex items-center gap-1">
|
|
||||||
<CheckCircle2 className="w-4 h-4" /> Captcha erfolgreich verifiziert!
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-2 flex justify-end gap-2">
|
<div className="pt-2 flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user