refractor redis service

This commit is contained in:
2026-01-06 23:42:12 +03:00
parent 2e14a7f364
commit 423ce8bc62
5 changed files with 109 additions and 27 deletions
+1
View File
@@ -21,6 +21,7 @@ class Settings(BaseSettings):
SECRET_KEY: str SECRET_KEY: str
ALGORITHM: str ALGORITHM: str
ACCESS_TOKEN_EXPIRE_MINUTES: int = 15 ACCESS_TOKEN_EXPIRE_MINUTES: int = 15
EMAIL_TOKEN_EXPIRE_MINUTES: int = 60
REFRESH_TOKEN_EXPIRE_DAYS: int = 30 REFRESH_TOKEN_EXPIRE_DAYS: int = 30
SMTP_SERVER: str SMTP_SERVER: str
+1 -1
View File
@@ -16,7 +16,7 @@ class EmailService:
template_path="confirm_email.html", template_path="confirm_email.html",
username=username, username=username,
url=url, url=url,
expire_minutes=60, expire_minutes=settings.EMAIL_TOKEN_EXPIRE_MINUTES,
company_name=settings.COMPANY_NAME company_name=settings.COMPANY_NAME
) )
body = EmailClient.render( body = EmailClient.render(
+72
View File
@@ -0,0 +1,72 @@
import logging
import uuid
from app.utils.redis import get_redis
log = logging.getLogger(__name__)
class RefreshTokenStorage:
PREFIX: str = "refresh"
@classmethod
async def save_token(cls, token: uuid.UUID, user_id: int, ttl: int):
redis_client = await get_redis()
await redis_client.setex(f"{cls.PREFIX}:{token}", ttl, user_id)
await redis_client.sadd(f"user:{user_id}:{cls.PREFIX}", str(token))
await redis_client.expire(f"user:{user_id}:{cls.PREFIX}", ttl+3600)
log.info("Save new refresh token from redis", extra={"user_id": user_id, "token": token})
@classmethod
async def getdel_token(cls, token: uuid.UUID) -> int:
redis_client = await get_redis()
user_id = await redis_client.getdel(f"{cls.PREFIX}:{token}")
await redis_client.srem(f"user:{user_id}:{cls.PREFIX}", str(token))
log.info("Remove token", extra={"user_id": user_id, "token": token})
return user_id
@classmethod
async def get_token(cls, token: uuid.UUID) -> int:
redis_client = await get_redis()
user_id = await redis_client.get(f"{cls.PREFIX}:{token}")
log.debug("User_id fetched from refresh token", extra={"user_id": user_id, "token": token})
return user_id
@classmethod
async def abort_all_tokens(cls, user_id: int):
redis_client = await get_redis()
log.debug("Start abort all tokens", extra={"user_id": user_id})
tokens = await redis_client.smembers(f"user:{user_id}:{cls.PREFIX}")
for token in tokens:
await redis_client.delete(f"{cls.PREFIX}:{token}")
await redis_client.delete(f"user:{user_id}:{cls.PREFIX}")
log.info("Successfully abort all tokens", extra={"user_id": user_id})
class EmailTokenStorage:
PREFIX: str = "email"
@classmethod
async def save_token(cls, token: uuid.UUID, user_id: int, ttl: int):
redis_client = await get_redis()
await redis_client.setex(f"{cls.PREFIX}:{token}", ttl, user_id)
log.info("Save new refresh token from redis", extra={"user_id": user_id, "token": token})
@classmethod
async def getdel_token(cls, token: uuid.UUID) -> int:
redis_client = await get_redis()
log.debug("User_id fetched from email token", extra={"token": token})
return await redis_client.getdel(f"{cls.PREFIX}:{token}")
+16 -8
View File
@@ -1,7 +1,8 @@
from typing import Dict
import logging import logging
import uuid import uuid
from fastapi import APIRouter, status, Response, Depends, Request from fastapi import APIRouter, status, Response, Depends, Request, HTTPException
from fastapi.security import OAuth2PasswordRequestForm from fastapi.security import OAuth2PasswordRequestForm
from app.users.schemas import UserCreate, User, Token from app.users.schemas import UserCreate, User, Token
@@ -16,18 +17,20 @@ auth_router = APIRouter(prefix="/auth", tags=["Auth"])
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@auth_router.post("/register", status_code=status.HTTP_201_CREATED) @auth_router.post("/register", status_code=status.HTTP_201_CREATED)
async def register(user: UserCreate) -> User: async def register(user: UserCreate) -> User:
return await UserService.register_new_user(user) return await UserService.register_new_user(user)
@auth_router.get("/verify/{token}") @auth_router.get("/verify/{token}")
async def verify_email(token: uuid.UUID): async def verify_email(token: uuid.UUID) -> Dict:
await UserService.verify_email(token) await UserService.verify_email(token)
return {"status": True} return {"status": True, "message": "User successfully verified email"}
@auth_router.post("/send/verify-email")
async def resend_verify_email(user: UserModel = Depends(get_current_user)) -> Dict:
if user.is_verified:
raise HTTPException(status.HTTP_403_FORBIDDEN, detail="Email already verified")
@auth_router.post("/send/verify_email")
async def resend_verify_email(user: UserModel = Depends(get_current_user)):
await UserService.send_verify_email(user) await UserService.send_verify_email(user)
return {"status": True, "message": "Successfully send email letter"} return {"status": True, "message": "Successfully send email letter"}
@@ -72,9 +75,14 @@ async def refresh_token(request: Request, response: Response) -> Token:
return new_token return new_token
@auth_router.post("/logout") @auth_router.post("/logout")
async def logout(request: Request, response: Response, user: UserModel = Depends(get_current_user)): async def logout(request: Request, response: Response, user: UserModel = Depends(get_current_user)) -> Dict:
response.delete_cookie("access_token") response.delete_cookie("access_token")
response.delete_cookie("refresh_token") response.delete_cookie("refresh_token")
await AuthService.logout(uuid.UUID(request.cookies.get("refresh_token"))) await AuthService.logout(uuid.UUID(request.cookies.get("refresh_token")))
return {"status": True, "message": "Logged out successfully"} return {"status": True, "message": "Logged out successfully"}
@auth_router.post("/abort")
async def abort_all_sessions(user: UserModel = Depends(get_current_user)) -> Dict:
await AuthService.abort_all_sessions(user.id)
return {"status": True, "message": "All sessions was aborted"}
+19 -18
View File
@@ -8,6 +8,7 @@ from jose import jwt
from sqlalchemy import or_ from sqlalchemy import or_
from app.utils.hash_password import hash_password, verify_password from app.utils.hash_password import hash_password, verify_password
from app.services.redis_service import RefreshTokenStorage, EmailTokenStorage
from app.utils.redis import get_redis from app.utils.redis import get_redis
from app.exceptions import InvalidTokenException, TokenExpiredException from app.exceptions import InvalidTokenException, TokenExpiredException
from app.users.models import UserModel from app.users.models import UserModel
@@ -23,29 +24,26 @@ log = logging.getLogger(__name__)
class AuthService: class AuthService:
@classmethod @classmethod
async def create_token(cls, user_id: int) -> Token: async def create_token(cls, user_id: int) -> Token:
redis_client = await get_redis()
access_token = cls._create_access_token(user_id) access_token = cls._create_access_token(user_id)
refresh_token_expires = timedelta( refresh_token_expires = timedelta(
days=settings.REFRESH_TOKEN_EXPIRE_DAYS) days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
refresh_token = cls._create_refresh_token() refresh_token = cls._create_refresh_token()
await redis_client.setex(f"refresh:{refresh_token}", int(refresh_token_expires.total_seconds()), user_id) await RefreshTokenStorage.save_token(refresh_token, user_id, int(refresh_token_expires.total_seconds()))
log.info("Token created has user", extra={"user_id": user_id}) log.info("Token created has user", extra={"user_id": user_id})
return Token(access_token=access_token, refresh_token=refresh_token, token_type="bearer") return Token(access_token=access_token, refresh_token=refresh_token, token_type="bearer")
@classmethod @classmethod
async def logout(cls, token: uuid.UUID) -> None: async def logout(cls, token: uuid.UUID) -> None:
redis_client = await get_redis() user_id = await RefreshTokenStorage.getdel_token(token)
user_id = await redis_client.getdel(f"refresh:{token}")
log.info("User logged out", extra={"user_id": user_id}) log.info("User logged out", extra={"user_id": user_id})
@classmethod @classmethod
async def refresh_token(cls, token: uuid.UUID) -> Token: async def refresh_token(cls, token: uuid.UUID) -> Token:
redis_client = await get_redis()
async with async_session_maker() as session: async with async_session_maker() as session:
refresh_session = await redis_client.getdel(f"refresh:{token}") refresh_session = await RefreshTokenStorage.getdel_token(token)
if refresh_session is None: if refresh_session is None:
log.warning("Refresh token not found") log.warning("Refresh token not found")
@@ -61,10 +59,10 @@ class AuthService:
days=settings.REFRESH_TOKEN_EXPIRE_DAYS) days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
refresh_token = cls._create_refresh_token() refresh_token = cls._create_refresh_token()
await redis_client.setex( await RefreshTokenStorage.save_token(
f"refresh:{refresh_token}", refresh_token,
int(refresh_token_expires.total_seconds()), user.id,
user.id int(refresh_token_expires.total_seconds())
) )
await session.commit() await session.commit()
@@ -87,11 +85,9 @@ class AuthService:
log.warning("Authentication failed", extra={"email": email_or_username}) log.warning("Authentication failed", extra={"email": email_or_username})
return None return None
# @classmethod @classmethod
# async def abort_all_sessions(cls, user_id: uuid.UUID): async def abort_all_sessions(cls, user_id: int):
# async with async_session_maker() as session: await RefreshTokenStorage.abort_all_tokens(user_id)
# await RefreshSessionDAO.delete(session, RefreshSessionModel.user_id == user_id)
# await session.commit()
@classmethod @classmethod
def _create_access_token(cls, user_id: int) -> str: def _create_access_token(cls, user_id: int) -> str:
@@ -105,7 +101,7 @@ class AuthService:
return f'Bearer {encoded_jwt}' return f'Bearer {encoded_jwt}'
@classmethod @classmethod
def _create_refresh_token(cls) -> str: def _create_refresh_token(cls) -> uuid.UUID:
return uuid.uuid4() return uuid.uuid4()
@@ -159,8 +155,13 @@ class UserService:
token = cls._create_email_verification_token() token = cls._create_email_verification_token()
url = f"{settings.URL}/api/v1/auth/verify/{token}" url = f"{settings.URL}/api/v1/auth/verify/{token}"
email_token_expires = timedelta(minutes=settings.EMAIL_TOKEN_EXPIRE_MINUTES)
await redis_client.setex(f"email:{token}", timedelta(minutes=60), user.id) await EmailTokenStorage.save_token(
token,
user.id,
int(email_token_expires.total_seconds())
)
EmailTasks.send_verify_email_task.delay(email=user.email, username=user.username, url=url) EmailTasks.send_verify_email_task.delay(email=user.email, username=user.username, url=url)
@@ -173,7 +174,7 @@ class UserService:
async def verify_email(cls, token: uuid.UUID): async def verify_email(cls, token: uuid.UUID):
redis_client = await get_redis() redis_client = await get_redis()
async with async_session_maker() as session: async with async_session_maker() as session:
user_id = await redis_client.getdel(f"email:{token}") user_id = await EmailTokenStorage.getdel_token(token)
if user_id is None: if user_id is None:
raise TokenExpiredException raise TokenExpiredException