Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
43cb44d
Create guest data summary endpoint
jzgom067 Jul 3, 2026
5fe2251
Create frontend mapping for guest data summary
jzgom067 Jul 3, 2026
2642833
Remove event creation participation filter
jzgom067 Jul 3, 2026
7b3f19a
Create initial guest import page
jzgom067 Jul 3, 2026
c5fc9c2
Change summary endpoint to give full data
jzgom067 Jul 3, 2026
2893a33
Add use client to client components
jzgom067 Jul 4, 2026
1f4ce8a
Add conflict indicator and information
jzgom067 Jul 4, 2026
df04288
Adjust guest data found text
jzgom067 Jul 4, 2026
3fc4821
Add mobile drawer conversion to FormDialog
jzgom067 Jul 4, 2026
9cd3eef
Remove unnecessary asNestedDrawer props
jzgom067 Jul 4, 2026
c6ffd9b
Add submitDisabled prop to FormDialog
jzgom067 Jul 4, 2026
d38882a
Update conflict banner
jzgom067 Jul 6, 2026
5e6967c
Create guest import dialog
jzgom067 Jul 6, 2026
53fab72
Add conflict resolution logic
jzgom067 Jul 6, 2026
d0a3a04
Simplify styling for conflict option
jzgom067 Jul 6, 2026
b39b118
Convert guest import review to page
jzgom067 Jul 7, 2026
820bee8
Overhaul guest data review layout
jzgom067 Jul 7, 2026
4dc3bc3
Update submission deletion message
jzgom067 Jul 7, 2026
1d8dde7
Add conflict banner to review page
jzgom067 Jul 7, 2026
0d0cf39
Create serializers file
jzgom067 Jul 7, 2026
54e1b71
Fix guest data endpoint response
jzgom067 Jul 7, 2026
ecc296c
Re-add guest data summary endpoint
jzgom067 Jul 7, 2026
51cb64b
Convert settings page to use summary
jzgom067 Jul 7, 2026
1bad28a
Simplify error checking
jzgom067 Jul 7, 2026
8b744e6
Simplify conflict resolution logic
jzgom067 Jul 7, 2026
69cf59a
Add none display for import data
jzgom067 Jul 7, 2026
1364989
Remove drawer nesting on conflict selector
jzgom067 Jul 7, 2026
7f4062d
Create import data endpoint
jzgom067 Jul 8, 2026
c8ee72a
Add backend functionality to guest review page
jzgom067 Jul 8, 2026
edf832f
Add tooltip to disabled submit button
jzgom067 Jul 8, 2026
4f6ef02
Move guest import to separate route
jzgom067 Jul 8, 2026
1e0732d
Add support for children in MessagePage
jzgom067 Jul 8, 2026
866a48e
Move guest data summary to component
jzgom067 Jul 8, 2026
7a457de
Create guest import login page and redirect
jzgom067 Jul 8, 2026
2636827
Create client page for login guest import
jzgom067 Jul 8, 2026
dc40d92
Add don't show again toggle and local storage
jzgom067 Jul 8, 2026
cdd7f32
Add don't show again to login redirect
jzgom067 Jul 8, 2026
686e4bb
Create loading page for import review
jzgom067 Jul 8, 2026
2c697f0
Adjust stroke width to match bold text
jzgom067 Jul 9, 2026
2bbc633
Add padding to login import page
jzgom067 Jul 9, 2026
ee7a757
Fix button activation area
jzgom067 Jul 9, 2026
089189a
Add guest-import to reserved keywords
jzgom067 Jul 9, 2026
9ee37fc
Add metadata to guest import pages
jzgom067 Jul 9, 2026
a9fb9cc
Rename and reroute guest import review page
jzgom067 Jul 9, 2026
2c2d5aa
Update dashboard guest banner text
jzgom067 Jul 9, 2026
4a708a6
Add public ID to UserEvent
jzgom067 Jul 9, 2026
3f317a0
Add UUIDField support to docs
jzgom067 Jul 9, 2026
cb5c955
Add public ID and remove null URL code filter
jzgom067 Jul 9, 2026
594d3d8
Add public ID to endpoint type mappings
jzgom067 Jul 9, 2026
13edb89
Fix UUID comparison
jzgom067 Jul 9, 2026
a759555
Add support for public ID and null URL code
jzgom067 Jul 9, 2026
c5d21d9
Add error handling for null URL code
jzgom067 Jul 9, 2026
e27390e
Add logging to guest import endpoints
jzgom067 Jul 9, 2026
e5b781e
Update guest mode banner text
jzgom067 Jul 9, 2026
35cccd4
Update guest summary wording
jzgom067 Jul 9, 2026
1397ca6
Add className support to SelectorDrawer
jzgom067 Jul 9, 2026
b85dcc0
Change mobile settings nav to dropdown
jzgom067 Jul 9, 2026
fc11d97
Update mobile loading skeleton
jzgom067 Jul 9, 2026
ae0d963
Update endpoint comment with errors
jzgom067 Jul 10, 2026
de5ddd4
Move session logic outside of try block
jzgom067 Jul 10, 2026
1fe1e79
Add select related call
jzgom067 Jul 10, 2026
ee9cb7b
Optimize conflict assignment
jzgom067 Jul 10, 2026
2fc7c97
Update docstring for import endpoint
jzgom067 Jul 10, 2026
e6244b5
Guard local storage read on server
jzgom067 Jul 10, 2026
493d660
Add unchecked don't show again logic
jzgom067 Jul 10, 2026
ee47898
Add aria-current to settings nav
jzgom067 Jul 10, 2026
9ffe02b
Create require_captcha decorator
jzgom067 Jul 13, 2026
ece5604
Add DEBUG bypass for CAPTCHA
jzgom067 Jul 13, 2026
83e6ad0
Add captcha_required field to docs
jzgom067 Jul 13, 2026
8668a89
Add UUIDField support to docs
jzgom067 Jul 13, 2026
c24d6f2
Rename checked CAPTCHA body property
jzgom067 Jul 13, 2026
9398d56
Add CAPTCHA requirement to relevant endpoints
jzgom067 Jul 13, 2026
cf4b365
Add turnstile testing keys
jzgom067 Jul 13, 2026
e7d4903
Change error message on backend failure
jzgom067 Jul 14, 2026
3d38601
Create CAPTCHA component
jzgom067 Jul 14, 2026
ee97e79
Add CAPTCHA failed property to ApiErrorData
jzgom067 Jul 14, 2026
e37c8b3
Add CAPTCHA to register page
jzgom067 Jul 14, 2026
ea6481c
Merge branch 'guest-account-import' into captcha
jzgom067 Jul 14, 2026
dfff712
Add text-left to CAPTCHA banner
jzgom067 Jul 14, 2026
300d801
Add debugShowBanner prop
jzgom067 Jul 14, 2026
91e5306
Remove CAPTCHA from resend register email
jzgom067 Jul 14, 2026
d0e3205
Add CAPTCHA to forgot password
jzgom067 Jul 14, 2026
66dab53
Add CAPTCHA to new event editor
jzgom067 Jul 14, 2026
5c36ebf
Add CAPTCHA to painting page
jzgom067 Jul 14, 2026
7549cf2
Add dynamic widget theming
jzgom067 Jul 14, 2026
6097cad
Loosen account creation rate limits
jzgom067 Jul 14, 2026
546d449
Add onInitError and submit button disabling
jzgom067 Jul 14, 2026
236d502
Remove debugShowBanner property
jzgom067 Jul 14, 2026
a8e2ee7
Add non-dict JSON handling
jzgom067 Jul 14, 2026
2b6212f
Fix token reset after recovery
jzgom067 Jul 14, 2026
5b902b4
Add strict boolean casting
jzgom067 Jul 14, 2026
3cd974b
Implement spread operator for safer type checking
jzgom067 Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ AWS_SES_REGION_NAME=region_name
AWS_SES_REGION_ENDPOINT=region_endpoint
DEFAULT_FROM_EMAIL=email

