Add subcription feature 6
This commit is contained in:
@@ -454,28 +454,33 @@ class AuthService:
|
||||
try:
|
||||
logger.info(f"Validating user subscription, user_id={user.id}, email={user.email}")
|
||||
|
||||
# Check if user has a tenant_id (indicates they should have a subscription)
|
||||
if user.tenant_id:
|
||||
logger.info(f"User has tenant - subscription validation required, user_id={user.id}, tenant_id={user.tenant_id}")
|
||||
# Since tenant relationships are managed by tenant service, we need to call tenant service
|
||||
# to get the user's primary tenant and validate subscription status
|
||||
|
||||
# Get user's primary tenant from tenant service
|
||||
primary_tenant = await self.tenant_client.get_user_primary_tenant(str(user.id))
|
||||
|
||||
if primary_tenant:
|
||||
tenant_id = primary_tenant.get('tenant_id')
|
||||
logger.info(f"User has primary tenant - subscription validation required, user_id={user.id}, tenant_id={tenant_id}")
|
||||
|
||||
# Call tenant service to validate subscription status
|
||||
subscription_status = await self.tenant_client.get_subscription_status(user.tenant_id)
|
||||
subscription_status = await self.tenant_client.get_subscription_status(tenant_id)
|
||||
|
||||
if subscription_status:
|
||||
is_active = subscription_status.get('is_active', False)
|
||||
status = subscription_status.get('status', 'unknown')
|
||||
|
||||
logger.info(f"Subscription status retrieved from tenant service, user_id={user.id}, tenant_id={user.tenant_id}, status={status}, is_active={is_active}")
|
||||
logger.info(f"Subscription status retrieved from tenant service, user_id={user.id}, tenant_id={tenant_id}, status={status}")
|
||||
|
||||
# Consider subscription valid if it's active, trialing, or in grace period
|
||||
valid_statuses = ['active', 'trialing', 'grace_period']
|
||||
return is_active and status in valid_statuses
|
||||
return status in valid_statuses
|
||||
else:
|
||||
logger.warning(f"No subscription status returned from tenant service, user_id={user.id}, tenant_id={user.tenant_id}")
|
||||
logger.warning(f"No subscription status returned from tenant service, user_id={user.id}, tenant_id={tenant_id}")
|
||||
return False
|
||||
else:
|
||||
# Users without tenant might be in registration flow
|
||||
logger.info(f"User without tenant - no subscription validation, user_id={user.id}")
|
||||
# Users without primary tenant might be in registration flow or using free tier
|
||||
logger.info(f"User without primary tenant - no subscription validation required, user_id={user.id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@@ -496,24 +501,32 @@ class AuthService:
|
||||
try:
|
||||
logger.info(f"Getting subscription details, user_id={user.id}")
|
||||
|
||||
if user.tenant_id:
|
||||
# Get user's primary tenant from tenant service
|
||||
primary_tenant = await self.tenant_client.get_user_primary_tenant(str(user.id))
|
||||
|
||||
if primary_tenant:
|
||||
tenant_id = primary_tenant.get('tenant_id')
|
||||
|
||||
# Call tenant service to get subscription details
|
||||
subscription = await self.tenant_client.get_subscription_details(user.tenant_id)
|
||||
subscription = await self.tenant_client.get_subscription_details(tenant_id)
|
||||
|
||||
if subscription:
|
||||
logger.info(f"Subscription details retrieved from tenant service, user_id={user.id}, tenant_id={user.tenant_id}, plan={subscription.get('plan')}, status={subscription.get('status')}")
|
||||
logger.info(f"Subscription details retrieved from tenant service, user_id={user.id}, tenant_id={tenant_id}, plan={subscription.get('plan')}, status={subscription.get('status')}")
|
||||
# Add tenant_id to subscription details for JWT
|
||||
subscription['tenant_id'] = tenant_id
|
||||
return subscription
|
||||
else:
|
||||
logger.warning(f"No subscription details returned from tenant service, user_id={user.id}, tenant_id={user.tenant_id}")
|
||||
logger.warning(f"No subscription details returned from tenant service, user_id={user.id}, tenant_id={tenant_id}")
|
||||
return {
|
||||
"status": "no_subscription",
|
||||
"plan": None,
|
||||
"billing_cycle": None,
|
||||
"current_period_end": None,
|
||||
"trial_period_days": 0
|
||||
"trial_period_days": 0,
|
||||
"tenant_id": tenant_id
|
||||
}
|
||||
else:
|
||||
logger.info(f"User without tenant - no subscription details available, user_id={user.id}")
|
||||
logger.info(f"User without primary tenant - no subscription details available, user_id={user.id}")
|
||||
return {
|
||||
"status": "no_tenant",
|
||||
"plan": None,
|
||||
@@ -531,7 +544,35 @@ class AuthService:
|
||||
"current_period_end": None,
|
||||
"trial_period_days": 0
|
||||
}
|
||||
|
||||
|
||||
async def get_user_by_id(self, user_id: str) -> Optional[User]:
|
||||
"""
|
||||
Get user by ID
|
||||
|
||||
Args:
|
||||
user_id: User ID to retrieve
|
||||
|
||||
Returns:
|
||||
User object if found, None otherwise
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Getting user by ID, user_id={user_id}")
|
||||
|
||||
async with self.database_manager.get_session() as session:
|
||||
user_repo = UserRepository(User, session)
|
||||
user = await user_repo.get_by_id(user_id)
|
||||
|
||||
if user:
|
||||
logger.info(f"User retrieved successfully, user_id={user_id}, email={user.email}")
|
||||
else:
|
||||
logger.warning(f"User not found, user_id={user_id}")
|
||||
|
||||
return user
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Get user by ID failed, error={str(e)}, user_id={user_id}", exc_info=True)
|
||||
raise
|
||||
|
||||
async def _generate_auth_tokens(self, user: User) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate authentication tokens for user
|
||||
@@ -578,6 +619,9 @@ class AuthService:
|
||||
# Generate refresh token using SecurityManager
|
||||
refresh_token = SecurityManager.create_refresh_token(refresh_token_data)
|
||||
|
||||
# ✅ CRITICAL FIX: Store refresh token in Redis for later validation
|
||||
await SecurityManager.store_refresh_token(str(user.id), refresh_token)
|
||||
|
||||
logger.info(f"Auth tokens generated successfully, user_id={user.id}, access_token_length={len(access_token)}, refresh_token_length={len(refresh_token)}")
|
||||
|
||||
return {
|
||||
@@ -595,6 +639,323 @@ class AuthService:
|
||||
detail=f"Token generation failed: {str(e)}"
|
||||
) from e
|
||||
|
||||
async def refresh_auth_tokens(self, refresh_token: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Refresh authentication tokens using a valid refresh token
|
||||
|
||||
Args:
|
||||
refresh_token: Valid refresh token
|
||||
|
||||
Returns:
|
||||
Dictionary with new access and refresh tokens
|
||||
|
||||
Raises:
|
||||
HTTPException: 401 for invalid refresh tokens
|
||||
"""
|
||||
try:
|
||||
logger.info("Refreshing auth tokens using refresh token")
|
||||
|
||||
# Import JWT dependencies
|
||||
from jose import jwt, JWTError
|
||||
from app.core.config import settings
|
||||
from app.core.security import SecurityManager
|
||||
|
||||
# Decode refresh token to get user info
|
||||
payload = jwt.decode(
|
||||
refresh_token,
|
||||
settings.JWT_SECRET_KEY,
|
||||
algorithms=[settings.JWT_ALGORITHM]
|
||||
)
|
||||
|
||||
# Verify this is a refresh token
|
||||
if payload.get("type") != "refresh":
|
||||
logger.warning("Invalid token type for refresh")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token type"
|
||||
)
|
||||
|
||||
user_id = payload.get("user_id")
|
||||
email = payload.get("email")
|
||||
|
||||
if not user_id or not email:
|
||||
logger.warning("Invalid refresh token payload")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid refresh token"
|
||||
)
|
||||
|
||||
# Check if refresh token is valid in Redis using SecurityManager
|
||||
is_valid = await SecurityManager.is_refresh_token_valid(user_id, refresh_token)
|
||||
if not is_valid:
|
||||
logger.warning(f"Invalid or expired refresh token for user {user_id}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid or expired refresh token"
|
||||
)
|
||||
|
||||
logger.info(f"Refresh token validated for user {user_id}, email={email}")
|
||||
|
||||
# Get user from database
|
||||
async with self.database_manager.get_session() as session:
|
||||
user_repo = UserRepository(User, session)
|
||||
user = await user_repo.get_by_id(user_id)
|
||||
|
||||
if not user:
|
||||
logger.warning(f"User not found for refresh token, user_id={user_id}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
if not user.is_active:
|
||||
logger.warning(f"Inactive user attempted token refresh, user_id={user_id}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Account is inactive"
|
||||
)
|
||||
|
||||
# Generate new tokens using existing method
|
||||
tokens = await self._generate_auth_tokens(user)
|
||||
|
||||
logger.info(f"Token refresh successful, user_id={user_id}, email={email}")
|
||||
|
||||
return tokens
|
||||
|
||||
except JWTError as e:
|
||||
logger.warning(f"JWT decode error during refresh: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid refresh token"
|
||||
)
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Token refresh failed: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Token refresh failed: {str(e)}"
|
||||
) from e
|
||||
|
||||
async def verify_access_token(self, access_token: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Verify the validity of an access token
|
||||
|
||||
Args:
|
||||
access_token: Access token to verify
|
||||
|
||||
Returns:
|
||||
Dictionary with token validation result and user info
|
||||
|
||||
Raises:
|
||||
HTTPException: 401 for invalid access tokens
|
||||
"""
|
||||
try:
|
||||
logger.info("Verifying access token")
|
||||
|
||||
# Import JWT dependencies
|
||||
from jose import jwt, JWTError
|
||||
from app.core.config import settings
|
||||
|
||||
# Decode and verify token
|
||||
payload = jwt.decode(
|
||||
access_token,
|
||||
settings.JWT_SECRET_KEY,
|
||||
algorithms=[settings.JWT_ALGORITHM]
|
||||
)
|
||||
|
||||
# Verify this is an access token
|
||||
if payload.get("type") != "access":
|
||||
logger.warning("Invalid token type for verification")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token type"
|
||||
)
|
||||
|
||||
user_id = payload.get("user_id")
|
||||
email = payload.get("email")
|
||||
|
||||
if not user_id or not email:
|
||||
logger.warning("Invalid access token payload")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid access token"
|
||||
)
|
||||
|
||||
logger.info(f"Token verification successful, user_id={user_id}, email={email}")
|
||||
|
||||
return {
|
||||
"valid": True,
|
||||
"user_id": user_id,
|
||||
"email": email
|
||||
}
|
||||
|
||||
except JWTError as e:
|
||||
logger.warning(f"JWT decode error during verification: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid access token"
|
||||
)
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Token verification failed: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Token verification failed: {str(e)}"
|
||||
) from e
|
||||
|
||||
async def revoke_refresh_token(self, refresh_token: str) -> bool:
|
||||
"""
|
||||
Revoke a refresh token
|
||||
|
||||
Args:
|
||||
refresh_token: Refresh token to revoke
|
||||
|
||||
Returns:
|
||||
True if revocation was successful or token didn't exist
|
||||
|
||||
Raises:
|
||||
Exception: For unexpected errors
|
||||
"""
|
||||
try:
|
||||
logger.info("Revoking refresh token")
|
||||
|
||||
# Try to decode token to get user_id
|
||||
from jose import jwt, JWTError
|
||||
from app.core.config import settings
|
||||
|
||||
user_id = "unknown"
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
refresh_token,
|
||||
settings.JWT_SECRET_KEY,
|
||||
algorithms=[settings.JWT_ALGORITHM]
|
||||
)
|
||||
user_id = payload.get("user_id") or "unknown"
|
||||
except JWTError as e:
|
||||
logger.warning(f"Could not decode refresh token during revocation: {str(e)}")
|
||||
|
||||
# Revoke the token using SecurityManager
|
||||
await SecurityManager.revoke_refresh_token(user_id, refresh_token)
|
||||
logger.info(f"Refresh token revoked successfully, user_id={user_id}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error revoking refresh token: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Token revocation failed: {str(e)}"
|
||||
) from e
|
||||
|
||||
async def change_user_password(self, user_id: str, current_password: str, new_password: str) -> bool:
|
||||
"""
|
||||
Change user password
|
||||
|
||||
Args:
|
||||
user_id: User ID
|
||||
current_password: Current password for verification
|
||||
new_password: New password to set
|
||||
|
||||
Returns:
|
||||
True if password was changed successfully
|
||||
|
||||
Raises:
|
||||
HTTPException: 401 for invalid current password, 404 for user not found
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Changing password for user {user_id}")
|
||||
|
||||
async with self.database_manager.get_session() as session:
|
||||
user_repo = UserRepository(User, session)
|
||||
|
||||
# Get current user
|
||||
user = await user_repo.get_by_id(user_id)
|
||||
if not user:
|
||||
logger.warning(f"User not found for password change, user_id={user_id}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
# Verify current password
|
||||
from app.core.security import verify_password
|
||||
is_valid = verify_password(current_password, user.hashed_password)
|
||||
|
||||
if not is_valid:
|
||||
logger.warning(f"Invalid current password for user {user_id}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Current password is incorrect"
|
||||
)
|
||||
|
||||
# Update password
|
||||
await user_repo.update_password(user_id, new_password)
|
||||
|
||||
logger.info(f"Password change successful, user_id={user_id}, email={user.email}")
|
||||
|
||||
return True
|
||||
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Password change failed: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Password change failed: {str(e)}"
|
||||
) from e
|
||||
|
||||
async def verify_user_email(self, email: str, verification_token: str) -> bool:
|
||||
"""
|
||||
Verify user email address
|
||||
|
||||
Args:
|
||||
email: User email
|
||||
verification_token: Email verification token
|
||||
|
||||
Returns:
|
||||
True if email was verified successfully
|
||||
|
||||
Raises:
|
||||
HTTPException: 404 for user not found
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Verifying email for {email}")
|
||||
|
||||
async with self.database_manager.get_session() as session:
|
||||
user_repo = UserRepository(User, session)
|
||||
|
||||
# Find user by email
|
||||
user = await user_repo.get_by_email(email)
|
||||
if not user:
|
||||
logger.warning(f"User not found for email verification, email={email}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
# TODO: Implement actual email verification token logic
|
||||
# For now, just mark as verified
|
||||
await user_repo.update_user(user.id, {"is_verified": True})
|
||||
|
||||
logger.info(f"Email verification successful, user_id={user.id}, email={email}")
|
||||
|
||||
return True
|
||||
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Email verification failed: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Email verification failed: {str(e)}"
|
||||
) from e
|
||||
|
||||
async def request_password_reset(self, email: str) -> bool:
|
||||
"""
|
||||
Request a password reset for a user
|
||||
|
||||
@@ -10,7 +10,8 @@ from fastapi import HTTPException, status
|
||||
import structlog
|
||||
|
||||
from app.repositories import UserRepository, TokenRepository
|
||||
from app.schemas.auth import UserResponse, UserUpdate
|
||||
from app.schemas.auth import UserResponse
|
||||
from app.schemas.users import UserUpdate
|
||||
from app.models.users import User
|
||||
from app.models.tokens import RefreshToken
|
||||
from app.core.security import SecurityManager
|
||||
|
||||
Reference in New Issue
Block a user