File size: 6,100 Bytes
78dfc96 acf2fb5 3e53907 78dfc96 acf2fb5 78dfc96 acf2fb5 78dfc96 3e53907 acf2fb5 78dfc96 3e53907 acf2fb5 5fb2d40 3e53907 78dfc96 3e53907 acf2fb5 78dfc96 acf2fb5 78dfc96 acf2fb5 3e53907 acf2fb5 3e53907 acf2fb5 3e53907 acf2fb5 3e53907 acf2fb5 3e53907 acf2fb5 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
from passlib.context import CryptContext
from sqlmodel import Session, select
from typing import Optional
from models.user import User, UserCreate # Direct import
from models.db import get_session_context
from services.logger import app_logger
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
def get_user_by_username(db: Session, username: str) -> Optional[User]:
"""Fetches a user by username."""
statement = select(User).where(User.username == username)
user = db.exec(statement).first()
return user
def create_user_in_db(user_data: UserCreate) -> Optional[User]:
"""
Creates a new user in the database.
Returns the User object with basic attributes loaded, suitable for immediate
use even after the session creating it has closed.
"""
hashed_password = get_password_hash(user_data.password)
# Create the Python object instance.
# Initialize relationships like chat_sessions to an empty list if appropriate,
# as they won't be populated from the DB for a new user yet.
new_user_instance = User(
username=user_data.username,
hashed_password=hashed_password,
email=user_data.email,
full_name=user_data.full_name,
chat_sessions=[] # Good practice to initialize for new instances
)
created_user_id: Optional[int] = None
try:
with get_session_context() as db:
# Check if user already exists
existing_user = get_user_by_username(db, user_data.username)
if existing_user:
app_logger.warning(f"User {user_data.username} already exists.")
return None # User already exists
db.add(new_user_instance)
db.commit() # Commit to save the user and allow DB to generate ID
db.refresh(new_user_instance) # Refresh to get all attributes, especially the DB-generated ID
# Store the ID so we can re-fetch a "clean" instance if needed,
# or to ensure we are working with the persisted state.
created_user_id = new_user_instance.id
# To ensure the returned object is safe for use after this session closes,
# especially for attributes that might be displayed immediately,
# we can load them explicitly or re-fetch.
# For simple attributes, refresh should be enough.
# We will return a freshly fetched instance to be absolutely sure.
except Exception as e:
app_logger.error(f"Error during database operation for user {user_data.username}: {e}")
return None # DB operation failed
# If user was created, fetch a fresh instance to return.
# This new instance will be detached but will have its basic attributes loaded.
if created_user_id is not None:
try:
with get_session_context() as db:
# .get() is efficient for fetching by primary key
final_user_to_return = db.get(User, created_user_id)
if final_user_to_return:
# "Touch" attributes that will be used immediately after this function returns
# to ensure they are loaded before this (new) session closes.
_ = final_user_to_return.id
_ = final_user_to_return.username
_ = final_user_to_return.email
_ = final_user_to_return.full_name
# Do NOT try to access final_user_to_return.chat_sessions here unless you
# intend to load them, which might be a heavy operation.
# They will be lazy-loaded when accessed within an active session later.
return final_user_to_return
except Exception as e:
app_logger.error(f"Error re-fetching created user {created_user_id}: {e}")
return None # Failed to re-fetch
return None # Fallback if user was not created or couldn't be re-fetched
def authenticate_user(username: str, password: str) -> Optional[User]:
"""
Authenticates a user.
Returns the User object with basic attributes loaded.
"""
try:
with get_session_context() as db:
user = get_user_by_username(db, username)
if not user:
return None # User not found
if not verify_password(password, user.hashed_password):
return None # Invalid password
# User is authenticated. Before returning the user object,
# ensure any attributes that will be immediately accessed by the caller
# (e.g., when setting st.session_state.authenticated_user) are loaded.
# This helps avoid DetachedInstanceError if Streamlit or other parts
# of the app try to access these attributes after this session closes.
user_id_to_return = user.id # Get ID to re-fetch
# Re-fetch the user in a new session context to return a "clean" detached object
# This is a robust way to prevent issues with detached instances being used across session boundaries.
if user_id_to_return is not None:
with get_session_context() as db:
authenticated_user = db.get(User, user_id_to_return)
if authenticated_user:
# "Touch" attributes
_ = authenticated_user.id
_ = authenticated_user.username
_ = authenticated_user.email
# Do not touch relationships unless intended for eager load here.
return authenticated_user
return None # Should not happen if authenticated
except Exception as e:
app_logger.error(f"Error during authentication for user {username}: {e}")
return None |