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:
parent
e0cab8c417
commit
9947e2629c
@ -5,6 +5,7 @@ A FastAPI backend for an AI-powered video dubbing tool that allows content creat
|
||||
## Features
|
||||
|
||||
🔐 **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)
|
||||
🧠 **Transcription**: Audio transcription using OpenAI Whisper 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/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
|
||||
- `POST /videos/upload` - Upload video with language settings
|
||||
- `GET /videos/` - Get user's videos
|
||||
|
43
alembic/versions/002_add_profile_fields.py
Normal file
43
alembic/versions/002_add_profile_fields.py
Normal 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')
|
@ -9,4 +9,11 @@ class User(Base):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
email = Column(String, unique=True, index=True, nullable=False)
|
||||
password_hash = Column(String, nullable=False)
|
||||
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
232
app/routes/profile.py
Normal 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"
|
||||
)
|
3
main.py
3
main.py
@ -6,7 +6,7 @@ import logging
|
||||
|
||||
from app.db.session import engine
|
||||
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
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
@ -48,6 +48,7 @@ app.add_middleware(
|
||||
|
||||
# Include routers
|
||||
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(transcription.router, prefix="/transcription", tags=["Transcription"])
|
||||
app.include_router(translation.router, prefix="/translation", tags=["Translation"])
|
||||
|
Loading…
x
Reference in New Issue
Block a user