Skip to content
Empty file added app/__init__.py
Empty file.
4 changes: 2 additions & 2 deletions app/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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}
Expand Down
35 changes: 35 additions & 0 deletions app/jwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Empty file added app/models/__init__.py
Empty file.
24 changes: 10 additions & 14 deletions app/models/database.py
Original file line number Diff line number Diff line change
@@ -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()

Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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)
Expand All @@ -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
22 changes: 0 additions & 22 deletions app/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Empty file added app/routers/__init__.py
Empty file.
39 changes: 21 additions & 18 deletions app/routers/otp.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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",
Expand Down
Empty file added app/schemas/__init__.py
Empty file.
8 changes: 8 additions & 0 deletions app/schemas/otp_schemas.py
Original file line number Diff line number Diff line change
@@ -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")
29 changes: 29 additions & 0 deletions app/schemas/user_schemas.py
Original file line number Diff line number Diff line change
@@ -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")