
Features: - Extended User model with profile fields (first_name, last_name, phone, bio, preferred_language, timezone) - Complete profile endpoints for viewing and updating user information - Password update functionality with current password verification - Email update functionality with duplicate email checking - Account deletion endpoint - Database migration for new profile fields - Enhanced logging and error handling for all profile operations - Updated API documentation with profile endpoints Profile fields include: - Personal information (name, phone, bio) - Preferences (language, timezone) - Account management (password, email updates) - Timestamps (created_at, updated_at)
232 lines
7.8 KiB
Python
232 lines
7.8 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.orm import Session
|
|
from pydantic import BaseModel, EmailStr
|
|
from typing import Optional
|
|
import logging
|
|
from app.db.session import get_db
|
|
from app.models.user import User
|
|
from app.utils.auth import get_current_user, get_password_hash, verify_password
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
|
|
|
|
class ProfileResponse(BaseModel):
|
|
id: int
|
|
email: str
|
|
first_name: Optional[str]
|
|
last_name: Optional[str]
|
|
phone: Optional[str]
|
|
bio: Optional[str]
|
|
preferred_language: str
|
|
timezone: str
|
|
created_at: str
|
|
updated_at: str
|
|
|
|
class Config:
|
|
orm_mode = True
|
|
|
|
|
|
class ProfileUpdate(BaseModel):
|
|
first_name: Optional[str] = None
|
|
last_name: Optional[str] = None
|
|
phone: Optional[str] = None
|
|
bio: Optional[str] = None
|
|
preferred_language: Optional[str] = None
|
|
timezone: Optional[str] = None
|
|
|
|
|
|
class PasswordUpdate(BaseModel):
|
|
current_password: str
|
|
new_password: str
|
|
|
|
|
|
class EmailUpdate(BaseModel):
|
|
new_email: EmailStr
|
|
password: str
|
|
|
|
|
|
@router.get("/", response_model=ProfileResponse)
|
|
async def get_profile(
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
logger.info(f"Profile requested for user: {current_user.email}")
|
|
|
|
return ProfileResponse(
|
|
id=current_user.id,
|
|
email=current_user.email,
|
|
first_name=current_user.first_name,
|
|
last_name=current_user.last_name,
|
|
phone=current_user.phone,
|
|
bio=current_user.bio,
|
|
preferred_language=current_user.preferred_language or "en",
|
|
timezone=current_user.timezone or "UTC",
|
|
created_at=str(current_user.created_at),
|
|
updated_at=str(current_user.updated_at) if hasattr(current_user, 'updated_at') and current_user.updated_at else str(current_user.created_at)
|
|
)
|
|
|
|
|
|
@router.put("/", response_model=ProfileResponse)
|
|
async def update_profile(
|
|
profile_data: ProfileUpdate,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
try:
|
|
logger.info(f"Profile update requested for user: {current_user.email}")
|
|
|
|
# Update only provided fields
|
|
if profile_data.first_name is not None:
|
|
current_user.first_name = profile_data.first_name
|
|
if profile_data.last_name is not None:
|
|
current_user.last_name = profile_data.last_name
|
|
if profile_data.phone is not None:
|
|
current_user.phone = profile_data.phone
|
|
if profile_data.bio is not None:
|
|
current_user.bio = profile_data.bio
|
|
if profile_data.preferred_language is not None:
|
|
current_user.preferred_language = profile_data.preferred_language
|
|
if profile_data.timezone is not None:
|
|
current_user.timezone = profile_data.timezone
|
|
|
|
db.commit()
|
|
db.refresh(current_user)
|
|
|
|
logger.info(f"Profile updated successfully for user: {current_user.email}")
|
|
|
|
return ProfileResponse(
|
|
id=current_user.id,
|
|
email=current_user.email,
|
|
first_name=current_user.first_name,
|
|
last_name=current_user.last_name,
|
|
phone=current_user.phone,
|
|
bio=current_user.bio,
|
|
preferred_language=current_user.preferred_language or "en",
|
|
timezone=current_user.timezone or "UTC",
|
|
created_at=str(current_user.created_at),
|
|
updated_at=str(current_user.updated_at) if hasattr(current_user, 'updated_at') and current_user.updated_at else str(current_user.created_at)
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Profile update error for {current_user.email}: {str(e)}")
|
|
db.rollback()
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Internal server error during profile update"
|
|
)
|
|
|
|
|
|
@router.put("/password")
|
|
async def update_password(
|
|
password_data: PasswordUpdate,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
try:
|
|
logger.info(f"Password update requested for user: {current_user.email}")
|
|
|
|
# Verify current password
|
|
if not verify_password(password_data.current_password, current_user.password_hash):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Current password is incorrect"
|
|
)
|
|
|
|
# Validate new password
|
|
if len(password_data.new_password) < 6:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="New password must be at least 6 characters long"
|
|
)
|
|
|
|
# Update password
|
|
current_user.password_hash = get_password_hash(password_data.new_password)
|
|
db.commit()
|
|
|
|
logger.info(f"Password updated successfully for user: {current_user.email}")
|
|
|
|
return {"message": "Password updated successfully"}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Password update error for {current_user.email}: {str(e)}")
|
|
db.rollback()
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Internal server error during password update"
|
|
)
|
|
|
|
|
|
@router.put("/email")
|
|
async def update_email(
|
|
email_data: EmailUpdate,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
try:
|
|
logger.info(f"Email update requested for user: {current_user.email}")
|
|
|
|
# Verify password
|
|
if not verify_password(email_data.password, current_user.password_hash):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Password is incorrect"
|
|
)
|
|
|
|
# Check if new email is already taken
|
|
existing_user = db.query(User).filter(User.email == email_data.new_email).first()
|
|
if existing_user and existing_user.id != current_user.id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Email already registered"
|
|
)
|
|
|
|
old_email = current_user.email
|
|
current_user.email = email_data.new_email
|
|
db.commit()
|
|
|
|
logger.info(f"Email updated successfully from {old_email} to {email_data.new_email}")
|
|
|
|
return {"message": "Email updated successfully"}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Email update error for {current_user.email}: {str(e)}")
|
|
db.rollback()
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Internal server error during email update"
|
|
)
|
|
|
|
|
|
@router.delete("/")
|
|
async def delete_account(
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
try:
|
|
logger.info(f"Account deletion requested for user: {current_user.email}")
|
|
|
|
# Note: In a production environment, you might want to:
|
|
# 1. Soft delete instead of hard delete
|
|
# 2. Clean up related data (videos, transcriptions, etc.)
|
|
# 3. Add confirmation mechanisms
|
|
|
|
user_email = current_user.email
|
|
db.delete(current_user)
|
|
db.commit()
|
|
|
|
logger.info(f"Account deleted successfully for user: {user_email}")
|
|
|
|
return {"message": "Account deleted successfully"}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Account deletion error for {current_user.email}: {str(e)}")
|
|
db.rollback()
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Internal server error during account deletion"
|
|
) |