Add subcription feature 3

This commit is contained in:
Urtzi Alfaro
2026-01-15 20:45:49 +01:00
parent a4c3b7da3f
commit b674708a4c
83 changed files with 9451 additions and 6828 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@ from datetime import datetime, timezone
from pydantic import BaseModel
from app.core.database import get_db
from app.services.user_service import UserService
from app.services.user_service import EnhancedUserService
from app.repositories.onboarding_repository import OnboardingRepository
from shared.auth.decorators import get_current_user_dep
@@ -150,7 +150,7 @@ class OnboardingService:
def __init__(self, db: AsyncSession):
self.db = db
self.user_service = UserService(db)
self.user_service = EnhancedUserService(db)
self.onboarding_repo = OnboardingRepository(db)
async def get_user_progress(self, user_id: str) -> UserProgress:

View File

@@ -12,7 +12,7 @@ from datetime import datetime, timezone
from app.core.database import get_db, get_background_db_session
from app.schemas.auth import UserResponse, PasswordChange
from app.schemas.users import UserUpdate, BatchUserRequest, OwnerUserCreate
from app.services.user_service import UserService, EnhancedUserService
from app.services.user_service import EnhancedUserService
from app.models.users import User
from sqlalchemy.ext.asyncio import AsyncSession
@@ -515,7 +515,7 @@ async def update_user_tenant(
user_id=user_id,
tenant_id=tenant_id)
user_service = UserService(db)
user_service = EnhancedUserService(db)
user = await user_service.get_user_by_id(uuid.UUID(user_id), session=db)
if not user:

View File

