Hp137 commited on
Commit
c1d7a04
·
1 Parent(s): f70727c

feat:Added refresh token

Browse files
Files changed (4) hide show
  1. src/auth/router.py +84 -4
  2. src/auth/schemas.py +4 -1
  3. src/auth/service.py +77 -18
  4. src/auth/utils.py +43 -13
src/auth/router.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from src.core.database import get_async_session
2
  from fastapi import APIRouter, Depends, HTTPException, status
3
  from jose import jwt, JWTError
@@ -5,11 +6,20 @@ from fastapi import APIRouter, Depends, HTTPException
5
  from sqlmodel import Session
6
  from sqlmodel.ext.asyncio.session import AsyncSession
7
  from src.auth.service import (
8
- create_user_and_send_verification_email,
9
  verify_email,
 
10
  login_user,
11
  )
12
- from .schemas import SignUpRequest, LoginRequest, BaseResponse
 
 
 
 
 
 
 
 
13
 
14
  router = APIRouter(prefix="/auth", tags=["Auth"])
15
 
@@ -19,7 +29,7 @@ async def signup(
19
  payload: SignUpRequest, session: AsyncSession = Depends(get_async_session)
20
  ):
21
  try:
22
- response = await create_user_and_send_verification_email(
23
  session, payload.name, payload.email, payload.password
24
  )
25
  return {"code": 200, "data": response}
@@ -27,12 +37,26 @@ async def signup(
27
  raise HTTPException(status_code=400, detail=str(e))
28
 
29
 
 
 
 
 
 
 
 
 
 
 
 
30
  @router.get("/verify-email", response_model=BaseResponse)
31
  async def verify_email_route(
32
  token: str, session: AsyncSession = Depends(get_async_session)
33
  ):
34
  response = await verify_email(session, token)
35
- return {"code": 200, "data": response}
 
 
 
36
 
37
 
38
  @router.post("/login", response_model=BaseResponse)
@@ -41,3 +65,59 @@ async def login(
41
  ):
42
  response = await login_user(session, payload.email, payload.password)
43
  return {"code": 200, "data": response}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
  from src.core.database import get_async_session
3
  from fastapi import APIRouter, Depends, HTTPException, status
4
  from jose import jwt, JWTError
 
6
  from sqlmodel import Session
7
  from sqlmodel.ext.asyncio.session import AsyncSession
8
  from src.auth.service import (
9
+ create_user,
10
  verify_email,
11
+ send_verification_link,
12
  login_user,
13
  )
14
+ from src.auth.utils import get_current_user
15
+ from src.core.models import Users
16
+ from src.core.config import settings
17
+ from fastapi.responses import RedirectResponse
18
+ from .schemas import SignUpRequest, LoginRequest, BaseResponse, SendVerificationRequest
19
+ from fastapi.security import OAuth2PasswordRequestForm
20
+ from src.auth.utils import create_access_token
21
+ from jose import jwt, JWTError
22
+
23
 
24
  router = APIRouter(prefix="/auth", tags=["Auth"])
25
 
 
29
  payload: SignUpRequest, session: AsyncSession = Depends(get_async_session)
30
  ):
31
  try:
32
+ response = await create_user(
33
  session, payload.name, payload.email, payload.password
34
  )
35
  return {"code": 200, "data": response}
 
37
  raise HTTPException(status_code=400, detail=str(e))
38
 
39
 
40
+ @router.post("/send-verification", response_model=BaseResponse)
41
+ async def send_verification(
42
+ payload: SendVerificationRequest, session: AsyncSession = Depends(get_async_session)
43
+ ):
44
+ if not payload.email:
45
+ raise HTTPException(status_code=400, detail="Email is required")
46
+
47
+ response = await send_verification_link(session, payload.email)
48
+ return {"code": 200, "data": response}
49
+
50
+
51
  @router.get("/verify-email", response_model=BaseResponse)
