arionxiv 1.0.32__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- arionxiv/__init__.py +40 -0
- arionxiv/__main__.py +10 -0
- arionxiv/arxiv_operations/__init__.py +0 -0
- arionxiv/arxiv_operations/client.py +225 -0
- arionxiv/arxiv_operations/fetcher.py +173 -0
- arionxiv/arxiv_operations/searcher.py +122 -0
- arionxiv/arxiv_operations/utils.py +293 -0
- arionxiv/cli/__init__.py +4 -0
- arionxiv/cli/commands/__init__.py +1 -0
- arionxiv/cli/commands/analyze.py +587 -0
- arionxiv/cli/commands/auth.py +365 -0
- arionxiv/cli/commands/chat.py +714 -0
- arionxiv/cli/commands/daily.py +482 -0
- arionxiv/cli/commands/fetch.py +217 -0
- arionxiv/cli/commands/library.py +295 -0
- arionxiv/cli/commands/preferences.py +426 -0
- arionxiv/cli/commands/search.py +254 -0
- arionxiv/cli/commands/settings_unified.py +1407 -0
- arionxiv/cli/commands/trending.py +41 -0
- arionxiv/cli/commands/welcome.py +168 -0
- arionxiv/cli/main.py +407 -0
- arionxiv/cli/ui/__init__.py +1 -0
- arionxiv/cli/ui/global_theme_manager.py +173 -0
- arionxiv/cli/ui/logo.py +127 -0
- arionxiv/cli/ui/splash.py +89 -0
- arionxiv/cli/ui/theme.py +32 -0
- arionxiv/cli/ui/theme_system.py +391 -0
- arionxiv/cli/utils/__init__.py +54 -0
- arionxiv/cli/utils/animations.py +522 -0
- arionxiv/cli/utils/api_client.py +583 -0
- arionxiv/cli/utils/api_config.py +505 -0
- arionxiv/cli/utils/command_suggestions.py +147 -0
- arionxiv/cli/utils/db_config_manager.py +254 -0
- arionxiv/github_actions_runner.py +206 -0
- arionxiv/main.py +23 -0
- arionxiv/prompts/__init__.py +9 -0
- arionxiv/prompts/prompts.py +247 -0
- arionxiv/rag_techniques/__init__.py +8 -0
- arionxiv/rag_techniques/basic_rag.py +1531 -0
- arionxiv/scheduler_daemon.py +139 -0
- arionxiv/server.py +1000 -0
- arionxiv/server_main.py +24 -0
- arionxiv/services/__init__.py +73 -0
- arionxiv/services/llm_client.py +30 -0
- arionxiv/services/llm_inference/__init__.py +58 -0
- arionxiv/services/llm_inference/groq_client.py +469 -0
- arionxiv/services/llm_inference/llm_utils.py +250 -0
- arionxiv/services/llm_inference/openrouter_client.py +564 -0
- arionxiv/services/unified_analysis_service.py +872 -0
- arionxiv/services/unified_auth_service.py +457 -0
- arionxiv/services/unified_config_service.py +456 -0
- arionxiv/services/unified_daily_dose_service.py +823 -0
- arionxiv/services/unified_database_service.py +1633 -0
- arionxiv/services/unified_llm_service.py +366 -0
- arionxiv/services/unified_paper_service.py +604 -0
- arionxiv/services/unified_pdf_service.py +522 -0
- arionxiv/services/unified_prompt_service.py +344 -0
- arionxiv/services/unified_scheduler_service.py +589 -0
- arionxiv/services/unified_user_service.py +954 -0
- arionxiv/utils/__init__.py +51 -0
- arionxiv/utils/api_helpers.py +200 -0
- arionxiv/utils/file_cleanup.py +150 -0
- arionxiv/utils/ip_helper.py +96 -0
- arionxiv-1.0.32.dist-info/METADATA +336 -0
- arionxiv-1.0.32.dist-info/RECORD +69 -0
- arionxiv-1.0.32.dist-info/WHEEL +5 -0
- arionxiv-1.0.32.dist-info/entry_points.txt +4 -0
- arionxiv-1.0.32.dist-info/licenses/LICENSE +21 -0
- arionxiv-1.0.32.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unified Authentication Service for ArionXiv
|
|
3
|
+
Consolidates auth_service.py and auth_utils.py into a single local-auth module
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import secrets
|
|
8
|
+
import re
|
|
9
|
+
import jwt
|
|
10
|
+
import os
|
|
11
|
+
import hmac
|
|
12
|
+
from typing import Dict, Any, Tuple, TYPE_CHECKING
|
|
13
|
+
from datetime import datetime, timedelta
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
from bson import ObjectId
|
|
17
|
+
|
|
18
|
+
from .unified_database_service import unified_database_service
|
|
19
|
+
|
|
20
|
+
# FastAPI imports - optional, only needed for server endpoints
|
|
21
|
+
try:
|
|
22
|
+
from fastapi import HTTPException, Depends
|
|
23
|
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
24
|
+
FASTAPI_AVAILABLE = True
|
|
25
|
+
security = HTTPBearer()
|
|
26
|
+
except ImportError:
|
|
27
|
+
FASTAPI_AVAILABLE = False
|
|
28
|
+
HTTPException = Exception # Fallback for raising errors
|
|
29
|
+
security = None
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class UnifiedAuthenticationService:
|
|
35
|
+
"""Authentication service for local (password) accounts"""
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
# Password security settings
|
|
39
|
+
self.password_salt_length = int(os.getenv("PASSWORD_SALT_LENGTH", "32"))
|
|
40
|
+
self.pbkdf2_iterations = int(os.getenv("PBKDF2_ITERATIONS", "100000"))
|
|
41
|
+
self.password_algorithm = os.getenv("PASSWORD_HASH_ALGO", "pbkdf2_sha256")
|
|
42
|
+
self.min_password_length = 8
|
|
43
|
+
self.max_password_length = 128
|
|
44
|
+
self.min_user_name_length = 3
|
|
45
|
+
self.max_user_name_length = 32
|
|
46
|
+
self._user_name_pattern = re.compile(r'^[a-z0-9._-]+$')
|
|
47
|
+
self.session_duration_days = 30
|
|
48
|
+
|
|
49
|
+
# JWT settings - lazy loaded to allow module import without env vars
|
|
50
|
+
# This is needed for GitHub Actions runner which imports the module before setting env vars
|
|
51
|
+
self._secret_key = None
|
|
52
|
+
self.algorithm = "HS256"
|
|
53
|
+
self.token_expiry_hours = 24
|
|
54
|
+
|
|
55
|
+
logger.info("UnifiedAuthenticationService initialized")
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def secret_key(self) -> str:
|
|
59
|
+
"""Lazy-load JWT secret key - only required when auth methods are actually called."""
|
|
60
|
+
if self._secret_key is None:
|
|
61
|
+
self._secret_key = os.getenv("JWT_SECRET_KEY")
|
|
62
|
+
if not self._secret_key:
|
|
63
|
+
raise ValueError("JWT_SECRET_KEY environment variable is required for security.")
|
|
64
|
+
return self._secret_key
|
|
65
|
+
|
|
66
|
+
# ============================================================
|
|
67
|
+
# PASSWORD AUTHENTICATION (from auth_service.py)
|
|
68
|
+
# ============================================================
|
|
69
|
+
|
|
70
|
+
def _hash_password(self, password: str, salt: bytes = None) -> Tuple[str, str]:
|
|
71
|
+
"""
|
|
72
|
+
Hash password with salt
|
|
73
|
+
|
|
74
|
+
Purpose: Securely hash a plaintext password using PBKDF2 with SHA-256 and a salt.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
password (str): The plaintext password to hash
|
|
78
|
+
salt (bytes, optional): Optional salt. If None, a new salt is generated. Defaults to None.
|
|
79
|
+
"""
|
|
80
|
+
if salt is None:
|
|
81
|
+
salt = secrets.token_bytes(self.password_salt_length)
|
|
82
|
+
|
|
83
|
+
# Use PBKDF2 with SHA-256
|
|
84
|
+
password_hash = hashlib.pbkdf2_hmac(
|
|
85
|
+
'sha256',
|
|
86
|
+
password.encode('utf-8'),
|
|
87
|
+
salt,
|
|
88
|
+
self.pbkdf2_iterations
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return password_hash.hex(), salt.hex()
|
|
92
|
+
|
|
93
|
+
def _verify_password(self, password: str, stored_hash: str, stored_salt: str) -> bool:
|
|
94
|
+
"""
|
|
95
|
+
Verify password against stored hash using the stored salt.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
password (str): The plaintext password to verify
|
|
99
|
+
stored_hash (str): The stored password hash
|
|
100
|
+
stored_salt (str): The stored salt used for hashing
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
salt = bytes.fromhex(stored_salt)
|
|
104
|
+
password_hash, _ = self._hash_password(password, salt)
|
|
105
|
+
return hmac.compare_digest(password_hash, stored_hash)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.error("Password verification failed: %s", str(e), exc_info=True)
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
def _validate_password(self, password: str) -> Tuple[bool, str]:
|
|
111
|
+
"""
|
|
112
|
+
Validate password strength
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
password (str): The plaintext password to validate
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
if len(password) < self.min_password_length:
|
|
119
|
+
return False, f"Password must be at least {self.min_password_length} characters long"
|
|
120
|
+
|
|
121
|
+
if len(password) > self.max_password_length:
|
|
122
|
+
return False, f"Password cannot exceed {self.max_password_length} characters"
|
|
123
|
+
|
|
124
|
+
# Check for at least one letter and one digit
|
|
125
|
+
if not re.search(r'[a-zA-Z]', password):
|
|
126
|
+
return False, "Password must contain at least one letter"
|
|
127
|
+
|
|
128
|
+
if not re.search(r'\d', password):
|
|
129
|
+
return False, "Password must contain at least one digit"
|
|
130
|
+
|
|
131
|
+
return True, "Password is valid"
|
|
132
|
+
|
|
133
|
+
def _validate_email(self, email: str) -> bool:
|
|
134
|
+
"""
|
|
135
|
+
Validate email format
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
email (str): The email address to validate
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
|
142
|
+
return re.match(pattern, email) is not None
|
|
143
|
+
|
|
144
|
+
def _validate_user_name(self, user_name: str) -> Tuple[bool, str]:
|
|
145
|
+
"""Validate user_name and return normalized value"""
|
|
146
|
+
if not user_name:
|
|
147
|
+
return False, "Username is required"
|
|
148
|
+
normalized = user_name.strip().lower()
|
|
149
|
+
if len(normalized) < self.min_user_name_length or len(normalized) > self.max_user_name_length:
|
|
150
|
+
return False, f"Username must be between {self.min_user_name_length} and {self.max_user_name_length} characters"
|
|
151
|
+
if not self._user_name_pattern.match(normalized):
|
|
152
|
+
return False, "Username can only contain lowercase letters, numbers, dot, underscore, or hyphen"
|
|
153
|
+
return True, normalized
|
|
154
|
+
|
|
155
|
+
async def register_user(self, email: str, user_name: str, password: str, full_name: str = "") -> Dict[str, Any]:
|
|
156
|
+
"""Register a new user with user_name as the primary identifier"""
|
|
157
|
+
try:
|
|
158
|
+
if not self._validate_email(email):
|
|
159
|
+
return {'success': False, 'error': 'Invalid email format'}
|
|
160
|
+
|
|
161
|
+
name_valid, normalized_user_name = self._validate_user_name(user_name)
|
|
162
|
+
if not name_valid:
|
|
163
|
+
return {'success': False, 'error': normalized_user_name}
|
|
164
|
+
|
|
165
|
+
password_valid, password_message = self._validate_password(password)
|
|
166
|
+
if not password_valid:
|
|
167
|
+
return {'success': False, 'error': password_message}
|
|
168
|
+
|
|
169
|
+
# Enforce uniqueness on user_name and best-effort on email
|
|
170
|
+
existing_by_name = await unified_database_service.find_one('users', {'user_name': normalized_user_name})
|
|
171
|
+
if existing_by_name:
|
|
172
|
+
return {'success': False, 'error': 'Username is already taken'}
|
|
173
|
+
|
|
174
|
+
existing_by_email = await unified_database_service.find_one('users', {'email': email})
|
|
175
|
+
if existing_by_email:
|
|
176
|
+
return {'success': False, 'error': 'User with this email already exists'}
|
|
177
|
+
|
|
178
|
+
password_hash, salt = self._hash_password(password)
|
|
179
|
+
|
|
180
|
+
user_data = {
|
|
181
|
+
'email': email,
|
|
182
|
+
'user_name': normalized_user_name,
|
|
183
|
+
'username': normalized_user_name,
|
|
184
|
+
'full_name': full_name,
|
|
185
|
+
'password_hash': password_hash,
|
|
186
|
+
'password_salt': salt,
|
|
187
|
+
'password_algo': self.password_algorithm,
|
|
188
|
+
'created_at': datetime.utcnow(),
|
|
189
|
+
'last_login': None,
|
|
190
|
+
'is_active': True,
|
|
191
|
+
'auth_provider': 'local'
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
result = await unified_database_service.insert_one('users', user_data)
|
|
195
|
+
|
|
196
|
+
if result and getattr(result, 'inserted_id', None):
|
|
197
|
+
user_payload = {
|
|
198
|
+
'id': str(result.inserted_id),
|
|
199
|
+
'email': email,
|
|
200
|
+
'user_name': normalized_user_name,
|
|
201
|
+
'full_name': full_name
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
'success': True,
|
|
205
|
+
'message': 'User registered successfully',
|
|
206
|
+
'user': user_payload
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return {'success': False, 'error': 'Failed to create user'}
|
|
210
|
+
|
|
211
|
+
except Exception as e:
|
|
212
|
+
logger.error("User registration failed: %s", str(e), exc_info=True)
|
|
213
|
+
return {
|
|
214
|
+
'success': False,
|
|
215
|
+
'error': 'Registration failed due to server error'
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async def authenticate_user(self, identifier: str, password: str) -> Dict[str, Any]:
|
|
219
|
+
"""Authenticate user by user_name or email"""
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
lookup_filter = None
|
|
223
|
+
if self._validate_email(identifier):
|
|
224
|
+
lookup_filter = {'email': identifier}
|
|
225
|
+
else:
|
|
226
|
+
name_valid, normalized_user_name = self._validate_user_name(identifier)
|
|
227
|
+
if not name_valid:
|
|
228
|
+
return {'success': False, 'error': 'Invalid username or password'}
|
|
229
|
+
lookup_filter = {'user_name': normalized_user_name}
|
|
230
|
+
|
|
231
|
+
user = await unified_database_service.find_one('users', lookup_filter)
|
|
232
|
+
if not user:
|
|
233
|
+
return {'success': False, 'error': 'Invalid username or password'}
|
|
234
|
+
|
|
235
|
+
if not user.get('is_active', True):
|
|
236
|
+
return {'success': False, 'error': 'Account is deactivated'}
|
|
237
|
+
|
|
238
|
+
if user.get('auth_provider') != 'local':
|
|
239
|
+
return {
|
|
240
|
+
'success': False,
|
|
241
|
+
'error': 'Please sign in with your linked provider'
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if not self._verify_password(password, user['password_hash'], user['password_salt']):
|
|
245
|
+
return {'success': False, 'error': 'Invalid username or password'}
|
|
246
|
+
|
|
247
|
+
await unified_database_service.update_one(
|
|
248
|
+
'users',
|
|
249
|
+
{'_id': user['_id']},
|
|
250
|
+
{'$set': {'last_login': datetime.utcnow()}}
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
token = self.create_access_token(user)
|
|
254
|
+
primary_user_name = user.get('user_name') or user.get('username') or user['email'].split('@')[0]
|
|
255
|
+
user_payload = {
|
|
256
|
+
'id': str(user['_id']),
|
|
257
|
+
'email': user['email'],
|
|
258
|
+
'user_name': primary_user_name,
|
|
259
|
+
'full_name': user.get('full_name', '')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
'success': True,
|
|
264
|
+
'message': 'Authentication successful',
|
|
265
|
+
'user': user_payload,
|
|
266
|
+
'token': token
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
except Exception as e:
|
|
270
|
+
logger.error("User authentication failed: %s", str(e), exc_info=True)
|
|
271
|
+
return {
|
|
272
|
+
'success': False,
|
|
273
|
+
'error': 'Authentication failed due to server error'
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async def login_user(self, identifier: str, password: str) -> Dict[str, Any]:
|
|
277
|
+
"""Compatibility wrapper for CLI login flow"""
|
|
278
|
+
return await self.authenticate_user(identifier, password)
|
|
279
|
+
|
|
280
|
+
# ============================================================
|
|
281
|
+
# JWT UTILITIES (from auth_utils.py)
|
|
282
|
+
# ============================================================
|
|
283
|
+
|
|
284
|
+
def create_access_token(self, user_data: Dict[str, Any]) -> str:
|
|
285
|
+
"""
|
|
286
|
+
Creates JWT access token
|
|
287
|
+
|
|
288
|
+
Purpose: Generate a JWT token for authenticated users to access protected resources during the active session.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
user_data (Dict[str, Any]): User data to include in the token payload
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
str: Encoded JWT token
|
|
295
|
+
"""
|
|
296
|
+
try:
|
|
297
|
+
payload = {
|
|
298
|
+
"user_id": str(user_data.get("_id", user_data.get("id"))),
|
|
299
|
+
"email": user_data.get("email"),
|
|
300
|
+
"user_name": user_data.get("user_name") or user_data.get("username"),
|
|
301
|
+
"exp": datetime.utcnow() + timedelta(hours=self.token_expiry_hours),
|
|
302
|
+
"iat": datetime.utcnow()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
|
|
306
|
+
return token
|
|
307
|
+
|
|
308
|
+
except Exception as e:
|
|
309
|
+
logger.error("Failed to create access token: %s", str(e), exc_info=True)
|
|
310
|
+
raise HTTPException(status_code=500, detail="Token creation failed")
|
|
311
|
+
|
|
312
|
+
def verify_token(self, token: str) -> Dict[str, Any]:
|
|
313
|
+
"""
|
|
314
|
+
Verifies JWT token and return payload
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
token (str): JWT token to verify
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Dict[str, Any]: Verification result with validity and payload or error message
|
|
321
|
+
"""
|
|
322
|
+
try:
|
|
323
|
+
payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
|
|
324
|
+
return {"valid": True, "payload": payload}
|
|
325
|
+
|
|
326
|
+
except jwt.ExpiredSignatureError:
|
|
327
|
+
return {"valid": False, "error": "Token has expired"}
|
|
328
|
+
except jwt.InvalidTokenError as e:
|
|
329
|
+
return {"valid": False, "error": f"Invalid token: {str(e)}"}
|
|
330
|
+
|
|
331
|
+
async def get_current_user(self, credentials) -> Dict[str, Any]:
|
|
332
|
+
"""
|
|
333
|
+
Get current user from token
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
credentials: Authorization credentials containing the JWT token
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Dict[str, Any]: Current user information extracted from the token
|
|
340
|
+
"""
|
|
341
|
+
result = self.verify_token(credentials.credentials)
|
|
342
|
+
|
|
343
|
+
if not result["valid"]:
|
|
344
|
+
raise HTTPException(status_code=401, detail=result["error"])
|
|
345
|
+
|
|
346
|
+
user_id = result["payload"]["user_id"]
|
|
347
|
+
|
|
348
|
+
# Get user from database
|
|
349
|
+
try:
|
|
350
|
+
user = await unified_database_service.find_one('users', {'_id': ObjectId(user_id)})
|
|
351
|
+
if not user:
|
|
352
|
+
raise HTTPException(status_code=401, detail="User not found")
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
"id": str(user["_id"]),
|
|
356
|
+
"email": user["email"],
|
|
357
|
+
"user_name": user.get("user_name") or user.get("username") or user["email"].split("@")[0]
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
except Exception as e:
|
|
361
|
+
logger.error("Failed to get current user: %s", str(e), exc_info=True)
|
|
362
|
+
raise HTTPException(status_code=401, detail="Authentication failed")
|
|
363
|
+
|
|
364
|
+
async def get_user_settings(self, user_id: str) -> Dict[str, Any]:
|
|
365
|
+
"""Get user settings from database"""
|
|
366
|
+
try:
|
|
367
|
+
user_object_id = ObjectId(user_id)
|
|
368
|
+
user = await unified_database_service.find_one('users', {'_id': user_object_id})
|
|
369
|
+
if not user:
|
|
370
|
+
return {
|
|
371
|
+
'success': False,
|
|
372
|
+
'error': 'User not found'
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
'success': True,
|
|
377
|
+
'settings': user.get('settings', {})
|
|
378
|
+
}
|
|
379
|
+
except Exception as e:
|
|
380
|
+
logger.error("Failed to get user settings: %s", str(e), exc_info=True)
|
|
381
|
+
return {
|
|
382
|
+
'success': False,
|
|
383
|
+
'error': 'Failed to retrieve user settings'
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async def update_user_settings(self, user_id: str, settings: Dict[str, Any]) -> Dict[str, Any]:
|
|
387
|
+
"""Update user settings in database"""
|
|
388
|
+
try:
|
|
389
|
+
user_object_id = ObjectId(user_id)
|
|
390
|
+
result = await unified_database_service.update_one(
|
|
391
|
+
'users',
|
|
392
|
+
{'_id': user_object_id},
|
|
393
|
+
{'$set': {'settings': settings}}
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Check if document was matched (user exists)
|
|
397
|
+
matched_count = getattr(result, 'matched_count', None)
|
|
398
|
+
if matched_count is None and isinstance(result, dict):
|
|
399
|
+
matched_count = result.get('matched_count', 0)
|
|
400
|
+
|
|
401
|
+
modified_count = getattr(result, 'modified_count', None)
|
|
402
|
+
if modified_count is None and isinstance(result, dict):
|
|
403
|
+
modified_count = result.get('modified_count', 0)
|
|
404
|
+
|
|
405
|
+
# Success if document was found (matched), even if no changes (same values)
|
|
406
|
+
if matched_count and matched_count > 0:
|
|
407
|
+
return {
|
|
408
|
+
'success': True,
|
|
409
|
+
'message': 'Settings updated successfully'
|
|
410
|
+
}
|
|
411
|
+
else:
|
|
412
|
+
return {
|
|
413
|
+
'success': False,
|
|
414
|
+
'error': 'User not found'
|
|
415
|
+
}
|
|
416
|
+
except Exception as e:
|
|
417
|
+
logger.error("Failed to update user settings: %s", str(e), exc_info=True)
|
|
418
|
+
return {
|
|
419
|
+
'success': False,
|
|
420
|
+
'error': 'Failed to update user settings'
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# Global instance
|
|
425
|
+
unified_auth_service = UnifiedAuthenticationService()
|
|
426
|
+
|
|
427
|
+
# Backwards compatibility
|
|
428
|
+
auth_service = unified_auth_service
|
|
429
|
+
auth_utils = unified_auth_service
|
|
430
|
+
|
|
431
|
+
# Export commonly used functions for convenience
|
|
432
|
+
create_access_token = unified_auth_service.create_access_token
|
|
433
|
+
verify_token = unified_auth_service.verify_token
|
|
434
|
+
get_current_user = unified_auth_service.get_current_user
|
|
435
|
+
register_user = unified_auth_service.register_user
|
|
436
|
+
authenticate_user = unified_auth_service.authenticate_user
|
|
437
|
+
login_user = unified_auth_service.login_user
|
|
438
|
+
get_user_settings = unified_auth_service.get_user_settings
|
|
439
|
+
update_user_settings = unified_auth_service.update_user_settings
|
|
440
|
+
|
|
441
|
+
# Make available for imports
|
|
442
|
+
__all__ = [
|
|
443
|
+
'UnifiedAuthenticationService',
|
|
444
|
+
'unified_auth_service',
|
|
445
|
+
'auth_service',
|
|
446
|
+
'auth_utils',
|
|
447
|
+
'create_access_token',
|
|
448
|
+
'verify_token',
|
|
449
|
+
'get_current_user',
|
|
450
|
+
'register_user',
|
|
451
|
+
'authenticate_user',
|
|
452
|
+
'login_user',
|
|
453
|
+
'get_user_settings',
|
|
454
|
+
'update_user_settings',
|
|
455
|
+
'security',
|
|
456
|
+
'FASTAPI_AVAILABLE'
|
|
457
|
+
]
|