BladeSzaSza Claude commited on
Commit
290d5ce
·
1 Parent(s): c626b26

Implement official HuggingFace Hub authentication and remove offline mode

Browse files

- Replace custom API calls with official huggingface_hub.whoami() function
- Remove offline mode, demo tester functionality, and development workarounds
- Update authentication to use proper HF Hub error handling with HfHubHTTPError
- Simplify authentication flow to only support valid HuggingFace tokens
- Remove offline_toggle from Gradio interface and related event handlers
- Update user creation to handle official HF Hub API response format

This provides more secure and reliable authentication using official HF methods.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>

digipal/auth/auth_manager.py CHANGED
@@ -3,11 +3,10 @@ HuggingFace authentication manager for DigiPal application.
3
  """
4
 
5
  import logging
6
- import requests
7
- import json
8
- from datetime import datetime, timedelta
9
  from typing import Optional, Dict, Any
10
- from pathlib import Path
 
11
 
12
  from .models import User, AuthSession, AuthResult, AuthStatus
13
  from .session_manager import SessionManager
@@ -17,34 +16,24 @@ logger = logging.getLogger(__name__)
17
 
18
 
19
  class AuthManager:
20
- """Manages HuggingFace authentication with offline support."""
21
 
22
- # HuggingFace API endpoints
23
- HF_API_BASE = "https://huggingface.co/api"
24
- HF_USER_ENDPOINT = f"{HF_API_BASE}/whoami"
25
-
26
- def __init__(self, db_connection: DatabaseConnection, offline_mode: bool = False, cache_dir: Optional[str] = None):
27
  """
28
  Initialize authentication manager.
29
 
30
  Args:
31
  db_connection: Database connection for user storage
32
- offline_mode: Enable offline development mode
33
  cache_dir: Directory for authentication cache
34
  """
35
  self.db = db_connection
36
- self.offline_mode = offline_mode
37
  self.session_manager = SessionManager(db_connection, cache_dir)
38
 
39
- # Request session for connection pooling
40
- self.session = requests.Session()
41
- self.session.timeout = 10 # 10 second timeout
42
-
43
- logger.info(f"AuthManager initialized (offline_mode: {offline_mode})")
44
 
45
  def authenticate(self, token: str) -> AuthResult:
46
  """
47
- Authenticate user with HuggingFace token.
48
 
49
  Args:
50
  token: HuggingFace authentication token
@@ -52,12 +41,16 @@ class AuthManager:
52
  Returns:
53
  Authentication result with user and session info
54
  """
55
- if self.offline_mode:
56
- return self._authenticate_offline(token)
 
 
 
57
 
58
  try:
59
- # Validate token with HuggingFace API
60
- user_info = self._validate_hf_token(token)
 
61
  if not user_info:
62
  return AuthResult(
63
  status=AuthStatus.INVALID_TOKEN,
@@ -65,7 +58,7 @@ class AuthManager:
65
  )
66
 
67
  # Create or update user
68
- user = self._create_or_update_user(user_info, token)
69
  if not user:
70
  return AuthResult(
71
  status=AuthStatus.USER_NOT_FOUND,
@@ -73,7 +66,7 @@ class AuthManager:
73
  )
74
 
75
  # Create session
76
- session = self.session_manager.create_session(user, token)
77
 
78
  logger.info(f"Successfully authenticated user: {user.username}")
79
  return AuthResult(
@@ -82,10 +75,19 @@ class AuthManager:
82
  session=session
83
  )
84
 
85
- except requests.exceptions.RequestException as e:
86
- logger.warning(f"Network error during authentication: {e}")
87
- # Try offline authentication as fallback
88
- return self._authenticate_offline(token)
 
 
 
 
 
 
 
 
 
89
 
90
  except Exception as e:
91
  logger.error(f"Authentication error: {e}")
@@ -125,9 +127,8 @@ class AuthManager:
125
  # Refresh session
126
  self.session_manager.refresh_session(user_id)
127
 
128
- status = AuthStatus.OFFLINE_MODE if session.is_offline else AuthStatus.SUCCESS
129
  return AuthResult(
130
- status=status,
131
  user=user,
132
  session=session
133
  )
@@ -186,17 +187,14 @@ class AuthManager:
186
  Returns:
187
  Updated user object
188
  """
