cryptologin 1.0.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.
- cryptologin/__init__.py +103 -0
- cryptologin/api/__init__.py +7 -0
- cryptologin/api/dependencies.py +113 -0
- cryptologin/api/models.py +133 -0
- cryptologin/api/routes/__init__.py +6 -0
- cryptologin/api/routes/auth.py +198 -0
- cryptologin/api/routes/health.py +59 -0
- cryptologin/api/routes/user.py +289 -0
- cryptologin/api/routes/vault.py +7 -0
- cryptologin/cli.py +223 -0
- cryptologin/config.py +82 -0
- cryptologin/core/__init__.py +7 -0
- cryptologin/core/constants.py +17 -0
- cryptologin/core/crypto_engine.py +138 -0
- cryptologin/core/data_vault.py +344 -0
- cryptologin/core/exceptions.py +50 -0
- cryptologin/core/user_manager.py +373 -0
- cryptologin/main.py +125 -0
- cryptologin/storage/__init__.py +13 -0
- cryptologin/storage/base.py +113 -0
- cryptologin/storage/memory.py +64 -0
- cryptologin/storage/sqlite.py +434 -0
- cryptologin-1.0.0.dist-info/METADATA +289 -0
- cryptologin-1.0.0.dist-info/RECORD +28 -0
- cryptologin-1.0.0.dist-info/WHEEL +5 -0
- cryptologin-1.0.0.dist-info/entry_points.txt +2 -0
- cryptologin-1.0.0.dist-info/licenses/LICENSE +215 -0
- cryptologin-1.0.0.dist-info/top_level.txt +1 -0
cryptologin/__init__.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CryptoLogin - Zero-Knowledge Authentication System
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__version__ = "1.0.0"
|
|
6
|
+
__author__ = "CryptoLogin Team"
|
|
7
|
+
__license__ = "Apache-2.0"
|
|
8
|
+
|
|
9
|
+
from .core.crypto_engine import CryptoEngine
|
|
10
|
+
from .core.user_manager import UserManager
|
|
11
|
+
from .core.data_vault import DataVault
|
|
12
|
+
from .core.exceptions import (
|
|
13
|
+
CryptoLoginError,
|
|
14
|
+
CryptoError,
|
|
15
|
+
InvalidSecretError,
|
|
16
|
+
DecryptionError,
|
|
17
|
+
IntegrityError,
|
|
18
|
+
UserNotFoundError,
|
|
19
|
+
UserAlreadyExistsError,
|
|
20
|
+
AuthenticationError,
|
|
21
|
+
DataVaultError,
|
|
22
|
+
)
|
|
23
|
+
from .storage.sqlite import SQLiteStorage
|
|
24
|
+
from .storage.memory import MemoryStorage
|
|
25
|
+
from .config import get_settings, Settings
|
|
26
|
+
from .main import app
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CryptoLogin:
|
|
30
|
+
"""
|
|
31
|
+
Main entry point for CryptoLogin.
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
>>> from cryptologin import CryptoLogin
|
|
35
|
+
>>> auth = CryptoLogin()
|
|
36
|
+
>>> user_id = auth.register("my-secret", {"name": "John"})
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, db_path: str = "cryptologin.db"):
|
|
40
|
+
from .core.user_manager import UserManager
|
|
41
|
+
from .storage.sqlite import SQLiteStorage
|
|
42
|
+
from .core.crypto_engine import CryptoEngine
|
|
43
|
+
|
|
44
|
+
self.storage = SQLiteStorage(db_path=db_path)
|
|
45
|
+
self.crypto_engine = CryptoEngine()
|
|
46
|
+
self.user_manager = UserManager(storage=self.storage)
|
|
47
|
+
|
|
48
|
+
def register(self, master_secret: str, user_data: dict = None) -> str:
|
|
49
|
+
"""Register a new user."""
|
|
50
|
+
return self.user_manager.register_user(master_secret, user_data)
|
|
51
|
+
|
|
52
|
+
def login_init(self, master_secret: str) -> str:
|
|
53
|
+
"""Initiate login - returns challenge."""
|
|
54
|
+
return self.user_manager.initiate_login(master_secret)
|
|
55
|
+
|
|
56
|
+
def login_verify(self, master_secret: str, challenge_response: str):
|
|
57
|
+
"""Complete login - returns session."""
|
|
58
|
+
return self.user_manager.complete_login(master_secret, challenge_response)
|
|
59
|
+
|
|
60
|
+
def get_user_data(self, user_id: str, master_secret: str) -> dict:
|
|
61
|
+
"""Get user data."""
|
|
62
|
+
return self.user_manager.get_user_data(user_id, master_secret)
|
|
63
|
+
|
|
64
|
+
def update_user_data(self, user_id: str, master_secret: str, data: dict) -> bool:
|
|
65
|
+
"""Update user data."""
|
|
66
|
+
return self.user_manager.update_user_data(user_id, master_secret, data)
|
|
67
|
+
|
|
68
|
+
def delete_user(self, user_id: str, master_secret: str) -> bool:
|
|
69
|
+
"""Delete a user."""
|
|
70
|
+
from .core.exceptions import AuthenticationError
|
|
71
|
+
# Verify secret matches
|
|
72
|
+
derived_id = self.crypto_engine.derive_user_id(master_secret)
|
|
73
|
+
if derived_id != user_id:
|
|
74
|
+
raise AuthenticationError("Secret does not match user")
|
|
75
|
+
return self.user_manager.delete_user(user_id)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Export cli main for entry point
|
|
79
|
+
from . import cli
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
__all__ = [
|
|
83
|
+
"CryptoLogin",
|
|
84
|
+
"CryptoEngine",
|
|
85
|
+
"UserManager",
|
|
86
|
+
"DataVault",
|
|
87
|
+
"SQLiteStorage",
|
|
88
|
+
"MemoryStorage",
|
|
89
|
+
"CryptoLoginError",
|
|
90
|
+
"CryptoError",
|
|
91
|
+
"InvalidSecretError",
|
|
92
|
+
"DecryptionError",
|
|
93
|
+
"IntegrityError",
|
|
94
|
+
"UserNotFoundError",
|
|
95
|
+
"UserAlreadyExistsError",
|
|
96
|
+
"AuthenticationError",
|
|
97
|
+
"DataVaultError",
|
|
98
|
+
"get_settings",
|
|
99
|
+
"Settings",
|
|
100
|
+
"app",
|
|
101
|
+
"__version__",
|
|
102
|
+
"cli",
|
|
103
|
+
]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dépendances pour l'API CryptoLogin
|
|
3
|
+
"""
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from fastapi import Request, HTTPException, status, Depends
|
|
6
|
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
7
|
+
from fastapi.responses import JSONResponse
|
|
8
|
+
|
|
9
|
+
from ..core.user_manager import UserManager
|
|
10
|
+
from ..storage.sqlite import SQLiteStorage
|
|
11
|
+
from ..config import get_settings, Settings
|
|
12
|
+
from slowapi import Limiter
|
|
13
|
+
from slowapi.util import get_remote_address
|
|
14
|
+
from slowapi.errors import RateLimitExceeded
|
|
15
|
+
|
|
16
|
+
# Singleton pour UserManager
|
|
17
|
+
_user_manager_instance = None
|
|
18
|
+
_storage_instance = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ============================================================
|
|
22
|
+
# STORAGE - Singleton
|
|
23
|
+
# ============================================================
|
|
24
|
+
|
|
25
|
+
def get_storage(settings: Settings = Depends(get_settings)) -> SQLiteStorage:
|
|
26
|
+
"""Retourne l'instance de stockage (Singleton)."""
|
|
27
|
+
global _storage_instance
|
|
28
|
+
if _storage_instance is None:
|
|
29
|
+
db_path = settings.DATABASE_URL.replace("sqlite:///", "")
|
|
30
|
+
_storage_instance = SQLiteStorage(db_path=db_path, auto_migrate=True)
|
|
31
|
+
return _storage_instance
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ============================================================
|
|
35
|
+
# USER MANAGER - Singleton
|
|
36
|
+
# ============================================================
|
|
37
|
+
|
|
38
|
+
def get_user_manager(storage: SQLiteStorage = Depends(get_storage)) -> UserManager:
|
|
39
|
+
"""Retourne l'instance du UserManager (Singleton)."""
|
|
40
|
+
global _user_manager_instance
|
|
41
|
+
if _user_manager_instance is None:
|
|
42
|
+
_user_manager_instance = UserManager(storage=storage, session_duration_hours=24)
|
|
43
|
+
return _user_manager_instance
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ============================================================
|
|
47
|
+
# AUTHENTIFICATION
|
|
48
|
+
# ============================================================
|
|
49
|
+
|
|
50
|
+
security = HTTPBearer(auto_error=False)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def get_current_user(
|
|
54
|
+
request: Request,
|
|
55
|
+
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
|
56
|
+
user_manager: UserManager = Depends(get_user_manager)
|
|
57
|
+
) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Valide la session et retourne l'ID utilisateur.
|
|
60
|
+
"""
|
|
61
|
+
if not credentials:
|
|
62
|
+
raise HTTPException(
|
|
63
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
64
|
+
detail="Not authenticated",
|
|
65
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
user_id = credentials.credentials
|
|
69
|
+
|
|
70
|
+
# Vérifier la session
|
|
71
|
+
if not user_manager.validate_session(user_id):
|
|
72
|
+
raise HTTPException(
|
|
73
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
74
|
+
detail="Invalid or expired session",
|
|
75
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
return user_id
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ============================================================
|
|
82
|
+
# RATE LIMITING
|
|
83
|
+
# ============================================================
|
|
84
|
+
|
|
85
|
+
limiter = Limiter(key_func=get_remote_address)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_limiter() -> Limiter:
|
|
89
|
+
"""Retourne l'instance du rate limiter."""
|
|
90
|
+
return limiter
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def rate_limit_exceeded_handler(request: Request, exc: RateLimitExceeded) -> JSONResponse:
|
|
94
|
+
"""
|
|
95
|
+
Gestionnaire personnalisé pour les erreurs de rate limiting.
|
|
96
|
+
"""
|
|
97
|
+
return JSONResponse(
|
|
98
|
+
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
|
99
|
+
content={
|
|
100
|
+
"error": "Rate limit exceeded",
|
|
101
|
+
"detail": f"Too many requests. Please wait {exc.retry_after} seconds.",
|
|
102
|
+
"status_code": status.HTTP_429_TOO_MANY_REQUESTS,
|
|
103
|
+
"retry_after": exc.retry_after
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# Réinitialiser les singletons pour les tests
|
|
109
|
+
def reset_singletons():
|
|
110
|
+
"""Réinitialise les singletons (pour les tests)."""
|
|
111
|
+
global _user_manager_instance, _storage_instance
|
|
112
|
+
_user_manager_instance = None
|
|
113
|
+
_storage_instance = None
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modèles Pydantic pour l'API CryptoLogin - Version Pydantic v2
|
|
3
|
+
"""
|
|
4
|
+
from pydantic import BaseModel, Field, field_validator
|
|
5
|
+
from typing import Optional, Dict, Any
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
# ============================================================
|
|
9
|
+
# REQUESTS
|
|
10
|
+
# ============================================================
|
|
11
|
+
|
|
12
|
+
class RegisterRequest(BaseModel):
|
|
13
|
+
"""Requête d'enregistrement."""
|
|
14
|
+
master_secret: str = Field(..., min_length=32, description="Secret maître de l'utilisateur")
|
|
15
|
+
user_data: Optional[Dict[str, Any]] = Field(default=None, description="Données initiales de l'utilisateur")
|
|
16
|
+
|
|
17
|
+
@field_validator('master_secret')
|
|
18
|
+
@classmethod
|
|
19
|
+
def validate_secret(cls, v: str) -> str:
|
|
20
|
+
if len(v) < 32:
|
|
21
|
+
raise ValueError('Master secret must be at least 32 characters')
|
|
22
|
+
return v
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LoginInitRequest(BaseModel):
|
|
26
|
+
"""Requête d'initiation de login."""
|
|
27
|
+
master_secret: str = Field(..., min_length=32, description="Secret maître de l'utilisateur")
|
|
28
|
+
|
|
29
|
+
@field_validator('master_secret')
|
|
30
|
+
@classmethod
|
|
31
|
+
def validate_secret(cls, v: str) -> str:
|
|
32
|
+
if len(v) < 32:
|
|
33
|
+
raise ValueError('Master secret must be at least 32 characters')
|
|
34
|
+
return v
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class LoginVerifyRequest(BaseModel):
|
|
38
|
+
"""Requête de vérification de login."""
|
|
39
|
+
master_secret: str = Field(..., min_length=32, description="Secret maître de l'utilisateur")
|
|
40
|
+
challenge_response: str = Field(..., description="Réponse au challenge (déchiffré)")
|
|
41
|
+
|
|
42
|
+
@field_validator('master_secret')
|
|
43
|
+
@classmethod
|
|
44
|
+
def validate_secret(cls, v: str) -> str:
|
|
45
|
+
if len(v) < 32:
|
|
46
|
+
raise ValueError('Master secret must be at least 32 characters')
|
|
47
|
+
return v
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class UpdateDataRequest(BaseModel):
|
|
51
|
+
"""Requête de mise à jour des données."""
|
|
52
|
+
master_secret: str = Field(..., min_length=32, description="Secret maître de l'utilisateur")
|
|
53
|
+
data: Dict[str, Any] = Field(..., description="Nouvelles données")
|
|
54
|
+
|
|
55
|
+
@field_validator('master_secret')
|
|
56
|
+
@classmethod
|
|
57
|
+
def validate_secret(cls, v: str) -> str:
|
|
58
|
+
if len(v) < 32:
|
|
59
|
+
raise ValueError('Master secret must be at least 32 characters')
|
|
60
|
+
return v
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RotateSecretRequest(BaseModel):
|
|
64
|
+
"""Requête de rotation de secret."""
|
|
65
|
+
old_secret: str = Field(..., min_length=32, description="Ancien secret")
|
|
66
|
+
new_secret: str = Field(..., min_length=32, description="Nouveau secret")
|
|
67
|
+
|
|
68
|
+
@field_validator('old_secret', 'new_secret')
|
|
69
|
+
@classmethod
|
|
70
|
+
def validate_secret(cls, v: str) -> str:
|
|
71
|
+
if len(v) < 32:
|
|
72
|
+
raise ValueError('Secret must be at least 32 characters')
|
|
73
|
+
return v
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class DeleteUserRequest(BaseModel):
|
|
77
|
+
"""Requête de suppression d'utilisateur."""
|
|
78
|
+
master_secret: str = Field(..., min_length=32, description="Secret maître de l'utilisateur")
|
|
79
|
+
|
|
80
|
+
@field_validator('master_secret')
|
|
81
|
+
@classmethod
|
|
82
|
+
def validate_secret(cls, v: str) -> str:
|
|
83
|
+
if len(v) < 32:
|
|
84
|
+
raise ValueError('Master secret must be at least 32 characters')
|
|
85
|
+
return v
|
|
86
|
+
|
|
87
|
+
# ============================================================
|
|
88
|
+
# RESPONSES
|
|
89
|
+
# ============================================================
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class UserResponse(BaseModel):
|
|
93
|
+
"""Réponse utilisateur."""
|
|
94
|
+
user_id: str
|
|
95
|
+
created_at: datetime
|
|
96
|
+
updated_at: datetime
|
|
97
|
+
last_activity_at: Optional[datetime]
|
|
98
|
+
has_data: bool
|
|
99
|
+
has_vault: bool
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class DataResponse(BaseModel):
|
|
103
|
+
"""Réponse des données."""
|
|
104
|
+
data: Dict[str, Any]
|
|
105
|
+
version: str = "1.0"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class AuthInitResponse(BaseModel):
|
|
109
|
+
"""Réponse d'initiation de login."""
|
|
110
|
+
challenge: str
|
|
111
|
+
message: str = "Please decrypt the challenge and submit it with /verify"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class AuthResponse(BaseModel):
|
|
115
|
+
"""Réponse d'authentification."""
|
|
116
|
+
user_id: str
|
|
117
|
+
session_id: str
|
|
118
|
+
expires_at: datetime
|
|
119
|
+
message: str = "Authentication successful"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class MessageResponse(BaseModel):
|
|
123
|
+
"""Réponse de message simple."""
|
|
124
|
+
message: str
|
|
125
|
+
success: bool = True
|
|
126
|
+
data: Optional[Dict[str, Any]] = None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ErrorResponse(BaseModel):
|
|
130
|
+
"""Réponse d'erreur."""
|
|
131
|
+
error: str
|
|
132
|
+
detail: Optional[str] = None
|
|
133
|
+
status_code: int
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Routes d'authentification
|
|
3
|
+
"""
|
|
4
|
+
from typing import Any
|
|
5
|
+
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
|
6
|
+
from fastapi.responses import JSONResponse
|
|
7
|
+
|
|
8
|
+
from ...core.user_manager import UserManager
|
|
9
|
+
from ...core.exceptions import (
|
|
10
|
+
UserNotFoundError,
|
|
11
|
+
UserAlreadyExistsError,
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
InvalidSecretError
|
|
14
|
+
)
|
|
15
|
+
from ..models import (
|
|
16
|
+
RegisterRequest,
|
|
17
|
+
LoginInitRequest,
|
|
18
|
+
LoginVerifyRequest,
|
|
19
|
+
AuthInitResponse,
|
|
20
|
+
AuthResponse,
|
|
21
|
+
MessageResponse,
|
|
22
|
+
ErrorResponse
|
|
23
|
+
)
|
|
24
|
+
from ..dependencies import get_user_manager, get_current_user
|
|
25
|
+
from ..dependencies import limiter as rate_limiter
|
|
26
|
+
|
|
27
|
+
router = APIRouter(prefix="/auth", tags=["Authentication"])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@router.post(
|
|
31
|
+
"/register",
|
|
32
|
+
response_model=MessageResponse,
|
|
33
|
+
responses={
|
|
34
|
+
400: {"model": ErrorResponse},
|
|
35
|
+
409: {"model": ErrorResponse},
|
|
36
|
+
500: {"model": ErrorResponse}
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
@rate_limiter.limit("10/minute")
|
|
40
|
+
async def register(
|
|
41
|
+
request: Request, # Ajouté pour rate_limiter
|
|
42
|
+
user_data: RegisterRequest,
|
|
43
|
+
user_manager: UserManager = Depends(get_user_manager)
|
|
44
|
+
) -> Any:
|
|
45
|
+
"""Enregistre un nouvel utilisateur."""
|
|
46
|
+
try:
|
|
47
|
+
user_id = user_manager.register_user(
|
|
48
|
+
user_data.master_secret,
|
|
49
|
+
user_data.user_data or {}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return MessageResponse(
|
|
53
|
+
message="User registered successfully",
|
|
54
|
+
data={"user_id": user_id}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
except InvalidSecretError as e:
|
|
58
|
+
raise HTTPException(
|
|
59
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
60
|
+
detail=str(e)
|
|
61
|
+
)
|
|
62
|
+
except UserAlreadyExistsError as e:
|
|
63
|
+
raise HTTPException(
|
|
64
|
+
status_code=status.HTTP_409_CONFLICT,
|
|
65
|
+
detail=str(e)
|
|
66
|
+
)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
raise HTTPException(
|
|
69
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
70
|
+
detail=f"Registration failed: {str(e)}"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@router.post(
|
|
75
|
+
"/login/init",
|
|
76
|
+
response_model=AuthInitResponse,
|
|
77
|
+
responses={
|
|
78
|
+
400: {"model": ErrorResponse},
|
|
79
|
+
401: {"model": ErrorResponse},
|
|
80
|
+
404: {"model": ErrorResponse},
|
|
81
|
+
500: {"model": ErrorResponse}
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
@rate_limiter.limit("30/minute")
|
|
85
|
+
async def login_init(
|
|
86
|
+
request: Request, # Ajouté pour rate_limiter
|
|
87
|
+
login_data: LoginInitRequest,
|
|
88
|
+
user_manager: UserManager = Depends(get_user_manager)
|
|
89
|
+
) -> Any:
|
|
90
|
+
"""Initie le processus de login."""
|
|
91
|
+
try:
|
|
92
|
+
challenge = user_manager.initiate_login(login_data.master_secret)
|
|
93
|
+
|
|
94
|
+
return AuthInitResponse(
|
|
95
|
+
challenge=challenge,
|
|
96
|
+
message="Please decrypt the challenge and submit it with /verify"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
except InvalidSecretError as e:
|
|
100
|
+
raise HTTPException(
|
|
101
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
102
|
+
detail=str(e)
|
|
103
|
+
)
|
|
104
|
+
except UserNotFoundError as e:
|
|
105
|
+
raise HTTPException(
|
|
106
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
107
|
+
detail=str(e)
|
|
108
|
+
)
|
|
109
|
+
except AuthenticationError as e:
|
|
110
|
+
raise HTTPException(
|
|
111
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
112
|
+
detail=str(e)
|
|
113
|
+
)
|
|
114
|
+
except Exception as e:
|
|
115
|
+
raise HTTPException(
|
|
116
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
117
|
+
detail=f"Login initiation failed: {str(e)}"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@router.post(
|
|
122
|
+
"/login/verify",
|
|
123
|
+
response_model=AuthResponse,
|
|
124
|
+
responses={
|
|
125
|
+
400: {"model": ErrorResponse},
|
|
126
|
+
401: {"model": ErrorResponse},
|
|
127
|
+
404: {"model": ErrorResponse},
|
|
128
|
+
500: {"model": ErrorResponse}
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
@rate_limiter.limit("30/minute")
|
|
132
|
+
async def login_verify(
|
|
133
|
+
request: Request, # Ajouté pour rate_limiter
|
|
134
|
+
verify_data: LoginVerifyRequest,
|
|
135
|
+
user_manager: UserManager = Depends(get_user_manager)
|
|
136
|
+
) -> Any:
|
|
137
|
+
"""Vérifie la réponse au challenge et crée une session."""
|
|
138
|
+
try:
|
|
139
|
+
session = user_manager.complete_login(
|
|
140
|
+
verify_data.master_secret,
|
|
141
|
+
verify_data.challenge_response
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return AuthResponse(
|
|
145
|
+
user_id=session.user_id,
|
|
146
|
+
session_id=session.user_id,
|
|
147
|
+
expires_at=session.expires_at,
|
|
148
|
+
message="Authentication successful"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
except InvalidSecretError as e:
|
|
152
|
+
raise HTTPException(
|
|
153
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
154
|
+
detail=str(e)
|
|
155
|
+
)
|
|
156
|
+
except UserNotFoundError as e:
|
|
157
|
+
raise HTTPException(
|
|
158
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
159
|
+
detail=str(e)
|
|
160
|
+
)
|
|
161
|
+
except AuthenticationError as e:
|
|
162
|
+
raise HTTPException(
|
|
163
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
164
|
+
detail=str(e)
|
|
165
|
+
)
|
|
166
|
+
except Exception as e:
|
|
167
|
+
raise HTTPException(
|
|
168
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
169
|
+
detail=f"Login verification failed: {str(e)}"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@router.post(
|
|
174
|
+
"/logout",
|
|
175
|
+
response_model=MessageResponse,
|
|
176
|
+
responses={
|
|
177
|
+
401: {"model": ErrorResponse},
|
|
178
|
+
500: {"model": ErrorResponse}
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
@rate_limiter.limit("60/minute")
|
|
182
|
+
async def logout(
|
|
183
|
+
request: Request, # Ajouté pour rate_limiter
|
|
184
|
+
user_id: str = Depends(get_current_user),
|
|
185
|
+
user_manager: UserManager = Depends(get_user_manager)
|
|
186
|
+
) -> Any:
|
|
187
|
+
"""Déconnecte l'utilisateur."""
|
|
188
|
+
try:
|
|
189
|
+
user_manager.logout(user_id)
|
|
190
|
+
return MessageResponse(
|
|
191
|
+
message="Logged out successfully"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
except Exception as e:
|
|
195
|
+
raise HTTPException(
|
|
196
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
197
|
+
detail=f"Logout failed: {str(e)}"
|
|
198
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Routes de santé - Pas de rate limiting nécessaire
|
|
3
|
+
"""
|
|
4
|
+
from typing import Any
|
|
5
|
+
from fastapi import APIRouter, Depends, status
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from ...config import get_settings, Settings
|
|
10
|
+
from ...storage.sqlite import SQLiteStorage
|
|
11
|
+
from ..dependencies import get_storage
|
|
12
|
+
|
|
13
|
+
router = APIRouter(prefix="/health", tags=["Health"])
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HealthResponse(BaseModel):
|
|
17
|
+
"""Réponse de santé."""
|
|
18
|
+
status: str
|
|
19
|
+
version: str
|
|
20
|
+
timestamp: str
|
|
21
|
+
database: str
|
|
22
|
+
uptime: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ReadyResponse(BaseModel):
|
|
26
|
+
"""Réponse de disponibilité."""
|
|
27
|
+
ready: bool
|
|
28
|
+
database: bool
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@router.get("", response_model=HealthResponse)
|
|
32
|
+
async def health_check(
|
|
33
|
+
settings: Settings = Depends(get_settings)
|
|
34
|
+
) -> Any:
|
|
35
|
+
"""Vérifie l'état de santé de l'application."""
|
|
36
|
+
return HealthResponse(
|
|
37
|
+
status="healthy",
|
|
38
|
+
version=settings.APP_VERSION,
|
|
39
|
+
timestamp=datetime.now().isoformat(),
|
|
40
|
+
database="sqlite",
|
|
41
|
+
uptime="running"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@router.get("/ready", response_model=ReadyResponse)
|
|
46
|
+
async def ready_check(
|
|
47
|
+
storage: SQLiteStorage = Depends(get_storage)
|
|
48
|
+
) -> Any:
|
|
49
|
+
"""Vérifie si l'application est prête à servir."""
|
|
50
|
+
try:
|
|
51
|
+
storage.get_user_count()
|
|
52
|
+
db_ready = True
|
|
53
|
+
except Exception:
|
|
54
|
+
db_ready = False
|
|
55
|
+
|
|
56
|
+
return ReadyResponse(
|
|
57
|
+
ready=db_ready,
|
|
58
|
+
database=db_ready
|
|
59
|
+
)
|