Switch from email to username for authentication
- Add username field to User model - Update authentication endpoints to use username instead of email - Create migration for adding username column to user table - Update user services to handle username validation and uniqueness - Maintain email for compatibility, but make username the primary identifier
This commit is contained in:
parent
07dc69217a
commit
da59077885
@ -26,11 +26,11 @@ def login_access_token(
|
|||||||
"""
|
"""
|
||||||
OAuth2 compatible token login, get an access token for future requests
|
OAuth2 compatible token login, get an access token for future requests
|
||||||
"""
|
"""
|
||||||
user = user_service.authenticate(db, email=form_data.username, password=form_data.password)
|
user = user_service.authenticate(db, username=form_data.username, password=form_data.password)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Incorrect email or password",
|
detail="Incorrect username or password",
|
||||||
)
|
)
|
||||||
elif not user_service.is_active(user):
|
elif not user_service.is_active(user):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -55,15 +55,21 @@ def register_user(
|
|||||||
"""
|
"""
|
||||||
Register a new user and return an access token
|
Register a new user and return an access token
|
||||||
"""
|
"""
|
||||||
user = user_service.get_by_email(db, email=form_data.username)
|
existing_user = user_service.get_by_username(db, username=form_data.username)
|
||||||
if user:
|
if existing_user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="A user with this email already exists",
|
detail="A user with this username already exists",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create user
|
# Create user - using username as the primary identifier, but also require an email
|
||||||
user_in = UserCreate(email=form_data.username, password=form_data.password)
|
# For OAuth2PasswordRequestForm, we'll use the username field from the form
|
||||||
|
# and set a default email based on the username
|
||||||
|
user_in = UserCreate(
|
||||||
|
username=form_data.username,
|
||||||
|
email=f"{form_data.username}@example.com", # Default email since OAuth2 form doesn't have email field
|
||||||
|
password=form_data.password
|
||||||
|
)
|
||||||
user = user_service.create(db, obj_in=user_in)
|
user = user_service.create(db, obj_in=user_in)
|
||||||
|
|
||||||
# Generate token
|
# Generate token
|
||||||
|
@ -38,12 +38,22 @@ def create_user(
|
|||||||
"""
|
"""
|
||||||
Create new user.
|
Create new user.
|
||||||
"""
|
"""
|
||||||
|
# Check if username already exists
|
||||||
|
user = user_service.get_by_username(db, username=user_in.username)
|
||||||
|
if user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="A user with this username already exists",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Also check email for uniqueness
|
||||||
user = user_service.get_by_email(db, email=user_in.email)
|
user = user_service.get_by_email(db, email=user_in.email)
|
||||||
if user:
|
if user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="A user with this email already exists",
|
detail="A user with this email already exists",
|
||||||
)
|
)
|
||||||
|
|
||||||
user = user_service.create(db, obj_in=user_in)
|
user = user_service.create(db, obj_in=user_in)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@ -54,6 +64,7 @@ def update_user_me(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
password: str = Body(None),
|
password: str = Body(None),
|
||||||
full_name: str = Body(None),
|
full_name: str = Body(None),
|
||||||
|
username: str = Body(None),
|
||||||
email: EmailStr = Body(None),
|
email: EmailStr = Body(None),
|
||||||
current_user: User = Depends(get_current_active_user),
|
current_user: User = Depends(get_current_active_user),
|
||||||
) -> Any:
|
) -> Any:
|
||||||
@ -62,12 +73,32 @@ def update_user_me(
|
|||||||
"""
|
"""
|
||||||
current_user_data = jsonable_encoder(current_user)
|
current_user_data = jsonable_encoder(current_user)
|
||||||
user_in = UserUpdate(**current_user_data)
|
user_in = UserUpdate(**current_user_data)
|
||||||
|
|
||||||
if password is not None:
|
if password is not None:
|
||||||
user_in.password = password
|
user_in.password = password
|
||||||
if full_name is not None:
|
if full_name is not None:
|
||||||
user_in.full_name = full_name
|
user_in.full_name = full_name
|
||||||
if email is not None:
|
|
||||||
|
# Check for username uniqueness if username is being updated
|
||||||
|
if username is not None and username != current_user.username:
|
||||||
|
existing_user = user_service.get_by_username(db, username=username)
|
||||||
|
if existing_user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="A user with this username already exists",
|
||||||
|
)
|
||||||
|
user_in.username = username
|
||||||
|
|
||||||
|
# Check for email uniqueness if email is being updated
|
||||||
|
if email is not None and email != current_user.email:
|
||||||
|
existing_user = user_service.get_by_email(db, email=email)
|
||||||
|
if existing_user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="A user with this email already exists",
|
||||||
|
)
|
||||||
user_in.email = email
|
user_in.email = email
|
||||||
|
|
||||||
user = user_service.update(db, db_obj=current_user, obj_in=user_in)
|
user = user_service.update(db, db_obj=current_user, obj_in=user_in)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ from app.db.base_class import Base
|
|||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
id = Column(String, primary_key=True, index=True)
|
id = Column(String, primary_key=True, index=True)
|
||||||
|
username = Column(String, unique=True, index=True)
|
||||||
email = Column(String, unique=True, index=True)
|
email = Column(String, unique=True, index=True)
|
||||||
hashed_password = Column(String)
|
hashed_password = Column(String)
|
||||||
full_name = Column(String, index=True)
|
full_name = Column(String, index=True)
|
||||||
|
@ -5,6 +5,7 @@ from pydantic import BaseModel, EmailStr
|
|||||||
|
|
||||||
# Shared properties
|
# Shared properties
|
||||||
class UserBase(BaseModel):
|
class UserBase(BaseModel):
|
||||||
|
username: Optional[str] = None
|
||||||
email: Optional[EmailStr] = None
|
email: Optional[EmailStr] = None
|
||||||
full_name: Optional[str] = None
|
full_name: Optional[str] = None
|
||||||
is_active: Optional[bool] = True
|
is_active: Optional[bool] = True
|
||||||
@ -13,6 +14,7 @@ class UserBase(BaseModel):
|
|||||||
|
|
||||||
# Properties to receive via API on creation
|
# Properties to receive via API on creation
|
||||||
class UserCreate(UserBase):
|
class UserCreate(UserBase):
|
||||||
|
username: str
|
||||||
email: EmailStr
|
email: EmailStr
|
||||||
password: str
|
password: str
|
||||||
|
|
||||||
|
@ -16,9 +16,14 @@ def get_by_email(db: Session, email: str) -> Optional[User]:
|
|||||||
return db.query(User).filter(User.email == email).first()
|
return db.query(User).filter(User.email == email).first()
|
||||||
|
|
||||||
|
|
||||||
|
def get_by_username(db: Session, username: str) -> Optional[User]:
|
||||||
|
return db.query(User).filter(User.username == username).first()
|
||||||
|
|
||||||
|
|
||||||
def create(db: Session, *, obj_in: UserCreate) -> User:
|
def create(db: Session, *, obj_in: UserCreate) -> User:
|
||||||
db_obj = User(
|
db_obj = User(
|
||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
|
username=obj_in.username,
|
||||||
email=obj_in.email,
|
email=obj_in.email,
|
||||||
hashed_password=get_password_hash(obj_in.password),
|
hashed_password=get_password_hash(obj_in.password),
|
||||||
full_name=obj_in.full_name,
|
full_name=obj_in.full_name,
|
||||||
@ -46,8 +51,8 @@ def update(db: Session, *, db_obj: User, obj_in: UserUpdate) -> User:
|
|||||||
return db_obj
|
return db_obj
|
||||||
|
|
||||||
|
|
||||||
def authenticate(db: Session, *, email: str, password: str) -> Optional[User]:
|
def authenticate(db: Session, *, username: str, password: str) -> Optional[User]:
|
||||||
user = get_by_email(db, email=email)
|
user = get_by_username(db, username=username)
|
||||||
if not user:
|
if not user:
|
||||||
return None
|
return None
|
||||||
if not verify_password(password, user.hashed_password):
|
if not verify_password(password, user.hashed_password):
|
||||||
|
42
migrations/versions/2a3b4c5d6e7f_add_username_field.py
Normal file
42
migrations/versions/2a3b4c5d6e7f_add_username_field.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"""Add username field
|
||||||
|
|
||||||
|
Revision ID: 2a3b4c5d6e7f
|
||||||
|
Revises: 1a2b3c4d5e6f
|
||||||
|
Create Date: 2023-11-17 00:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '2a3b4c5d6e7f'
|
||||||
|
down_revision = '1a2b3c4d5e6f'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Add username column to user table
|
||||||
|
with op.batch_alter_table('user') as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('username', sa.String(), nullable=True))
|
||||||
|
batch_op.create_index(op.f('ix_user_username'), 'username', unique=True)
|
||||||
|
|
||||||
|
# For existing users, initialize username from email
|
||||||
|
# This is just SQL template code - in a real migration with existing data,
|
||||||
|
# you would need to handle this appropriately
|
||||||
|
op.execute("""
|
||||||
|
UPDATE "user" SET username = email
|
||||||
|
WHERE username IS NULL
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Now make username non-nullable for future records
|
||||||
|
with op.batch_alter_table('user') as batch_op:
|
||||||
|
batch_op.alter_column('username', nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Remove username column from user table
|
||||||
|
with op.batch_alter_table('user') as batch_op:
|
||||||
|
batch_op.drop_index(op.f('ix_user_username'))
|
||||||
|
batch_op.drop_column('username')
|
Loading…
x
Reference in New Issue
Block a user