From 2b7816cf331b43bf71a26dc9f28df50abadc1d59 Mon Sep 17 00:00:00 2001 From: mohamad Date: Tue, 20 May 2025 01:17:47 +0200 Subject: [PATCH] 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. --- .../5271d18372e5_initial_database_schema.py | 42 +++++++++++++++++++ ...0fc_update_user_model_for_fastapi_users.py | 13 +++++- .../5ed3ccbf05f7_initial_database_schema.py | 32 ++++++++++++++ .../8efbdc779a76_check_models_alignment.py | 32 ++++++++++++++ be/app/database.py | 37 +++++++--------- 5 files changed, 133 insertions(+), 23 deletions(-) create mode 100644 be/alembic/versions/5271d18372e5_initial_database_schema.py create mode 100644 be/alembic/versions/5ed3ccbf05f7_initial_database_schema.py create mode 100644 be/alembic/versions/8efbdc779a76_check_models_alignment.py diff --git a/be/alembic/versions/5271d18372e5_initial_database_schema.py b/be/alembic/versions/5271d18372e5_initial_database_schema.py new file mode 100644 index 0000000..87f7e8c --- /dev/null +++ b/be/alembic/versions/5271d18372e5_initial_database_schema.py @@ -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 ### diff --git a/be/alembic/versions/5e8b6dde50fc_update_user_model_for_fastapi_users.py b/be/alembic/versions/5e8b6dde50fc_update_user_model_for_fastapi_users.py index 195ebc0..f6bcb4c 100644 --- a/be/alembic/versions/5e8b6dde50fc_update_user_model_for_fastapi_users.py +++ b/be/alembic/versions/5e8b6dde50fc_update_user_model_for_fastapi_users.py @@ -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") diff --git a/be/alembic/versions/5ed3ccbf05f7_initial_database_schema.py b/be/alembic/versions/5ed3ccbf05f7_initial_database_schema.py new file mode 100644 index 0000000..d2954b7 --- /dev/null +++ b/be/alembic/versions/5ed3ccbf05f7_initial_database_schema.py @@ -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 ### diff --git a/be/alembic/versions/8efbdc779a76_check_models_alignment.py b/be/alembic/versions/8efbdc779a76_check_models_alignment.py new file mode 100644 index 0000000..5613a2f --- /dev/null +++ b/be/alembic/versions/8efbdc779a76_check_models_alignment.py @@ -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 ### diff --git a/be/app/database.py b/be/app/database.py index 20fb047..37781f4 100644 --- a/be/app/database.py +++ b/be/app/database.py @@ -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 \ No newline at end of file + 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 \ No newline at end of file