189
- if self.offline_mode:
190
- return self.get_user(user_id)
191
-
192
  try:
193
  # Get current session to get token
194
  session = self.session_manager.get_session(user_id)
195
- if not session or session.is_offline:
196
  return self.get_user(user_id)
197
 
198
- # Fetch updated user info
199
- user_info = self._validate_hf_token(session.token)
200
  if user_info:
201
  user = self._create_or_update_user(user_info, session.token)
202
  logger.info(f"Refreshed profile for user: {user_id}")
@@ -211,124 +209,26 @@ class AuthManager:
211
  """Clean up expired sessions."""
212
  return self.session_manager.cleanup_expired_sessions()
213
 
214
- def _authenticate_offline(self, token: str) -> AuthResult:
215
- """
216
- Authenticate in offline mode using cached data.
217
-
218
- Args:
219
- token: Authentication token
220
-
221
- Returns:
222
- Authentication result for offline mode
223
- """
224
- # In offline mode, we create a development user
225
- # This is for development purposes only
226
-
227
- if not token or len(token) < 10:
228
- return AuthResult(
229
- status=AuthStatus.INVALID_TOKEN,
230
- error_message="Token too short for offline mode"
231
- )
232
-
233
- # Create a deterministic user ID from token
234
- import hashlib
235
- user_id = f"offline_{hashlib.md5(token.encode()).hexdigest()[:16]}"
236
- username = f"dev_user_{user_id[-8:]}"
237
-
238
- # Check if offline user exists
239
- user = self.get_user(user_id)
240
- if not user:
241
- # Create offline development user
242
- user = User(
243
- id=user_id,
244
- username=username,
245
- email=f"{username}@offline.dev",
246
- full_name=f"Development User {username}",
247
- created_at=datetime.now()
248
- )
249
-
250
- # Save to database
251
- try:
252
- self.db.execute_update(
253
- '''INSERT OR REPLACE INTO users
254
- (id, username, huggingface_token, created_at, last_login)
255
- VALUES (?, ?, ?, ?, ?)''',
256
- (user.id, user.username, token,
257
- user.created_at.isoformat(), datetime.now().isoformat())
258
- )
259
- except Exception as e:
260
- logger.error(f"Error creating offline user: {e}")
261
- return AuthResult(
262
- status=AuthStatus.NETWORK_ERROR,
263
- error_message="Failed to create offline user"
264
- )
265
-
266
- # Create offline session
267
- session = self.session_manager.create_session(
268
- user, token, expires_hours=168, is_offline=True # 1 week for offline
269
- )
270
-
271
- logger.info(f"Offline authentication successful for: {username}")
272
- return AuthResult(
273
- status=AuthStatus.OFFLINE_MODE,
274
- user=user,
275
- session=session
276
- )
277
-
278
- def _validate_hf_token(self, token: str) -> Optional[Dict[str, Any]]:
279
- """
280
- Validate token with HuggingFace API.
281
-
282
- Args:
283
- token: HuggingFace token
284
-
285
- Returns:
286
- User info dict if valid, None otherwise
287
- """
288
- try:
289
- headers = {
290
- 'Authorization': f'Bearer {token}',
291
- 'User-Agent': 'DigiPal/1.0'
292
- }
293
-
294
- response = self.session.get(self.HF_USER_ENDPOINT, headers=headers)
295
-
296
- if response.status_code == 200:
297
- user_info = response.json()
298
- logger.debug(f"HF API response: {user_info}")
299
- return user_info
300
- elif response.status_code == 401:
301
- logger.warning("Invalid HuggingFace token")
302
- return None
303
- else:
304
- logger.error(f"HF API error: {response.status_code} - {response.text}")
305
- return None
306
-
307
- except requests.exceptions.RequestException as e:
308
- logger.error(f"Network error validating HF token: {e}")
309
- raise
310
- except Exception as e:
311
- logger.error(f"Error validating HF token: {e}")
312
- return None
313
 
