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:
parent
5abe7839f1
commit
2b7816cf33
42
be/alembic/versions/5271d18372e5_initial_database_schema.py
Normal file
42
be/alembic/versions/5271d18372e5_initial_database_schema.py
Normal 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 ###
|
@ -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")
|
||||
|
32
be/alembic/versions/5ed3ccbf05f7_initial_database_schema.py
Normal file
32
be/alembic/versions/5ed3ccbf05f7_initial_database_schema.py
Normal 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 ###
|
32
be/alembic/versions/8efbdc779a76_check_models_alignment.py
Normal file
32
be/alembic/versions/8efbdc779a76_check_models_alignment.py
Normal 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 ###
|
@ -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
|
Loading…
Reference in New Issue
Block a user