Hp137 commited on
Commit
b9669e1
·
1 Parent(s): f450d9b

feat:Added auth service

Browse files
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
- home_settings = HomeSettings()
 
 
 
 
 
 
 
 
 
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
- from typing import List
2
- from uuid import UUID
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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():