feat: Add base project structure with API and web components

This commit is contained in:
Ke Sun
2025-12-02 20:28:01 +08:00
parent f3de6d6cc9
commit c1adc62ec6
817 changed files with 111226 additions and 106 deletions

View File

@@ -0,0 +1,160 @@
from typing import Optional
import json
from datetime import datetime, timedelta, timezone
from app.aioRedis import aio_redis_set, aio_redis_get, aio_redis_delete
from app.core.config import settings
class SessionService:
"""用户会话管理服务"""
@staticmethod
def _get_user_session_key(username: str) -> str:
"""获取用户会话的Redis键"""
return f"user_session:{username}"
@staticmethod
def _get_token_blacklist_key(token_id: str) -> str:
"""获取token黑名单的Redis键"""
return f"token_blacklist:{token_id}"
@staticmethod
async def set_user_active_session(username: str, token_id: str, expires_at: datetime) -> None:
"""设置用户的活跃会话
Args:
username: 用户名
token_id: token的唯一标识
expires_at: token过期时间
"""
if not settings.ENABLE_SINGLE_SESSION:
return
session_key = SessionService._get_user_session_key(username)
session_data = {
"token_id": token_id,
"created_at": datetime.now(timezone.utc).isoformat(),
"expires_at": expires_at.isoformat()
}
# 计算过期时间(秒)
expire_seconds = int((expires_at - datetime.now(timezone.utc)).total_seconds())
if expire_seconds > 0:
await aio_redis_set(session_key, session_data, expire=expire_seconds)
@staticmethod
async def get_user_active_session(username: str) -> Optional[dict]:
"""获取用户的活跃会话
Args:
username: 用户名
Returns:
会话数据字典或None
"""
if not settings.ENABLE_SINGLE_SESSION:
return None
session_key = SessionService._get_user_session_key(username)
session_data = await aio_redis_get(session_key)
if session_data:
try:
return json.loads(session_data) if isinstance(session_data, str) else session_data
except json.JSONDecodeError:
return None
return None
@staticmethod
async def invalidate_old_session(username: str, new_token_id: str) -> None:
"""使旧会话失效
Args:
username: 用户名
new_token_id: 新token的ID
"""
if not settings.ENABLE_SINGLE_SESSION:
return
# 获取当前活跃会话
current_session = await SessionService.get_user_active_session(username)
if current_session and current_session.get("token_id") != new_token_id:
# 将旧token加入黑名单
old_token_id = current_session.get("token_id")
if old_token_id:
await SessionService.blacklist_token(old_token_id)
@staticmethod
async def blacklist_token(token_id: str, expire_seconds: int = None) -> None:
"""将token加入黑名单
Args:
token_id: token的唯一标识
expire_seconds: 黑名单过期时间默认为refresh token的过期时间
"""
if expire_seconds is None:
# 默认使用refresh token的过期时间
expire_seconds = settings.REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60
blacklist_key = SessionService._get_token_blacklist_key(token_id)
await aio_redis_set(blacklist_key, "blacklisted", expire=expire_seconds)
@staticmethod
async def is_token_blacklisted(token_id: str) -> bool:
"""检查token是否在黑名单中
Args:
token_id: token的唯一标识
Returns:
True如果token在黑名单中否则False
"""
if not settings.ENABLE_SINGLE_SESSION:
return False
blacklist_key = SessionService._get_token_blacklist_key(token_id)
result = await aio_redis_get(blacklist_key)
return result is not None
@staticmethod
async def clear_user_session(username: str) -> None:
"""清除用户会话
Args:
username: 用户名
"""
session_key = SessionService._get_user_session_key(username)
await aio_redis_delete(session_key)
@staticmethod
async def invalidate_all_user_tokens(user_id: str) -> None:
"""使用户的所有 tokens 失效(用于密码重置等场景)
通过在 Redis 中设置一个用户级别的失效标记来实现。
所有在此时间点之前签发的 tokens 都将被视为无效。
Args:
user_id: 用户ID
"""
invalidation_key = f"user_token_invalidation:{user_id}"
current_time = datetime.now(timezone.utc).isoformat()
# 设置失效时间戳,过期时间为 refresh token 的最大有效期
expire_seconds = settings.REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60
await aio_redis_set(invalidation_key, current_time, expire=expire_seconds)
@staticmethod
async def get_user_token_invalidation_time(user_id: str) -> Optional[str]:
"""获取用户 token 失效时间
Args:
user_id: 用户ID
Returns:
失效时间的 ISO 格式字符串,如果没有失效记录则返回 None
"""
invalidation_key = f"user_token_invalidation:{user_id}"
result = await aio_redis_get(invalidation_key)
return result if result else None