Spaces:
Runtime error
Runtime error
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 +39 -143
- digipal/ui/gradio_interface.py +11 -29
digipal/auth/auth_manager.py
CHANGED
@@ -3,11 +3,10 @@ HuggingFace authentication manager for DigiPal application.
|
|
3 |
"""
|
4 |
|
5 |
import logging
|
6 |
-
import
|
7 |
-
import json
|
8 |
-
from datetime import datetime, timedelta
|
9 |
from typing import Optional, Dict, Any
|
10 |
-
from
|
|
|
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
|
21 |
|
22 |
-
|
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 |
-
|
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
|
56 |
-
return
|
|
|
|
|
|
|
57 |
|
58 |
try:
|
59 |
-
# Validate token
|
60 |
-
user_info =
|
|
|
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
|
86 |
-
|
87 |
-
|
88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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=
|
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
|
196 |
return self.get_user(user_id)
|
197 |
|
198 |
-
# Fetch updated user info
|
199 |
-
user_info =
|
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('
|
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,
|
604 |
-
|
605 |
-
|
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 |
-
#
|
615 |
-
|
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
|
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:
|