From 1d0222d9801a582c7c33049f13ee8f091ba3be65 Mon Sep 17 00:00:00 2001 From: coder-bot Date: Tue, 18 Aug 2026 14:39:29 +0000 Subject: [PATCH] WIP checkpoint: Enforce 18+ age gate at account registration --- backend/app/models/user.py | 14 +++++++++++++ backend/app/routers/auth.py | 40 +++++++++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/backend/app/models/user.py b/backend/app/models/user.py index b893418..8ce7f88 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -1,4 +1,6 @@ from datetime import date +from typing import Optional + from sqlalchemy import Column, Integer, String, Boolean, DateTime, Date from sqlalchemy.orm import relationship from sqlalchemy.sql import func @@ -21,3 +23,15 @@ class User(Base): sessions = relationship("Session", back_populates="user", cascade="all, delete-orphan") worlds = relationship("World", back_populates="owner") assets = relationship("Asset", back_populates="owner") + + @property + def is_adult(self) -> bool: + """Return True if the user is 18 or older based on date_of_birth.""" + if self.date_of_birth is None: + return False + today = date.today() + age = today.year - self.date_of_birth.year + # Adjust if birthday hasn't occurred yet this year + if (today.month, today.day) < (self.date_of_birth.month, self.date_of_birth.day): + age -= 1 + return age >= 18 diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 5059174..380e29c 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -4,7 +4,7 @@ from datetime import datetime, timedelta, timezone from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm -from pydantic import BaseModel, EmailStr +from pydantic import BaseModel, EmailStr, field_validator from jose import jwt from passlib.context import CryptContext @@ -32,12 +32,24 @@ class RegisterRequest(BaseModel): username: str email: EmailStr password: str + date_of_birth: str # ISO format date string (YYYY-MM-DD) + + @field_validator("date_of_birth") + @classmethod + def validate_date_of_birth(cls, v: str) -> str: + """Validate that the date string is a valid date in YYYY-MM-DD format.""" + try: + datetime.strptime(v, "%Y-%m-%d") + except ValueError: + raise ValueError("date_of_birth must be in YYYY-MM-DD format") + return v class UserResponse(BaseModel): id: int username: str email: str + date_of_birth: str | None = None is_active: bool created_at: datetime | None = None @@ -65,11 +77,34 @@ def hash_password(password: str) -> str: return pwd_context.hash(password) +def calculate_age(birth_date: datetime) -> int: + """Calculate age in years from a birth date.""" + today = datetime.now(timezone.utc).date() + age = today.year - birth_date.year + # Adjust if birthday hasn't occurred yet this year + if (today.month, today.day) < (birth_date.month, birth_date.day): + age -= 1 + return age + + # --- Endpoints --- @router.post("/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED) async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)): - """Register a new user and return an access token.""" + """Register a new user and return an access token. + + Requires date_of_birth and rejects users under 18. + """ + # Parse and validate date_of_birth + dob = datetime.strptime(req.date_of_birth, "%Y-%m-%d").date() + age = calculate_age(dob) + + if age < 18: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Must be 18 or older to register (provided date of birth: {req.date_of_birth})", + ) + # Check if username already exists result = await db.execute(select(User).where(User.username == req.username)) if result.scalar_one_or_none(): @@ -90,6 +125,7 @@ async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)): username=req.username, email=req.email, hashed_password=hash_password(req.password), + date_of_birth=dob, ) db.add(user) await db.commit()