- Frontend: Next.js 14 with TypeScript - Backend: FastAPI with SQLAlchemy - Agent: Carmodoo sync agent - Deployment: Docker Compose based staging/production setup - Scripts: Automated deployment with rollback support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
232 lines
6.7 KiB
Python
232 lines
6.7 KiB
Python
"""
|
|
Verification API Endpoints
|
|
Handles email and phone verification for users
|
|
"""
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.orm import Session
|
|
from pydantic import BaseModel, EmailStr
|
|
from typing import Optional
|
|
|
|
from ..database import get_db
|
|
from ..models import User
|
|
from ..services import verification_service
|
|
from .auth import get_current_user, get_current_user_optional
|
|
|
|
router = APIRouter(prefix="/verification", tags=["verification"])
|
|
|
|
|
|
# Request/Response schemas
|
|
class SendEmailCodeRequest(BaseModel):
|
|
email: EmailStr
|
|
language: str = "en"
|
|
|
|
|
|
class SendPhoneCodeRequest(BaseModel):
|
|
phone: str
|
|
language: str = "en"
|
|
|
|
|
|
class VerifyCodeRequest(BaseModel):
|
|
code: str
|
|
email: Optional[str] = None
|
|
phone: Optional[str] = None
|
|
|
|
|
|
class VerificationResponse(BaseModel):
|
|
success: bool
|
|
message: str
|
|
|
|
|
|
class VerificationStatusResponse(BaseModel):
|
|
email_verified: bool
|
|
phone_verified: bool
|
|
email: Optional[str] = None
|
|
phone: Optional[str] = None
|
|
|
|
|
|
# Email Verification Endpoints
|
|
@router.post("/email/send", response_model=VerificationResponse)
|
|
async def send_email_code(
|
|
request: SendEmailCodeRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user_optional)
|
|
):
|
|
"""Send email verification code"""
|
|
user_id = current_user.id if current_user else None
|
|
|
|
# If user is logged in, only allow sending to their email
|
|
if current_user and current_user.email != request.email:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="You can only verify your own email address"
|
|
)
|
|
|
|
success, message = await verification_service.send_email_verification(
|
|
db=db,
|
|
email=request.email,
|
|
user_id=user_id,
|
|
language=request.language
|
|
)
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
|
|
|
|
return VerificationResponse(success=True, message=message)
|
|
|
|
|
|
@router.post("/email/verify", response_model=VerificationResponse)
|
|
async def verify_email_code(
|
|
request: VerifyCodeRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user_optional)
|
|
):
|
|
"""Verify email code"""
|
|
email = request.email
|
|
if current_user:
|
|
email = current_user.email
|
|
|
|
if not email:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Email is required"
|
|
)
|
|
|
|
success, message = verification_service.verify_code(
|
|
db=db,
|
|
code=request.code,
|
|
code_type="email",
|
|
email=email
|
|
)
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
|
|
|
|
# If user is logged in, mark their email as verified
|
|
if current_user:
|
|
verification_service.mark_email_verified(db, current_user)
|
|
|
|
return VerificationResponse(success=True, message=message)
|
|
|
|
|
|
# Phone Verification Endpoints
|
|
@router.post("/phone/send", response_model=VerificationResponse)
|
|
async def send_phone_code(
|
|
request: SendPhoneCodeRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user) # Requires login
|
|
):
|
|
"""Send phone verification code (requires login)"""
|
|
success, message = await verification_service.send_sms_verification(
|
|
db=db,
|
|
phone=request.phone,
|
|
user_id=current_user.id,
|
|
language=request.language
|
|
)
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
|
|
|
|
return VerificationResponse(success=True, message=message)
|
|
|
|
|
|
@router.post("/phone/verify", response_model=VerificationResponse)
|
|
async def verify_phone_code(
|
|
request: VerifyCodeRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user) # Requires login
|
|
):
|
|
"""Verify phone code (requires login)"""
|
|
if not request.phone:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Phone number is required"
|
|
)
|
|
|
|
# Normalize phone number
|
|
phone = request.phone.strip().replace(" ", "").replace("-", "")
|
|
if not phone.startswith("+"):
|
|
if phone.startswith("9") and len(phone) == 8:
|
|
phone = "+976" + phone
|
|
|
|
success, message = verification_service.verify_code(
|
|
db=db,
|
|
code=request.code,
|
|
code_type="phone",
|
|
phone=phone
|
|
)
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
|
|
|
|
# Mark phone as verified
|
|
verification_service.mark_phone_verified(db, current_user, phone)
|
|
|
|
return VerificationResponse(success=True, message=message)
|
|
|
|
|
|
# Status Endpoint
|
|
@router.get("/status", response_model=VerificationStatusResponse)
|
|
async def get_verification_status(
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""Get current user's verification status"""
|
|
return VerificationStatusResponse(
|
|
email_verified=current_user.email_verified or False,
|
|
phone_verified=current_user.phone_verified or False,
|
|
email=current_user.email,
|
|
phone=current_user.phone
|
|
)
|
|
|
|
|
|
# Pre-registration email verification (for signup flow)
|
|
@router.post("/email/send-preregister", response_model=VerificationResponse)
|
|
async def send_preregister_email_code(
|
|
request: SendEmailCodeRequest,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Send email verification code for new registration (no login required)"""
|
|
# Check if email is already registered
|
|
existing = db.query(User).filter(User.email == request.email).first()
|
|
if existing:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="This email is already registered"
|
|
)
|
|
|
|
success, message = await verification_service.send_email_verification(
|
|
db=db,
|
|
email=request.email,
|
|
user_id=None,
|
|
language=request.language
|
|
)
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
|
|
|
|
return VerificationResponse(success=True, message=message)
|
|
|
|
|
|
@router.post("/email/verify-preregister", response_model=VerificationResponse)
|
|
async def verify_preregister_email_code(
|
|
request: VerifyCodeRequest,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Verify email code for new registration (no login required)"""
|
|
if not request.email:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Email is required"
|
|
)
|
|
|
|
success, message = verification_service.verify_code(
|
|
db=db,
|
|
code=request.code,
|
|
code_type="email",
|
|
email=request.email
|
|
)
|
|
|
|
if not success:
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
|
|
|
|
return VerificationResponse(success=True, message=message)
|