Spaces:
Sleeping
Sleeping
feat:Added auth service
Browse files- alembic/versions/e8066533b622_delete_user_verification_cols.py +49 -0
- requirements/dev.txt +12 -0
- src/auth/config.py +12 -1
- src/auth/router.py +89 -0
- src/auth/schemas.py +17 -0
- src/auth/service.py +199 -2
- src/auth/utils.py +138 -0
- src/core/models.py +0 -2
- src/main.py +3 -0
alembic/versions/e8066533b622_delete_user_verification_cols.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""delete:user/verification cols
|
| 2 |
+
|
| 3 |
+
Revision ID: e8066533b622
|
| 4 |
+
Revises: 584a5111e60f
|
| 5 |
+
Create Date: 2025-11-11 10:47:38.171691
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from typing import Sequence, Union
|
| 10 |
+
|
| 11 |
+
from alembic import op
|
| 12 |
+
import sqlalchemy as sa
|
| 13 |
+
import sqlmodel.sql.sqltypes
|
| 14 |
+
from sqlalchemy.dialects import postgresql
|
| 15 |
+
|
| 16 |
+
# revision identifiers, used by Alembic.
|
| 17 |
+
revision: str = "e8066533b622"
|
| 18 |
+
down_revision: Union[str, Sequence[str], None] = "584a5111e60f"
|
| 19 |
+
branch_labels: Union[str, Sequence[str], None] = None
|
| 20 |
+
depends_on: Union[str, Sequence[str], None] = None
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def upgrade() -> None:
|
| 24 |
+
"""Upgrade schema."""
|
| 25 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 26 |
+
op.drop_column("users", "verification_token")
|
| 27 |
+
op.drop_column("users", "verification_expires_at")
|
| 28 |
+
# ### end Alembic commands ###
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def downgrade() -> None:
|
| 32 |
+
"""Downgrade schema."""
|
| 33 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 34 |
+
op.add_column(
|
| 35 |
+
"users",
|
| 36 |
+
sa.Column(
|
| 37 |
+
"verification_expires_at",
|
| 38 |
+
postgresql.TIMESTAMP(),
|
| 39 |
+
autoincrement=False,
|
| 40 |
+
nullable=True,
|
| 41 |
+
),
|
| 42 |
+
)
|
| 43 |
+
op.add_column(
|
| 44 |
+
"users",
|
| 45 |
+
sa.Column(
|
| 46 |
+
"verification_token", sa.VARCHAR(), autoincrement=False, nullable=True
|
| 47 |
+
),
|
| 48 |
+
)
|
| 49 |
+
# ### end Alembic commands ###
|
requirements/dev.txt
CHANGED
|
@@ -3,17 +3,29 @@ annotated-doc==0.0.3
|
|
| 3 |
annotated-types==0.7.0
|
| 4 |
anyio==4.11.0
|
| 5 |
asyncpg==0.30.0
|
|
|
|
|
|
|
| 6 |
click==8.3.0
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
fastapi==0.121.0
|
| 8 |
greenlet==3.2.4
|
| 9 |
h11==0.16.0
|
| 10 |
idna==3.11
|
| 11 |
Mako==1.3.10
|
| 12 |
MarkupSafe==3.0.3
|
|
|
|
| 13 |
psycopg2-binary==2.9.11
|
|
|
|
|
|
|
| 14 |
pydantic==1.10.24
|
| 15 |
pydantic_core==2.41.4
|
| 16 |
python-dotenv==1.2.1
|
|
|
|
|
|
|
|
|
|
| 17 |
sniffio==1.3.1
|
| 18 |
SQLAlchemy==2.0.44
|
| 19 |
sqlmodel==0.0.27
|
|
|
|
| 3 |
annotated-types==0.7.0
|
| 4 |
anyio==4.11.0
|
| 5 |
asyncpg==0.30.0
|
| 6 |
+
bcrypt==3.2.2
|
| 7 |
+
cffi==2.0.0
|
| 8 |
click==8.3.0
|
| 9 |
+
cryptography==46.0.3
|
| 10 |
+
dnspython==2.8.0
|
| 11 |
+
ecdsa==0.19.1
|
| 12 |
+
email-validator==2.3.0
|
| 13 |
fastapi==0.121.0
|
| 14 |
greenlet==3.2.4
|
| 15 |
h11==0.16.0
|
| 16 |
idna==3.11
|
| 17 |
Mako==1.3.10
|
| 18 |
MarkupSafe==3.0.3
|
| 19 |
+
passlib==1.7.4
|
| 20 |
psycopg2-binary==2.9.11
|
| 21 |
+
pyasn1==0.6.1
|
| 22 |
+
pycparser==2.23
|
| 23 |
pydantic==1.10.24
|
| 24 |
pydantic_core==2.41.4
|
| 25 |
python-dotenv==1.2.1
|
| 26 |
+
python-jose==3.5.0
|
| 27 |
+
rsa==4.9.1
|
| 28 |
+
six==1.17.0
|
| 29 |
sniffio==1.3.1
|
| 30 |
SQLAlchemy==2.0.44
|
| 31 |
sqlmodel==0.0.27
|
src/auth/config.py
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
|
|
| 1 |
from pydantic import BaseSettings
|
|
|
|
|
|
|
| 2 |
|
| 3 |
class HomeSettings(BaseSettings):
|
| 4 |
FEATURE_ENABLED: bool = True
|
| 5 |
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
from pydantic import BaseSettings
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
|
| 5 |
|
| 6 |
class HomeSettings(BaseSettings):
|
| 7 |
FEATURE_ENABLED: bool = True
|
| 8 |
|
| 9 |
+
|
| 10 |
+
home_settings = HomeSettings()
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
load_dotenv()
|
| 14 |
+
SECRET_KEY = os.getenv("SECRET_KEY")
|
| 15 |
+
|
| 16 |
+
ALGORITHM = "HS256"
|
| 17 |
+
ACCESS_TOKEN_EXPIRE_MINUTES = 60
|
src/auth/router.py
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from src.core.database import get_async_session
|
| 2 |
+
from fastapi import APIRouter, Depends, HTTPException, status
|
| 3 |
+
|
| 4 |
+
# from src.home.router import get_session
|
| 5 |
+
# from sqlmodel import Session
|
| 6 |
+
# from src.auth.service import login_user
|
| 7 |
+
from jose import jwt, JWTError
|
| 8 |
+
from src.auth.config import SECRET_KEY, ALGORITHM
|
| 9 |
+
from fastapi.security import OAuth2PasswordBearer
|
| 10 |
+
|
| 11 |
+
# from .schemas import SignUpRequest, VerifyOtpRequest, LoginRequest
|
| 12 |
+
# from .service import create_user_and_send_otp, verify_otp
|
| 13 |
+
|
| 14 |
+
# router = APIRouter(prefix="/auth", tags=["Auth"])
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# @router.post("/signup")
|
| 18 |
+
# def signup(payload: SignUpRequest, session: Session = Depends(get_session)):
|
| 19 |
+
# try:
|
| 20 |
+
# return create_user_and_send_otp(
|
| 21 |
+
# session, payload.name, payload.email, payload.password
|
| 22 |
+
# )
|
| 23 |
+
# except ValueError as e:
|
| 24 |
+
# raise HTTPException(status_code=400, detail=str(e))
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# @router.post("/verify-otp")
|
| 28 |
+
# def verify_otp_route(
|
| 29 |
+
# payload: VerifyOtpRequest, session: Session = Depends(get_session)
|
| 30 |
+
# ):
|
| 31 |
+
# try:
|
| 32 |
+
# return verify_otp(session, payload.email, payload.otp)
|
| 33 |
+
# except ValueError as e:
|
| 34 |
+
# raise HTTPException(status_code=400, detail=str(e))
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
# @router.post("/login")
|
| 38 |
+
# def login(payload: LoginRequest, session: Session = Depends(get_session)):
|
| 39 |
+
# return login_user(session, payload.email, payload.password)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 43 |
+
from sqlmodel import Session
|
| 44 |
+
from src.auth.service import (
|
| 45 |
+
create_user_and_send_verification_email,
|
| 46 |
+
verify_email,
|
| 47 |
+
login_user,
|
| 48 |
+
)
|
| 49 |
+
from .schemas import SignUpRequest, LoginRequest
|
| 50 |
+
|
| 51 |
+
router = APIRouter(prefix="/auth", tags=["Auth"])
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@router.post("/signup")
|
| 55 |
+
async def signup(payload: SignUpRequest, session: Session = Depends(get_async_session)):
|
| 56 |
+
try:
|
| 57 |
+
return await create_user_and_send_verification_email(
|
| 58 |
+
session, payload.name, payload.email, payload.password
|
| 59 |
+
)
|
| 60 |
+
except ValueError as e:
|
| 61 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@router.get("/verify-email")
|
| 65 |
+
async def verify_email_route(token: str, session: Session = Depends(get_async_session)):
|
| 66 |
+
return await verify_email(session, token)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
@router.post("/login")
|
| 70 |
+
async def login(payload: LoginRequest, session: Session = Depends(get_async_session)):
|
| 71 |
+
return await login_user(session, payload.email, payload.password)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def get_current_user(token: str = Depends(oauth2_scheme)):
|
| 78 |
+
try:
|
| 79 |
+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
| 80 |
+
user_id: str = payload.get("sub")
|
| 81 |
+
if user_id is None:
|
| 82 |
+
raise HTTPException(
|
| 83 |
+
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token"
|
| 84 |
+
)
|
| 85 |
+
return user_id
|
| 86 |
+
except JWTError:
|
| 87 |
+
raise HTTPException(
|
| 88 |
+
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token"
|
| 89 |
+
)
|
src/auth/schemas.py
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class SignUpRequest(BaseModel):
|
| 5 |
+
name: str
|
| 6 |
+
email: str
|
| 7 |
+
password: str
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class VerifyOtpRequest(BaseModel):
|
| 11 |
+
email: str
|
| 12 |
+
otp: str
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class LoginRequest(BaseModel):
|
| 16 |
+
email: str
|
| 17 |
+
password: str
|
src/auth/service.py
CHANGED
|
@@ -1,2 +1,199 @@
|
|
| 1 |
-
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
import random
|
| 3 |
+
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from src.auth.utils import (
|
| 6 |
+
# send_otp_email,
|
| 7 |
+
verify_password,
|
| 8 |
+
verify_verification_token,
|
| 9 |
+
create_access_token,
|
| 10 |
+
hash_password,
|
| 11 |
+
send_verification_email,
|
| 12 |
+
create_verification_token,
|
| 13 |
+
)
|
| 14 |
+
from src.core.models import Users
|
| 15 |
+
from sqlmodel import Session, select
|
| 16 |
+
from fastapi import HTTPException
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def generate_otp():
|
| 20 |
+
return str(random.randint(100000, 999999))
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# def create_user_and_send_otp(session: Session, name: str, email: str, password: str):
|
| 24 |
+
# # 1. If already verified user exists, block new registration
|
| 25 |
+
# existing_user = session.exec(select(Users).where(Users.email_id == email)).first()
|
| 26 |
+
# if existing_user:
|
| 27 |
+
# raise ValueError("User already exists")
|
| 28 |
+
|
| 29 |
+
# otp = generate_otp()
|
| 30 |
+
# expires_at = datetime.now() + timedelta(minutes=5)
|
| 31 |
+
|
| 32 |
+
# existing_otp = session.exec(
|
| 33 |
+
# select(OtpVerification).where(OtpVerification.email == email)
|
| 34 |
+
# ).first()
|
| 35 |
+
|
| 36 |
+
# if existing_otp:
|
| 37 |
+
# session.delete(existing_otp)
|
| 38 |
+
# session.commit()
|
| 39 |
+
|
| 40 |
+
# new_otp = OtpVerification(
|
| 41 |
+
# email=email,
|
| 42 |
+
# otp=otp,
|
| 43 |
+
# expires_at=expires_at,
|
| 44 |
+
# temp_name=name,
|
| 45 |
+
# temp_password=password,
|
| 46 |
+
# )
|
| 47 |
+
# session.add(new_otp)
|
| 48 |
+
# session.commit()
|
| 49 |
+
# send_otp_email(email, otp)
|
| 50 |
+
|
| 51 |
+
# return {"message": "OTP sent successfully"}
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
# def create_user_and_send_verification_email(
|
| 55 |
+
# session: Session, name: str, email: str, password: str
|
| 56 |
+
# ):
|
| 57 |
+
# existing_user = session.exec(select(Users).where(Users.email_id == email)).first()
|
| 58 |
+
# if existing_user:
|
| 59 |
+
# raise ValueError("User already exists")
|
| 60 |
+
|
| 61 |
+
# # Create secure token
|
| 62 |
+
# token = generate_opaque_token()
|
| 63 |
+
# expires_at = datetime.utcnow() + timedelta(hours=24)
|
| 64 |
+
|
| 65 |
+
# # Create user
|
| 66 |
+
# new_user = Users(
|
| 67 |
+
# user_name=name,
|
| 68 |
+
# email_id=email,
|
| 69 |
+
# password=hash_password(password),
|
| 70 |
+
# is_verified=False,
|
| 71 |
+
# verification_token=token,
|
| 72 |
+
# verification_expires_at=expires_at,
|
| 73 |
+
# )
|
| 74 |
+
# session.add(new_user)
|
| 75 |
+
# session.commit()
|
| 76 |
+
|
| 77 |
+
# # Send verification email
|
| 78 |
+
# send_verification_email(email, token)
|
| 79 |
+
|
| 80 |
+
# return {"message": "Verification email sent. Please check your inbox."}
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
async def create_user_and_send_verification_email(
|
| 84 |
+
session: Session, name: str, email: str, password: str
|
| 85 |
+
):
|
| 86 |
+
user = await session.exec(select(Users).where(Users.email_id == email))
|
| 87 |
+
existing_user = user.first()
|
| 88 |
+
if existing_user:
|
| 89 |
+
raise ValueError("User already exists")
|
| 90 |
+
|
| 91 |
+
new_user = Users(
|
| 92 |
+
user_name=name,
|
| 93 |
+
email_id=email,
|
| 94 |
+
password=hash_password(password),
|
| 95 |
+
is_verified=False,
|
| 96 |
+
)
|
| 97 |
+
session.add(new_user)
|
| 98 |
+
await session.commit()
|
| 99 |
+
|
| 100 |
+
# Create encrypted token using Fernet
|
| 101 |
+
token = create_verification_token(str(new_user.id))
|
| 102 |
+
|
| 103 |
+
# Send email
|
| 104 |
+
send_verification_email(email, token)
|
| 105 |
+
|
| 106 |
+
return {"message": "Verification email sent. Please check your inbox."}
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
# def verify_email(session: Session, token: str):
|
| 110 |
+
# user = session.exec(select(Users).where(Users.verification_token == token)).first()
|
| 111 |
+
|
| 112 |
+
# if not user:
|
| 113 |
+
# raise HTTPException(status_code=400, detail="Invalid or expired token")
|
| 114 |
+
|
| 115 |
+
# if user.verification_expires_at < datetime.utcnow():
|
| 116 |
+
# raise HTTPException(status_code=400, detail="Verification link expired")
|
| 117 |
+
|
| 118 |
+
# user.is_verified = True
|
| 119 |
+
# user.verification_token = None
|
| 120 |
+
# user.verification_expires_at = None
|
| 121 |
+
# session.commit()
|
| 122 |
+
|
| 123 |
+
# return {"message": "Email verified successfully!"}
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
async def verify_email(session: Session, token: str):
|
| 127 |
+
try:
|
| 128 |
+
user_id = await verify_verification_token(token)
|
| 129 |
+
except ValueError as e:
|
| 130 |
+
raise HTTPException(status_code=400, detail=str(e))
|
| 131 |
+
|
| 132 |
+
user = await session.get(Users, uuid.UUID(user_id))
|
| 133 |
+
if not user:
|
| 134 |
+
raise HTTPException(status_code=404, detail="User not found")
|
| 135 |
+
|
| 136 |
+
if user.is_verified:
|
| 137 |
+
return {"message": "Email already verified"}
|
| 138 |
+
|
| 139 |
+
user.is_verified = True
|
| 140 |
+
await session.commit()
|
| 141 |
+
|
| 142 |
+
return {"message": "Email verified successfully!"}
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
# def verify_otp(session: Session, email: str, otp: str):
|
| 146 |
+
# record = session.exec(
|
| 147 |
+
# select(OtpVerification).where(OtpVerification.email == email)
|
| 148 |
+
# ).first()
|
| 149 |
+
|
| 150 |
+
# if not record:
|
| 151 |
+
# raise ValueError("No OTP record found")
|
| 152 |
+
# if record.is_verified:
|
| 153 |
+
# return {"message": "Email already verified"}
|
| 154 |
+
# if record.expires_at < datetime.now():
|
| 155 |
+
# raise ValueError("OTP expired")
|
| 156 |
+
# if record.otp != otp:
|
| 157 |
+
# raise ValueError("Invalid OTP")
|
| 158 |
+
|
| 159 |
+
# new_user = Users(
|
| 160 |
+
# user_name=record.temp_name,
|
| 161 |
+
# email_id=email,
|
| 162 |
+
# password=hash_password(record.temp_password),
|
| 163 |
+
# )
|
| 164 |
+
# session.add(new_user)
|
| 165 |
+
|
| 166 |
+
# record.is_verified = True
|
| 167 |
+
# session.commit()
|
| 168 |
+
|
| 169 |
+
# return {"message": "Email verified successfully. Account created!"}
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
async def login_user(session: Session, email: str, password: str):
|
| 173 |
+
users = await session.exec(select(Users).where(Users.email_id == email))
|
| 174 |
+
user = users.first()
|
| 175 |
+
|
| 176 |
+
if not user:
|
| 177 |
+
raise HTTPException(status_code=400, detail="Invalid email or password")
|
| 178 |
+
|
| 179 |
+
if not verify_password(password, user.password):
|
| 180 |
+
raise HTTPException(status_code=400, detail="Invalid email or password")
|
| 181 |
+
|
| 182 |
+
if not user.is_verified:
|
| 183 |
+
raise HTTPException(
|
| 184 |
+
status_code=403, detail="Please verify your email before logging in"
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
access_token = create_access_token(
|
| 188 |
+
data={"sub": str(user.id), "name": user.user_name, "email": user.email_id}
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
return {
|
| 192 |
+
"access_token": access_token,
|
| 193 |
+
"token_type": "bearer",
|
| 194 |
+
"user": {
|
| 195 |
+
"id": str(user.id),
|
| 196 |
+
"name": user.user_name,
|
| 197 |
+
"email": user.email_id,
|
| 198 |
+
},
|
| 199 |
+
}
|
src/auth/utils.py
CHANGED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
import smtplib
|
| 3 |
+
from email.mime.text import MIMEText
|
| 4 |
+
from passlib.context import CryptContext
|
| 5 |
+
from jose import JWTError, jwt
|
| 6 |
+
from datetime import datetime, timedelta
|
| 7 |
+
from src.auth.config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
|
| 8 |
+
import secrets
|
| 9 |
+
from datetime import datetime, timedelta
|
| 10 |
+
import os
|
| 11 |
+
import json
|
| 12 |
+
from datetime import datetime, timedelta
|
| 13 |
+
from cryptography.fernet import Fernet, InvalidToken
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
SMTP_SERVER = "smtp.gmail.com"
|
| 17 |
+
SMTP_PORT = 587
|
| 18 |
+
SMTP_EMAIL = "[email protected]"
|
| 19 |
+
SMTP_PASSWORD = "jdtc qyaq fmqd xvse"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def send_otp_email(to_email: str, otp: str):
|
| 23 |
+
subject = "Your Yuvabe OTP Code"
|
| 24 |
+
body = f"Your OTP is {otp}. It will expire in 5 minutes."
|
| 25 |
+
|
| 26 |
+
msg = MIMEText(body)
|
| 27 |
+
msg["Subject"] = subject
|
| 28 |
+
msg["From"] = SMTP_EMAIL
|
| 29 |
+
msg["To"] = to_email
|
| 30 |
+
|
| 31 |
+
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
|
| 32 |
+
server.starttls()
|
| 33 |
+
server.login(SMTP_EMAIL, SMTP_PASSWORD)
|
| 34 |
+
server.send_message(msg)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def hash_password(password: str) -> str:
|
| 41 |
+
return pwd_context.hash(password)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
| 45 |
+
return pwd_context.verify(plain_password, hashed_password)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def create_access_token(data: dict):
|
| 49 |
+
to_encode = data.copy()
|
| 50 |
+
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
| 51 |
+
to_encode.update({"exp": expire})
|
| 52 |
+
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
| 53 |
+
return encoded_jwt
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def generate_opaque_token():
|
| 57 |
+
return secrets.token_urlsafe(32)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
# def send_verification_email(to_email: str, token: str):
|
| 61 |
+
# subject = "Verify your Yuvabe Account"
|
| 62 |
+
# verification_link = (
|
| 63 |
+
# f"https://68c71e06225c.ngrok-free.app/auth/verify-email?token={token}"
|
| 64 |
+
# )
|
| 65 |
+
# body = f"""
|
| 66 |
+
# Hi,
|
| 67 |
+
|
| 68 |
+
# Please click the link below to verify your email address:
|
| 69 |
+
# {verification_link}
|
| 70 |
+
|
| 71 |
+
# This link will expire in 24 hours.
|
| 72 |
+
# """
|
| 73 |
+
|
| 74 |
+
# msg = MIMEText(body)
|
| 75 |
+
# msg["Subject"] = subject
|
| 76 |
+
# msg["From"] = SMTP_EMAIL
|
| 77 |
+
# msg["To"] = to_email
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
# with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
|
| 81 |
+
# server.starttls()
|
| 82 |
+
# server.login(SMTP_EMAIL, SMTP_PASSWORD)
|
| 83 |
+
# server.send_message(msg)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def send_verification_email(to_email: str, token: str):
|
| 87 |
+
subject = "Verify your Yuvabe Account"
|
| 88 |
+
verification_link = (
|
| 89 |
+
f"https://68c71e06225c.ngrok-free.app/auth/verify-email?token={token}"
|
| 90 |
+
)
|
| 91 |
+
body = f"""
|
| 92 |
+
Hi,
|
| 93 |
+
|
| 94 |
+
Please verify your Yuvabe account by clicking the link below:
|
| 95 |
+
{verification_link}
|
| 96 |
+
|
| 97 |
+
This link will expire in 24 hours.
|
| 98 |
+
"""
|
| 99 |
+
|
| 100 |
+
msg = MIMEText(body)
|
| 101 |
+
msg["Subject"] = subject
|
| 102 |
+
msg["From"] = SMTP_EMAIL
|
| 103 |
+
msg["To"] = to_email
|
| 104 |
+
|
| 105 |
+
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
|
| 106 |
+
server.starttls()
|
| 107 |
+
server.login(SMTP_EMAIL, SMTP_PASSWORD)
|
| 108 |
+
server.send_message(msg)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
FERNET_KEY = os.getenv("FERNET_KEY")
|
| 112 |
+
fernet = Fernet(FERNET_KEY)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def create_verification_token(user_id: str, expires_in_hours: int = 24) -> str:
|
| 116 |
+
"""Create an encrypted verification token containing the user_id and expiry time."""
|
| 117 |
+
payload = {
|
| 118 |
+
"sub": user_id,
|
| 119 |
+
"exp": (datetime.utcnow() + timedelta(hours=expires_in_hours)).timestamp(),
|
| 120 |
+
}
|
| 121 |
+
token = fernet.encrypt(json.dumps(payload).encode()).decode()
|
| 122 |
+
return token
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
async def verify_verification_token(token: str) -> str:
|
| 126 |
+
"""Verify and decrypt a verification token, returning the user_id if valid."""
|
| 127 |
+
try:
|
| 128 |
+
decrypted = fernet.decrypt(token.encode())
|
| 129 |
+
data = json.loads(decrypted.decode())
|
| 130 |
+
|
| 131 |
+
exp = datetime.fromtimestamp(data["exp"])
|
| 132 |
+
if datetime.utcnow() > exp:
|
| 133 |
+
raise ValueError("Verification link expired")
|
| 134 |
+
|
| 135 |
+
return data["sub"]
|
| 136 |
+
|
| 137 |
+
except InvalidToken:
|
| 138 |
+
raise ValueError("Invalid verification link")
|
src/core/models.py
CHANGED
|
@@ -23,8 +23,6 @@ class Users(SQLModel, table=True):
|
|
| 23 |
is_verified: bool = Field(
|
| 24 |
default=False, sa_column_kwargs={"server_default": "false"}
|
| 25 |
)
|
| 26 |
-
verification_token: Optional[str] = None
|
| 27 |
-
verification_expires_at: Optional[datetime] = None
|
| 28 |
dob: Optional[date] = None
|
| 29 |
address: Optional[str] = None
|
| 30 |
profile_picture: Optional[str] = None
|
|
|
|
| 23 |
is_verified: bool = Field(
|
| 24 |
default=False, sa_column_kwargs={"server_default": "false"}
|
| 25 |
)
|
|
|
|
|
|
|
| 26 |
dob: Optional[date] = None
|
| 27 |
address: Optional[str] = None
|
| 28 |
profile_picture: Optional[str] = None
|
src/main.py
CHANGED
|
@@ -2,6 +2,7 @@ from fastapi import FastAPI
|
|
| 2 |
|
| 3 |
from src.core.database import init_db
|
| 4 |
from src.home.router import router as home_router
|
|
|
|
| 5 |
|
| 6 |
app = FastAPI(title="Yuvabe App API")
|
| 7 |
|
|
@@ -9,6 +10,8 @@ app.include_router(home_router, prefix="/home", tags=["Home"])
|
|
| 9 |
|
| 10 |
init_db()
|
| 11 |
|
|
|
|
|
|
|
| 12 |
|
| 13 |
@app.get("/")
|
| 14 |
def root():
|
|
|
|
| 2 |
|
| 3 |
from src.core.database import init_db
|
| 4 |
from src.home.router import router as home_router
|
| 5 |
+
from src.auth.router import router as auth_router
|
| 6 |
|
| 7 |
app = FastAPI(title="Yuvabe App API")
|
| 8 |
|
|
|
|
| 10 |
|
| 11 |
init_db()
|
| 12 |
|
| 13 |
+
app.include_router(auth_router)
|
| 14 |
+
|
| 15 |
|
| 16 |
@app.get("/")
|
| 17 |
def root():
|