# Cloudflare Turnstile secret key for CAPTCHA verification. This placeholder is the test
# key provided by Cloudflare which always returns success on verification for development.
CF_TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA

# The URL of the main site, used for generating links (no trailing slash)
BASE_URL=https://example.com

Expand Down
3 changes: 3 additions & 0 deletions backend/api/auth/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from api.decorators import (
api_endpoint,
require_account_auth,
require_captcha,
validate_json_input,
validate_output,
)
Expand Down Expand Up @@ -51,6 +52,7 @@


@api_endpoint("POST")
@require_captcha
@validate_json_input(RegisterAccountSerializer)
@validate_output(MessageOutputSerializer)
def register(request):
Expand Down Expand Up @@ -307,6 +309,7 @@ def check_account_auth(request):


@api_endpoint("POST")
@require_captcha
@validate_json_input(EmailSerializer)
@validate_output(MessageOutputSerializer)
def start_password_reset(request):
Expand Down
2 changes: 2 additions & 0 deletions backend/api/availability/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
api_endpoint,
check_auth,
require_auth,
require_captcha,
validate_json_input,
validate_output,
validate_query_param_input,
Expand Down Expand Up @@ -46,6 +47,7 @@ class InvalidTimeslotError(Exception):


@api_endpoint("POST")
@require_captcha
@require_auth
@validate_json_input(AvailabilityAddSerializer)
@validate_output(MessageOutputSerializer)
Expand Down
51 changes: 51 additions & 0 deletions backend/api/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
import uuid

