diff --git a/app/models/user.py b/app/models/user.py index 5094861..18a79db 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -9,7 +9,7 @@ import uuid from datetime import datetime -from sqlalchemy import Boolean, Column, DateTime, Integer, String, Text +from sqlalchemy import Boolean, Column, DateTime, Integer, Text from sqlalchemy.dialects.postgresql import UUID from app.models import Base @@ -44,7 +44,7 @@ class User(Base): user_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) - username = Column(String(50), unique=True, nullable=False) + username = Column(Text, unique=True, nullable=False) email_encrypted = Column(Text, nullable=False) email_hash = Column(Text, unique=True, nullable=False) diff --git a/app/routes/v1/endpoints/authentication.py b/app/routes/v1/endpoints/authentication.py index 7b99292..d0925d9 100644 --- a/app/routes/v1/endpoints/authentication.py +++ b/app/routes/v1/endpoints/authentication.py @@ -1,8 +1,8 @@ -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession -from app.models.user import User -from app.routes.v1.schemas.user.create import UserCreate +from app.routes.v1.schemas.user.register import UserRegister from app.utility.database import get_db from app.utility.security import ( encrypt_email, @@ -11,23 +11,59 @@ hash_password, hash_phone, ) +from app.utility.string_utils import sanitize_username router = APIRouter() @router.post("/register") -async def register(data: UserCreate, db: AsyncSession = Depends(get_db)): +async def register(data: UserRegister, db: AsyncSession = Depends(get_db)): """Endpoint for user registration.""" - user = User( - username=data.username, - email_encrypted=encrypt_email(data.email), - email_hash=hash_email(data.email), - password_hash=hash_password(data.password), - phone_encrypted=encrypt_phone(data.phone) if data.phone else None, - phone_hash=hash_phone(data.phone) if data.phone else None, - language_id=data.language_id, + username = sanitize_username(data.username) + email_encrypted = encrypt_email(data.email) + email_hash = hash_email(data.email) + password_hash = hash_password(data.password) + phone_encrypted = encrypt_phone(data.phone) if data.phone else None + phone_hash = hash_phone(data.phone) if data.phone else None + language_id = data.language_id + + # Check if user is available + result = await db.execute( + text( + """ + SELECT is_user_available(:username, :email_hash, :phone_hash) AS available + """ + ), + {"username": username, "email_hash": email_hash, "phone_hash": phone_hash}, + ) + available = result.scalar() + + if not available: + raise HTTPException(409, "Username, email, or phone already exists") + + await db.execute( + text( + """ + CALL register_user( + :username, + :email_encrypted, + :email_hash, + :password_hash, + :phone_encrypted, + :phone_hash, + :preferred_language_id + ) + """ + ), + { + "username": username, + "email_encrypted": email_encrypted, + "email_hash": email_hash, + "password_hash": password_hash, + "phone_encrypted": phone_encrypted, + "phone_hash": phone_hash, + "preferred_language_id": language_id, + }, ) - db.add(user) await db.commit() - await db.refresh(user) - return {"message": "User registered successfully", "user_id": str(user.user_id)} + return {"message": "User registered successfully", "username": username} diff --git a/app/routes/v1/schemas/user/create.py b/app/routes/v1/schemas/user/register.py similarity index 84% rename from app/routes/v1/schemas/user/create.py rename to app/routes/v1/schemas/user/register.py index ce54bb0..dd6228f 100644 --- a/app/routes/v1/schemas/user/create.py +++ b/app/routes/v1/schemas/user/register.py @@ -1,7 +1,7 @@ from pydantic import BaseModel, EmailStr -class UserCreate(BaseModel): +class UserRegister(BaseModel): username: str email: EmailStr password: str diff --git a/app/utility/string_utils.py b/app/utility/string_utils.py new file mode 100644 index 0000000..abd323b --- /dev/null +++ b/app/utility/string_utils.py @@ -0,0 +1,9 @@ +import re + + +def sanitize_username(username: str) -> str: + """ + Replace all characters not allowed by the database username constraint + (^[a-zA-Z0-9_]+$) with underscores. + """ + return re.sub(r"[^a-zA-Z0-9_]", "_", username)