314
  def _create_or_update_user(self, user_info: Dict[str, Any], token: str) -> Optional[User]:
315
  """
316
  Create or update user from HuggingFace user info.
317
 
318
  Args:
319
- user_info: User info from HuggingFace API
320
  token: Authentication token
321
 
322
  Returns:
323
  User object
324
  """
325
  try:
326
- # Extract user data from HF response
 
327
  user_id = user_info.get('name', user_info.get('id', ''))
328
  username = user_info.get('name', user_id)
329
  email = user_info.get('email')
330
- full_name = user_info.get('fullname', user_info.get('name'))
331
- avatar_url = user_info.get('avatarUrl')
332
 
333
  if not user_id:
334
  logger.error("No user ID in HuggingFace response")
@@ -378,7 +278,3 @@ class AuthManager:
378
  logger.error(f"Error creating/updating user: {e}")
379
  return None
380
 
381
- def __del__(self):
382
- """Cleanup resources."""
383
- if hasattr(self, 'session'):
384
- self.session.close()
 
3
  """
4
 
5
  import logging
6
+ from datetime import datetime
 
 
7
  from typing import Optional, Dict, Any
8
+ from huggingface_hub import HfApi, whoami
9
+ from huggingface_hub.utils import HfHubHTTPError
10
 
11
  from .models import User, AuthSession, AuthResult, AuthStatus
12
  from .session_manager import SessionManager
 
16
 
17
 
18
  class AuthManager:
19
+ """Manages HuggingFace authentication using official HF Hub methods."""
20
 
21
+ def __init__(self, db_connection: DatabaseConnection, cache_dir: Optional[str] = None):
 
 
 
 
22
  """
23
  Initialize authentication manager.
24
 
25
  Args:
26
  db_connection: Database connection for user storage
 
27
  cache_dir: Directory for authentication cache
28
  """
29
  self.db = db_connection
 
30
  self.session_manager = SessionManager(db_connection, cache_dir)
31
 
32
+ logger.info("AuthManager initialized with official HuggingFace Hub authentication")
 
 
 
 
33
 
34
  def authenticate(self, token: str) -> AuthResult:
35
  """
36
+ Authenticate user with HuggingFace token using official HF Hub methods.
37
 
38
  Args:
39
  token: HuggingFace authentication token
 
41
  Returns:
42
  Authentication result with user and session info
43
  """
44
+ if not token or not token.strip():
45
+ return AuthResult(
46
+ status=AuthStatus.INVALID_TOKEN,
47
+ error_message="Token cannot be empty"
48
+ )
49
 
50
  try:
51
+ # Validate token using official HuggingFace Hub whoami function
52
+ user_info = whoami(token=token.strip())
53
+
54
  if not user_info:
55
  return AuthResult(
56
  status=AuthStatus.INVALID_TOKEN,
 
58
  )
59
 
60
  # Create or update user
61
+ user = self._create_or_update_user(user_info, token.strip())
62
  if not user:
63
  return AuthResult(
64
  status=AuthStatus.USER_NOT_FOUND,
 
66
  )
67
 
68
  # Create session
69
+ session = self.session_manager.create_session(user, token.strip())
70
 
71
  logger.info(f"Successfully authenticated user: {user.username}")
72
  return AuthResult(
 
75
  session=session
76
  )
77
 
78
+ except HfHubHTTPError as e:
79
+ if e.response.status_code == 401:
80
+ logger.warning("Invalid HuggingFace token")
81
+ return AuthResult(
82
+ status=AuthStatus.INVALID_TOKEN,
83
+ error_message="Invalid or expired HuggingFace token"
84
+ )
85
+ else:
86
+ logger.error(f"HuggingFace API error: {e}")
87
+ return AuthResult(
88
+ status=AuthStatus.NETWORK_ERROR,
89
+ error_message=f"HuggingFace API error: {str(e)}"
90
+ )
91
 
92
  except Exception as e:
93
  logger.error(f"Authentication error: {e}")
 
127
  # Refresh session
128
  self.session_manager.refresh_session(user_id)
129
 
 
130
  return AuthResult(
131
+ status=AuthStatus.SUCCESS,
132
  user=user,
133
  session=session
134
  )
 
187
  Returns:
188
  Updated user object
189
  """
 
 
 