52
  async def verify_email_route(
53
  token: str, session: AsyncSession = Depends(get_async_session)
54
  ):
55
  response = await verify_email(session, token)
56
+ access_token = response["access_token"]
57
+ redirect_url = f"yuvabe://verified?token={access_token}"
58
+
59
+ return {"code": 200, "data": response} , RedirectResponse(url=redirect_url)
60
 
61
 
62
  @router.post("/login", response_model=BaseResponse)
 
65
  ):
66
  response = await login_user(session, payload.email, payload.password)
67
  return {"code": 200, "data": response}
68
+
69
+
70
+ @router.post("/refresh", response_model=BaseResponse)
71
+ async def refresh_token(request: dict):
72
+ """Generate new access token using refresh token"""
73
+ refresh_token = request.get("refresh_token")
74
+ if not refresh_token:
75
+ raise HTTPException(status_code=400, detail="Refresh token is required")
76
+
77
+ try:
78
+ payload = jwt.decode(
79
+ refresh_token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM]
80
+ )
81
+ if payload.get("type") != "refresh":
82
+ raise HTTPException(status_code=400, detail="Invalid refresh token")
83
+
84
+ user_data = {
85
+ "sub": payload["sub"],
86
+ "name": payload.get("name"),
87
+ "email": payload.get("email"),
88
+ }
89
+ new_access_token = create_access_token(data=user_data)
90
+ return {"code": 200, "data": {"access_token": new_access_token}}
91
+
92
+ except JWTError:
93
+ raise HTTPException(status_code=401, detail="Invalid or expired refresh token")
94
+
95
+
96
+ @router.get("/home", response_model=BaseResponse)
97
+ async def get_home(
98
+ user_id: str = Depends(get_current_user),
99
+ session: AsyncSession = Depends(get_async_session),
100
+ ):
101
+ """
102
+ Protected home endpoint. Requires a valid access token (Bearer).
103
+ """
104
+ user = await session.get(Users, uuid.UUID(user_id))
105
+ if not user:
106
+ raise HTTPException(status_code=404, detail="User not found")
107
+
108
+ # Example payload — replace with your real app data
109
+ return {
110
+ "code": 200,
111
+ "data": {
112
+ "message": f"Welcome to Home, {user.user_name}!",
113
+ "user": {
114
+ "id": str(user.id),
115
+ "name": user.user_name,
116
+ "email": user.email_id,
117
+ },
118
+ "home_data": {
119
+ "announcements": ["Welcome!", "New protocol released"],
120
+ "timestamp": user.created_at.isoformat() if user.created_at else None,
121
+ },
122
+ },
123
+ }
src/auth/schemas.py CHANGED
@@ -1,4 +1,4 @@
1
- from pydantic import BaseModel
2
  from typing import Optional, Union, Dict
3
 
4
 
@@ -17,6 +17,9 @@ class LoginRequest(BaseModel):
17
  email: str
18
  password: str
19
 
 
 
 
20
 
21
  class UserResponse(BaseModel):
22
  id: str
 
1
+ from pydantic import BaseModel ,EmailStr
2
  from typing import Optional, Union, Dict
3
 
4
 
 
17
  email: str
18
  password: str
19
 
20
+ class SendVerificationRequest(BaseModel):
21
+ email: EmailStr
22
+
23
 
24
  class UserResponse(BaseModel):
25
  id: str
