mgbam commited on
Commit
acf2fb5
·
verified ·
1 Parent(s): 3e53907

Update services/auth.py

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