Spaces:
Sleeping
Sleeping
feat:Added refresh token
Browse files- src/auth/router.py +84 -4
- src/auth/schemas.py +4 -1
- src/auth/service.py +77 -18
- 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 |
-
|
| 9 |
verify_email,
|
|
|
|
| 10 |
login_user,
|
| 11 |
)
|
| 12 |
-
from .
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 17 |
-
|
| 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 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
|
|
|
| 58 |
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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
|
| 9 |
from datetime import datetime, timedelta
|
| 10 |
from cryptography.fernet import Fernet, InvalidToken
|
| 11 |
from fastapi import Depends, HTTPException, status
|
| 12 |
-
from src.core.
|
| 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 |
-
|
|
|
|
| 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,
|
|
|
|
| 121 |
)
|
| 122 |
return user_id
|
|
|
|
| 123 |
except JWTError:
|
| 124 |
raise HTTPException(
|
| 125 |
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|