import requests
from django.db import DatabaseError, transaction
from rest_framework import serializers
from rest_framework.decorators import api_view
Expand All @@ -11,6 +12,9 @@
from api.models import UserAccount, UserSession
from api.settings import (
ACCOUNT_COOKIE_NAME,
CF_TURNSTILE_SECRET_KEY,
CF_TURNSTILE_VERIFY_URL,
DEBUG,
GENERIC_ERR_RESPONSE,
GUEST_COOKIE_NAME,
ThrottleScopes,
Expand All @@ -19,6 +23,7 @@
RateLimitError,
check_rate_limit,
delete_session_cookie,
get_client_ip_address,
get_metadata,
get_session,
set_session_cookie,
Expand Down Expand Up @@ -517,3 +522,49 @@ def decorator(func):
return func

return decorator


def require_captcha(func):
"""
A decorator to check if the request has a valid Cloudflare Turnstile token.

If the token is invalid, an error response will be returned.
"""

@functools.wraps(func)
def wrapper(request, *args, **kwargs):
client_token = (
request.data.get("captcha_token")
if isinstance(request.data, dict)
else None
)
if not client_token:
return Response(
{"error": {"general": ["CAPTCHA token is required."]}}, status=400
)

# Assemble payload, including client IP if possible for extra security
payload = {
"secret": CF_TURNSTILE_SECRET_KEY,
"response": client_token,
}
if client_ip := get_client_ip_address(request):
payload["remoteip"] = client_ip

try:
# Verify the token with Cloudflare Turnstile
response = requests.post(CF_TURNSTILE_VERIFY_URL, data=payload, timeout=5)
response.raise_for_status()
result = response.json()
if not result.get("success"):
return Response(
{"error": {"general": ["CAPTCHA verification failed."]}}, status=400
)
except Exception as e:
logger.warning("CAPTCHA verification had a problem: %s", e)
return GENERIC_ERR_RESPONSE

return func(request, *args, **kwargs)

get_metadata(wrapper).captcha_required = True
return wrapper
2 changes: 2 additions & 0 deletions backend/api/docs/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ def get_readable_field_name(field_name):
return "string"
case "RegexField":
return "string"
case "UUIDField":
return "string"
case _:
return "object"

Expand Down
2 changes: 2 additions & 0 deletions backend/api/docs/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class EndpointSerializer(serializers.Serializer):
output_format = serializers.JSONField(allow_null=True)
min_auth_required = serializers.CharField(allow_null=True)
rate_limit = serializers.CharField(allow_null=True)
captcha_required = serializers.BooleanField()


class DocsSerializer(serializers.Serializer):
Expand Down Expand Up @@ -49,6 +50,7 @@ def get_docs(request):
),
"min_auth_required": metadata.min_auth_required,
"rate_limit": metadata.rate_limit,
"captcha_required": metadata.captcha_required,
}
)
return Response({"endpoints": endpoints})
1 change: 1 addition & 0 deletions backend/api/event/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def check_custom_code(code):
"api",
"dashboard",
"forgot-password",
"guest-import",
"login",
"new-event",
"settings",
Expand Down
3 changes: 3 additions & 0 deletions backend/api/event/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
api_endpoint,
check_auth,
require_auth,
require_captcha,
sse_endpoint,
validate_json_input,
validate_output,
Expand Down Expand Up @@ -75,6 +76,7 @@


