Update user model migration to include secure password hashing; set default hashed password for existing users. Refactor database session management for improved transaction handling and ensure session closure after use.

This commit is contained in:
mohamad 2025-05-20 01:17:47 +02:00
parent 5abe7839f1
commit 2b7816cf33
5 changed files with 133 additions and 23 deletions

View File

@ -0,0 +1,42 @@
"""Initial database schema
Revision ID: 5271d18372e5
Revises: 5e8b6dde50fc
Create Date: 2025-05-17 14:39:03.690180
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '5271d18372e5'
down_revision: Union[str, None] = '5e8b6dde50fc'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('expenses', sa.Column('created_by_user_id', sa.Integer(), nullable=False))
op.create_index(op.f('ix_expenses_created_by_user_id'), 'expenses', ['created_by_user_id'], unique=False)
op.create_foreign_key(None, 'expenses', 'users', ['created_by_user_id'], ['id'])
op.add_column('settlements', sa.Column('created_by_user_id', sa.Integer(), nullable=False))
op.create_index(op.f('ix_settlements_created_by_user_id'), 'settlements', ['created_by_user_id'], unique=False)
op.create_foreign_key(None, 'settlements', 'users', ['created_by_user_id'], ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'settlements', type_='foreignkey')
op.drop_index(op.f('ix_settlements_created_by_user_id'), table_name='settlements')
op.drop_column('settlements', 'created_by_user_id')
op.drop_constraint(None, 'expenses', type_='foreignkey')
op.drop_index(op.f('ix_expenses_created_by_user_id'), table_name='expenses')
op.drop_column('expenses', 'created_by_user_id')
# ### end Alembic commands ###

View File

@ -6,6 +6,8 @@ Create Date: 2025-05-13 23:30:02.005611
""" """
from typing import Sequence, Union from typing import Sequence, Union
import secrets
from passlib.context import CryptContext
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
@ -20,14 +22,21 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None: def upgrade() -> None:
"""Upgrade schema.""" """Upgrade schema."""
# Create password hasher
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Generate a secure random password and hash it
random_password = secrets.token_urlsafe(32) # 32 bytes of randomness
secure_hash = pwd_context.hash(random_password)
# 1. Add columns as nullable or with a default # 1. Add columns as nullable or with a default
op.add_column('users', sa.Column('hashed_password', sa.String(), nullable=True)) op.add_column('users', sa.Column('hashed_password', sa.String(), nullable=True))
op.add_column('users', sa.Column('is_active', sa.Boolean(), nullable=True, server_default=sa.sql.expression.true())) op.add_column('users', sa.Column('is_active', sa.Boolean(), nullable=True, server_default=sa.sql.expression.true()))
op.add_column('users', sa.Column('is_superuser', sa.Boolean(), nullable=True, server_default=sa.sql.expression.false())) op.add_column('users', sa.Column('is_superuser', sa.Boolean(), nullable=True, server_default=sa.sql.expression.false()))
op.add_column('users', sa.Column('is_verified', sa.Boolean(), nullable=True, server_default=sa.sql.expression.false())) op.add_column('users', sa.Column('is_verified', sa.Boolean(), nullable=True, server_default=sa.sql.expression.false()))
# 2. Set default values for existing rows # 2. Set default values for existing rows with secure hash
op.execute("UPDATE users SET hashed_password = '$INVALID_PASSWORD_PLACEHOLDER$' WHERE hashed_password IS NULL") op.execute(f"UPDATE users SET hashed_password = '{secure_hash}' WHERE hashed_password IS NULL")
op.execute("UPDATE users SET is_active = true WHERE is_active IS NULL") op.execute("UPDATE users SET is_active = true WHERE is_active IS NULL")
op.execute("UPDATE users SET is_superuser = false WHERE is_superuser IS NULL") op.execute("UPDATE users SET is_superuser = false WHERE is_superuser IS NULL")
op.execute("UPDATE users SET is_verified = false WHERE is_verified IS NULL") op.execute("UPDATE users SET is_verified = false WHERE is_verified IS NULL")

View File

@ -0,0 +1,32 @@
"""Initial database schema
Revision ID: 5ed3ccbf05f7
Revises: 5271d18372e5
Create Date: 2025-05-17 14:40:52.165607
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '5ed3ccbf05f7'
down_revision: Union[str, None] = '5271d18372e5'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -0,0 +1,32 @@
"""check_models_alignment
Revision ID: 8efbdc779a76
Revises: 5ed3ccbf05f7
Create Date: 2025-05-17 15:03:08.242908
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '8efbdc779a76'
down_revision: Union[str, None] = '5ed3ccbf05f7'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

View File

@ -2,9 +2,6 @@
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, declarative_base from sqlalchemy.orm import sessionmaker, declarative_base
from app.config import settings from app.config import settings
import logging
logger = logging.getLogger(__name__)
# Ensure DATABASE_URL is set before proceeding # Ensure DATABASE_URL is set before proceeding
if not settings.DATABASE_URL: if not settings.DATABASE_URL:
@ -33,7 +30,7 @@ AsyncSessionLocal = sessionmaker(
Base = declarative_base() Base = declarative_base()
# Dependency to get DB session in path operations # Dependency to get DB session in path operations
async def get_async_session() -> AsyncSession: # type: ignore async def get_session() -> AsyncSession: # type: ignore
""" """
Dependency function that yields an AsyncSession. Dependency function that yields an AsyncSession.
Ensures the session is closed after the request. Ensures the session is closed after the request.
@ -43,24 +40,22 @@ async def get_async_session() -> AsyncSession: # type: ignore
# The 'async with' block handles session.close() automatically. # The 'async with' block handles session.close() automatically.
# Commit/rollback should be handled by the functions using the session. # Commit/rollback should be handled by the functions using the session.
# Alias for backward compatibility
get_db = get_async_session
async def get_transactional_session() -> AsyncSession: # type: ignore async def get_transactional_session() -> AsyncSession: # type: ignore
""" """
Dependency function that yields an AsyncSession wrapped in a transaction. Dependency function that yields an AsyncSession and manages a transaction.
Commits on successful completion of the request handler, rolls back on exceptions. Commits the transaction if the request handler succeeds, otherwise rollbacks.
Ensures the session is closed after the request.
""" """
async with AsyncSessionLocal() as session: async with AsyncSessionLocal() as session:
async with session.begin(): # Start a transaction try:
try: await session.begin()
logger.debug(f"Transaction started for session {id(session)}") yield session
yield session await session.commit()
# If no exceptions were raised by the endpoint, the 'session.begin()' except Exception:
# context manager will automatically commit here. await session.rollback()
logger.debug(f"Transaction committed for session {id(session)}") raise
except Exception as e: finally:
# The 'session.begin()' context manager will automatically await session.close()
# rollback on any exception.
logger.error(f"Transaction rolled back for session {id(session)} due to: {e}", exc_info=True) # Alias for backward compatibility
raise # Re-raise the exception to be handled by FastAPI's error handlers get_db = get_session