src/auth/service.py CHANGED
@@ -2,6 +2,7 @@ import uuid
2
  from src.auth.utils import (
3
  # send_otp_email,
4
  verify_password,
 
5
  verify_verification_token,
6
  create_access_token,
7
  hash_password,
@@ -11,11 +12,11 @@ from src.auth.utils import (
11
  from src.core.models import Users
12
  from sqlmodel import Session, select
13
  from fastapi import HTTPException
 
14
 
15
 
16
- async def create_user_and_send_verification_email(
17
- session: Session, name: str, email: str, password: str
18
- ):
19
  user = await session.exec(select(Users).where(Users.email_id == email))
20
  existing_user = user.first()
21
  if existing_user:
@@ -27,17 +28,61 @@ async def create_user_and_send_verification_email(
27
  password=hash_password(password),
28
  is_verified=False,
29
  )
 
30
  session.add(new_user)
 
 
31
 
32
- # Create encrypted token using Fernet
33
- token = create_verification_token(str(new_user.id))
34
-
 
 
 
 
35
 
36
- # Send email
37
- send_verification_email(email, token)
38
- await session.commit()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
- return {"message": "Verification email sent. Please check your inbox."}
 
 
 
 
41
 
42
 
43
  async def verify_email(session: Session, token: str):
@@ -50,13 +95,24 @@ async def verify_email(session: Session, token: str):
50
  if not user:
51
  raise HTTPException(status_code=404, detail="User not found")
52
 
53
- if user.is_verified:
54
- return {"message": "Email already verified"}
 
55
 
56
- user.is_verified = True
57
- await session.commit()
 
58
 
59
- return {"message": "Email verified successfully!"}
 
 
 
 
 
 
 
 
 
60
 
61
 
62
  async def login_user(session: Session, email: str, password: str):
@@ -70,16 +126,19 @@ async def login_user(session: Session, email: str, password: str):
70
  raise HTTPException(status_code=400, detail="Invalid email or password")
71
 
72
  if not user.is_verified:
73
- raise HTTPException(
74
- status_code=403, detail="Please verify your email before logging in"
75
- )
76
 
77
  access_token = create_access_token(
78
  data={"sub": str(user.id), "name": user.user_name, "email": user.email_id}
79
  )
80
 
 
 
 
 
81
  return {
82
  "access_token": access_token,
 
83
  "token_type": "bearer",
84
  "user": {
85
  "id": str(user.id),
 
2
  from src.auth.utils import (
3
  # send_otp_email,
4
  verify_password,
5
+ create_refresh_token,
6
  verify_verification_token,
7
  create_access_token,
8
  hash_password,
 
12
  from src.core.models import Users
13
  from sqlmodel import Session, select
14
  from fastapi import HTTPException
15
+ from sqlmodel.ext.asyncio.session import AsyncSession
16
 
17
 
18
+ async def create_user(session: AsyncSession, name: str, email: str, password: str):
19
+ """Create user without sending email"""
 
20
  user = await session.exec(select(Users).where(Users.email_id == email))
21
  existing_user = user.first()
22
  if existing_user:
 
28
  password=hash_password(password),
29
  is_verified=False,
30
  )
31
+
32
  session.add(new_user)
33
+ await session.commit()
34
+ await session.refresh(new_user)
35
 
36
+ access_token = create_access_token(
37
+ data={
38
+ "sub": str(new_user.id),
39
+ "name": new_user.user_name,
40
+ "email": new_user.email_id,
41
+ }
42
+ )
43
 
44
+ refresh_token = create_refresh_token(
45
+ data={
46
+ "sub": str(new_user.id),
47
+ "name": new_user.user_name,
48
+ "email": new_user.email_id,
49
+ }
50
+ )
51
+
52
+ return {
53
+ "message": "User created successfully",
54
+ "user_id": str(new_user.id),
55
+ "access_token": access_token,
56
+ "refresh_token": refresh_token,
57
+ }
58
+
59
+
60
+ async def send_verification_link(session: Session, email: str):
61
+ """Send verification email for an existing user."""
62
+ result = await session.exec(select(Users).where(Users.email_id == email))
63
+ user = result.first()
64
+
65
+ if not user:
66
+ raise HTTPException(status_code=404, detail="User not found")
67
+
68
+ if user.is_verified:
69
+ raise HTTPException(status_code=400, detail="User is already verified")
70
+
71
+ # Create a token using existing user ID (opaque token)
72
+ token = create_verification_token(str(user.id))
73
+
74
+ try:
75
+ send_verification_email(email, token)
76
+ except Exception as e:
77
+ raise HTTPException(
78
+ status_code=500, detail=f"Failed to send verification email: {str(e)}"
79
+ )
80
 
81
+ return {
82
+ "message": "Verification link sent successfully",
83
+ "user_id": str(user.id),
84
+ "email": user.email_id,
85
+ }
86
 
87
 
88
  async def verify_email(session: Session, token: str):
 
95
  if not user:
96
  raise HTTPException(status_code=404, detail="User not found")
97
 
98
+ if not user.is_verified:
99
+ user.is_verified = True
100
+ await session.commit()
101
 
102
+ access_token = create_access_token(
103
+ data={"sub": str(user.id), "name": user.user_name, "email": user.email_id}
104
+ )
105
 
106
+ refresh_token = create_refresh_token(
107
+ data={"sub": str(user.id), "name": user.user_name, "email": user.email_id}
108
+ )
109
+
110
+ return {
111
+ "message": "Email verified successfully!",
112
+ "access_token": access_token,
113
+ "refresh_token": refresh_token,
114
+ "token_type": "bearer",
115
+ }
116
 
117
 
118
  async def login_user(session: Session, email: str, password: str):
 
126
  raise HTTPException(status_code=400, detail="Invalid email or password")
127
 
128
  if not user.is_verified:
129
+ print(f"⚠️ User {user.email_id} logged in but not verified.")
 
 
130
 
131
  access_token = create_access_token(
132
  data={"sub": str(user.id), "name": user.user_name, "email": user.email_id}
133
  )
134
 
135
+ refresh_token = create_refresh_token(
136
+ data={"sub": str(user.id), "name": user.user_name, "email": user.email_id}
137
+ )
138
+
139
  return {
140
  "access_token": access_token,
141
+ "refresh_token": refresh_token,
142
  "token_type": "bearer",
143
  "user": {
144
  "id": str(user.id),
src/auth/utils.py CHANGED
@@ -4,13 +4,15 @@ import os
4
  import uuid
5
  from email.mime.text import MIMEText
6
  from passlib.context import CryptContext
 
 
7
  from jose import jwt, JWTError
8
- from fastapi.security import OAuth2PasswordBearer
9
  from datetime import datetime, timedelta
10
  from cryptography.fernet import Fernet, InvalidToken
11
  from fastapi import Depends, HTTPException, status
12
- from src.core.config import settings
13
-
14
 
15
 
16
  SECRET_KEY = settings.SECRET_KEY
@@ -26,7 +28,6 @@ FERNET_KEY = settings.FERNET_KEY
26
  VERIFICATION_BASE_URL = settings.VERIFICATION_BASE_URL
27
 
28
 
29
-
30
  pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
31
 
32
 
@@ -40,7 +41,6 @@ def verify_password(plain_password: str, hashed_password: str) -> bool:
40
  return pwd_context.verify(plain_password, hashed_password)
41
 
42
 
43
-
44
  def create_access_token(data: dict):
45
  """Create JWT token with expiry"""
46
  to_encode = data.copy()
@@ -50,7 +50,6 @@ def create_access_token(data: dict):
50
  return encoded_jwt
51
 
52
 
53
-
54
  def send_verification_email(to_email: str, token: str):
55
  """Send email with verification link"""
56
  subject = f"Verify your {settings.APP_NAME} Account"
@@ -69,14 +68,12 @@ def send_verification_email(to_email: str, token: str):
69
  msg["From"] = SMTP_EMAIL
70
  msg["To"] = to_email
71
 
72
-
73
  with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
74
  server.starttls()
75
  server.login(SMTP_EMAIL, SMTP_PASSWORD)
76
  server.send_message(msg)
77
 
78
 
79
-
80
  fernet = Fernet(FERNET_KEY.encode())
81
 
82
 
@@ -106,21 +103,54 @@ async def verify_verification_token(token: str) -> str:
106
  raise ValueError("Invalid verification link")
107
 
108
 
 
109
 
110
- oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
111
 
112
-
113
- def get_current_user(token: str = Depends(oauth2_scheme)):
 
114
  """Decode JWT token and extract current user ID"""
 
 
115
  try:
116
  payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
117
  user_id: str = payload.get("sub")
 
118
  if user_id is None:
119
  raise HTTPException(
120
- status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token"
 
121
  )
122
  return user_id
 
123
  except JWTError:
124
  raise HTTPException(
125
- status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  )
 
 
 
 
 
 
 
 
 
 
 
4
  import uuid
5
  from email.mime.text import MIMEText
6
  from passlib.context import CryptContext
7
+ from src.core.database import get_async_session
8
+ from sqlmodel.ext.asyncio.session import AsyncSession
9
  from jose import jwt, JWTError
10
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
11
  from datetime import datetime, timedelta
12
  from cryptography.fernet import Fernet, InvalidToken
13
  from fastapi import Depends, HTTPException, status
14
+ from src.core.models import Users
15
+ from src.core.config import settings
16
 
17
 
18
  SECRET_KEY = settings.SECRET_KEY
 
28
  VERIFICATION_BASE_URL = settings.VERIFICATION_BASE_URL
29
 
30
 
 
31
  pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
32
 
33
 
 
41
  return pwd_context.verify(plain_password, hashed_password)
42
 
43
 
 
44
  def create_access_token(data: dict):
45
  """Create JWT token with expiry"""
46
  to_encode = data.copy()
 
50
  return encoded_jwt
51
 
52
 
 
53
  def send_verification_email(to_email: str, token: str):
54
  """Send email with verification link"""
55
  subject = f"Verify your {settings.APP_NAME} Account"
 
68
  msg["From"] = SMTP_EMAIL
69
  msg["To"] = to_email
70
 
 
71
  with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
72
  server.starttls()
73
  server.login(SMTP_EMAIL, SMTP_PASSWORD)
74
  server.send_message(msg)
75
 
76
 
 
77
  fernet = Fernet(FERNET_KEY.encode())
78
 
79
 
 
103
  raise ValueError("Invalid verification link")
104
 
105
 
106
+ bearer_scheme = HTTPBearer()
107
 
 
108
 
109
+ def get_current_user(
110
+ credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
111
+ ):
112
  """Decode JWT token and extract current user ID"""
113
+ token = credentials.credentials
114
+
115
  try:
116
  payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
117
  user_id: str = payload.get("sub")
118
+
119
  if user_id is None:
120
  raise HTTPException(
121
+ status_code=status.HTTP_401_UNAUTHORIZED,
122
+ detail="Invalid token: missing user id",
123
  )
124
  return user_id
125
+
126
  except JWTError:
127
  raise HTTPException(
128
+ status_code=status.HTTP_401_UNAUTHORIZED,
129
+ detail="Invalid or expired token",
130
+ )
131
+
132
+
133
+ async def get_current_active_user(
134
+ session: AsyncSession = Depends(get_async_session),
135
+ user_id: str = Depends(get_current_user),
136
+ ) -> Users:
137
+ """Return the full user model for the currently authenticated user."""
138
+ user = await session.get(Users, uuid.UUID(user_id))
139
+ if not user:
140
+ raise HTTPException(
141
+ status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
142
+ )
143
+ if not user.is_verified:
144
+ raise HTTPException(
145
+ status_code=status.HTTP_403_FORBIDDEN, detail="User not verified"
146
  )
147
+ return user
148
+
149
+
150
+ def create_refresh_token(data: dict, expires_days: int = 7):
151
+ """Create a long-lived JWT refresh token"""
152
+ to_encode = data.copy()
153
+ expire = datetime.utcnow() + timedelta(days=expires_days)
154
+ to_encode.update({"exp": expire, "type": "refresh"})
155
+ encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
156
+ return encoded_jwt