diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/auth.py b/app/auth.py index 199943a..5beda32 100644 --- a/app/auth.py +++ b/app/auth.py @@ -6,7 +6,7 @@ def send_otp(phone: str): try: - verification = client.verify.services(TWILIO_VERIFY_SERVICE_SID).verifications.create( + verification = client.verify.v2.services(TWILIO_VERIFY_SERVICE_SID).verifications.create( to=phone, channel="sms" ) return {"status": verification.status} @@ -16,7 +16,7 @@ def send_otp(phone: str): def verify_otp(phone: str, code: str): try: - verification_check = client.verify.services(TWILIO_VERIFY_SERVICE_SID).verification_checks.create( + verification_check = client.verify.v2.services(TWILIO_VERIFY_SERVICE_SID).verification_checks.create( to=phone, code=code ) return {"status": verification_check.status} diff --git a/app/jwt.py b/app/jwt.py index ea72bc2..3186e64 100644 --- a/app/jwt.py +++ b/app/jwt.py @@ -2,17 +2,52 @@ from datetime import datetime, timedelta import os from dotenv import load_dotenv +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from app.models.user import UserORM +from app.models.database import get_db +from sqlalchemy.orm import Session load_dotenv() +security = HTTPBearer() + # Load secret from env SECRET_KEY = os.getenv("JWT_SECRET_KEY") ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 30 # 30 minutes validity + def create_access_token(data: dict): to_encode = data.copy() expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update({"exp": expire}) token = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return token + +def verify_access_token(credentials: HTTPAuthorizationCredentials = Depends(security),) -> dict: + token = credentials.credentials + try: + payload = jwt.decode( + token, + SECRET_KEY, + algorithms=[ALGORITHM], + options={"verify_aud": False}, + ) + except JWTError: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials" + ) + + return payload + +def get_current_user(token_data: dict = Depends(verify_access_token), db: Session = Depends(get_db),) -> UserORM: + user_id = token_data.get("user_id") + user = db.query(UserORM).get(user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found" + ) + return user \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/models/database.py b/app/models/database.py index ae0f1ee..ea8f35d 100644 --- a/app/models/database.py +++ b/app/models/database.py @@ -1,7 +1,11 @@ +from pathlib import Path from sqlalchemy import create_engine, text, select -from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import sessionmaker, Session import os from dotenv import load_dotenv +from app.models.user import Base +from app.schemas.user_schemas import User +from app.models.user import UserORM load_dotenv() @@ -26,7 +30,6 @@ def get_db(): db.close() def init_db(): - from app.models.user import Base # Drop all tables and recreate them (for development only!) # Base.metadata.drop_all(bind=engine) Base.metadata.create_all(bind=engine) @@ -41,10 +44,7 @@ def test_connection(): print(f" Database test failed: {e}") return False -def create_user(phone_number: str, name: str, email: str): - from app.models.user import UserORM, User - - db = SessionLocal() +def create_user(db:Session,phone_number: str, name: str, email: str): try: db_user = UserORM( phone_number=phone_number, @@ -68,13 +68,9 @@ def create_user(phone_number: str, name: str, email: str): except Exception as e: db.rollback() raise e - finally: - db.close() -def get_user_by_phone(phone_number: str): - from app.models.user import UserORM, User - - db = SessionLocal() + +def get_user_by_phone(db:Session,phone_number: str): try: stmt = select(UserORM).where(UserORM.phone_number == phone_number) result = db.execute(stmt) @@ -90,5 +86,5 @@ def get_user_by_phone(phone_number: str): return user return None - finally: - db.close() + except Exception as e: + raise e diff --git a/app/models/user.py b/app/models/user.py index ef7aa63..1ce875e 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -14,26 +14,4 @@ class UserORM(Base): name = Column(String(255), nullable=False) email = Column(String(255), nullable=False, unique=True) -# Pydantic Models for API validation and data handling -class User(BaseModel): - id: Optional[int] = None - phone_number: str - name: str - email: EmailStr - - class Config: - from_attributes = True # For Pydantic v2 - -class UserCreate(BaseModel): - phone_number: str - name: str - email: EmailStr -class UserResponse(BaseModel): - id: int - phone_number: str - name: str - email: str - - class Config: - from_attributes = True diff --git a/app/routers/__init__.py b/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routers/otp.py b/app/routers/otp.py index 2499cfe..30c2136 100644 --- a/app/routers/otp.py +++ b/app/routers/otp.py @@ -1,23 +1,26 @@ -from fastapi import APIRouter, Query +from fastapi import APIRouter, status , Depends from app.jwt import create_access_token from app.auth import verify_otp, send_otp -from app.models.database import get_user_by_phone, create_user +from app.models.database import get_user_by_phone, create_user, get_db +from app.schemas.otp_schemas import OTPRequest, OTPVerifyRequest +from app.schemas.user_schemas import UserCreate +from sqlalchemy.orm import Session router = APIRouter(prefix="/otp", tags=["Otp"]) -@router.post("/send-otp") -def send_otp_route(phone: str = Query(...)): - return send_otp(phone) +@router.post("/send-otp", status_code= status.HTTP_202_ACCEPTED) +def send_otp_route(data : OTPRequest): + return send_otp(data.phone) -@router.post("/verify-otp") -def verify_otp_route(phone: str = Query(...), code: str = Query(...)): - result = verify_otp(phone, code) +@router.post("/verify-otp", status_code=status.HTTP_200_OK) +def verify_otp_route(data: OTPVerifyRequest, db: Session = Depends(get_db)): + result = verify_otp(data.phone, data.code) if result["status"] == "approved": - existing_user = get_user_by_phone(phone) + existing_user = get_user_by_phone(db,data.phone) if existing_user: - token = create_access_token({"phone": phone, "user_id": existing_user.id}) + token = create_access_token({"phone": data.phone, "user_id": existing_user.id}) return { "status": "approved", "access_token": token, @@ -43,20 +46,20 @@ def verify_otp_route(phone: str = Query(...), code: str = Query(...)): "user_exists": False } -@router.post("/register") -def register_user(phone_number: str, name: str, email: str): +@router.post("/register",status_code=status.HTTP_201_CREATED) +def register_user(data: UserCreate, db: Session = Depends(get_db)): try: - existing_user = get_user_by_phone(phone_number) + existing_user = get_user_by_phone(db,data.phone_number) if existing_user: return {"status": "error", "message": "User already exists with this phone number"} - new_user = create_user( - phone_number=phone_number, - name=name, - email=email + new_user = create_user(db, + phone_number=data.phone_number, + name=data.name, + email=data.email ) - token = create_access_token({"phone": str(phone_number), "user_id": new_user.id}) + token = create_access_token({"phone": str(data.phone_number), "user_id": new_user.id}) return { "status": "registered", diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/schemas/otp_schemas.py b/app/schemas/otp_schemas.py new file mode 100644 index 0000000..a4bb4c4 --- /dev/null +++ b/app/schemas/otp_schemas.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel, Field + +class OTPRequest(BaseModel): + phone: str = Field(..., example="+91551234567") + +class OTPVerifyRequest(BaseModel): + phone: str = Field(..., example="+91551234567") + code: str = Field(..., example="123456") diff --git a/app/schemas/user_schemas.py b/app/schemas/user_schemas.py new file mode 100644 index 0000000..b9edba9 --- /dev/null +++ b/app/schemas/user_schemas.py @@ -0,0 +1,29 @@ +from typing import Optional + +from pydantic import BaseModel, EmailStr, Field + + +class User(BaseModel): + id: Optional[int] = None + phone_number: str + name: str + email: EmailStr + + class Config: + from_attributes = True + + +class UserResponse(BaseModel): + id: int + phone_number: str + name: str + email: str + + class Config: + from_attributes = True + + +class UserCreate(BaseModel): + phone_number: str = Field(..., example="+91551234567") + name: str = Field(..., example="Alice") + email: EmailStr = Field(..., example="alice@example.com") \ No newline at end of file