@api_endpoint("POST")
@require_captcha
@require_auth
@validate_json_input(DateEventCreateSerializer)
@validate_output(EventCodeSerializer)
Expand Down Expand Up @@ -138,6 +140,7 @@ def create_date_event(request):


@api_endpoint("POST")
@require_captcha
@require_auth
@validate_json_input(WeekEventCreateSerializer)
@validate_output(EventCodeSerializer)
Expand Down
39 changes: 39 additions & 0 deletions backend/api/guest_import/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from rest_framework import serializers


class GuestDataSummarySerializer(serializers.Serializer):
created_events = serializers.IntegerField()
participated_events = serializers.IntegerField()


class GuestEventSerializer(serializers.Serializer):
url_code = serializers.CharField(allow_null=True)
public_id = serializers.UUIDField()
title = serializers.CharField()


class GuestParticipationSerializer(serializers.Serializer):
url_code = serializers.CharField(allow_null=True)
public_id = serializers.UUIDField()
title = serializers.CharField()
guest_display_name = serializers.CharField()
account_display_name = serializers.CharField(allow_null=True)


class GuestDataSerializer(serializers.Serializer):
created_events = serializers.ListField(
child=GuestEventSerializer(),
allow_empty=True,
)
participated_events = serializers.ListField(
child=GuestParticipationSerializer(),
allow_empty=True,
)


class GuestImportDataSerializer(serializers.Serializer):
availability_choices = serializers.DictField(
required=True,
child=serializers.ChoiceField(required=True, choices=["guest", "account"]),
allow_empty=True,
)
9 changes: 9 additions & 0 deletions backend/api/guest_import/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.urls import path

from . import views

urlpatterns = [
path("get-summary/", views.get_summary),
path("get-data/", views.get_data),
path("import-data/", views.import_data),
]
38 changes: 38 additions & 0 deletions backend/api/guest_import/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import logging

from django.db import transaction

from api.models import UserSession
from api.settings import GUEST_COOKIE_NAME
from api.utils import get_session, set_session_cookie, update_session_metadata

logger = logging.getLogger("api")


def get_guest_account(request, response):
"""
Returns the guest account associated with the request, refreshing the session token
cookie and updating the metadata on the session.

If there is no guest account, this will return `None`.

This does NOT make the guest user available on the `request` object.
"""
guest_token = request.COOKIES.get(GUEST_COOKIE_NAME)

if guest_token:
logger.debug("Guest session token: %s", guest_token)
try:
with transaction.atomic():
session = get_session(guest_token)
if not session:
raise UserSession.DoesNotExist

update_session_metadata(request, session)
except UserSession.DoesNotExist:
logger.info("Guest session expired.")
else:
set_session_cookie(response, GUEST_COOKIE_NAME, session.session_token, True)
return session.user_account

return None
Loading
Loading