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
import secrets
from passlib.context import CryptContext
from alembic import op
import sqlalchemy as sa
@ -20,14 +22,21 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""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
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_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()))
# 2. Set default values for existing rows
op.execute("UPDATE users SET hashed_password = '$INVALID_PASSWORD_PLACEHOLDER$' WHERE hashed_password IS NULL")
# 2. Set default values for existing rows with secure hash
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_superuser = false WHERE is_superuser 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.orm import sessionmaker, declarative_base
from app.config import settings
import logging
logger = logging.getLogger(__name__)
# Ensure DATABASE_URL is set before proceeding
if not settings.DATABASE_URL:
@ -33,7 +30,7 @@ AsyncSessionLocal = sessionmaker(
Base = declarative_base()
# 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.
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.
# 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
"""
Dependency function that yields an AsyncSession wrapped in a transaction.
Commits on successful completion of the request handler, rolls back on exceptions.
Dependency function that yields an AsyncSession and manages a transaction.
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 session.begin(): # Start a transaction
try:
logger.debug(f"Transaction started for session {id(session)}")
yield session
# If no exceptions were raised by the endpoint, the 'session.begin()'
# context manager will automatically commit here.
logger.debug(f"Transaction committed for session {id(session)}")
except Exception as e:
# The 'session.begin()' context manager will automatically
# rollback on any exception.
logger.error(f"Transaction rolled back for session {id(session)} due to: {e}", exc_info=True)
raise # Re-raise the exception to be handled by FastAPI's error handlers
try:
await session.begin()
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()
# Alias for backward compatibility
get_db = get_session