190
  try:
191
  # Get current session to get token
192
  session = self.session_manager.get_session(user_id)
193
+ if not session:
194
  return self.get_user(user_id)
195
 
196
+ # Fetch updated user info using official HF Hub methods
197
+ user_info = whoami(token=session.token)
198
  if user_info:
199
  user = self._create_or_update_user(user_info, session.token)
200
  logger.info(f"Refreshed profile for user: {user_id}")
 
209
  """Clean up expired sessions."""
210
  return self.session_manager.cleanup_expired_sessions()
211
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
  def _create_or_update_user(self, user_info: Dict[str, Any], token: str) -> Optional[User]:
214
  """
215
  Create or update user from HuggingFace user info.
216
 
217
  Args:
218
+ user_info: User info from HuggingFace Hub API
219
  token: Authentication token
220
 
221
  Returns:
222
  User object
223
  """
224
  try:
225
+ # Extract user data from HF Hub API response
226
+ # The official HF Hub API returns different field names
227
  user_id = user_info.get('name', user_info.get('id', ''))
228
  username = user_info.get('name', user_id)
229
  email = user_info.get('email')
230
+ full_name = user_info.get('fullname', user_info.get('fullName', username))
231
+ avatar_url = user_info.get('avatarUrl', user_info.get('avatar_url'))
232
 
233
  if not user_id:
234
  logger.error("No user ID in HuggingFace response")
 
278
  logger.error(f"Error creating/updating user: {e}")
279
  return None
280
 
 
 
 
 
digipal/ui/gradio_interface.py CHANGED
@@ -124,13 +124,6 @@ class GradioInterface:
124
  "",
125
  elem_classes=["auth-status"]
126
  )
127
-
128
- # Offline mode toggle for development (outside accordion to avoid context issues)
129
- offline_toggle = gr.Checkbox(
130
- label="Enable Offline Mode (Development)",
131
- value=False,
132
- info="For development without HuggingFace token"
133
- )
134
 
135
  with gr.Column(scale=1):
136
  pass # Empty column for centering
@@ -154,8 +147,7 @@ class GradioInterface:
154
  return {
155
  'token_input': token_input,
156
  'login_btn': login_btn,
157
- 'auth_status': auth_status,
158
- 'offline_toggle': offline_toggle
159
  }
160
 
161
  def _create_egg_selection_interface(self) -> Dict[str, Any]:
@@ -430,7 +422,6 @@ class GradioInterface:
430
  fn=self._handle_login,
431
  inputs=[
432
  auth_components['token_input'],
433
- auth_components['offline_toggle'],
434
  user_state,
435
  token_state
436
  ],
@@ -600,27 +591,22 @@ class GradioInterface:
600
  outputs=[main_components['status_info']]
601
  )
602
 
603
- def _handle_login(self, token: str, offline_mode: bool,
604
- current_user: Optional[str], current_token: Optional[str]) -> Tuple:
605
- """Handle user login."""
606
- if not token:
607
  return (
608
- '<div class="error">Please enter a token</div>',
609
  current_user,
610
  current_token,
611
  gr.update(selected="auth_tab") # Stay on auth tab
612
  )
