Improve the frontend modals
This commit is contained in:
@@ -10,7 +10,6 @@ from datetime import datetime, timezone
|
||||
import structlog
|
||||
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.routing import RouteBuilder
|
||||
from app.core.database import get_db
|
||||
from app.services.admin_delete import AdminUserDeleteService
|
||||
from app.models.users import User
|
||||
@@ -21,7 +20,6 @@ import httpx
|
||||
logger = structlog.get_logger()
|
||||
|
||||
router = APIRouter()
|
||||
route_builder = RouteBuilder('auth')
|
||||
|
||||
|
||||
class AccountDeletionRequest(BaseModel):
|
||||
@@ -39,7 +37,7 @@ class DeletionScheduleResponse(BaseModel):
|
||||
grace_period_days: int = 30
|
||||
|
||||
|
||||
@router.post("/api/v1/users/me/delete/request")
|
||||
@router.delete("/api/v1/auth/me/account")
|
||||
async def request_account_deletion(
|
||||
deletion_request: AccountDeletionRequest,
|
||||
request: Request,
|
||||
@@ -62,7 +60,7 @@ async def request_account_deletion(
|
||||
- Current password verification
|
||||
"""
|
||||
try:
|
||||
user_id = UUID(current_user["sub"])
|
||||
user_id = UUID(current_user["user_id"])
|
||||
user_email = current_user.get("email")
|
||||
|
||||
if deletion_request.confirm_email.lower() != user_email.lower():
|
||||
@@ -149,7 +147,7 @@ async def request_account_deletion(
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"account_deletion_failed",
|
||||
user_id=current_user.get("sub"),
|
||||
user_id=current_user.get("user_id"),
|
||||
error=str(e)
|
||||
)
|
||||
raise HTTPException(
|
||||
@@ -158,7 +156,7 @@ async def request_account_deletion(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/v1/users/me/delete/info")
|
||||
@router.get("/api/v1/auth/me/account/deletion-info")
|
||||
async def get_deletion_info(
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
@@ -170,7 +168,7 @@ async def get_deletion_info(
|
||||
account deletion. Transparency requirement under GDPR.
|
||||
"""
|
||||
try:
|
||||
user_id = UUID(current_user["sub"])
|
||||
user_id = UUID(current_user["user_id"])
|
||||
|
||||
deletion_service = AdminUserDeleteService(db)
|
||||
preview = await deletion_service.preview_user_deletion(str(user_id))
|
||||
@@ -207,7 +205,7 @@ async def get_deletion_info(
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"deletion_info_failed",
|
||||
user_id=current_user.get("sub"),
|
||||
user_id=current_user.get("user_id"),
|
||||
error=str(e)
|
||||
)
|
||||
raise HTTPException(
|
||||
|
||||
@@ -5,6 +5,8 @@ Business logic for login, register, token refresh, password reset, and email ver
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Dict, Any
|
||||
import structlog
|
||||
|
||||
from app.schemas.auth import (
|
||||
@@ -12,16 +14,17 @@ from app.schemas.auth import (
|
||||
PasswordChange, PasswordReset, UserResponse
|
||||
)
|
||||
from app.services.auth_service import EnhancedAuthService
|
||||
from app.models.users import User
|
||||
from app.core.database import get_db
|
||||
from shared.database.base import create_database_manager
|
||||
from shared.monitoring.decorators import track_execution_time
|
||||
from shared.monitoring.metrics import get_metrics_collector
|
||||
from shared.routing import RouteBuilder
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from app.core.config import settings
|
||||
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter(tags=["auth-operations"])
|
||||
security = HTTPBearer()
|
||||
route_builder = RouteBuilder('auth')
|
||||
|
||||
|
||||
def get_auth_service():
|
||||
@@ -30,7 +33,7 @@ def get_auth_service():
|
||||
return EnhancedAuthService(database_manager)
|
||||
|
||||
|
||||
@router.post(route_builder.build_base_route("register", include_tenant_prefix=False), response_model=TokenResponse)
|
||||
@router.post("/api/v1/auth/register", response_model=TokenResponse)
|
||||
@track_execution_time("enhanced_registration_duration_seconds", "auth-service")
|
||||
async def register(
|
||||
user_data: UserRegistration,
|
||||
@@ -100,7 +103,7 @@ async def register(
|
||||
)
|
||||
|
||||
|
||||
@router.post(route_builder.build_base_route("login", include_tenant_prefix=False), response_model=TokenResponse)
|
||||
@router.post("/api/v1/auth/login", response_model=TokenResponse)
|
||||
@track_execution_time("enhanced_login_duration_seconds", "auth-service")
|
||||
async def login(
|
||||
login_data: UserLogin,
|
||||
@@ -164,7 +167,7 @@ async def login(
|
||||
)
|
||||
|
||||
|
||||
@router.post(route_builder.build_base_route("refresh", include_tenant_prefix=False))
|
||||
@router.post("/api/v1/auth/refresh")
|
||||
@track_execution_time("enhanced_token_refresh_duration_seconds", "auth-service")
|
||||
async def refresh_token(
|
||||
refresh_data: RefreshTokenRequest,
|
||||
@@ -201,7 +204,7 @@ async def refresh_token(
|
||||
)
|
||||
|
||||
|
||||
@router.post(route_builder.build_base_route("verify", include_tenant_prefix=False))
|
||||
@router.post("/api/v1/auth/verify")
|
||||
@track_execution_time("enhanced_token_verify_duration_seconds", "auth-service")
|
||||
async def verify_token(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
@@ -249,7 +252,7 @@ async def verify_token(
|
||||
)
|
||||
|
||||
|
||||
@router.post(route_builder.build_base_route("logout", include_tenant_prefix=False))
|
||||
@router.post("/api/v1/auth/logout")
|
||||
@track_execution_time("enhanced_logout_duration_seconds", "auth-service")
|
||||
async def logout(
|
||||
refresh_data: RefreshTokenRequest,
|
||||
@@ -295,7 +298,7 @@ async def logout(
|
||||
)
|
||||
|
||||
|
||||
@router.post(route_builder.build_base_route("change-password", include_tenant_prefix=False))
|
||||
@router.post("/api/v1/auth/change-password")
|
||||
async def change_password(
|
||||
password_data: PasswordChange,
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
@@ -358,98 +361,116 @@ async def change_password(
|
||||
)
|
||||
|
||||
|
||||
@router.get(route_builder.build_base_route("profile", include_tenant_prefix=False), response_model=UserResponse)
|
||||
@router.get("/api/v1/auth/me", response_model=UserResponse)
|
||||
async def get_profile(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
auth_service: EnhancedAuthService = Depends(get_auth_service)
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get user profile using repository pattern"""
|
||||
"""Get user profile - works for JWT auth AND demo sessions"""
|
||||
try:
|
||||
if not credentials:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Authentication required"
|
||||
)
|
||||
|
||||
# Verify token and get user_id
|
||||
payload = await auth_service.verify_user_token(credentials.credentials)
|
||||
user_id = payload.get("user_id")
|
||||
user_id = current_user.get("user_id")
|
||||
|
||||
if not user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token"
|
||||
detail="Invalid user context"
|
||||
)
|
||||
|
||||
# Get user profile using enhanced service
|
||||
profile = await auth_service.get_user_profile(user_id)
|
||||
if not 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="User profile not found"
|
||||
)
|
||||
|
||||
return profile
|
||||
return UserResponse(
|
||||
id=str(user.id),
|
||||
email=user.email,
|
||||
full_name=user.full_name,
|
||||
is_active=user.is_active,
|
||||
is_verified=user.is_verified,
|
||||
phone=user.phone,
|
||||
language=user.language or "es",
|
||||
timezone=user.timezone or "Europe/Madrid",
|
||||
created_at=user.created_at,
|
||||
last_login=user.last_login,
|
||||
role=user.role,
|
||||
tenant_id=current_user.get("tenant_id")
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Get profile error using repository pattern", error=str(e))
|
||||
logger.error("Get profile error", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get profile"
|
||||
)
|
||||
|
||||
|
||||
@router.put(route_builder.build_base_route("profile", include_tenant_prefix=False), response_model=UserResponse)
|
||||
@router.put("/api/v1/auth/me", response_model=UserResponse)
|
||||
async def update_profile(
|
||||
update_data: dict,
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
auth_service: EnhancedAuthService = Depends(get_auth_service)
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Update user profile using repository pattern"""
|
||||
"""Update user profile - works for JWT auth AND demo sessions"""
|
||||
try:
|
||||
if not credentials:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Authentication required"
|
||||
)
|
||||
|
||||
# Verify token and get user_id
|
||||
payload = await auth_service.verify_user_token(credentials.credentials)
|
||||
user_id = payload.get("user_id")
|
||||
user_id = current_user.get("user_id")
|
||||
|
||||
if not user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token"
|
||||
detail="Invalid user context"
|
||||
)
|
||||
|
||||
# Update profile using enhanced service
|
||||
updated_profile = await auth_service.update_user_profile(user_id, update_data)
|
||||
if not updated_profile:
|
||||
# Prepare update data - filter out read-only fields
|
||||
from app.repositories import UserRepository
|
||||
user_repo = UserRepository(User, db)
|
||||
|
||||
# Update user profile
|
||||
updated_user = await user_repo.update(user_id, update_data)
|
||||
|
||||
if not updated_user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
|
||||
logger.info("Profile updated using repository pattern",
|
||||
logger.info("Profile updated",
|
||||
user_id=user_id,
|
||||
updated_fields=list(update_data.keys()))
|
||||
|
||||
return updated_profile
|
||||
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,
|
||||
timezone=updated_user.timezone,
|
||||
created_at=updated_user.created_at,
|
||||
last_login=updated_user.last_login,
|
||||
role=updated_user.role,
|
||||
tenant_id=current_user.get("tenant_id")
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Update profile error using repository pattern", error=str(e))
|
||||
logger.error("Update profile error", error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to update profile"
|
||||
)
|
||||
|
||||
|
||||
@router.post(route_builder.build_base_route("verify-email", include_tenant_prefix=False))
|
||||
@router.post("/api/v1/auth/verify-email")
|
||||
async def verify_email(
|
||||
user_id: str,
|
||||
verification_token: str,
|
||||
@@ -473,7 +494,7 @@ async def verify_email(
|
||||
)
|
||||
|
||||
|
||||
@router.post(route_builder.build_base_route("reset-password", include_tenant_prefix=False))
|
||||
@router.post("/api/v1/auth/reset-password")
|
||||
async def reset_password(
|
||||
reset_data: PasswordReset,
|
||||
request: Request,
|
||||
@@ -504,7 +525,7 @@ async def reset_password(
|
||||
)
|
||||
|
||||
|
||||
@router.get(route_builder.build_base_route("health", include_tenant_prefix=False))
|
||||
@router.get("/api/v1/auth/health")
|
||||
async def health_check():
|
||||
"""Health check endpoint for enhanced auth service"""
|
||||
return {
|
||||
|
||||
@@ -59,7 +59,7 @@ def hash_text(text: str) -> str:
|
||||
return hashlib.sha256(text.encode()).hexdigest()
|
||||
|
||||
|
||||
@router.post("/consent", response_model=ConsentResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post("/api/v1/auth/me/consent", response_model=ConsentResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def record_consent(
|
||||
consent_data: ConsentRequest,
|
||||
request: Request,
|
||||
@@ -71,7 +71,7 @@ async def record_consent(
|
||||
GDPR Article 7 - Conditions for consent
|
||||
"""
|
||||
try:
|
||||
user_id = UUID(current_user["sub"])
|
||||
user_id = UUID(current_user["user_id"])
|
||||
|
||||
ip_address = request.client.host if request.client else None
|
||||
user_agent = request.headers.get("user-agent")
|
||||
@@ -129,14 +129,14 @@ async def record_consent(
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error("error_recording_consent", error=str(e), user_id=current_user.get("sub"))
|
||||
logger.error("error_recording_consent", error=str(e), user_id=current_user.get("user_id"))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to record consent"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/consent/current", response_model=Optional[ConsentResponse])
|
||||
@router.get("/api/v1/auth/me/consent/current", response_model=Optional[ConsentResponse])
|
||||
async def get_current_consent(
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
@@ -145,7 +145,7 @@ async def get_current_consent(
|
||||
Get current active consent for user
|
||||
"""
|
||||
try:
|
||||
user_id = UUID(current_user["sub"])
|
||||
user_id = UUID(current_user["user_id"])
|
||||
|
||||
query = select(UserConsent).where(
|
||||
and_(
|
||||
@@ -174,14 +174,14 @@ async def get_current_consent(
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("error_getting_consent", error=str(e), user_id=current_user.get("sub"))
|
||||
logger.error("error_getting_consent", error=str(e), user_id=current_user.get("user_id"))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve consent"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/consent/history", response_model=List[ConsentHistoryResponse])
|
||||
@router.get("/api/v1/auth/me/consent/history", response_model=List[ConsentHistoryResponse])
|
||||
async def get_consent_history(
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
@@ -191,7 +191,7 @@ async def get_consent_history(
|
||||
GDPR Article 7(1) - Demonstrating consent
|
||||
"""
|
||||
try:
|
||||
user_id = UUID(current_user["sub"])
|
||||
user_id = UUID(current_user["user_id"])
|
||||
|
||||
query = select(ConsentHistory).where(
|
||||
ConsentHistory.user_id == user_id
|
||||
@@ -212,14 +212,14 @@ async def get_consent_history(
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
logger.error("error_getting_consent_history", error=str(e), user_id=current_user.get("sub"))
|
||||
logger.error("error_getting_consent_history", error=str(e), user_id=current_user.get("user_id"))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve consent history"
|
||||
)
|
||||
|
||||
|
||||
@router.put("/consent", response_model=ConsentResponse)
|
||||
@router.put("/api/v1/auth/me/consent", response_model=ConsentResponse)
|
||||
async def update_consent(
|
||||
consent_data: ConsentRequest,
|
||||
request: Request,
|
||||
@@ -231,7 +231,7 @@ async def update_consent(
|
||||
GDPR Article 7(3) - Withdrawal of consent
|
||||
"""
|
||||
try:
|
||||
user_id = UUID(current_user["sub"])
|
||||
user_id = UUID(current_user["user_id"])
|
||||
|
||||
query = select(UserConsent).where(
|
||||
and_(
|
||||
@@ -309,14 +309,14 @@ async def update_consent(
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error("error_updating_consent", error=str(e), user_id=current_user.get("sub"))
|
||||
logger.error("error_updating_consent", error=str(e), user_id=current_user.get("user_id"))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to update consent"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/consent/withdraw", status_code=status.HTTP_200_OK)
|
||||
@router.post("/api/v1/auth/me/consent/withdraw", status_code=status.HTTP_200_OK)
|
||||
async def withdraw_consent(
|
||||
request: Request,
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
@@ -327,7 +327,7 @@ async def withdraw_consent(
|
||||
GDPR Article 7(3) - Right to withdraw consent
|
||||
"""
|
||||
try:
|
||||
user_id = UUID(current_user["sub"])
|
||||
user_id = UUID(current_user["user_id"])
|
||||
|
||||
query = select(UserConsent).where(
|
||||
and_(
|
||||
@@ -365,7 +365,7 @@ async def withdraw_consent(
|
||||
|
||||
except Exception as e:
|
||||
await db.rollback()
|
||||
logger.error("error_withdrawing_consent", error=str(e), user_id=current_user.get("sub"))
|
||||
logger.error("error_withdrawing_consent", error=str(e), user_id=current_user.get("user_id"))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to withdraw consent"
|
||||
|
||||
@@ -9,17 +9,15 @@ from fastapi.responses import JSONResponse
|
||||
import structlog
|
||||
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.routing import RouteBuilder
|
||||
from app.core.database import get_db
|
||||
from app.services.data_export_service import DataExportService
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
router = APIRouter()
|
||||
route_builder = RouteBuilder('auth')
|
||||
|
||||
|
||||
@router.get("/api/v1/users/me/export")
|
||||
@router.get("/api/v1/auth/me/export")
|
||||
async def export_my_data(
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db = Depends(get_db)
|
||||
@@ -40,7 +38,7 @@ async def export_my_data(
|
||||
Response is provided in JSON format for easy data portability.
|
||||
"""
|
||||
try:
|
||||
user_id = UUID(current_user["sub"])
|
||||
user_id = UUID(current_user["user_id"])
|
||||
|
||||
export_service = DataExportService(db)
|
||||
data = await export_service.export_user_data(user_id)
|
||||
@@ -63,7 +61,7 @@ async def export_my_data(
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"data_export_failed",
|
||||
user_id=current_user.get("sub"),
|
||||
user_id=current_user.get("user_id"),
|
||||
error=str(e)
|
||||
)
|
||||
raise HTTPException(
|
||||
@@ -72,7 +70,7 @@ async def export_my_data(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/v1/users/me/export/summary")
|
||||
@router.get("/api/v1/auth/me/export/summary")
|
||||
async def get_export_summary(
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db = Depends(get_db)
|
||||
@@ -84,7 +82,7 @@ async def get_export_summary(
|
||||
before they request full export.
|
||||
"""
|
||||
try:
|
||||
user_id = UUID(current_user["sub"])
|
||||
user_id = UUID(current_user["user_id"])
|
||||
|
||||
export_service = DataExportService(db)
|
||||
data = await export_service.export_user_data(user_id)
|
||||
@@ -114,7 +112,7 @@ async def get_export_summary(
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"export_summary_failed",
|
||||
user_id=current_user.get("sub"),
|
||||
user_id=current_user.get("user_id"),
|
||||
error=str(e)
|
||||
)
|
||||
raise HTTPException(
|
||||
|
||||
@@ -13,11 +13,9 @@ from app.core.database import get_db
|
||||
from app.services.user_service import UserService
|
||||
from app.repositories.onboarding_repository import OnboardingRepository
|
||||
from shared.auth.decorators import get_current_user_dep
|
||||
from shared.routing import RouteBuilder
|
||||
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter(tags=["onboarding"])
|
||||
route_builder = RouteBuilder('auth')
|
||||
|
||||
# Request/Response Models
|
||||
class OnboardingStepStatus(BaseModel):
|
||||
@@ -356,7 +354,7 @@ class OnboardingService:
|
||||
|
||||
# API Routes
|
||||
|
||||
@router.get(route_builder.build_base_route("me/onboarding/progress", include_tenant_prefix=False), response_model=UserProgress)
|
||||
@router.get("/api/v1/auth/me/onboarding/progress", response_model=UserProgress)
|
||||
async def get_user_progress(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
@@ -375,7 +373,7 @@ async def get_user_progress(
|
||||
detail="Failed to get onboarding progress"
|
||||
)
|
||||
|
||||
@router.get(route_builder.build_base_route("{user_id}/onboarding/progress", include_tenant_prefix=False), response_model=UserProgress)
|
||||
@router.get("/api/v1/auth/users/{user_id}/onboarding/progress", response_model=UserProgress)
|
||||
async def get_user_progress_by_id(
|
||||
user_id: str,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
@@ -408,7 +406,7 @@ async def get_user_progress_by_id(
|
||||
detail="Failed to get onboarding progress"
|
||||
)
|
||||
|
||||
@router.put(route_builder.build_base_route("me/onboarding/step", include_tenant_prefix=False), response_model=UserProgress)
|
||||
@router.put("/api/v1/auth/me/onboarding/step", response_model=UserProgress)
|
||||
async def update_onboarding_step(
|
||||
update_request: UpdateStepRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
@@ -433,7 +431,7 @@ async def update_onboarding_step(
|
||||
detail="Failed to update onboarding step"
|
||||
)
|
||||
|
||||
@router.get(route_builder.build_base_route("me/onboarding/next-step", include_tenant_prefix=False))
|
||||
@router.get("/api/v1/auth/me/onboarding/next-step")
|
||||
async def get_next_step(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
@@ -452,7 +450,7 @@ async def get_next_step(
|
||||
detail="Failed to get next step"
|
||||
)
|
||||
|
||||
@router.get(route_builder.build_base_route("me/onboarding/can-access/{step_name}", include_tenant_prefix=False))
|
||||
@router.get("/api/v1/auth/me/onboarding/can-access/{step_name}")
|
||||
async def can_access_step(
|
||||
step_name: str,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
@@ -475,7 +473,7 @@ async def can_access_step(
|
||||
detail="Failed to check step access"
|
||||
)
|
||||
|
||||
@router.post(route_builder.build_base_route("me/onboarding/complete", include_tenant_prefix=False))
|
||||
@router.post("/api/v1/auth/me/onboarding/complete")
|
||||
async def complete_onboarding(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
|
||||
@@ -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
|
||||
from app.services.user_service import UserService, EnhancedUserService
|
||||
from app.models.users import User
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -24,133 +24,15 @@ from shared.auth.decorators import (
|
||||
get_current_user_dep,
|
||||
require_admin_role_dep
|
||||
)
|
||||
from shared.routing import RouteBuilder
|
||||
from shared.security import create_audit_logger, AuditSeverity, AuditAction
|
||||
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter(tags=["users"])
|
||||
route_builder = RouteBuilder('auth')
|
||||
|
||||
# Initialize audit logger
|
||||
audit_logger = create_audit_logger("auth-service")
|
||||
|
||||
|
||||
@router.get(route_builder.build_base_route("me", include_tenant_prefix=False), response_model=UserResponse)
|
||||
async def get_current_user_info(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get current user information - FIXED VERSION"""
|
||||
try:
|
||||
logger.debug(f"Getting user info for: {current_user}")
|
||||
|
||||
# Handle both User object (direct auth) and dict (from gateway headers)
|
||||
if isinstance(current_user, dict):
|
||||
# Coming from gateway headers - need to fetch user from DB
|
||||
user_id = current_user.get("user_id")
|
||||
if not user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid user context"
|
||||
)
|
||||
|
||||
# ✅ FIX: Fetch full user from database to get the real role
|
||||
from app.repositories import UserRepository
|
||||
user_repo = UserRepository(User, db)
|
||||
user = await user_repo.get_by_id(user_id)
|
||||
|
||||
logger.debug(f"Fetched user from DB - Role: {user.role}, Email: {user.email}")
|
||||
|
||||
# ✅ FIX: Return role from database, not from JWT headers
|
||||
return UserResponse(
|
||||
id=str(user.id),
|
||||
email=user.email,
|
||||
full_name=user.full_name,
|
||||
is_active=user.is_active,
|
||||
is_verified=user.is_verified,
|
||||
phone=user.phone,
|
||||
language=user.language or "es",
|
||||
timezone=user.timezone or "Europe/Madrid",
|
||||
created_at=user.created_at,
|
||||
last_login=user.last_login,
|
||||
role=user.role, # ✅ CRITICAL: Use role from database, not headers
|
||||
tenant_id=current_user.get("tenant_id")
|
||||
)
|
||||
else:
|
||||
# Direct User object (shouldn't happen in microservice architecture)
|
||||
logger.debug(f"Direct user object received - Role: {current_user.role}")
|
||||
return UserResponse(
|
||||
id=str(current_user.id),
|
||||
email=current_user.email,
|
||||
full_name=current_user.full_name,
|
||||
is_active=current_user.is_active,
|
||||
is_verified=current_user.is_verified,
|
||||
phone=current_user.phone,
|
||||
language=current_user.language or "es",
|
||||
timezone=current_user.timezone or "Europe/Madrid",
|
||||
created_at=current_user.created_at,
|
||||
last_login=current_user.last_login,
|
||||
role=current_user.role, # ✅ Use role from database
|
||||
tenant_id=None
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Get user info error: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get user information"
|
||||
)
|
||||
|
||||
@router.put(route_builder.build_base_route("me", include_tenant_prefix=False), response_model=UserResponse)
|
||||
async def update_current_user(
|
||||
user_update: UserUpdate,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Update current user information"""
|
||||
try:
|
||||
user_id = current_user.get("user_id") if isinstance(current_user, dict) else current_user.id
|
||||
from app.repositories import UserRepository
|
||||
user_repo = UserRepository(User, db)
|
||||
|
||||
# Prepare update data
|
||||
update_data = {}
|
||||
if user_update.full_name is not None:
|
||||
update_data["full_name"] = user_update.full_name
|
||||
if user_update.phone is not None:
|
||||
update_data["phone"] = user_update.phone
|
||||
if user_update.language is not None:
|
||||
update_data["language"] = user_update.language
|
||||
if user_update.timezone is not None:
|
||||
update_data["timezone"] = user_update.timezone
|
||||
|
||||
updated_user = await user_repo.update(user_id, update_data)
|
||||
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,
|
||||
timezone=updated_user.timezone,
|
||||
created_at=updated_user.created_at,
|
||||
last_login=updated_user.last_login,
|
||||
role=updated_user.role, # ✅ Include role
|
||||
tenant_id=current_user.get("tenant_id") if isinstance(current_user, dict) else None
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Update user error: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to update user"
|
||||
)
|
||||
|
||||
@router.delete(route_builder.build_base_route("delete/{user_id}", include_tenant_prefix=False))
|
||||
@router.delete("/api/v1/auth/users/{user_id}")
|
||||
async def delete_admin_user(
|
||||
background_tasks: BackgroundTasks,
|
||||
user_id: str = Path(..., description="User ID"),
|
||||
@@ -244,7 +126,7 @@ async def execute_admin_user_deletion(user_id: str, requesting_user_id: str):
|
||||
result=result)
|
||||
|
||||
|
||||
@router.get(route_builder.build_base_route("delete/{user_id}/deletion-preview", include_tenant_prefix=False))
|
||||
@router.get("/api/v1/auth/users/{user_id}/deletion-preview")
|
||||
async def preview_user_deletion(
|
||||
user_id: str = Path(..., description="User ID"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
@@ -294,7 +176,7 @@ async def preview_user_deletion(
|
||||
return preview
|
||||
|
||||
|
||||
@router.get(route_builder.build_base_route("users/{user_id}", include_tenant_prefix=False), response_model=UserResponse)
|
||||
@router.get("/api/v1/auth/users/{user_id}", response_model=UserResponse)
|
||||
async def get_user_by_id(
|
||||
user_id: str = Path(..., description="User ID"),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
@@ -353,7 +235,7 @@ async def get_user_by_id(
|
||||
)
|
||||
|
||||
|
||||
@router.post(route_builder.build_base_route("users/create-by-owner", include_tenant_prefix=False), response_model=UserResponse)
|
||||
@router.post("/api/v1/auth/users/create-by-owner", response_model=UserResponse)
|
||||
async def create_user_by_owner(
|
||||
user_data: OwnerUserCreate,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
@@ -448,7 +330,7 @@ async def create_user_by_owner(
|
||||
)
|
||||
|
||||
|
||||
@router.post(route_builder.build_base_route("users/batch", include_tenant_prefix=False), response_model=Dict[str, Any])
|
||||
@router.post("/api/v1/auth/users/batch", response_model=Dict[str, Any])
|
||||
async def get_users_batch(
|
||||
request: BatchUserRequest,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
@@ -526,3 +408,75 @@ async def get_users_batch(
|
||||
detail="Failed to fetch users"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/v1/auth/users/{user_id}/activity")
|
||||
async def get_user_activity(
|
||||
user_id: str = Path(..., description="User ID"),
|
||||
current_user = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get user activity information.
|
||||
|
||||
This endpoint returns detailed activity information for a user including:
|
||||
- Last login timestamp
|
||||
- Account creation date
|
||||
- Active session count
|
||||
- Last activity timestamp
|
||||
- User status information
|
||||
|
||||
**Permissions:** User can view their own activity, admins can view any user's activity
|
||||
"""
|
||||
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 view their own activity, admins can view 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 view this user's activity"
|
||||
)
|
||||
|
||||
# Initialize enhanced user service
|
||||
from app.core.config import settings
|
||||
from shared.database.base import create_database_manager
|
||||
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
||||
user_service = EnhancedUserService(database_manager)
|
||||
|
||||
# Get user activity data
|
||||
activity_data = await user_service.get_user_activity(user_id)
|
||||
|
||||
if "error" in activity_data:
|
||||
if activity_data["error"] == "User not found":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found"
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to get user activity: {activity_data['error']}"
|
||||
)
|
||||
|
||||
logger.debug("Retrieved user activity", user_id=user_id)
|
||||
|
||||
return activity_data
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Get user activity error", user_id=user_id, error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get user activity information"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user