Add subcription feature 6
This commit is contained in:
@@ -104,7 +104,7 @@ async def request_account_deletion(
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
cancel_response = await client.get(
|
||||
f"http://tenant-service:8000/api/v1/subscriptions/{tenant_id}/status",
|
||||
f"http://tenant-service:8000/api/v1/tenants/{tenant_id}/subscription/status",
|
||||
headers={"Authorization": request.headers.get("Authorization")}
|
||||
)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ from shared.exceptions.auth_exceptions import (
|
||||
RegistrationError,
|
||||
PaymentOrchestrationError
|
||||
)
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
|
||||
# Configure logging
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -291,8 +292,15 @@ async def login(
|
||||
|
||||
logger.info(f"Login successful, email={login_data.email}, user_id={result['user'].id}")
|
||||
|
||||
# Extract tokens from result for top-level response
|
||||
tokens = result.get('tokens', {})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"access_token": tokens.get('access_token'),
|
||||
"refresh_token": tokens.get('refresh_token'),
|
||||
"token_type": tokens.get('token_type'),
|
||||
"expires_in": tokens.get('expires_in'),
|
||||
"user": {
|
||||
"id": str(result['user'].id),
|
||||
"email": result['user'].email,
|
||||
@@ -300,7 +308,6 @@ async def login(
|
||||
"is_active": result['user'].is_active,
|
||||
"last_login": result['user'].last_login.isoformat() if result['user'].last_login else None
|
||||
},
|
||||
"tokens": result.get('tokens', {}),
|
||||
"subscription": result.get('subscription', {}),
|
||||
"message": "Login successful"
|
||||
}
|
||||
@@ -317,3 +324,309 @@ async def login(
|
||||
) from e
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# TOKEN MANAGEMENT ENDPOINTS - NEWLY ADDED
|
||||
# ============================================================================
|
||||
|
||||
@router.post("/refresh",
|
||||
response_model=Dict[str, Any],
|
||||
summary="Refresh access token using refresh token")
|
||||
async def refresh_token(
|
||||
request: Request,
|
||||
refresh_data: Dict[str, Any],
|
||||
auth_service: AuthService = Depends(get_auth_service)
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Refresh access token using a valid refresh token
|
||||
|
||||
This endpoint:
|
||||
1. Validates the refresh token
|
||||
2. Generates new access and refresh tokens
|
||||
3. Returns the new tokens
|
||||
|
||||
Args:
|
||||
refresh_data: Dictionary containing refresh_token
|
||||
|
||||
Returns:
|
||||
New authentication tokens
|
||||
|
||||
Raises:
|
||||
HTTPException: 401 for invalid refresh tokens
|
||||
"""
|
||||
try:
|
||||
logger.info("Token refresh request initiated")
|
||||
|
||||
# Extract refresh token from request
|
||||
refresh_token = refresh_data.get("refresh_token")
|
||||
if not refresh_token:
|
||||
logger.warning("Refresh token missing from request")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Refresh token is required"
|
||||
)
|
||||
|
||||
# Use service layer to refresh tokens
|
||||
tokens = await auth_service.refresh_auth_tokens(refresh_token)
|
||||
|
||||
logger.info("Token refresh successful via service layer")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"access_token": tokens.get("access_token"),
|
||||
"refresh_token": tokens.get("refresh_token"),
|
||||
"token_type": "bearer",
|
||||
"expires_in": 1800, # 30 minutes
|
||||
"message": "Token refresh successful"
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
||||
@router.post("/verify",
|
||||
response_model=Dict[str, Any],
|
||||
summary="Verify token validity")
|
||||
async def verify_token(
|
||||
request: Request,
|
||||
token_data: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Verify the validity of an access token
|
||||
|
||||
Args:
|
||||
token_data: Dictionary containing access_token
|
||||
|
||||
Returns:
|
||||
Token validation result
|
||||
"""
|
||||
try:
|
||||
logger.info("Token verification request initiated")
|
||||
|
||||
# Extract access token from request
|
||||
access_token = token_data.get("access_token")
|
||||
if not access_token:
|
||||
logger.warning("Access token missing from verification request")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Access token is required"
|
||||
)
|
||||
|
||||
# Use service layer to verify token
|
||||
result = await auth_service.verify_access_token(access_token)
|
||||
|
||||
logger.info("Token verification successful via service layer")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"valid": result.get("valid"),
|
||||
"user_id": result.get("user_id"),
|
||||
"email": result.get("email"),
|
||||
"message": "Token is valid"
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
||||
@router.post("/logout",
|
||||
response_model=Dict[str, Any],
|
||||
summary="Logout and revoke refresh token")
|
||||
async def logout(
|
||||
request: Request,
|
||||
logout_data: Dict[str, Any],
|
||||
auth_service: AuthService = Depends(get_auth_service)
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Logout user and revoke refresh token
|
||||
|
||||
Args:
|
||||
logout_data: Dictionary containing refresh_token
|
||||
|
||||
Returns:
|
||||
Logout confirmation
|
||||
"""
|
||||
try:
|
||||
logger.info("Logout request initiated")
|
||||
|
||||
# Extract refresh token from request
|
||||
refresh_token = logout_data.get("refresh_token")
|
||||
if not refresh_token:
|
||||
logger.warning("Refresh token missing from logout request")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Refresh token is required"
|
||||
)
|
||||
|
||||
# Use service layer to revoke refresh token
|
||||
try:
|
||||
await auth_service.revoke_refresh_token(refresh_token)
|
||||
logger.info("Logout successful via service layer")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Logout successful"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during logout: {str(e)}")
|
||||
# Don't fail logout if revocation fails
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Logout successful (token revocation failed but user logged out)"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Logout failed: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Logout failed: {str(e)}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.post("/change-password",
|
||||
response_model=Dict[str, Any],
|
||||
summary="Change user password")
|
||||
async def change_password(
|
||||
request: Request,
|
||||
password_data: Dict[str, Any],
|
||||
auth_service: AuthService = Depends(get_auth_service)
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Change user password
|
||||
|
||||
Args:
|
||||
password_data: Dictionary containing current_password and new_password
|
||||
|
||||
Returns:
|
||||
Password change confirmation
|
||||
"""
|
||||
try:
|
||||
logger.info("Password change request initiated")
|
||||
|
||||
# Extract user from request state
|
||||
if not hasattr(request.state, 'user') or not request.state.user:
|
||||
logger.warning("Unauthorized password change attempt - no user context")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Authentication required"
|
||||
)
|
||||
|
||||
user_id = request.state.user.get("user_id")
|
||||
if not user_id:
|
||||
logger.warning("Unauthorized password change attempt - no user_id")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid user context"
|
||||
)
|
||||
|
||||
# Extract password data
|
||||
current_password = password_data.get("current_password")
|
||||
new_password = password_data.get("new_password")
|
||||
|
||||
if not current_password or not new_password:
|
||||
logger.warning("Password change missing required fields")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Current password and new password are required"
|
||||
)
|
||||
|
||||
if len(new_password) < 8:
|
||||
logger.warning("New password too short")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="New password must be at least 8 characters long"
|
||||
)
|
||||
|
||||
# Use service layer to change password
|
||||
await auth_service.change_user_password(user_id, current_password, new_password)
|
||||
|
||||
logger.info(f"Password change successful via service layer, user_id={user_id}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Password changed successfully"
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
||||
@router.post("/verify-email",
|
||||
response_model=Dict[str, Any],
|
||||
summary="Verify user email")
|
||||
async def verify_email(
|
||||
request: Request,
|
||||
email_data: Dict[str, Any],
|
||||
auth_service: AuthService = Depends(get_auth_service)
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Verify user email (placeholder implementation)
|
||||
|
||||
Args:
|
||||
email_data: Dictionary containing email and verification_token
|
||||
|
||||
Returns:
|
||||
Email verification confirmation
|
||||
"""
|
||||
try:
|
||||
logger.info("Email verification request initiated")
|
||||
|
||||
# Extract email and token
|
||||
email = email_data.get("email")
|
||||
verification_token = email_data.get("verification_token")
|
||||
|
||||
if not email or not verification_token:
|
||||
logger.warning("Email verification missing required fields")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Email and verification token are required"
|
||||
)
|
||||
|
||||
# Use service layer to verify email
|
||||
await auth_service.verify_user_email(email, verification_token)
|
||||
|
||||
logger.info("Email verification successful via service layer")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Email verified successfully"
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -238,6 +238,116 @@ async def get_user_by_id(
|
||||
)
|
||||
|
||||
|
||||
@router.put("/api/v1/auth/users/{user_id}", response_model=UserResponse)
|
||||
async def update_user_profile(
|
||||
user_id: str = Path(..., description="User ID"),
|
||||
update_data: UserUpdate = Body(..., description="User profile update data"),
|
||||
current_user = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Update user profile information.
|
||||
|
||||
This endpoint allows users to update their profile information including:
|
||||
- Full name
|
||||
- Phone number
|
||||
- Language preference
|
||||
- Timezone
|
||||
|
||||
**Permissions:** Users can update their own profile, admins can update any user's profile
|
||||
"""
|
||||
try:
|
||||
# Validate UUID format
|
||||
try:
|
||||
uuid.UUID(user_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid user ID format"
|
||||
)
|
||||
|
||||
# Check permissions - user can update their own profile, admins can update any
|
||||
if current_user["user_id"] != user_id:
|
||||
# Check if current user has admin privileges
|
||||
user_role = current_user.get("role", "user")
|
||||
if user_role not in ["admin", "super_admin", "manager"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Insufficient permissions to update this user's profile"
|
||||
)
|
||||
|
||||
# Fetch user from database
|
||||
from app.repositories import UserRepository
|
||||
user_repo = UserRepository(User, db)
|
||||
user = await user_repo.get_by_id(user_id)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"User {user_id} not found"
|
||||
)
|
||||
|
||||
# Prepare update data (only include fields that are provided)
|
||||
update_fields = update_data.dict(exclude_unset=True)
|
||||
if not update_fields:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="No update data provided"
|
||||
)
|
||||
|
||||
# Update user
|
||||
updated_user = await user_repo.update(user_id, update_fields)
|
||||
|
||||
logger.info("User profile updated", user_id=user_id, updated_fields=list(update_fields.keys()))
|
||||
|
||||
# Log audit event for user profile update
|
||||
try:
|
||||
# Get tenant_id from current_user or use a placeholder for system-level operations
|
||||
tenant_id_str = current_user.get("tenant_id", "00000000-0000-0000-0000-000000000000")
|
||||
await audit_logger.log_event(
|
||||
db_session=db,
|
||||
tenant_id=tenant_id_str,
|
||||
user_id=current_user["user_id"],
|
||||
action=AuditAction.UPDATE.value,
|
||||
resource_type="user",
|
||||
resource_id=user_id,
|
||||
severity=AuditSeverity.MEDIUM.value,
|
||||
description=f"User {current_user.get('email', current_user['user_id'])} updated profile for user {user.email}",
|
||||
changes={"updated_fields": list(update_fields.keys())},
|
||||
audit_metadata={"updated_data": update_fields},
|
||||
endpoint="/users/{user_id}",
|
||||
method="PUT"
|
||||
)
|
||||
except Exception as audit_error:
|
||||
logger.warning("Failed to log audit event", error=str(audit_error))
|
||||
|
||||
return UserResponse(
|
||||
id=str(updated_user.id),
|
||||
email=updated_user.email,
|
||||
full_name=updated_user.full_name,
|
||||
is_active=updated_user.is_active,
|
||||
is_verified=updated_user.is_verified,
|
||||
phone=updated_user.phone,
|
||||
language=updated_user.language or "es",
|
||||
timezone=updated_user.timezone or "Europe/Madrid",
|
||||
created_at=updated_user.created_at,
|
||||
last_login=updated_user.last_login,
|
||||
role=updated_user.role,
|
||||
tenant_id=None,
|
||||
payment_customer_id=updated_user.payment_customer_id,
|
||||
default_payment_method_id=updated_user.default_payment_method_id
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Update user profile error", user_id=user_id, error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to update user profile"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/v1/auth/users/create-by-owner", response_model=UserResponse)
|
||||
async def create_user_by_owner(
|
||||
user_data: OwnerUserCreate,
|
||||
@@ -524,21 +634,20 @@ async def update_user_tenant(
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
# Update user's tenant_id
|
||||
user.tenant_id = uuid.UUID(tenant_id)
|
||||
user.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
|
||||
logger.info("Successfully updated user tenant_id",
|
||||
user_id=user_id,
|
||||
tenant_id=tenant_id)
|
||||
# DEPRECATED: User-tenant relationships are now managed by tenant service
|
||||
# This endpoint is kept for backward compatibility but does nothing
|
||||
# The tenant service should manage user-tenant relationships internally
|
||||
|
||||
logger.warning("DEPRECATED: update_user_tenant endpoint called - user-tenant relationships are now managed by tenant service",
|
||||
user_id=user_id,
|
||||
tenant_id=tenant_id)
|
||||
|
||||
# Return success for backward compatibility, but don't actually update anything
|
||||
return {
|
||||
"success": True,
|
||||
"user_id": str(user.id),
|
||||
"tenant_id": str(user.tenant_id)
|
||||
"tenant_id": tenant_id,
|
||||
"message": "User-tenant relationships are now managed by tenant service. This endpoint is deprecated."
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
|
||||
Reference in New Issue
Block a user