@@ -87,7 +87,7 @@ async def get_db_health() -> bool:
return True
except Exception as e:
logger.error("Database health check failed", error=str(e))
logger.error(f"Database health check failed: {str(e)}")
return False
async def create_tables():
@@ -173,7 +173,7 @@ class AuthDatabaseUtils:
}
except Exception as e:
logger.error("Failed to get auth statistics", error=str(e))
logger.error(f"Failed to get auth statistics: {str(e)}")
return {
"active_users": 0,
"inactive_users": 0,
@@ -234,7 +234,7 @@ async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
logger.debug("Database session created")
yield session
except Exception as e:
logger.error("Database session error", error=str(e), exc_info=True)
logger.error(f"Database session error: {str(e)}", exc_info=True)
await session.rollback()
raise
finally:
@@ -257,7 +257,7 @@ async def initialize_auth_database():
logger.info("Auth service database initialized successfully")
except Exception as e:
logger.error("Failed to initialize auth service database", error=str(e))
logger.error(f"Failed to initialize auth service database: {str(e)}")
raise
# Database cleanup for auth service
@@ -273,7 +273,7 @@ async def cleanup_auth_database():
logger.info("Auth service database cleanup completed")
except Exception as e:
logger.error("Failed to cleanup auth service database", error=str(e))
logger.error(f"Failed to cleanup auth service database: {str(e)}")
# Export the commonly used items to maintain compatibility
__all__ = [

View File

@@ -303,8 +303,9 @@ class SecurityManager:
async def track_login_attempt(email: str, ip_address: str, success: bool) -> None:
"""Track login attempts for security monitoring"""
try:
redis_client = await get_redis_client()
key = f"login_attempts:{email}:{ip_address}"
if success:
# Clear failed attempts on successful login
await redis_client.delete(key)
@@ -314,14 +315,16 @@ class SecurityManager:
if attempts == 1:
# Set expiration on first failed attempt
await redis_client.expire(key, settings.LOCKOUT_DURATION_MINUTES * 60)
if attempts >= settings.MAX_LOGIN_ATTEMPTS:
logger.warning(f"Account locked for {email} from {ip_address}")
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail=f"Too many failed login attempts. Try again in {settings.LOCKOUT_DURATION_MINUTES} minutes."
)
except HTTPException:
raise # Re-raise HTTPException
except Exception as e:
logger.error(f"Failed to track login attempt: {e}")
@@ -329,16 +332,17 @@ class SecurityManager:
async def is_account_locked(email: str, ip_address: str) -> bool:
"""Check if account is locked due to failed login attempts"""
try:
redis_client = await get_redis_client()
key = f"login_attempts:{email}:{ip_address}"
attempts = await redis_client.get(key)
if attempts:
attempts = int(attempts)
return attempts >= settings.MAX_LOGIN_ATTEMPTS
except Exception as e:
logger.error(f"Failed to check account lock status: {e}")
return False
@staticmethod
@@ -364,31 +368,34 @@ class SecurityManager:
async def check_login_attempts(email: str) -> bool:
"""Check if user has exceeded login attempts"""
try:
redis_client = await get_redis_client()
key = f"login_attempts:{email}"
attempts = await redis_client.get(key)
if attempts is None:
return True
return int(attempts) < settings.MAX_LOGIN_ATTEMPTS
except Exception as e:
logger.error(f"Error checking login attempts: {e}")
return True # Allow on error
@staticmethod
async def increment_login_attempts(email: str) -> None:
"""Increment login attempts for email"""
try:
redis_client = await get_redis_client()
key = f"login_attempts:{email}"
await redis_client.incr(key)
await redis_client.expire(key, settings.LOCKOUT_DURATION_MINUTES * 60)
except Exception as e:
logger.error(f"Error incrementing login attempts: {e}")
@staticmethod
async def clear_login_attempts(email: str) -> None:
"""Clear login attempts for email after successful login"""
try:
redis_client = await get_redis_client()
key = f"login_attempts:{email}"
await redis_client.delete(key)
logger.debug(f"Cleared login attempts for {email}")
@@ -399,39 +406,42 @@ class SecurityManager:
async def store_refresh_token(user_id: str, token: str) -> None:
"""Store refresh token in Redis"""
try:
redis_client = await get_redis_client()
token_hash = SecurityManager.hash_api_key(token) # Reuse hash method
key = f"refresh_token:{user_id}:{token_hash}"
# Store with expiration matching JWT refresh token expiry
expire_seconds = settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60
await redis_client.setex(key, expire_seconds, "valid")
except Exception as e:
logger.error(f"Error storing refresh token: {e}")
@staticmethod
async def is_refresh_token_valid(user_id: str, token: str) -> bool:
"""Check if refresh token is still valid in Redis"""
try:
redis_client = await get_redis_client()
token_hash = SecurityManager.hash_api_key(token)
key = f"refresh_token:{user_id}:{token_hash}"
exists = await redis_client.exists(key)
return bool(exists)
except Exception as e:
logger.error(f"Error checking refresh token validity: {e}")
return False
@staticmethod
async def revoke_refresh_token(user_id: str, token: str) -> None:
"""Revoke refresh token by removing from Redis"""
try:
redis_client = await get_redis_client()
token_hash = SecurityManager.hash_api_key(token)
key = f"refresh_token:{user_id}:{token_hash}"
await redis_client.delete(key)
logger.debug(f"Revoked refresh token for user {user_id}")
except Exception as e:
logger.error(f"Error revoking refresh token: {e}")

View File

@@ -5,15 +5,13 @@ Business logic services for authentication and user management
from .auth_service import AuthService
from .auth_service import EnhancedAuthService
from .user_service import UserService
from .auth_service import EnhancedUserService
from .user_service import EnhancedUserService
from .auth_service_clients import AuthServiceClientFactory
from .admin_delete import AdminUserDeleteService
__all__ = [
"AuthService",
"EnhancedAuthService",
"UserService",
"EnhancedUserService",
"AuthServiceClientFactory",
"AdminUserDeleteService"

File diff suppressed because it is too large Load Diff

View File

@@ -43,7 +43,7 @@ class AuthTenantServiceClient(BaseServiceClient):
result = await self.get(f"tenants/user/{user_id}")
return result.get("memberships", []) if result else []
except Exception as e:
logger.error("Failed to get user tenants", user_id=user_id, error=str(e))
logger.error(f"Failed to get user tenants: {str(e)}, user_id: {user_id}")
return []
async def get_user_owned_tenants(self, user_id: str) -> Optional[List[Dict[str, Any]]]:
@@ -54,7 +54,7 @@ class AuthTenantServiceClient(BaseServiceClient):
return [m for m in memberships if m.get('role') == 'owner']
return []
except Exception as e:
logger.error("Failed to get owned tenants", user_id=user_id, error=str(e))
logger.error(f"Failed to get owned tenants: {str(e)}, user_id: {user_id}")
return []
async def transfer_tenant_ownership(
@@ -81,7 +81,7 @@ class AuthTenantServiceClient(BaseServiceClient):
try:
return await self.delete(f"tenants/{tenant_id}")
except Exception as e:
logger.error("Failed to delete tenant", tenant_id=tenant_id, error=str(e))
logger.error(f"Failed to delete tenant: {str(e)}, tenant_id: {tenant_id}")
return None
async def delete_user_memberships(self, user_id: str) -> Optional[Dict[str, Any]]:
@@ -89,7 +89,7 @@ class AuthTenantServiceClient(BaseServiceClient):
try:
return await self.delete(f"/tenants/user/{user_id}/memberships")
except Exception as e:
logger.error("Failed to delete user memberships", user_id=user_id, error=str(e))
logger.error(f"Failed to delete user memberships: {str(e)}, user_id: {user_id}")
return None
async def get_tenant_members(self, tenant_id: str) -> Optional[List[Dict[str, Any]]]:
@@ -98,7 +98,7 @@ class AuthTenantServiceClient(BaseServiceClient):
result = await self.get(f"tenants/{tenant_id}/members")
return result.get("members", []) if result else []
except Exception as e:
logger.error("Failed to get tenant members", tenant_id=tenant_id, error=str(e))
logger.error(f"Failed to get tenant members: {str(e)}, tenant_id: {tenant_id}")
return []
async def check_tenant_has_other_admins(self, tenant_id: str, excluding_user_id: str) -> bool:

View File

@@ -34,28 +34,29 @@ class EnhancedUserService:
if session:
# Use provided session (for direct session injection)
user_repo = UserRepository(User, session)
user = await user_repo.get_by_id(user_id)
else:
# Use database manager to get session
async with self.database_manager.get_session() as session:
user_repo = UserRepository(User, session)
user = await user_repo.get_by_id(user_id)
async with self.database_manager.get_session() as db_session:
user_repo = UserRepository(User, db_session)
user = await user_repo.get_by_id(user_id)
if not user:
return None
return UserResponse(
id=str(user.id),
email=user.email,
full_name=user.full_name,
is_active=user.is_active,
is_verified=user.is_verified,
created_at=user.created_at,
role=user.role,
phone=getattr(user, 'phone', None),
language=getattr(user, 'language', None),
timezone=getattr(user, 'timezone', None)
)
created_at=user.created_at,
role=user.role,
phone=getattr(user, 'phone', None),
language=getattr(user, 'language', None),
timezone=getattr(user, 'timezone', None)
)
except Exception as e:
logger.error("Failed to get user by ID using repository pattern",
user_id=user_id,
@@ -521,6 +522,3 @@ class EnhancedUserService:
error=str(e))
return {"error": str(e)}
# Legacy compatibility - alias EnhancedUserService as UserService
UserService = EnhancedUserService