ud-resolver 1.1.0__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.
- backend/__init__.py +20 -0
- backend/api/__init__.py +110 -0
- backend/api/auth.py +433 -0
- backend/api/dependencies.py +40 -0
- backend/api/exceptions.py +80 -0
- backend/api/main.py +434 -0
- backend/api/middleware.py +597 -0
- backend/api/routes/__init__.py +45 -0
- backend/api/routes/auth.py +297 -0
- backend/api/routes/packages.py +1145 -0
- backend/api/routes/scan.py +184 -0
- backend/api/routes/system.py +1692 -0
- backend/api/schemas.py +37 -0
- backend/cli.py +657 -0
- backend/core/__init__.py +5 -0
- backend/core/cache.py +385 -0
- backend/core/conflict_resolver.py +1185 -0
- backend/core/data_aggregator.py +1221 -0
- backend/core/export_generator.py +510 -0
- backend/core/system_scanner.py +2641 -0
- backend/core/utils.py +162 -0
- backend/data_sources/__init__.py +35 -0
- backend/data_sources/apk_client.py +354 -0
- backend/data_sources/apt_client.py +338 -0
- backend/data_sources/base_client.py +212 -0
- backend/data_sources/cocoapods_client.py +263 -0
- backend/data_sources/conda_client.py +627 -0
- backend/data_sources/crates_client.py +752 -0
- backend/data_sources/documentation_scraper.py +1044 -0
- backend/data_sources/gomodules_client.py +297 -0
- backend/data_sources/homebrew_client.py +532 -0
- backend/data_sources/maven_client.py +1561 -0
- backend/data_sources/npm_client.py +1099 -0
- backend/data_sources/nuget_client.py +666 -0
- backend/data_sources/packagist_client.py +534 -0
- backend/data_sources/pypi_client.py +877 -0
- backend/data_sources/rubygems_client.py +527 -0
- backend/data_sources/utils.py +14 -0
- backend/database/__init__.py +28 -0
- backend/database/compatibility_db.py +874 -0
- backend/database/models.py +440 -0
- backend/logging_config.py +60 -0
- backend/manifest_detector.py +401 -0
- backend/settings.py +831 -0
- backend/tracing_config.py +192 -0
- backend/utils/errors.py +173 -0
- ud_resolver-1.1.0.dist-info/METADATA +346 -0
- ud_resolver-1.1.0.dist-info/RECORD +52 -0
- ud_resolver-1.1.0.dist-info/WHEEL +5 -0
- ud_resolver-1.1.0.dist-info/entry_points.txt +2 -0
- ud_resolver-1.1.0.dist-info/licenses/LICENSE +21 -0
- ud_resolver-1.1.0.dist-info/top_level.txt +1 -0
backend/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# backend/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
Universal Dependency Resolver Backend Package
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .settings import get_ecosystem_config
|
|
7
|
+
from .core import DataAggregator, ConflictResolver, SystemScanner, ExportGenerator
|
|
8
|
+
from .manifest_detector import ManifestDetector
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"get_ecosystem_config",
|
|
12
|
+
"DataAggregator",
|
|
13
|
+
"ConflictResolver",
|
|
14
|
+
"SystemScanner",
|
|
15
|
+
"ExportGenerator",
|
|
16
|
+
"ManifestDetector",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
backend/api/__init__.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# backend/api/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
Universal Dependency Resolver API Package
|
|
4
|
+
|
|
5
|
+
This package contains all API-related components including routes,
|
|
6
|
+
middleware, authentication, and exception handling.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Import main app instance
|
|
10
|
+
from .main import app
|
|
11
|
+
|
|
12
|
+
# Import routers
|
|
13
|
+
from .routes import packages, system
|
|
14
|
+
|
|
15
|
+
# Import middleware
|
|
16
|
+
from .middleware import (
|
|
17
|
+
CorrelationIDMiddleware,
|
|
18
|
+
LoggingMiddleware,
|
|
19
|
+
PerformanceMiddleware,
|
|
20
|
+
CompressionMiddleware,
|
|
21
|
+
SecurityHeadersMiddleware,
|
|
22
|
+
RequestSizeLimitMiddleware,
|
|
23
|
+
CacheMiddleware,
|
|
24
|
+
MetricsMiddleware,
|
|
25
|
+
MaintenanceModeMiddleware,
|
|
26
|
+
AuditLogMiddleware,
|
|
27
|
+
CSRFProtectionMiddleware,
|
|
28
|
+
setup_middleware,
|
|
29
|
+
get_client_ip,
|
|
30
|
+
get_user_agent,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Import exceptions
|
|
34
|
+
from .exceptions import (
|
|
35
|
+
DependencyResolverError,
|
|
36
|
+
ValidationError,
|
|
37
|
+
PackageNotFoundError,
|
|
38
|
+
EcosystemNotSupportedError,
|
|
39
|
+
ConflictResolutionError,
|
|
40
|
+
RateLimitExceededError,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Import auth components
|
|
44
|
+
from .auth import (
|
|
45
|
+
# Models
|
|
46
|
+
Token,
|
|
47
|
+
TokenData,
|
|
48
|
+
UserCreate,
|
|
49
|
+
UserLogin,
|
|
50
|
+
APIKeyCreate,
|
|
51
|
+
# Functions
|
|
52
|
+
verify_password,
|
|
53
|
+
get_password_hash,
|
|
54
|
+
create_access_token,
|
|
55
|
+
create_refresh_token,
|
|
56
|
+
generate_api_key,
|
|
57
|
+
get_current_user,
|
|
58
|
+
get_current_active_user,
|
|
59
|
+
require_scopes,
|
|
60
|
+
# Service
|
|
61
|
+
AuthService,
|
|
62
|
+
# OAuth2
|
|
63
|
+
oauth2_scheme,
|
|
64
|
+
login_for_access_token,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Version info
|
|
68
|
+
__version__ = "1.0.0"
|
|
69
|
+
__author__ = "Universal Dependency Resolver Team"
|
|
70
|
+
|
|
71
|
+
# Export main components
|
|
72
|
+
__all__ = [
|
|
73
|
+
# App
|
|
74
|
+
"app",
|
|
75
|
+
# Routers
|
|
76
|
+
"packages",
|
|
77
|
+
"system",
|
|
78
|
+
# Middleware
|
|
79
|
+
"setup_middleware",
|
|
80
|
+
"CorrelationIDMiddleware",
|
|
81
|
+
"LoggingMiddleware",
|
|
82
|
+
"PerformanceMiddleware",
|
|
83
|
+
"CompressionMiddleware",
|
|
84
|
+
"SecurityHeadersMiddleware",
|
|
85
|
+
"RequestSizeLimitMiddleware",
|
|
86
|
+
"CacheMiddleware",
|
|
87
|
+
"MetricsMiddleware",
|
|
88
|
+
"MaintenanceModeMiddleware",
|
|
89
|
+
"AuditLogMiddleware",
|
|
90
|
+
"CSRFProtectionMiddleware",
|
|
91
|
+
# Exceptions
|
|
92
|
+
"DependencyResolverError",
|
|
93
|
+
"ValidationError",
|
|
94
|
+
"PackageNotFoundError",
|
|
95
|
+
"EcosystemNotSupportedError",
|
|
96
|
+
"ConflictResolutionError",
|
|
97
|
+
"RateLimitExceededError",
|
|
98
|
+
# Auth
|
|
99
|
+
"Token",
|
|
100
|
+
"UserCreate",
|
|
101
|
+
"UserLogin",
|
|
102
|
+
"APIKeyCreate",
|
|
103
|
+
"AuthService",
|
|
104
|
+
"get_current_user",
|
|
105
|
+
"get_current_active_user",
|
|
106
|
+
"require_scopes",
|
|
107
|
+
# Utilities
|
|
108
|
+
"get_client_ip",
|
|
109
|
+
"get_user_agent",
|
|
110
|
+
]
|
backend/api/auth.py
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# backend/api/auth.py
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from typing import Optional, Dict, List, Any
|
|
4
|
+
from fastapi import Depends, HTTPException, status, Request
|
|
5
|
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader
|
|
6
|
+
from jose import JWTError, jwt
|
|
7
|
+
from passlib.context import CryptContext
|
|
8
|
+
from pydantic import BaseModel, EmailStr, Field
|
|
9
|
+
import secrets
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Optional
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
from backend.settings import (
|
|
15
|
+
SECRET_KEY,
|
|
16
|
+
ALGORITHM,
|
|
17
|
+
ACCESS_TOKEN_EXPIRE_MINUTES,
|
|
18
|
+
REFRESH_TOKEN_EXPIRE_DAYS,
|
|
19
|
+
API_KEY_HEADER,
|
|
20
|
+
ENABLE_API_KEY_AUTH,
|
|
21
|
+
FEATURES,
|
|
22
|
+
)
|
|
23
|
+
from backend.database.models import User, APIKey, db_session
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# Password hashing
|
|
28
|
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
29
|
+
|
|
30
|
+
# Security schemes
|
|
31
|
+
bearer_scheme = HTTPBearer(auto_error=False)
|
|
32
|
+
api_key_header = APIKeyHeader(name=API_KEY_HEADER, auto_error=False)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Pydantic models
|
|
36
|
+
class Token(BaseModel):
|
|
37
|
+
access_token: str
|
|
38
|
+
refresh_token: Optional[str] = None
|
|
39
|
+
token_type: str = "bearer"
|
|
40
|
+
expires_in: int = Field(default=ACCESS_TOKEN_EXPIRE_MINUTES * 60)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TokenData(BaseModel):
|
|
44
|
+
username: Optional[str] = None
|
|
45
|
+
user_id: Optional[int] = None
|
|
46
|
+
scopes: List[str] = []
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class UserCreate(BaseModel):
|
|
50
|
+
username: str = Field(..., min_length=3, max_length=50)
|
|
51
|
+
email: EmailStr
|
|
52
|
+
password: str = Field(..., min_length=8)
|
|
53
|
+
full_name: Optional[str] = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class UserLogin(BaseModel):
|
|
57
|
+
username: str
|
|
58
|
+
password: str
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class APIKeyCreate(BaseModel):
|
|
62
|
+
name: str = Field(..., min_length=3, max_length=100)
|
|
63
|
+
description: Optional[str] = None
|
|
64
|
+
scopes: List[str] = Field(default_factory=list)
|
|
65
|
+
expires_at: Optional[datetime] = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Authentication functions
|
|
69
|
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|
70
|
+
"""Verify a password against its hash"""
|
|
71
|
+
return pwd_context.verify(plain_password, hashed_password)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_password_hash(password: str) -> str:
|
|
75
|
+
"""Hash a password"""
|
|
76
|
+
return pwd_context.hash(password)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def create_access_token(
|
|
80
|
+
data: Dict[str, Any], expires_delta: Optional[timedelta] = None
|
|
81
|
+
) -> str:
|
|
82
|
+
"""Create a JWT access token"""
|
|
83
|
+
to_encode = data.copy()
|
|
84
|
+
if expires_delta:
|
|
85
|
+
expire = datetime.utcnow() + expires_delta
|
|
86
|
+
else:
|
|
87
|
+
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
88
|
+
|
|
89
|
+
to_encode.update({"exp": expire, "type": "access"})
|
|
90
|
+
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
|
91
|
+
return encoded_jwt
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def create_refresh_token(data: Dict[str, Any]) -> str:
|
|
95
|
+
"""Create a JWT refresh token"""
|
|
96
|
+
to_encode = data.copy()
|
|
97
|
+
expire = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
|
|
98
|
+
to_encode.update({"exp": expire, "type": "refresh"})
|
|
99
|
+
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
|
100
|
+
return encoded_jwt
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def generate_api_key() -> str:
|
|
104
|
+
"""Generate a secure API key"""
|
|
105
|
+
return f"udr_{secrets.token_urlsafe(32)}"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def get_current_user_from_token(token: str) -> Optional[User]:
|
|
109
|
+
"""Extract and validate user from JWT token"""
|
|
110
|
+
credentials_exception = HTTPException(
|
|
111
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
112
|
+
detail="Could not validate credentials",
|
|
113
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
|
118
|
+
username: str = payload.get("sub")
|
|
119
|
+
token_type: str = payload.get("type")
|
|
120
|
+
|
|
121
|
+
if username is None or token_type != "access":
|
|
122
|
+
raise credentials_exception
|
|
123
|
+
|
|
124
|
+
token_data = TokenData(username=username, scopes=payload.get("scopes", []))
|
|
125
|
+
except JWTError:
|
|
126
|
+
raise credentials_exception
|
|
127
|
+
|
|
128
|
+
# Get user from database
|
|
129
|
+
with db_session() as db:
|
|
130
|
+
user = db.query(User).filter(User.username == token_data.username).first()
|
|
131
|
+
if user is None:
|
|
132
|
+
raise credentials_exception
|
|
133
|
+
|
|
134
|
+
return user
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def get_current_user_from_api_key(api_key: str) -> Optional[User]:
|
|
138
|
+
"""Validate API key and return associated user"""
|
|
139
|
+
if not api_key:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
with db_session() as db:
|
|
143
|
+
key_record = (
|
|
144
|
+
db.query(APIKey)
|
|
145
|
+
.filter(APIKey.key == api_key, APIKey.is_active == True)
|
|
146
|
+
.first()
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if not key_record:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
# Check expiration
|
|
153
|
+
if key_record.expires_at and key_record.expires_at < datetime.utcnow():
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
# Update last used timestamp
|
|
157
|
+
key_record.last_used_at = datetime.utcnow()
|
|
158
|
+
key_record.usage_count += 1
|
|
159
|
+
db.commit()
|
|
160
|
+
|
|
161
|
+
return key_record.user
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# Dependency functions
|
|
165
|
+
async def get_current_user(
|
|
166
|
+
request: Request,
|
|
167
|
+
bearer_token: Optional[HTTPAuthorizationCredentials] = Depends(bearer_scheme),
|
|
168
|
+
api_key: Optional[str] = Depends(api_key_header),
|
|
169
|
+
) -> User:
|
|
170
|
+
"""Get current user from either JWT token or API key"""
|
|
171
|
+
if not FEATURES.get("ENABLE_AUTH", False):
|
|
172
|
+
# Return a mock user if auth is disabled
|
|
173
|
+
return User(id=1, username="anonymous", email="anonymous@example.com")
|
|
174
|
+
|
|
175
|
+
user = None
|
|
176
|
+
|
|
177
|
+
# Try JWT token first
|
|
178
|
+
if bearer_token and bearer_token.credentials:
|
|
179
|
+
try:
|
|
180
|
+
user = await get_current_user_from_token(bearer_token.credentials)
|
|
181
|
+
except HTTPException:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
# Try API key if enabled and no user from JWT
|
|
185
|
+
if not user and ENABLE_API_KEY_AUTH and api_key:
|
|
186
|
+
user = await get_current_user_from_api_key(api_key)
|
|
187
|
+
|
|
188
|
+
if not user:
|
|
189
|
+
raise HTTPException(
|
|
190
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
191
|
+
detail="Not authenticated",
|
|
192
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Check if user is active
|
|
196
|
+
if not user.is_active:
|
|
197
|
+
raise HTTPException(
|
|
198
|
+
status_code=status.HTTP_403_FORBIDDEN, detail="Inactive user"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return user
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async def get_current_active_user(
|
|
205
|
+
current_user: User = Depends(get_current_user),
|
|
206
|
+
) -> User:
|
|
207
|
+
"""Ensure the current user is active"""
|
|
208
|
+
if not current_user.is_active:
|
|
209
|
+
raise HTTPException(status_code=400, detail="Inactive user")
|
|
210
|
+
return current_user
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def require_scopes(*required_scopes: str):
|
|
214
|
+
"""Dependency to require specific scopes"""
|
|
215
|
+
|
|
216
|
+
async def scope_checker(current_user: User = Depends(get_current_user)):
|
|
217
|
+
if not FEATURES.get("ENABLE_AUTH", False):
|
|
218
|
+
return current_user
|
|
219
|
+
|
|
220
|
+
user_scopes = set(current_user.scopes or [])
|
|
221
|
+
required = set(required_scopes)
|
|
222
|
+
|
|
223
|
+
if not required.issubset(user_scopes):
|
|
224
|
+
raise HTTPException(
|
|
225
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
226
|
+
detail=f"Not enough permissions. Required scopes: {', '.join(required_scopes)}",
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
return current_user
|
|
230
|
+
|
|
231
|
+
return scope_checker
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# Authentication service class
|
|
235
|
+
class AuthService:
|
|
236
|
+
"""Service for authentication operations"""
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
async def register_user(user_data: UserCreate) -> User:
|
|
240
|
+
"""Register a new user"""
|
|
241
|
+
with db_session() as db:
|
|
242
|
+
# Check if user exists
|
|
243
|
+
if (
|
|
244
|
+
db.query(User)
|
|
245
|
+
.filter(
|
|
246
|
+
(User.username == user_data.username)
|
|
247
|
+
| (User.email == user_data.email)
|
|
248
|
+
)
|
|
249
|
+
.first()
|
|
250
|
+
):
|
|
251
|
+
raise HTTPException(
|
|
252
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
253
|
+
detail="Username or email already registered",
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Create new user
|
|
257
|
+
hashed_password = get_password_hash(user_data.password)
|
|
258
|
+
user = User(
|
|
259
|
+
username=user_data.username,
|
|
260
|
+
email=user_data.email,
|
|
261
|
+
hashed_password=hashed_password,
|
|
262
|
+
full_name=user_data.full_name,
|
|
263
|
+
is_active=True,
|
|
264
|
+
created_at=datetime.utcnow(),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
db.add(user)
|
|
268
|
+
db.commit()
|
|
269
|
+
db.refresh(user)
|
|
270
|
+
|
|
271
|
+
logger.info(f"New user registered: {user.username}")
|
|
272
|
+
return user
|
|
273
|
+
|
|
274
|
+
@staticmethod
|
|
275
|
+
async def authenticate_user(username: str, password: str) -> Optional[User]:
|
|
276
|
+
"""Authenticate a user with username and password"""
|
|
277
|
+
with db_session() as db:
|
|
278
|
+
user = db.query(User).filter(User.username == username).first()
|
|
279
|
+
|
|
280
|
+
if not user or not verify_password(password, user.hashed_password):
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
# Update last login
|
|
284
|
+
user.last_login = datetime.utcnow()
|
|
285
|
+
db.commit()
|
|
286
|
+
|
|
287
|
+
return user
|
|
288
|
+
|
|
289
|
+
@staticmethod
|
|
290
|
+
async def login(user_data: UserLogin) -> Token:
|
|
291
|
+
"""Login user and return tokens"""
|
|
292
|
+
user = await AuthService.authenticate_user(
|
|
293
|
+
user_data.username, user_data.password
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
if not user:
|
|
297
|
+
raise HTTPException(
|
|
298
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
299
|
+
detail="Incorrect username or password",
|
|
300
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Create tokens
|
|
304
|
+
access_token_data = {
|
|
305
|
+
"sub": user.username,
|
|
306
|
+
"user_id": user.id,
|
|
307
|
+
"scopes": user.scopes or [],
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
access_token = create_access_token(access_token_data)
|
|
311
|
+
refresh_token = create_refresh_token({"sub": user.username})
|
|
312
|
+
|
|
313
|
+
return Token(
|
|
314
|
+
access_token=access_token,
|
|
315
|
+
refresh_token=refresh_token,
|
|
316
|
+
token_type="bearer",
|
|
317
|
+
expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
@staticmethod
|
|
321
|
+
async def refresh_token(refresh_token: str) -> Token:
|
|
322
|
+
"""Refresh access token using refresh token"""
|
|
323
|
+
try:
|
|
324
|
+
payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=[ALGORITHM])
|
|
325
|
+
username: str = payload.get("sub")
|
|
326
|
+
token_type: str = payload.get("type")
|
|
327
|
+
|
|
328
|
+
if username is None or token_type != "refresh":
|
|
329
|
+
raise HTTPException(
|
|
330
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
331
|
+
detail="Invalid refresh token",
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# Get user
|
|
335
|
+
with db_session() as db:
|
|
336
|
+
user = db.query(User).filter(User.username == username).first()
|
|
337
|
+
if not user:
|
|
338
|
+
raise HTTPException(
|
|
339
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
340
|
+
detail="User not found",
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Create new access token
|
|
344
|
+
access_token_data = {
|
|
345
|
+
"sub": user.username,
|
|
346
|
+
"user_id": user.id,
|
|
347
|
+
"scopes": user.scopes or [],
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
access_token = create_access_token(access_token_data)
|
|
351
|
+
|
|
352
|
+
return Token(
|
|
353
|
+
access_token=access_token,
|
|
354
|
+
token_type="bearer",
|
|
355
|
+
expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
except JWTError:
|
|
359
|
+
raise HTTPException(
|
|
360
|
+
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token"
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
@staticmethod
|
|
364
|
+
async def create_api_key(user: User, key_data: APIKeyCreate) -> APIKey:
|
|
365
|
+
"""Create a new API key for a user"""
|
|
366
|
+
with db_session() as db:
|
|
367
|
+
api_key = APIKey(
|
|
368
|
+
key=generate_api_key(),
|
|
369
|
+
name=key_data.name,
|
|
370
|
+
description=key_data.description,
|
|
371
|
+
user_id=user.id,
|
|
372
|
+
scopes=key_data.scopes,
|
|
373
|
+
expires_at=key_data.expires_at,
|
|
374
|
+
created_at=datetime.utcnow(),
|
|
375
|
+
is_active=True,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
db.add(api_key)
|
|
379
|
+
db.commit()
|
|
380
|
+
db.refresh(api_key)
|
|
381
|
+
|
|
382
|
+
logger.info(f"API key created for user {user.username}: {api_key.name}")
|
|
383
|
+
return api_key
|
|
384
|
+
|
|
385
|
+
@staticmethod
|
|
386
|
+
async def revoke_api_key(user: User, key_id: int) -> bool:
|
|
387
|
+
"""Revoke an API key"""
|
|
388
|
+
with db_session() as db:
|
|
389
|
+
api_key = (
|
|
390
|
+
db.query(APIKey)
|
|
391
|
+
.filter(APIKey.id == key_id, APIKey.user_id == user.id)
|
|
392
|
+
.first()
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
if not api_key:
|
|
396
|
+
return False
|
|
397
|
+
|
|
398
|
+
api_key.is_active = False
|
|
399
|
+
api_key.revoked_at = datetime.utcnow()
|
|
400
|
+
db.commit()
|
|
401
|
+
|
|
402
|
+
logger.info(f"API key revoked: {api_key.name}")
|
|
403
|
+
return True
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
# Optional: OAuth2 password flow for testing
|
|
407
|
+
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
|
408
|
+
|
|
409
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/token", auto_error=False)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
async def login_for_access_token(
|
|
413
|
+
form_data: OAuth2PasswordRequestForm = Depends(),
|
|
414
|
+
) -> Token:
|
|
415
|
+
"""OAuth2 compatible token endpoint"""
|
|
416
|
+
user = await AuthService.authenticate_user(form_data.username, form_data.password)
|
|
417
|
+
|
|
418
|
+
if not user:
|
|
419
|
+
raise HTTPException(
|
|
420
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
421
|
+
detail="Incorrect username or password",
|
|
422
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
access_token_data = {
|
|
426
|
+
"sub": user.username,
|
|
427
|
+
"user_id": user.id,
|
|
428
|
+
"scopes": form_data.scopes,
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
access_token = create_access_token(access_token_data)
|
|
432
|
+
|
|
433
|
+
return {"access_token": access_token, "token_type": "bearer"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from slowapi import Limiter, _rate_limit_exceeded_handler
|
|
5
|
+
from slowapi.util import get_remote_address
|
|
6
|
+
from slowapi.middleware import SlowAPIMiddleware
|
|
7
|
+
|
|
8
|
+
from backend.core.system_scanner import SystemScanner
|
|
9
|
+
from backend.core.data_aggregator import DataAggregator
|
|
10
|
+
from backend.core.conflict_resolver import ConflictResolver
|
|
11
|
+
from backend.core.export_generator import ExportGenerator
|
|
12
|
+
from backend.database.compatibility_db import CompatibilityDB
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
redis_url = os.getenv("REDIS_URL")
|
|
17
|
+
if redis_url:
|
|
18
|
+
limiter = Limiter(key_func=get_remote_address, storage_uri=redis_url)
|
|
19
|
+
else:
|
|
20
|
+
limiter = Limiter(key_func=get_remote_address)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_system_scanner() -> SystemScanner:
|
|
24
|
+
return SystemScanner()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_data_aggregator() -> DataAggregator:
|
|
28
|
+
return DataAggregator()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_conflict_resolver() -> ConflictResolver:
|
|
32
|
+
return ConflictResolver()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_export_generator() -> ExportGenerator:
|
|
36
|
+
return ExportGenerator()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_compatibility_db() -> CompatibilityDB:
|
|
40
|
+
return CompatibilityDB()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Custom exception classes for structured error handling"""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any, Optional, List
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DependencyResolverError(Exception):
|
|
7
|
+
"""Base exception for dependency resolver errors"""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
message: str,
|
|
12
|
+
error_code: str,
|
|
13
|
+
status_code: int = 500,
|
|
14
|
+
details: Optional[Dict[str, Any]] = None,
|
|
15
|
+
):
|
|
16
|
+
self.message = message
|
|
17
|
+
self.error_code = error_code
|
|
18
|
+
self.status_code = status_code
|
|
19
|
+
self.details = details or {}
|
|
20
|
+
super().__init__(self.message)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ValidationError(DependencyResolverError):
|
|
24
|
+
"""Raised when input validation fails"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, message: str, field: Optional[str] = None):
|
|
27
|
+
super().__init__(
|
|
28
|
+
message=message,
|
|
29
|
+
error_code="VALIDATION_ERROR",
|
|
30
|
+
status_code=400,
|
|
31
|
+
details={"field": field} if field else {},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PackageNotFoundError(DependencyResolverError):
|
|
36
|
+
"""Raised when a package cannot be found"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, package_name: str, ecosystem: Optional[str] = None):
|
|
39
|
+
super().__init__(
|
|
40
|
+
message=f"Package '{package_name}' not found",
|
|
41
|
+
error_code="PACKAGE_NOT_FOUND",
|
|
42
|
+
status_code=404,
|
|
43
|
+
details={"package_name": package_name, "ecosystem": ecosystem},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class EcosystemNotSupportedError(DependencyResolverError):
|
|
48
|
+
"""Raised when an ecosystem is not supported"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, ecosystem: str):
|
|
51
|
+
super().__init__(
|
|
52
|
+
message=f"Ecosystem '{ecosystem}' is not supported",
|
|
53
|
+
error_code="ECOSYSTEM_NOT_SUPPORTED",
|
|
54
|
+
status_code=400,
|
|
55
|
+
details={"ecosystem": ecosystem},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ConflictResolutionError(DependencyResolverError):
|
|
60
|
+
"""Raised when dependency conflicts cannot be resolved"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, message: str, conflicts: Optional[List[Dict]] = None):
|
|
63
|
+
super().__init__(
|
|
64
|
+
message=message,
|
|
65
|
+
error_code="CONFLICT_RESOLUTION_FAILED",
|
|
66
|
+
status_code=409,
|
|
67
|
+
details={"conflicts": conflicts or []},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class RateLimitExceededError(DependencyResolverError):
|
|
72
|
+
"""Raised when rate limit is exceeded"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, retry_after: Optional[int] = None):
|
|
75
|
+
super().__init__(
|
|
76
|
+
message="Rate limit exceeded",
|
|
77
|
+
error_code="RATE_LIMIT_EXCEEDED",
|
|
78
|
+
status_code=429,
|
|
79
|
+
details={"retry_after": retry_after},
|
|
80
|
+
)
|