Enforce a single birthday calendar per account (server-side)

- POST /local/calendars with is_birthday returns the existing birthday calendar
  if one exists (idempotent, no second created)
- PUT /local/calendars rejects (422) marking a second calendar as is_birthday

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-13 21:22:10 +02:00
parent 0ce77ccdf8
commit 29ddc5acbb

View File

@@ -211,6 +211,21 @@ def create_calendar(
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_user: models.User = Depends(get_current_user), current_user: models.User = Depends(get_current_user),
): ):
# Only ONE birthday calendar per account — hard server guarantee. If the
# user already has one, "create birthday calendar" is idempotent: return the
# existing one instead of creating a second.
if data.is_birthday:
existing = (
db.query(models.LocalCalendar)
.filter(
models.LocalCalendar.user_id == current_user.id,
models.LocalCalendar.is_birthday == True,
)
.first()
)
if existing is not None:
return _cal_dict(existing)
cal = models.LocalCalendar( cal = models.LocalCalendar(
user_id=current_user.id, user_id=current_user.id,
name=data.name, name=data.name,
@@ -251,6 +266,19 @@ def update_calendar(
if data.reminders_enabled is not None: if data.reminders_enabled is not None:
cal.reminders_enabled = data.reminders_enabled cal.reminders_enabled = data.reminders_enabled
if data.is_birthday is not None: if data.is_birthday is not None:
# Never let a second calendar be marked as the birthday calendar.
if data.is_birthday and not cal.is_birthday:
other = (
db.query(models.LocalCalendar)
.filter(
models.LocalCalendar.user_id == current_user.id,
models.LocalCalendar.is_birthday == True,
models.LocalCalendar.id != cal.id,
)
.first()
)
if other is not None:
raise HTTPException(422, "A birthday calendar already exists")
cal.is_birthday = data.is_birthday cal.is_birthday = data.is_birthday
if data.birthday_notify_days_before is not None: if data.birthday_notify_days_before is not None:
# -1 is the client's sentinel for "clear the reminder" (JSON has no way to # -1 is the client's sentinel for "clear the reminder" (JSON has no way to