613
 
614
- # Set offline mode if requested
615
- if offline_mode:
616
- self.auth_manager.offline_mode = True
617
-
618
- # Authenticate user
619
- auth_result = self.auth_manager.authenticate(token)
620
 
621
- if auth_result.status in [AuthStatus.SUCCESS, AuthStatus.OFFLINE_MODE]:
622
  self.current_user_id = auth_result.user.id
623
- self.current_token = token
624
 
625
  # Check if user has existing DigiPal
626
  existing_pet = self.digipal_core.load_existing_pet(auth_result.user.id)
@@ -628,25 +614,21 @@ class GradioInterface:
628
  if existing_pet:
629
  # User has existing pet, go to main interface
630
  status_msg = f'<div class="success">Welcome back, {auth_result.user.username}!</div>'
631
- if auth_result.status == AuthStatus.OFFLINE_MODE:
632
- status_msg += '<div class="info">Running in offline mode</div>'
633
 
634
  return (
635
  status_msg,
636
  auth_result.user.id,
637
- token,
638
  gr.update(selected="main_tab") # Go to main tab
639
  )
640
  else:
641
  # New user, go to egg selection
642
  status_msg = f'<div class="success">Welcome, {auth_result.user.username}! Choose your first egg.</div>'
643
- if auth_result.status == AuthStatus.OFFLINE_MODE:
644
- status_msg += '<div class="info">Running in offline mode</div>'
645
 
646
  return (
647
  status_msg,
648
  auth_result.user.id,
649
- token,
650
  gr.update(selected="egg_tab") # Go to egg tab
651
  )
652
  else:
 
124
  "",
125
  elem_classes=["auth-status"]
126
  )
 
 
 
 
 
 
 
127
 
128
  with gr.Column(scale=1):
129
  pass # Empty column for centering
 
147
  return {
148
  'token_input': token_input,
149
  'login_btn': login_btn,
150
+ 'auth_status': auth_status
 
151
  }
152
 
153
  def _create_egg_selection_interface(self) -> Dict[str, Any]:
 
422
  fn=self._handle_login,
423
  inputs=[
424
  auth_components['token_input'],
 
425
  user_state,
426
  token_state
427
  ],
 
591
  outputs=[main_components['status_info']]
592
  )
593
 
594
+ def _handle_login(self, token: str, current_user: Optional[str], current_token: Optional[str]) -> Tuple:
595
+ """Handle user login with HuggingFace authentication."""
596
+ if not token or not token.strip():
 
597
  return (
598
+ '<div class="error">Please enter a valid HuggingFace token</div>',
599
  current_user,
600
  current_token,
601
  gr.update(selected="auth_tab") # Stay on auth tab
602
  )
603
 
604
+ # Authenticate user using official HuggingFace Hub methods
605
+ auth_result = self.auth_manager.authenticate(token.strip())
 
 
 
 
606
 
607
+ if auth_result.status == AuthStatus.SUCCESS:
608
  self.current_user_id = auth_result.user.id
609
+ self.current_token = token.strip()
610
 
611
  # Check if user has existing DigiPal
612
  existing_pet = self.digipal_core.load_existing_pet(auth_result.user.id)
 
614
  if existing_pet:
615
  # User has existing pet, go to main interface
616
  status_msg = f'<div class="success">Welcome back, {auth_result.user.username}!</div>'
 
 
617
 
618
  return (
619
  status_msg,
620
  auth_result.user.id,
621
+ token.strip(),
622
  gr.update(selected="main_tab") # Go to main tab
623
  )
624
  else:
625
  # New user, go to egg selection
626
  status_msg = f'<div class="success">Welcome, {auth_result.user.username}! Choose your first egg.</div>'
 
 
627
 
628
  return (
629
  status_msg,
630
  auth_result.user.id,
631
+ token.strip(),
632
  gr.update(selected="egg_tab") # Go to egg tab
633
  )
634
  else: