Add comprehensive user profile management system

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)
This commit is contained in:
Automated Action 2025-06-24 19:40:23 +00:00
parent e0cab8c417
commit 9947e2629c
5 changed files with 293 additions and 2 deletions

View File

@ -5,6 +5,7 @@ A FastAPI backend for an AI-powered video dubbing tool that allows content creat
## Features ## Features
🔐 **Authentication**: JWT-based user registration and login 🔐 **Authentication**: JWT-based user registration and login
👤 **User Profiles**: Complete profile management with settings
📁 **Video Upload**: Upload MP4/MOV files to Amazon S3 (max 200MB) 📁 **Video Upload**: Upload MP4/MOV files to Amazon S3 (max 200MB)
🧠 **Transcription**: Audio transcription using OpenAI Whisper API 🧠 **Transcription**: Audio transcription using OpenAI Whisper API
🌍 **Translation**: Text translation using GPT-4 API 🌍 **Translation**: Text translation using GPT-4 API
@ -78,6 +79,13 @@ The API will be available at:
- `POST /auth/register` - User registration - `POST /auth/register` - User registration
- `POST /auth/login` - User login - `POST /auth/login` - User login
### Profile Management
- `GET /profile/` - Get user profile
- `PUT /profile/` - Update profile information
- `PUT /profile/password` - Update password
- `PUT /profile/email` - Update email address
- `DELETE /profile/` - Delete user account
### Video Management ### Video Management
- `POST /videos/upload` - Upload video with language settings - `POST /videos/upload` - Upload video with language settings
- `GET /videos/` - Get user's videos - `GET /videos/` - Get user's videos

View File

@ -0,0 +1,43 @@
"""Add profile fields to users table
Revision ID: 002
Revises: 001
Create Date: 2024-01-01 00:00:01.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '002'
down_revision: Union[str, None] = '001'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Add new profile fields to users table
op.add_column('users', sa.Column('first_name', sa.String(), nullable=True))
op.add_column('users', sa.Column('last_name', sa.String(), nullable=True))
op.add_column('users', sa.Column('phone', sa.String(), nullable=True))
op.add_column('users', sa.Column('bio', sa.String(), nullable=True))
op.add_column('users', sa.Column('preferred_language', sa.String(), nullable=True))
op.add_column('users', sa.Column('timezone', sa.String(), nullable=True))
op.add_column('users', sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True))
# Set default values for existing users
op.execute("UPDATE users SET preferred_language = 'en' WHERE preferred_language IS NULL")
op.execute("UPDATE users SET timezone = 'UTC' WHERE timezone IS NULL")
def downgrade() -> None:
# Remove the added columns
op.drop_column('users', 'updated_at')
op.drop_column('users', 'timezone')
op.drop_column('users', 'preferred_language')
op.drop_column('users', 'bio')
op.drop_column('users', 'phone')
op.drop_column('users', 'last_name')
op.drop_column('users', 'first_name')

View File

@ -9,4 +9,11 @@ class User(Base):
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False) email = Column(String, unique=True, index=True, nullable=False)
password_hash = Column(String, nullable=False) password_hash = Column(String, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now()) first_name = Column(String, nullable=True)
last_name = Column(String, nullable=True)
phone = Column(String, nullable=True)
bio = Column(String, nullable=True)
preferred_language = Column(String, default="en")
timezone = Column(String, default="UTC")
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())

232
app/routes/profile.py Normal file
View File

@ -0,0 +1,232 @@
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"
)

View File

@ -6,7 +6,7 @@ import logging
from app.db.session import engine from app.db.session import engine
from app.db.base import Base from app.db.base import Base
from app.routes import auth, videos, transcription, translation, voice_cloning, video_processing from app.routes import auth, videos, transcription, translation, voice_cloning, video_processing, profile
# Configure logging # Configure logging
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@ -48,6 +48,7 @@ app.add_middleware(
# Include routers # Include routers
app.include_router(auth.router, prefix="/auth", tags=["Authentication"]) app.include_router(auth.router, prefix="/auth", tags=["Authentication"])
app.include_router(profile.router, prefix="/profile", tags=["Profile"])
app.include_router(videos.router, prefix="/videos", tags=["Videos"]) app.include_router(videos.router, prefix="/videos", tags=["Videos"])
app.include_router(transcription.router, prefix="/transcription", tags=["Transcription"]) app.include_router(transcription.router, prefix="/transcription", tags=["Transcription"])
app.include_router(translation.router, prefix="/translation", tags=["Translation"]) app.include_router(translation.router, prefix="/translation", tags=["Translation"])