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
|
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")
|
||||||
|
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.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
|
Loading…
Reference in New Issue
Block a user