Fix .gitignore and add missing authentication source files
This commit fixes overly broad .gitignore patterns that were excluding important source code files from version control. Previously, wildcard patterns like *auth*, *token*, *secret*, *connection*, and *credential* were excluding ALL files containing these words, including critical application code. Changes: - Updated .gitignore with specific patterns for sensitive config files (*.json, *.txt, *.yml, *.yaml extensions only) - Removed broad wildcards that excluded source code files Added missing source files: - shared/auth/ (9 files): Complete authentication system - JWT handler, middleware, auth service, models, routes - reports-app/backend/app/routers/auth.py: Authentication API router - reports-app/backend/app/auth_middleware_wrapper.py: Middleware wrapper - reports-app/frontend/src/stores/auth.js: Vue.js auth store - reports-app/frontend/tests/: E2E tests and fixtures for auth - reports-app/telegram-bot/app/auth/: Telegram auth linking module - deployment/windows/scripts/Setup-ClaudeAuth.ps1: Windows deployment script - security/secrets_scanner.py: Security scanning utility These files are essential for the application to function and should have been included in the initial commit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
649
shared/auth/README.md
Normal file
649
shared/auth/README.md
Normal file
@@ -0,0 +1,649 @@
|
||||
# 🔐 ROA2WEB Shared Authentication System
|
||||
|
||||
Sistem de autentificare JWT partajat între toate aplicațiile din ecosistemul ROA2WEB, integrat cu Oracle Database și optimizat pentru aplicații FastAPI.
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
- [Features](#features)
|
||||
- [Architecture](#architecture)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Components](#components)
|
||||
- [Integration Guide](#integration-guide)
|
||||
- [Security Features](#security-features)
|
||||
- [API Reference](#api-reference)
|
||||
- [Testing](#testing)
|
||||
- [Deployment](#deployment)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### Core Features
|
||||
- **JWT Authentication**: Secure token-based authentication cu access și refresh tokens
|
||||
- **Oracle Database Integration**: Folosește `pack_drepturi.verificautilizator` pentru autentificare
|
||||
- **Multi-Company Support**: Acces controlat la multiple firme/schemas Oracle
|
||||
- **Permission System**: Sistem granular de permisiuni (read, write, admin, reports)
|
||||
- **FastAPI Integration**: Dependencies și middleware native pentru FastAPI
|
||||
- **Rate Limiting**: Protecție împotriva brute force attacks
|
||||
- **Caching**: Cache inteligent pentru performanță optimă
|
||||
|
||||
### Security Features
|
||||
- **Token Expiration**: Configurabil pentru access și refresh tokens
|
||||
- **SQL Injection Protection**: Parametri legați în toate query-urile
|
||||
- **Rate Limiting**: Configurabil per IP și endpoint
|
||||
- **CORS Protection**: Configurare flexibilă pentru origins
|
||||
- **Header Security**: Security headers automate
|
||||
- **Token Blacklisting**: Suport pentru invalidarea token-urilor (în dezvoltare)
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
```
|
||||
ROA2WEB Authentication Flow:
|
||||
┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐
|
||||
│ Client │───▶│ FastAPI │───▶│ JWT │───▶│ Oracle │
|
||||
│ (Frontend) │ │ Application │ │ Handler │ │ Database │
|
||||
└─────────────┘ └──────────────┘ └─────────────┘ └──────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌─────────────┐ ┌──────────────┐
|
||||
│ Auth Service │ │ Middleware │ │ User Cache │
|
||||
└──────────────┘ └─────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
### Components Overview
|
||||
|
||||
```
|
||||
shared/auth/
|
||||
├── jwt_handler.py # JWT token creation și validation
|
||||
├── auth_service.py # Oracle database integration
|
||||
├── models.py # Pydantic models pentru data validation
|
||||
├── middleware.py # FastAPI middleware pentru auto-authentication
|
||||
├── dependencies.py # FastAPI dependencies pentru protected routes
|
||||
├── routes.py # Pre-built authentication routes
|
||||
├── test_auth.py # Comprehensive test suite
|
||||
├── demo_app.py # Demo application cu examples
|
||||
└── README.md # Această documentație
|
||||
```
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### 1. Environment Setup
|
||||
|
||||
```bash
|
||||
# Copy și configurează environment variables
|
||||
cp .env.example .env
|
||||
|
||||
# Edit .env cu configurările tale
|
||||
JWT_SECRET_KEY=your-super-secret-jwt-key-change-in-production
|
||||
ORACLE_USER=your_oracle_username
|
||||
ORACLE_PASSWORD=your_oracle_password
|
||||
ORACLE_DSN=your_oracle_connection_string
|
||||
```
|
||||
|
||||
### 2. Basic Integration
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI, Depends
|
||||
from roa2web.shared.auth import (
|
||||
AuthenticationMiddleware, create_auth_router,
|
||||
get_current_user, CurrentUser
|
||||
)
|
||||
from roa2web.shared.database import oracle_pool
|
||||
|
||||
app = FastAPI(title="My ROA2WEB App")
|
||||
|
||||
# Add authentication middleware
|
||||
app.add_middleware(
|
||||
AuthenticationMiddleware,
|
||||
excluded_paths=["/", "/docs", "/health", "/auth/login"]
|
||||
)
|
||||
|
||||
# Include authentication routes
|
||||
auth_router = create_auth_router()
|
||||
app.include_router(auth_router)
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
await oracle_pool.initialize()
|
||||
|
||||
@app.get("/protected")
|
||||
async def protected_endpoint(
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
return {"message": f"Hello {current_user.username}!"}
|
||||
```
|
||||
|
||||
### 3. Test Authentication
|
||||
|
||||
```bash
|
||||
# Start demo application
|
||||
cd roa2web/shared/auth
|
||||
python demo_app.py
|
||||
|
||||
# Open browser
|
||||
open http://localhost:8000/docs
|
||||
|
||||
# Login prin Swagger UI cu credențialele Oracle
|
||||
```
|
||||
|
||||
## 🧩 Components
|
||||
|
||||
### JWT Handler (`jwt_handler.py`)
|
||||
|
||||
Gestionează crearea, validarea și refresh-ul token-urilor JWT.
|
||||
|
||||
```python
|
||||
from roa2web.shared.auth import jwt_handler
|
||||
|
||||
# Create access token
|
||||
token = jwt_handler.create_access_token(
|
||||
username="admin",
|
||||
companies=["COMP1", "COMP2"],
|
||||
permissions=["read", "write", "reports"]
|
||||
)
|
||||
|
||||
# Verify token
|
||||
token_data = jwt_handler.verify_token(token)
|
||||
if token_data:
|
||||
print(f"Valid token for user: {token_data.username}")
|
||||
```
|
||||
|
||||
### Auth Service (`auth_service.py`)
|
||||
|
||||
Integrează cu Oracle Database pentru autentificare și management utilizatori.
|
||||
|
||||
```python
|
||||
from roa2web.shared.auth import auth_service
|
||||
|
||||
# Authenticate user
|
||||
success, token_response, error = await auth_service.authenticate_and_create_tokens(
|
||||
"username", "password"
|
||||
)
|
||||
|
||||
if success:
|
||||
print(f"Access token: {token_response.access_token}")
|
||||
else:
|
||||
print(f"Authentication failed: {error}")
|
||||
```
|
||||
|
||||
### FastAPI Dependencies (`dependencies.py`)
|
||||
|
||||
Oferă dependencies pentru protejarea endpoint-urilor.
|
||||
|
||||
```python
|
||||
from fastapi import Depends
|
||||
from roa2web.shared.auth import (
|
||||
get_current_user, require_company_access,
|
||||
require_permissions, PermissionType
|
||||
)
|
||||
|
||||
@app.get("/admin-only")
|
||||
async def admin_endpoint(
|
||||
user: CurrentUser = Depends(require_permissions([PermissionType.ADMIN]))
|
||||
):
|
||||
return {"message": "Admin access granted"}
|
||||
|
||||
@app.get("/company/{company_code}/data")
|
||||
async def company_data(
|
||||
company_code: str,
|
||||
user: CurrentUser = Depends(require_company_access(company_code))
|
||||
):
|
||||
return {"company": company_code, "data": "..."}
|
||||
```
|
||||
|
||||
### Authentication Routes (`routes.py`)
|
||||
|
||||
Pre-built routes pentru operații de autentificare.
|
||||
|
||||
```python
|
||||
from roa2web.shared.auth import create_auth_router
|
||||
|
||||
# Basic auth router
|
||||
auth_router = create_auth_router()
|
||||
app.include_router(auth_router)
|
||||
|
||||
# Auth router cu admin routes
|
||||
auth_router_admin = create_auth_router(include_admin_routes=True)
|
||||
app.include_router(auth_router_admin)
|
||||
```
|
||||
|
||||
Available routes:
|
||||
- `POST /auth/login` - User authentication
|
||||
- `POST /auth/refresh` - Token refresh
|
||||
- `POST /auth/logout` - User logout
|
||||
- `GET /auth/me` - Current user info
|
||||
- `GET /auth/companies` - User companies
|
||||
- `GET /auth/status` - Authentication status
|
||||
|
||||
## 🔧 Integration Guide
|
||||
|
||||
### Full FastAPI Application
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI, Depends, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from roa2web.shared.auth import (
|
||||
AuthenticationMiddleware, create_auth_router,
|
||||
get_current_user, require_company_access,
|
||||
CurrentUser, PermissionType
|
||||
)
|
||||
from roa2web.shared.database import oracle_pool
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Startup
|
||||
await oracle_pool.initialize()
|
||||
yield
|
||||
# Shutdown
|
||||
await oracle_pool.close_pool()
|
||||
|
||||
app = FastAPI(
|
||||
title="ROA2WEB Application",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:3000"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Authentication
|
||||
app.add_middleware(
|
||||
AuthenticationMiddleware,
|
||||
excluded_paths=["/", "/docs", "/health"],
|
||||
rate_limit_paths=["/auth/login"]
|
||||
)
|
||||
|
||||
# Routes
|
||||
auth_router = create_auth_router()
|
||||
app.include_router(auth_router)
|
||||
|
||||
# Protected endpoints
|
||||
@app.get("/")
|
||||
async def public_endpoint():
|
||||
return {"message": "Public endpoint"}
|
||||
|
||||
@app.get("/me")
|
||||
async def my_info(current_user: CurrentUser = Depends(get_current_user)):
|
||||
return current_user
|
||||
|
||||
@app.get("/company/{company_code}/invoices")
|
||||
async def get_invoices(
|
||||
company_code: str,
|
||||
current_user: CurrentUser = Depends(require_company_access(company_code))
|
||||
):
|
||||
# Business logic here
|
||||
return {"company": company_code, "invoices": []}
|
||||
```
|
||||
|
||||
### Custom Permissions
|
||||
|
||||
```python
|
||||
from roa2web.shared.auth import require_permissions, PermissionType
|
||||
|
||||
# Define custom permissions
|
||||
class CustomPermissionType(str, Enum):
|
||||
INVOICE_READ = "invoice_read"
|
||||
INVOICE_WRITE = "invoice_write"
|
||||
REPORT_EXPORT = "report_export"
|
||||
|
||||
# Use in endpoints
|
||||
@app.get("/invoices")
|
||||
async def get_invoices(
|
||||
user: CurrentUser = Depends(require_permissions([CustomPermissionType.INVOICE_READ]))
|
||||
):
|
||||
return {"invoices": []}
|
||||
```
|
||||
|
||||
### Company-Specific Endpoints
|
||||
|
||||
```python
|
||||
from fastapi import Header
|
||||
from roa2web.shared.auth import get_current_company_from_header
|
||||
|
||||
@app.get("/current-company-data")
|
||||
async def get_current_company_data(
|
||||
company_code: str = Depends(get_current_company_from_header),
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
# company_code is automatically extracted from X-Company-Code header
|
||||
# and validated against user's accessible companies
|
||||
return {"company": company_code, "data": "..."}
|
||||
```
|
||||
|
||||
## 🔒 Security Features
|
||||
|
||||
### JWT Configuration
|
||||
|
||||
```python
|
||||
# Environment variables
|
||||
JWT_SECRET_KEY=your-super-secret-jwt-key-change-in-production
|
||||
JWT_ALGORITHM=HS256
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||
REFRESH_TOKEN_EXPIRE_DAYS=7
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
```python
|
||||
from roa2web.shared.auth import RateLimiter, AuthenticationMiddleware
|
||||
|
||||
# Custom rate limiter
|
||||
custom_rate_limiter = RateLimiter(
|
||||
max_requests=10, # 10 requests
|
||||
time_window=60 # per minute
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
AuthenticationMiddleware,
|
||||
rate_limit_paths=["/auth/login", "/auth/register"],
|
||||
rate_limiter=custom_rate_limiter
|
||||
)
|
||||
```
|
||||
|
||||
### Security Headers
|
||||
|
||||
Middleware-ul adaugă automat header-e de securitate:
|
||||
|
||||
```
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Frame-Options: DENY
|
||||
X-XSS-Protection: 1; mode=block
|
||||
```
|
||||
|
||||
## 📚 API Reference
|
||||
|
||||
### JWT Handler Methods
|
||||
|
||||
```python
|
||||
class JWTHandler:
|
||||
def create_access_token(username, companies, user_id=None, permissions=None) -> str
|
||||
def create_refresh_token(username, user_id=None) -> str
|
||||
def verify_token(token) -> Optional[TokenData]
|
||||
def refresh_access_token(refresh_token, companies, permissions=None) -> Optional[str]
|
||||
def create_token_response(username, companies, ...) -> TokenResponse
|
||||
```
|
||||
|
||||
### Auth Service Methods
|
||||
|
||||
```python
|
||||
class UserAuthService:
|
||||
async def verify_user_credentials(username, password) -> bool
|
||||
async def get_user_companies(username) -> List[str]
|
||||
async def get_user_permissions(username, company) -> List[str]
|
||||
async def authenticate_and_create_tokens(username, password) -> Tuple[bool, TokenResponse, str]
|
||||
async def validate_user_company_access(username, company) -> bool
|
||||
```
|
||||
|
||||
### FastAPI Dependencies
|
||||
|
||||
```python
|
||||
# User dependencies
|
||||
get_current_user() -> CurrentUser
|
||||
get_optional_user() -> Optional[CurrentUser]
|
||||
|
||||
# Permission dependencies
|
||||
require_permissions(permissions: List[PermissionType])
|
||||
require_company_access(company_code: str)
|
||||
require_company_and_permissions(company_code: str, permissions: List[PermissionType])
|
||||
|
||||
# Utility dependencies
|
||||
get_current_company_from_header() -> str
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Install test dependencies
|
||||
pip install pytest pytest-asyncio httpx
|
||||
|
||||
# Run all tests
|
||||
cd roa2web/shared/auth
|
||||
python -m pytest test_auth.py -v
|
||||
|
||||
# Run specific test categories
|
||||
python -m pytest test_auth.py::TestJWTHandler -v
|
||||
python -m pytest test_auth.py::TestUserAuthService -v
|
||||
python -m pytest test_auth.py::TestSecurityFeatures -v
|
||||
|
||||
# Run with coverage
|
||||
python -m pytest test_auth.py --cov=. --cov-report=html
|
||||
```
|
||||
|
||||
### Test Categories
|
||||
|
||||
- **Unit Tests**: JWT operations, auth service methods
|
||||
- **Integration Tests**: Database integration, full auth flow
|
||||
- **Security Tests**: Token tampering, SQL injection, rate limiting
|
||||
- **Performance Tests**: Token creation/verification speed
|
||||
|
||||
### Demo Application
|
||||
|
||||
```bash
|
||||
# Start demo app for manual testing
|
||||
cd roa2web/shared/auth
|
||||
python demo_app.py
|
||||
|
||||
# Available demo endpoints:
|
||||
# http://localhost:8000/ - Home page cu documentație
|
||||
# http://localhost:8000/docs - Swagger UI pentru testare
|
||||
# http://localhost:8000/demo/* - Various demo endpoints
|
||||
```
|
||||
|
||||
## 🚀 Deployment
|
||||
|
||||
### Production Configuration
|
||||
|
||||
```bash
|
||||
# Strong JWT secret key
|
||||
JWT_SECRET_KEY=$(python -c "import secrets; print(secrets.token_urlsafe(32))")
|
||||
|
||||
# Shorter token expiration
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=15
|
||||
REFRESH_TOKEN_EXPIRE_DAYS=1
|
||||
|
||||
# Strict rate limiting
|
||||
RATE_LIMIT_MAX_REQUESTS=3
|
||||
RATE_LIMIT_TIME_WINDOW=300
|
||||
|
||||
# Secure headers
|
||||
SECURE_SSL_REDIRECT=true
|
||||
SESSION_COOKIE_SECURE=true
|
||||
```
|
||||
|
||||
### Docker Integration
|
||||
|
||||
```dockerfile
|
||||
# În Dockerfile-ul aplicației
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
# Environment pentru container
|
||||
ENV JWT_SECRET_KEY=${JWT_SECRET_KEY}
|
||||
ENV ORACLE_USER=${ORACLE_USER}
|
||||
ENV ORACLE_PASSWORD=${ORACLE_PASSWORD}
|
||||
ENV ORACLE_DSN=${ORACLE_DSN}
|
||||
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
```
|
||||
|
||||
### Health Checks
|
||||
|
||||
```python
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
# Test database connection
|
||||
try:
|
||||
async with oracle_pool.get_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("SELECT 1 FROM DUAL")
|
||||
db_status = "healthy"
|
||||
except Exception as e:
|
||||
db_status = f"error: {str(e)}"
|
||||
|
||||
return {
|
||||
"status": "healthy" if db_status == "healthy" else "degraded",
|
||||
"database": db_status,
|
||||
"jwt": "functional",
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 1. "Invalid token" errors
|
||||
|
||||
```python
|
||||
# Check JWT secret key consistency
|
||||
print(f"JWT Secret: {os.getenv('JWT_SECRET_KEY')}")
|
||||
|
||||
# Verify token creation and validation
|
||||
token = jwt_handler.create_access_token("test", ["COMP1"])
|
||||
token_data = jwt_handler.verify_token(token)
|
||||
print(f"Token valid: {token_data is not None}")
|
||||
```
|
||||
|
||||
#### 2. Database connection errors
|
||||
|
||||
```python
|
||||
# Test Oracle connection
|
||||
try:
|
||||
async with oracle_pool.get_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("SELECT 1 FROM DUAL")
|
||||
result = cursor.fetchone()
|
||||
print("Database connection: OK")
|
||||
except Exception as e:
|
||||
print(f"Database error: {e}")
|
||||
```
|
||||
|
||||
#### 3. Rate limiting issues
|
||||
|
||||
```python
|
||||
# Check rate limiter stats
|
||||
from roa2web.shared.auth import default_rate_limiter
|
||||
|
||||
client_ip = "192.168.1.1"
|
||||
allowed = default_rate_limiter.is_allowed(client_ip)
|
||||
reset_time = default_rate_limiter.get_reset_time(client_ip)
|
||||
print(f"IP {client_ip} allowed: {allowed}, resets at: {reset_time}")
|
||||
```
|
||||
|
||||
#### 4. Permission denied errors
|
||||
|
||||
```python
|
||||
# Check user companies and permissions
|
||||
companies = await auth_service.get_user_companies("username")
|
||||
permissions = await auth_service.get_user_permissions("username", "COMP1")
|
||||
print(f"User companies: {companies}")
|
||||
print(f"User permissions: {permissions}")
|
||||
```
|
||||
|
||||
### Debug Mode
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
# Enable debug logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
# Specific loggers
|
||||
logging.getLogger("roa2web.shared.auth").setLevel(logging.DEBUG)
|
||||
```
|
||||
|
||||
### Environment Validation
|
||||
|
||||
```python
|
||||
from roa2web.shared.utils.config import shared_config
|
||||
|
||||
# Validate configuration
|
||||
print(f"Oracle User: {shared_config.oracle_user}")
|
||||
print(f"JWT Secret set: {'***' if shared_config.jwt_secret_key else 'NOT SET'}")
|
||||
print(f"Token expiry: {shared_config.access_token_expire_minutes} minutes")
|
||||
```
|
||||
|
||||
## 📈 Performance Optimization
|
||||
|
||||
### Caching
|
||||
|
||||
```python
|
||||
# Cache configuration
|
||||
AUTH_CACHE_TTL_MINUTES=15 # User data cache TTL
|
||||
|
||||
# Monitor cache performance
|
||||
stats = auth_service.get_cache_stats()
|
||||
print(f"Cache hit ratio: {stats['cache_hit_ratio']:.2%}")
|
||||
```
|
||||
|
||||
### Connection Pooling
|
||||
|
||||
```python
|
||||
# Oracle pool configuration
|
||||
DB_MIN_CONNECTIONS=2
|
||||
DB_MAX_CONNECTIONS=10
|
||||
DB_CONNECTION_INCREMENT=1
|
||||
```
|
||||
|
||||
### Token Optimization
|
||||
|
||||
```python
|
||||
# Optimize token size by limiting payload
|
||||
token = jwt_handler.create_access_token(
|
||||
username="user",
|
||||
companies=["COMP1"], # Limit companies in token
|
||||
permissions=["read"] # Essential permissions only
|
||||
)
|
||||
```
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Pentru contribuții la sistemul de autentificare:
|
||||
|
||||
1. **Fork repository-ul** și creează o ramură pentru feature
|
||||
2. **Implementează schimbările** cu tests comprehensive
|
||||
3. **Rulează toate testele** pentru a verifica compatibilitatea
|
||||
4. **Actualizează documentația** dacă este necesar
|
||||
5. **Creează Pull Request** cu descriere detaliată
|
||||
|
||||
### Development Setup
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone [repository-url]
|
||||
cd roa-flask
|
||||
|
||||
# Setup environment
|
||||
python -m venv venv
|
||||
source venv/bin/activate # Linux/Mac
|
||||
# or
|
||||
venv\Scripts\activate # Windows
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run tests
|
||||
cd roa2web/shared/auth
|
||||
python -m pytest test_auth.py -v
|
||||
```
|
||||
|
||||
## 📜 License
|
||||
|
||||
Acest sistem de autentificare face parte din proiectul ROA2WEB și este disponibil sub aceleași condiții de licențiere ca și proiectul principal.
|
||||
|
||||
---
|
||||
|
||||
**ROA2WEB Authentication System v1.0.0**
|
||||
*Secure, scalable, Oracle-integrated authentication pentru aplicații moderne*
|
||||
23
shared/auth/__init__.py
Normal file
23
shared/auth/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
ROA2WEB Shared Authentication Module
|
||||
|
||||
This module provides JWT-based authentication functionality that can be shared
|
||||
across all ROA2WEB microservices.
|
||||
|
||||
Components:
|
||||
- jwt_handler: JWT token creation, validation, and refresh
|
||||
- auth_service: Oracle database authentication integration
|
||||
- middleware: FastAPI middleware for token validation
|
||||
- dependencies: FastAPI dependencies for protected routes
|
||||
- models: Pydantic models for authentication data
|
||||
- routes: Template authentication routes for FastAPI apps
|
||||
"""
|
||||
|
||||
from .jwt_handler import jwt_handler, JWTHandler, TokenData, TokenResponse
|
||||
|
||||
__all__ = [
|
||||
'jwt_handler',
|
||||
'JWTHandler',
|
||||
'TokenData',
|
||||
'TokenResponse'
|
||||
]
|
||||
395
shared/auth/auth_service.py
Normal file
395
shared/auth/auth_service.py
Normal file
@@ -0,0 +1,395 @@
|
||||
"""
|
||||
Authentication Service - Oracle Database Integration pentru ROA2WEB
|
||||
|
||||
Acest modul integrează sistemul de autentificare JWT cu baza de date Oracle,
|
||||
reutilizând funcționalitatea existentă din aplicația Flask originală.
|
||||
|
||||
Funcționalități:
|
||||
- Verificare utilizatori prin pack_drepturi.verificautilizator
|
||||
- Obținere lista firmelor din vdef_util_grup
|
||||
- Gestionarea sesiunilor și permisiunilor utilizatorilor
|
||||
- Caching pentru performanță optimă
|
||||
"""
|
||||
|
||||
import logging
|
||||
import hashlib
|
||||
from typing import Optional, List, Dict, Any, Tuple
|
||||
from datetime import datetime, timedelta
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
# Import shared database pool
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from database.oracle_pool import oracle_pool
|
||||
from .jwt_handler import jwt_handler
|
||||
from .models import TokenResponse, CurrentUser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AuthenticationError(Exception):
|
||||
"""Excepție pentru erorile de autentificare"""
|
||||
pass
|
||||
|
||||
|
||||
class UserAuthService:
|
||||
"""
|
||||
Serviciu pentru autentificarea utilizatorilor folosind Oracle Database
|
||||
|
||||
Acest serviciu integrează:
|
||||
- Verificarea credențialelor prin pack_drepturi.verificautilizator
|
||||
- Obținerea listei de firme prin vdef_util_grup
|
||||
- Generarea token-urilor JWT
|
||||
- Cache pentru performanță
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Inițializează serviciul de autentificare"""
|
||||
self._user_cache: Dict[str, Dict[str, Any]] = {}
|
||||
self._cache_ttl = timedelta(minutes=15) # Cache 15 minute
|
||||
|
||||
def _get_cache_key(self, username: str) -> str:
|
||||
"""Generează cheia de cache pentru utilizator"""
|
||||
return f"auth_user_{username.lower()}"
|
||||
|
||||
def _is_cache_valid(self, cache_entry: Dict[str, Any]) -> bool:
|
||||
"""Verifică dacă entry-ul din cache este încă valid"""
|
||||
if not cache_entry or 'timestamp' not in cache_entry:
|
||||
return False
|
||||
|
||||
cache_time = cache_entry['timestamp']
|
||||
return datetime.now() - cache_time < self._cache_ttl
|
||||
|
||||
def _get_cached_user_data(self, username: str) -> Optional[Dict[str, Any]]:
|
||||
"""Obține datele utilizatorului din cache dacă sunt valide"""
|
||||
cache_key = self._get_cache_key(username)
|
||||
cache_entry = self._user_cache.get(cache_key)
|
||||
|
||||
if self._is_cache_valid(cache_entry):
|
||||
logger.debug(f"Cache hit for user {username}")
|
||||
return cache_entry['data']
|
||||
|
||||
return None
|
||||
|
||||
def _cache_user_data(self, username: str, data: Dict[str, Any]) -> None:
|
||||
"""Salvează datele utilizatorului în cache"""
|
||||
cache_key = self._get_cache_key(username)
|
||||
self._user_cache[cache_key] = {
|
||||
'data': data,
|
||||
'timestamp': datetime.now()
|
||||
}
|
||||
logger.debug(f"Cached data for user {username}")
|
||||
|
||||
async def verify_user_credentials(self, username: str, password: str) -> bool:
|
||||
"""
|
||||
Verifică credențialele utilizatorului folosind pack_drepturi.verificautilizator
|
||||
|
||||
Args:
|
||||
username: Numele utilizatorului
|
||||
password: Parola utilizatorului
|
||||
|
||||
Returns:
|
||||
True dacă credențialele sunt corecte, False altfel
|
||||
|
||||
Raises:
|
||||
AuthenticationError: Dacă apar erori în procesul de verificare
|
||||
"""
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
# Apelarea procedurii pack_drepturi.verificautilizator
|
||||
# Această procedură returnează ID-ul utilizatorului cu checksum pentru succes, -1 pentru eșec
|
||||
cursor.execute("""
|
||||
SELECT pack_drepturi.verificautilizator(:username, :password)
|
||||
FROM DUAL
|
||||
""", {
|
||||
'username': username.upper(),
|
||||
'password': password
|
||||
})
|
||||
|
||||
result = cursor.fetchone()
|
||||
verification_result = result[0] if result else -1
|
||||
|
||||
# Interpretarea rezultatului conform logicii VFP:
|
||||
# -1 = invalid credentials
|
||||
# > 0 = valid user ID with checksum
|
||||
# < -1000000 = admin/super user
|
||||
is_valid = verification_result != -1
|
||||
|
||||
if is_valid:
|
||||
# Extrage ID-ul real al utilizatorului conform logicii VFP
|
||||
if verification_result < -1000000:
|
||||
# Admin/Super user
|
||||
user_id = verification_result + 1000000
|
||||
logger.info(f"Admin/Super user {username} authenticated successfully (ID: {user_id})")
|
||||
else:
|
||||
# User normal - extrage ID-ul din checksum
|
||||
user_id = int(verification_result / 100)
|
||||
logger.info(f"User {username} authenticated successfully (ID: {user_id}, verification: {verification_result})")
|
||||
else:
|
||||
logger.warning(f"Authentication failed for user {username}")
|
||||
|
||||
return is_valid
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Database error during authentication for user {username}: {str(e)}")
|
||||
raise AuthenticationError(f"Database authentication error: {str(e)}")
|
||||
|
||||
async def get_user_companies(self, username: str) -> List[str]:
|
||||
"""
|
||||
Obține lista firmelor la care utilizatorul are acces din V_NOM_FIRME
|
||||
folosind ID-ul utilizatorului din UTILIZATORI
|
||||
|
||||
Args:
|
||||
username: Numele utilizatorului
|
||||
|
||||
Returns:
|
||||
Lista codurilor firmelor la care utilizatorul are acces
|
||||
|
||||
Raises:
|
||||
AuthenticationError: Dacă apar erori în procesul de obținere
|
||||
"""
|
||||
# Verifică cache-ul mai întâi
|
||||
cached_data = self._get_cached_user_data(username)
|
||||
if cached_data and 'companies' in cached_data:
|
||||
return cached_data['companies']
|
||||
|
||||
try:
|
||||
async with oracle_pool.get_connection() as connection:
|
||||
with connection.cursor() as cursor:
|
||||
try:
|
||||
# Debug: să vedem ce utilizatori există în tabela UTILIZATORI
|
||||
cursor.execute("""
|
||||
SELECT ID_UTIL, UTILIZATOR
|
||||
FROM UTILIZATORI
|
||||
WHERE UPPER(UTILIZATOR) LIKE '%MARIUS%'
|
||||
ORDER BY UTILIZATOR
|
||||
""")
|
||||
|
||||
debug_users = cursor.fetchall()
|
||||
logger.info(f"DEBUG: Users with MARIUS in name: {debug_users}")
|
||||
|
||||
# Primul pas: obținem ID-ul utilizatorului din UTILIZATORI
|
||||
cursor.execute("""
|
||||
SELECT ID_UTIL, UTILIZATOR
|
||||
FROM UTILIZATORI
|
||||
WHERE UPPER(UTILIZATOR) = :username
|
||||
""", {'username': username.upper()})
|
||||
|
||||
user_row = cursor.fetchone()
|
||||
if not user_row:
|
||||
logger.warning(f"User {username} not found in UTILIZATORI table")
|
||||
# Să încercăm să găsim utilizatori similari
|
||||
cursor.execute("""
|
||||
SELECT ID_UTIL, UTILIZATOR
|
||||
FROM UTILIZATORI
|
||||
WHERE UPPER(UTILIZATOR) LIKE :username_pattern
|
||||
ORDER BY UTILIZATOR
|
||||
""", {'username_pattern': f'%{username.upper()}%'})
|
||||
similar_users = cursor.fetchall()
|
||||
logger.info(f"Similar users found: {similar_users}")
|
||||
return []
|
||||
|
||||
user_id = user_row[0]
|
||||
actual_name = user_row[1]
|
||||
logger.info(f"Found user {username} with ID: {user_id}, actual name: {actual_name}")
|
||||
|
||||
# Al doilea pas: obținem firmele folosind query-ul corect (cu ID_FIRMA)
|
||||
cursor.execute("""
|
||||
SELECT A.ID_FIRMA, A.FIRMA
|
||||
FROM V_NOM_FIRME A
|
||||
WHERE A.ID_FIRMA IN (
|
||||
SELECT ID_FIRMA
|
||||
FROM VDEF_UTIL_FIRME
|
||||
WHERE ID_PROGRAM = 2
|
||||
AND ID_UTIL = :user_id
|
||||
)
|
||||
ORDER BY A.FIRMA
|
||||
""", {'user_id': user_id})
|
||||
|
||||
companies_rows = cursor.fetchall()
|
||||
companies = [str(row[0]) for row in companies_rows if row[0]]
|
||||
|
||||
if not companies:
|
||||
logger.warning(f"No companies found for user {username} (ID: {user_id})")
|
||||
return []
|
||||
|
||||
logger.info(f"User {username} has access to {len(companies)} companies: {companies}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Could not query companies for user {username}: {e}")
|
||||
# În caz de eroare, returnăm listă goală în loc de TEST_COMPANY
|
||||
return []
|
||||
|
||||
# Cache rezultatul
|
||||
self._cache_user_data(username, {'companies': companies})
|
||||
|
||||
return companies
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Database error getting companies for user {username}: {str(e)}")
|
||||
raise AuthenticationError(f"Error retrieving user companies: {str(e)}")
|
||||
|
||||
async def get_user_permissions(self, username: str, company: str) -> List[str]:
|
||||
"""
|
||||
Obține permisiunile utilizatorului pentru o anumită firmă
|
||||
|
||||
Args:
|
||||
username: Numele utilizatorului
|
||||
company: Codul firmei
|
||||
|
||||
Returns:
|
||||
Lista permisiunilor pentru firma specificată
|
||||
"""
|
||||
# Implementare de bază - poate fi extinsă în viitor
|
||||
companies = await self.get_user_companies(username)
|
||||
|
||||
# Dacă nu există companii sau compania nu este în listă, returnează permisiuni minime
|
||||
if not companies or company not in companies:
|
||||
return ["read"] if not companies else []
|
||||
|
||||
# Pentru moment, toți utilizatorii autentificați au permisiuni de citire
|
||||
# Acest sistem poate fi extins cu permisiuni granulare în viitor
|
||||
return ["read", "reports"]
|
||||
|
||||
async def authenticate_and_create_tokens(
|
||||
self,
|
||||
username: str,
|
||||
password: str
|
||||
) -> Tuple[bool, Optional[TokenResponse], Optional[str]]:
|
||||
"""
|
||||
Autentifică utilizatorul și creează token-urile JWT
|
||||
|
||||
Args:
|
||||
username: Numele utilizatorului
|
||||
password: Parola utilizatorului
|
||||
|
||||
Returns:
|
||||
Tuple cu (success, token_response, error_message)
|
||||
"""
|
||||
try:
|
||||
# Verifică credențialele
|
||||
is_valid = await self.verify_user_credentials(username, password)
|
||||
|
||||
if not is_valid:
|
||||
return False, None, "Invalid username or password"
|
||||
|
||||
# Obține firmele utilizatorului
|
||||
companies = await self.get_user_companies(username)
|
||||
|
||||
# Nu blocăm login-ul dacă utilizatorul nu are firme - îl lăsăm să vadă mesajul în frontend
|
||||
if not companies:
|
||||
logger.info(f"User {username} has no companies assigned - allowing login but with empty companies list")
|
||||
|
||||
# Obține permisiunile (pentru prima firmă ca default sau lista goală)
|
||||
permissions = await self.get_user_permissions(username, companies[0] if companies else "")
|
||||
|
||||
# Creează token-urile folosind jwt_handler
|
||||
jwt_tokens = jwt_handler.create_token_response(
|
||||
username=username,
|
||||
companies=companies,
|
||||
user_id=None, # Poate fi adăugat în viitor dacă avem user_id în DB
|
||||
permissions=permissions
|
||||
)
|
||||
|
||||
# Creează obiectul CurrentUser
|
||||
current_user = CurrentUser(
|
||||
username=username,
|
||||
user_id=None,
|
||||
companies=companies,
|
||||
permissions=permissions
|
||||
)
|
||||
|
||||
# Creează TokenResponse-ul complet cu user info
|
||||
token_response = TokenResponse(
|
||||
access_token=jwt_tokens.access_token,
|
||||
refresh_token=jwt_tokens.refresh_token,
|
||||
token_type=jwt_tokens.token_type,
|
||||
expires_in=jwt_tokens.expires_in,
|
||||
user=current_user
|
||||
)
|
||||
|
||||
logger.info(f"Successfully created tokens for user {username}")
|
||||
return True, token_response, None
|
||||
|
||||
except AuthenticationError as e:
|
||||
logger.error(f"Authentication error for user {username}: {str(e)}")
|
||||
return False, None, str(e)
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error during authentication for user {username}: {str(e)}")
|
||||
return False, None, "Internal authentication error"
|
||||
|
||||
async def validate_user_company_access(self, username: str, company: str) -> bool:
|
||||
"""
|
||||
Validează dacă utilizatorul are acces la o anumită firmă
|
||||
|
||||
Args:
|
||||
username: Numele utilizatorului
|
||||
company: Codul firmei de verificat
|
||||
|
||||
Returns:
|
||||
True dacă utilizatorul are acces, False altfel
|
||||
"""
|
||||
try:
|
||||
companies = await self.get_user_companies(username)
|
||||
has_access = company in companies
|
||||
|
||||
if not has_access:
|
||||
logger.warning(f"User {username} attempted to access unauthorized company {company}")
|
||||
|
||||
return has_access
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating company access for user {username}: {str(e)}")
|
||||
return False
|
||||
|
||||
async def refresh_user_data(self, username: str) -> bool:
|
||||
"""
|
||||
Reîmprospătează datele utilizatorului din cache
|
||||
|
||||
Args:
|
||||
username: Numele utilizatorului
|
||||
|
||||
Returns:
|
||||
True dacă refresh-ul a fost cu succes
|
||||
"""
|
||||
try:
|
||||
# Șterge din cache
|
||||
cache_key = self._get_cache_key(username)
|
||||
if cache_key in self._user_cache:
|
||||
del self._user_cache[cache_key]
|
||||
|
||||
# Reîncarcă datele
|
||||
await self.get_user_companies(username)
|
||||
logger.info(f"Refreshed user data for {username}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error refreshing user data for {username}: {str(e)}")
|
||||
return False
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
"""Șterge tot cache-ul utilizatorilor"""
|
||||
self._user_cache.clear()
|
||||
logger.info("User cache cleared")
|
||||
|
||||
def get_cache_stats(self) -> Dict[str, Any]:
|
||||
"""Returnează statistici despre cache"""
|
||||
total_entries = len(self._user_cache)
|
||||
valid_entries = sum(
|
||||
1 for entry in self._user_cache.values()
|
||||
if self._is_cache_valid(entry)
|
||||
)
|
||||
|
||||
return {
|
||||
'total_entries': total_entries,
|
||||
'valid_entries': valid_entries,
|
||||
'cache_hit_ratio': valid_entries / total_entries if total_entries > 0 else 0
|
||||
}
|
||||
|
||||
|
||||
# Instance globală pentru folosire în toate aplicațiile
|
||||
auth_service = UserAuthService()
|
||||
540
shared/auth/demo_app.py
Normal file
540
shared/auth/demo_app.py
Normal file
@@ -0,0 +1,540 @@
|
||||
"""
|
||||
FastAPI Demo App demonstrând sistemul de autentificare ROA2WEB
|
||||
|
||||
Această aplicație demonstrează integrarea completă a sistemului de autentificare:
|
||||
- Login și logout cu Oracle database
|
||||
- Protected routes cu JWT authentication
|
||||
- Company-specific access control
|
||||
- Permission-based authorization
|
||||
- Rate limiting și security features
|
||||
|
||||
Funcționează ca:
|
||||
1. Exemplu de integrare pentru dezvoltatori
|
||||
2. Tool de testare pentru sistemul de autentificare
|
||||
3. Demonstrație pentru managementul proiectului
|
||||
|
||||
Pentru a rula demo-ul:
|
||||
1. Configurează variabilele de mediu în .env
|
||||
2. Asigură-te că Oracle database este accesibil
|
||||
3. Rulează: python demo_app.py
|
||||
4. Acesează http://localhost:8000/docs pentru Swagger UI
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, Depends, HTTPException, Request, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
# Adaugă calea pentru modulele shared
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
# Import modulele de autentificare
|
||||
from .jwt_handler import jwt_handler
|
||||
from .auth_service import auth_service
|
||||
from .models import CurrentUser, LoginRequest, PermissionType
|
||||
from .routes import create_auth_router
|
||||
from .middleware import AuthenticationMiddleware, default_rate_limiter
|
||||
from .dependencies import (
|
||||
get_current_user, get_optional_user, require_company_access,
|
||||
require_permissions, get_current_company_from_header
|
||||
)
|
||||
|
||||
# Import componente shared
|
||||
from database.oracle_pool import oracle_pool
|
||||
from utils.config import shared_config
|
||||
|
||||
# Configurare logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""
|
||||
Lifecycle events pentru demo app
|
||||
"""
|
||||
# Startup
|
||||
logger.info("🚀 Starting ROA2WEB Authentication Demo")
|
||||
|
||||
try:
|
||||
# Inițializează Oracle pool
|
||||
await oracle_pool.initialize(
|
||||
user=shared_config.oracle_user,
|
||||
password=shared_config.oracle_password,
|
||||
dsn=shared_config.oracle_dsn,
|
||||
min_connections=2,
|
||||
max_connections=5
|
||||
)
|
||||
logger.info("✅ Oracle connection pool initialized")
|
||||
|
||||
# Test database connection
|
||||
async with oracle_pool.get_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("SELECT 'Database connected successfully' FROM DUAL")
|
||||
result = cursor.fetchone()
|
||||
logger.info(f"✅ Database test: {result[0]}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Startup error: {str(e)}")
|
||||
logger.warning("Demo will continue but database features may not work")
|
||||
|
||||
yield
|
||||
|
||||
# Shutdown
|
||||
logger.info("🛑 Shutting down ROA2WEB Authentication Demo")
|
||||
try:
|
||||
await oracle_pool.close_pool()
|
||||
logger.info("✅ Oracle connection pool closed")
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Shutdown error: {str(e)}")
|
||||
|
||||
|
||||
# Crearea aplicației FastAPI
|
||||
app = FastAPI(
|
||||
title="ROA2WEB Authentication Demo",
|
||||
description="""
|
||||
Demonstrație completă a sistemului de autentificare ROA2WEB
|
||||
|
||||
Această aplicație demonstrează:
|
||||
- JWT Authentication cu Oracle Database
|
||||
- Protected routes și company access control
|
||||
- Permission-based authorization
|
||||
- Rate limiting și security features
|
||||
- Integration patterns pentru aplicații ROA2WEB
|
||||
""",
|
||||
version="1.0.0",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
# CORS pentru development
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:3000", "http://localhost:5173", "http://localhost:8080"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Authentication middleware
|
||||
app.add_middleware(
|
||||
AuthenticationMiddleware,
|
||||
excluded_paths=["/", "/docs", "/redoc", "/openapi.json", "/health", "/demo", "/auth/login"],
|
||||
rate_limit_paths=["/auth/login"],
|
||||
rate_limiter=default_rate_limiter
|
||||
)
|
||||
|
||||
# Include authentication router
|
||||
auth_router = create_auth_router(include_admin_routes=True)
|
||||
app.include_router(auth_router)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DEMO ENDPOINTS
|
||||
# =============================================================================
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def demo_home():
|
||||
"""
|
||||
Pagina principală cu informații despre demo
|
||||
"""
|
||||
html_content = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>ROA2WEB Authentication Demo</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 40px; background: #f5f5f5; }
|
||||
.container { max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||
h1 { color: #2c3e50; border-bottom: 3px solid #3498db; padding-bottom: 10px; }
|
||||
h2 { color: #34495e; margin-top: 30px; }
|
||||
.endpoint { background: #ecf0f1; padding: 15px; margin: 10px 0; border-radius: 5px; border-left: 4px solid #3498db; }
|
||||
.method { font-weight: bold; color: #e74c3c; }
|
||||
.protected { border-left-color: #f39c12; }
|
||||
.public { border-left-color: #27ae60; }
|
||||
code { background: #34495e; color: white; padding: 2px 6px; border-radius: 3px; }
|
||||
.status { padding: 10px; margin: 15px 0; border-radius: 5px; }
|
||||
.success { background: #d5edda; border: 1px solid #c3e6cb; color: #155724; }
|
||||
.info { background: #d1ecf1; border: 1px solid #bee5eb; color: #0c5460; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔐 ROA2WEB Authentication Demo</h1>
|
||||
|
||||
<div class="status info">
|
||||
<strong>Status:</strong> Demo aplicație ROA2WEB Authentication System<br>
|
||||
<strong>Versiune:</strong> 1.0.0<br>
|
||||
<strong>Timp:</strong> """ + datetime.now().strftime("%Y-%m-%d %H:%M:%S") + """
|
||||
</div>
|
||||
|
||||
<h2>📋 Endpoints Disponibile</h2>
|
||||
|
||||
<div class="endpoint public">
|
||||
<div class="method">GET</div>
|
||||
<strong>/docs</strong> - Swagger UI pentru testarea API-ului
|
||||
</div>
|
||||
|
||||
<div class="endpoint public">
|
||||
<div class="method">GET</div>
|
||||
<strong>/health</strong> - Health check pentru aplicație și database
|
||||
</div>
|
||||
|
||||
<div class="endpoint public">
|
||||
<div class="method">POST</div>
|
||||
<strong>/auth/login</strong> - Autentificare utilizator cu username/password
|
||||
</div>
|
||||
|
||||
<div class="endpoint protected">
|
||||
<div class="method">GET</div>
|
||||
<strong>/auth/me</strong> - Informații utilizator curent (protejat)
|
||||
</div>
|
||||
|
||||
<div class="endpoint protected">
|
||||
<div class="method">GET</div>
|
||||
<strong>/demo/protected</strong> - Endpoint protejat simplu
|
||||
</div>
|
||||
|
||||
<div class="endpoint protected">
|
||||
<div class="method">GET</div>
|
||||
<strong>/demo/company/{company_code}</strong> - Endpoint cu verificare acces firmă
|
||||
</div>
|
||||
|
||||
<div class="endpoint protected">
|
||||
<div class="method">GET</div>
|
||||
<strong>/demo/admin</strong> - Endpoint cu verificare admin permissions
|
||||
</div>
|
||||
|
||||
<h2>🧪 Cum să testezi</h2>
|
||||
|
||||
<ol>
|
||||
<li>Accesează <a href="/docs">/docs</a> pentru Swagger UI</li>
|
||||
<li>Folosește <code>POST /auth/login</code> cu credențiale valide</li>
|
||||
<li>Copiază <code>access_token</code> din răspuns</li>
|
||||
<li>Click pe "Authorize" în Swagger UI și introdu: <code>Bearer YOUR_TOKEN</code></li>
|
||||
<li>Testează endpoint-urile protejate</li>
|
||||
</ol>
|
||||
|
||||
<h2>🔧 Configurare</h2>
|
||||
<p>Pentru a funcționa complet, demo-ul necesită:</p>
|
||||
<ul>
|
||||
<li>Variabile de mediu configurate în <code>.env</code></li>
|
||||
<li>Conexiune la Oracle Database</li>
|
||||
<li>Utilizatori valizi în sistemul Oracle</li>
|
||||
</ul>
|
||||
|
||||
<div class="status success">
|
||||
<strong>💡 Tip:</strong> Pentru dezvoltare rapidă, vezi <code>demo_app.py</code>
|
||||
pentru exemple de integrare a autentificării în aplicațiile tale FastAPI.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return HTMLResponse(content=html_content)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""
|
||||
Health check complet pentru demo
|
||||
"""
|
||||
health_status = {
|
||||
"service": "ROA2WEB Authentication Demo",
|
||||
"status": "healthy",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"version": "1.0.0"
|
||||
}
|
||||
|
||||
# Test database connection
|
||||
try:
|
||||
async with oracle_pool.get_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute("SELECT 1 FROM DUAL")
|
||||
cursor.fetchone()
|
||||
health_status["database"] = "connected"
|
||||
except Exception as e:
|
||||
health_status["database"] = f"error: {str(e)}"
|
||||
health_status["status"] = "degraded"
|
||||
|
||||
# Test JWT handler
|
||||
try:
|
||||
test_token = jwt_handler.create_access_token("healthcheck", ["TEST"])
|
||||
token_data = jwt_handler.verify_token(test_token)
|
||||
if token_data and token_data.username == "healthcheck":
|
||||
health_status["jwt"] = "functional"
|
||||
else:
|
||||
health_status["jwt"] = "error: token verification failed"
|
||||
health_status["status"] = "degraded"
|
||||
except Exception as e:
|
||||
health_status["jwt"] = f"error: {str(e)}"
|
||||
health_status["status"] = "degraded"
|
||||
|
||||
# Authentication service status
|
||||
try:
|
||||
cache_stats = auth_service.get_cache_stats()
|
||||
health_status["auth_cache"] = {
|
||||
"total_entries": cache_stats["total_entries"],
|
||||
"cache_hit_ratio": cache_stats["cache_hit_ratio"]
|
||||
}
|
||||
except Exception as e:
|
||||
health_status["auth_cache"] = f"error: {str(e)}"
|
||||
|
||||
status_code = 200 if health_status["status"] == "healthy" else 503
|
||||
return JSONResponse(content=health_status, status_code=status_code)
|
||||
|
||||
|
||||
@app.get("/demo/public")
|
||||
async def demo_public_endpoint():
|
||||
"""
|
||||
Endpoint public - nu necesită autentificare
|
||||
"""
|
||||
return {
|
||||
"message": "Acesta este un endpoint public",
|
||||
"authenticated": False,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"info": "Acest endpoint poate fi accesat fără autentificare"
|
||||
}
|
||||
|
||||
|
||||
@app.get("/demo/optional-auth")
|
||||
async def demo_optional_auth(
|
||||
current_user: Optional[CurrentUser] = Depends(get_optional_user)
|
||||
):
|
||||
"""
|
||||
Endpoint cu autentificare opțională
|
||||
"""
|
||||
if current_user:
|
||||
return {
|
||||
"message": f"Salut, {current_user.username}!",
|
||||
"authenticated": True,
|
||||
"user": current_user.username,
|
||||
"companies": current_user.companies,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"message": "Acesta este un endpoint cu autentificare opțională",
|
||||
"authenticated": False,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"info": "Poți accesa și fără autentificare, dar cu token obții mai multe informații"
|
||||
}
|
||||
|
||||
|
||||
@app.get("/demo/protected")
|
||||
async def demo_protected_endpoint(
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Endpoint protejat - necesită autentificare
|
||||
"""
|
||||
return {
|
||||
"message": f"Bună ziua, {current_user.username}!",
|
||||
"authenticated": True,
|
||||
"user_info": {
|
||||
"username": current_user.username,
|
||||
"companies": current_user.companies,
|
||||
"permissions": current_user.permissions,
|
||||
"companies_count": len(current_user.companies)
|
||||
},
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"info": "Acest endpoint necesită JWT token valid pentru acces"
|
||||
}
|
||||
|
||||
|
||||
@app.get("/demo/company/{company_code}")
|
||||
async def demo_company_specific_endpoint(
|
||||
company_code: str,
|
||||
current_user: CurrentUser = Depends(require_company_access("")) # Will be overridden
|
||||
):
|
||||
"""
|
||||
Endpoint cu verificare acces la firmă specifică
|
||||
|
||||
Demonstrează cum să verifici dacă utilizatorul are acces la o anumită firmă
|
||||
"""
|
||||
# Verificare manuală pentru demonstrație (în practică folosești dependency)
|
||||
if company_code not in current_user.companies:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Nu aveți acces la firma {company_code}"
|
||||
)
|
||||
|
||||
return {
|
||||
"message": f"Acces permis la firma {company_code}",
|
||||
"company_code": company_code,
|
||||
"user": current_user.username,
|
||||
"user_companies": current_user.companies,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"info": "Utilizatorul are acces la această firmă"
|
||||
}
|
||||
|
||||
|
||||
@app.get("/demo/admin")
|
||||
async def demo_admin_endpoint(
|
||||
current_user: CurrentUser = Depends(require_permissions([PermissionType.ADMIN]))
|
||||
):
|
||||
"""
|
||||
Endpoint cu verificare permisiuni admin
|
||||
"""
|
||||
return {
|
||||
"message": f"Bună ziua, admin {current_user.username}!",
|
||||
"admin_info": {
|
||||
"username": current_user.username,
|
||||
"permissions": current_user.permissions,
|
||||
"companies": current_user.companies,
|
||||
"admin_since": datetime.now().isoformat()
|
||||
},
|
||||
"system_stats": {
|
||||
"total_companies": len(current_user.companies),
|
||||
"demo_version": "1.0.0",
|
||||
"auth_system": "ROA2WEB JWT"
|
||||
},
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"info": "Acest endpoint necesită permisiuni de administrator"
|
||||
}
|
||||
|
||||
|
||||
@app.get("/demo/reports")
|
||||
async def demo_reports_endpoint(
|
||||
request: Request,
|
||||
current_user: CurrentUser = Depends(require_permissions([PermissionType.REPORTS]))
|
||||
):
|
||||
"""
|
||||
Endpoint pentru rapoarte - demonstrează integrarea cu header-ul Company
|
||||
"""
|
||||
# Obține company din header (X-Company-Code) sau folosește prima disponibilă
|
||||
company_code = request.headers.get("X-Company-Code")
|
||||
if not company_code:
|
||||
company_code = current_user.companies[0] if current_user.companies else None
|
||||
|
||||
if not company_code:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Nu s-a specificat codul firmei (X-Company-Code header)"
|
||||
)
|
||||
|
||||
if company_code not in current_user.companies:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Nu aveți acces la rapoartele firmei {company_code}"
|
||||
)
|
||||
|
||||
# Simulează generarea unui raport
|
||||
mock_report_data = {
|
||||
"company_code": company_code,
|
||||
"report_type": "demo_report",
|
||||
"generated_by": current_user.username,
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"data": {
|
||||
"total_invoices": 150,
|
||||
"total_amount": 125000.50,
|
||||
"paid_invoices": 120,
|
||||
"outstanding_amount": 25000.00
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"message": f"Raport generat pentru firma {company_code}",
|
||||
"report": mock_report_data,
|
||||
"user_info": {
|
||||
"username": current_user.username,
|
||||
"permissions": current_user.permissions
|
||||
},
|
||||
"info": "Acesta este un exemplu de endpoint pentru rapoarte cu verificare company access"
|
||||
}
|
||||
|
||||
|
||||
@app.get("/demo/rate-limited")
|
||||
async def demo_rate_limited_endpoint():
|
||||
"""
|
||||
Endpoint cu rate limiting pentru demonstrație
|
||||
"""
|
||||
return {
|
||||
"message": "Acest endpoint are rate limiting aplicat",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"info": "Încercați să faceți mai multe request-uri rapid pentru a vedea rate limiting-ul"
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DEMO UTILITIES
|
||||
# =============================================================================
|
||||
|
||||
@app.get("/demo/token-info")
|
||||
async def demo_token_info(
|
||||
request: Request,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Endpoint pentru afișarea informațiilor despre token-ul curent
|
||||
"""
|
||||
# Extrage token-ul din header
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
token = auth_header[7:]
|
||||
|
||||
# Decodează token-ul pentru informații (fără verificare pentru demo)
|
||||
payload = jwt_handler.decode_token_payload(token)
|
||||
|
||||
return {
|
||||
"message": "Informații despre token-ul curent",
|
||||
"token_info": {
|
||||
"user": current_user.username,
|
||||
"companies": current_user.companies,
|
||||
"permissions": current_user.permissions,
|
||||
"token_type": payload.get("type") if payload else "unknown",
|
||||
"issued_at": payload.get("iat") if payload else None,
|
||||
"expires_at": payload.get("exp") if payload else None
|
||||
},
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"error": "Nu s-a găsit token în header-ul Authorization"
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MAIN EXECUTION
|
||||
# =============================================================================
|
||||
|
||||
def main():
|
||||
"""
|
||||
Funcția principală pentru rularea demo-ului
|
||||
"""
|
||||
print("🚀 Starting ROA2WEB Authentication Demo")
|
||||
print("📋 Available endpoints:")
|
||||
print(" • http://localhost:8000/ - Demo home page")
|
||||
print(" • http://localhost:8000/docs - Swagger UI")
|
||||
print(" • http://localhost:8000/health - Health check")
|
||||
print(" • http://localhost:8000/demo/* - Demo endpoints")
|
||||
print("")
|
||||
print("💡 Pentru testare completă:")
|
||||
print(" 1. Configurează .env cu credențialele Oracle")
|
||||
print(" 2. Asigură-te că database-ul este accesibil")
|
||||
print(" 3. Folosește /docs pentru testarea interactivă")
|
||||
print("")
|
||||
|
||||
uvicorn.run(
|
||||
"demo_app:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=True,
|
||||
log_level="info"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
412
shared/auth/dependencies.py
Normal file
412
shared/auth/dependencies.py
Normal file
@@ -0,0 +1,412 @@
|
||||
"""
|
||||
FastAPI Authentication Dependencies pentru ROA2WEB
|
||||
|
||||
Acest modul oferă dependency functions pentru FastAPI care pot fi folosite
|
||||
pentru a proteja endpoint-urile și a obține informații despre utilizatorul curent.
|
||||
|
||||
Dependencies disponibile:
|
||||
- get_current_user: Obține utilizatorul curent (obligatoriu)
|
||||
- get_optional_user: Obține utilizatorul curent (opțional)
|
||||
- require_company_access: Verifică accesul la o firmă specifică
|
||||
- require_permissions: Verifică permisiunile necesare
|
||||
- get_current_company: Obține firma curentă din context
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional, List, Callable, Any
|
||||
from functools import wraps
|
||||
|
||||
from fastapi import Depends, HTTPException, status, Request
|
||||
from fastapi.security import HTTPAuthorizationCredentials
|
||||
|
||||
from .middleware import security_required, security_optional
|
||||
from .jwt_handler import jwt_handler, TokenData
|
||||
from .auth_service import auth_service
|
||||
from .models import CurrentUser, PermissionType, AuthError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AuthenticationRequired(Exception):
|
||||
"""Excepție pentru când autentificarea este obligatorie"""
|
||||
pass
|
||||
|
||||
|
||||
class InsufficientPermissions(Exception):
|
||||
"""Excepție pentru permisiuni insuficiente"""
|
||||
pass
|
||||
|
||||
|
||||
class CompanyAccessDenied(Exception):
|
||||
"""Excepție pentru acces refuzat la firmă"""
|
||||
pass
|
||||
|
||||
|
||||
async def get_current_user_from_token(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security_required)
|
||||
) -> CurrentUser:
|
||||
"""
|
||||
Extrage și validează utilizatorul curent din token JWT
|
||||
|
||||
Args:
|
||||
credentials: Credențialele HTTP de autentificare din header
|
||||
|
||||
Returns:
|
||||
Utilizatorul curent autentificat
|
||||
|
||||
Raises:
|
||||
HTTPException: Dacă token-ul este invalid sau utilizatorul nu există
|
||||
"""
|
||||
if not credentials:
|
||||
logger.warning("No credentials provided for protected endpoint")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Authentication required",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# Validează token-ul
|
||||
token_data = jwt_handler.verify_token(credentials.credentials)
|
||||
|
||||
if not token_data:
|
||||
logger.warning("Invalid token provided")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
if token_data.token_type != "access":
|
||||
logger.warning(f"Invalid token type: {token_data.token_type}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token type",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# Creează obiectul CurrentUser
|
||||
current_user = CurrentUser(
|
||||
username=token_data.username,
|
||||
user_id=token_data.user_id,
|
||||
companies=token_data.companies,
|
||||
permissions=token_data.permissions
|
||||
)
|
||||
|
||||
logger.debug(f"Successfully authenticated user: {current_user.username}")
|
||||
return current_user
|
||||
|
||||
|
||||
async def get_current_user_from_request(request: Request) -> CurrentUser:
|
||||
"""
|
||||
Obține utilizatorul curent din request state (setat de middleware)
|
||||
|
||||
Args:
|
||||
request: Request-ul HTTP curent
|
||||
|
||||
Returns:
|
||||
Utilizatorul curent autentificat
|
||||
|
||||
Raises:
|
||||
HTTPException: Dacă utilizatorul nu este autentificat
|
||||
"""
|
||||
print(f"[DEPENDENCY DEBUG] get_current_user_from_request called")
|
||||
print(f"[DEPENDENCY DEBUG] request.state attributes: {dir(request.state)}")
|
||||
print(f"[DEPENDENCY DEBUG] has is_authenticated: {hasattr(request.state, 'is_authenticated')}")
|
||||
print(f"[DEPENDENCY DEBUG] is_authenticated value: {getattr(request.state, 'is_authenticated', 'NOT_SET')}")
|
||||
print(f"[DEPENDENCY DEBUG] has user: {hasattr(request.state, 'user')}")
|
||||
print(f"[DEPENDENCY DEBUG] user value: {getattr(request.state, 'user', 'NOT_SET')}")
|
||||
|
||||
if not hasattr(request.state, 'is_authenticated') or not request.state.is_authenticated:
|
||||
print(f"[DEPENDENCY DEBUG] Returning 401: Authentication required")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Authentication required",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
if not hasattr(request.state, 'user') or not request.state.user:
|
||||
print(f"[DEPENDENCY DEBUG] Returning 401: User not found in request")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User not found in request",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
print(f"[DEPENDENCY DEBUG] Returning user: {request.state.user}")
|
||||
return request.state.user
|
||||
|
||||
|
||||
async def get_optional_user_from_request(request: Request) -> Optional[CurrentUser]:
|
||||
"""
|
||||
Obține utilizatorul curent din request (opțional)
|
||||
|
||||
Args:
|
||||
request: Request-ul HTTP curent
|
||||
|
||||
Returns:
|
||||
Utilizatorul curent sau None dacă nu este autentificat
|
||||
"""
|
||||
if (hasattr(request.state, 'is_authenticated') and
|
||||
request.state.is_authenticated and
|
||||
hasattr(request.state, 'user')):
|
||||
return request.state.user
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def get_optional_user_from_token(
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security_optional)
|
||||
) -> Optional[CurrentUser]:
|
||||
"""
|
||||
Extrage utilizatorul curent din token (opțional)
|
||||
|
||||
Args:
|
||||
credentials: Credențialele HTTP Bearer (opționale)
|
||||
|
||||
Returns:
|
||||
Utilizatorul curent sau None
|
||||
"""
|
||||
if not credentials:
|
||||
return None
|
||||
|
||||
try:
|
||||
return await get_current_user_from_token(credentials)
|
||||
except HTTPException:
|
||||
return None
|
||||
|
||||
|
||||
def require_company_access(company_code: str):
|
||||
"""
|
||||
Dependency factory care verifică accesul la o firmă specifică
|
||||
|
||||
Args:
|
||||
company_code: Codul firmei la care se verifică accesul
|
||||
|
||||
Returns:
|
||||
Dependency function pentru FastAPI
|
||||
"""
|
||||
async def check_company_access(
|
||||
current_user: CurrentUser = Depends(get_current_user_from_request)
|
||||
) -> CurrentUser:
|
||||
"""
|
||||
Verifică dacă utilizatorul curent are acces la firma specificată
|
||||
|
||||
Args:
|
||||
current_user: Utilizatorul curent autentificat
|
||||
|
||||
Returns:
|
||||
Utilizatorul curent dacă are acces
|
||||
|
||||
Raises:
|
||||
HTTPException: Dacă nu are acces la firmă
|
||||
"""
|
||||
if company_code not in current_user.companies:
|
||||
logger.warning(
|
||||
f"User {current_user.username} attempted to access "
|
||||
f"unauthorized company {company_code}"
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Access denied to company {company_code}"
|
||||
)
|
||||
|
||||
# Verifică și în baza de date pentru siguranță
|
||||
has_access = await auth_service.validate_user_company_access(
|
||||
current_user.username, company_code
|
||||
)
|
||||
|
||||
if not has_access:
|
||||
logger.error(
|
||||
f"Database access check failed for user {current_user.username} "
|
||||
f"and company {company_code}"
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Database access denied to company {company_code}"
|
||||
)
|
||||
|
||||
logger.debug(f"User {current_user.username} granted access to company {company_code}")
|
||||
return current_user
|
||||
|
||||
return check_company_access
|
||||
|
||||
|
||||
def require_permissions(required_permissions: List[PermissionType]):
|
||||
"""
|
||||
Dependency factory care verifică permisiunile necesare
|
||||
|
||||
Args:
|
||||
required_permissions: Lista permisiunilor necesare
|
||||
|
||||
Returns:
|
||||
Dependency function pentru FastAPI
|
||||
"""
|
||||
async def check_permissions(
|
||||
current_user: CurrentUser = Depends(get_current_user_from_request)
|
||||
) -> CurrentUser:
|
||||
"""
|
||||
Verifică dacă utilizatorul are permisiunile necesare
|
||||
|
||||
Args:
|
||||
current_user: Utilizatorul curent autentificat
|
||||
|
||||
Returns:
|
||||
Utilizatorul curent dacă are permisiunile
|
||||
|
||||
Raises:
|
||||
HTTPException: Dacă nu are permisiunile necesare
|
||||
"""
|
||||
user_permissions = set(current_user.permissions)
|
||||
missing_permissions = [
|
||||
perm for perm in required_permissions
|
||||
if perm not in user_permissions
|
||||
]
|
||||
|
||||
if missing_permissions:
|
||||
logger.warning(
|
||||
f"User {current_user.username} missing permissions: {missing_permissions}"
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Missing required permissions: {missing_permissions}"
|
||||
)
|
||||
|
||||
logger.debug(f"User {current_user.username} has required permissions")
|
||||
return current_user
|
||||
|
||||
return check_permissions
|
||||
|
||||
|
||||
def require_company_and_permissions(
|
||||
company_code: str,
|
||||
required_permissions: List[PermissionType]
|
||||
):
|
||||
"""
|
||||
Dependency factory care verifică atât accesul la firmă cât și permisiunile
|
||||
|
||||
Args:
|
||||
company_code: Codul firmei
|
||||
required_permissions: Lista permisiunilor necesare
|
||||
|
||||
Returns:
|
||||
Dependency function pentru FastAPI
|
||||
"""
|
||||
async def check_company_and_permissions(
|
||||
current_user: CurrentUser = Depends(get_current_user_from_request)
|
||||
) -> CurrentUser:
|
||||
"""
|
||||
Verifică accesul la firmă și permisiunile
|
||||
|
||||
Args:
|
||||
current_user: Utilizatorul curent
|
||||
|
||||
Returns:
|
||||
Utilizatorul curent dacă are acces și permisiuni
|
||||
"""
|
||||
# Verifică accesul la firmă
|
||||
company_checker = require_company_access(company_code)
|
||||
await company_checker(current_user)
|
||||
|
||||
# Verifică permisiunile
|
||||
permissions_checker = require_permissions(required_permissions)
|
||||
await permissions_checker(current_user)
|
||||
|
||||
return current_user
|
||||
|
||||
return check_company_and_permissions
|
||||
|
||||
|
||||
async def get_current_company_from_header(
|
||||
request: Request,
|
||||
current_user: CurrentUser = Depends(get_current_user_from_request)
|
||||
) -> str:
|
||||
"""
|
||||
Obține codul firmei curente din header-ul X-Company-Code
|
||||
|
||||
Args:
|
||||
request: Request-ul HTTP
|
||||
current_user: Utilizatorul curent
|
||||
|
||||
Returns:
|
||||
Codul firmei curente
|
||||
|
||||
Raises:
|
||||
HTTPException: Dacă header-ul lipsește sau utilizatorul nu are acces
|
||||
"""
|
||||
company_code = request.headers.get("X-Company-Code")
|
||||
|
||||
if not company_code:
|
||||
# Folosește prima firmă ca default dacă nu este specificată
|
||||
if current_user.companies:
|
||||
company_code = current_user.companies[0]
|
||||
logger.debug(f"Using default company {company_code} for user {current_user.username}")
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Company code required (X-Company-Code header or user default)"
|
||||
)
|
||||
|
||||
# Verifică accesul
|
||||
if company_code not in current_user.companies:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Access denied to company {company_code}"
|
||||
)
|
||||
|
||||
return company_code
|
||||
|
||||
|
||||
# Aliasuri pentru folosire mai ușoară
|
||||
get_current_user = get_current_user_from_request
|
||||
get_optional_user = get_optional_user_from_request
|
||||
|
||||
# Dependency-uri predefinite pentru permisiuni comune
|
||||
require_read_permission = require_permissions([PermissionType.READ])
|
||||
require_write_permission = require_permissions([PermissionType.WRITE])
|
||||
require_admin_permission = require_permissions([PermissionType.ADMIN])
|
||||
require_reports_permission = require_permissions([PermissionType.REPORTS])
|
||||
|
||||
# Decorator pentru validarea companiei în funcții
|
||||
def validate_company_access(company_param: str = "company"):
|
||||
"""
|
||||
Decorator pentru validarea automată a accesului la firmă
|
||||
|
||||
Args:
|
||||
company_param: Numele parametrului care conține codul firmei
|
||||
|
||||
Returns:
|
||||
Decorator function
|
||||
"""
|
||||
def decorator(func: Callable) -> Callable:
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
# Caută utilizatorul curent în argumentele funcției
|
||||
current_user = None
|
||||
for arg in args:
|
||||
if isinstance(arg, CurrentUser):
|
||||
current_user = arg
|
||||
break
|
||||
|
||||
if not current_user:
|
||||
# Caută în kwargs
|
||||
current_user = kwargs.get('current_user')
|
||||
|
||||
if not current_user:
|
||||
raise ValueError("CurrentUser not found in function arguments")
|
||||
|
||||
# Obține codul firmei
|
||||
company_code = kwargs.get(company_param)
|
||||
if not company_code:
|
||||
raise ValueError(f"Company parameter '{company_param}' not found")
|
||||
|
||||
# Validează accesul
|
||||
if company_code not in current_user.companies:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Access denied to company {company_code}"
|
||||
)
|
||||
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
239
shared/auth/jwt_handler.py
Normal file
239
shared/auth/jwt_handler.py
Normal file
@@ -0,0 +1,239 @@
|
||||
"""
|
||||
JWT Authentication Handler - Shared între toate aplicațiile ROA2WEB
|
||||
|
||||
Acest modul gestionează crearea, validarea și refresh-ul token-urilor JWT
|
||||
pentru autentificarea utilizatorilor în ecosistemul ROA2WEB.
|
||||
|
||||
Payload structure:
|
||||
{
|
||||
"username": "string",
|
||||
"user_id": "integer",
|
||||
"companies": ["schema1", "schema2"],
|
||||
"permissions": ["read", "write", "admin"],
|
||||
"exp": "timestamp",
|
||||
"iat": "timestamp",
|
||||
"type": "access|refresh"
|
||||
}
|
||||
"""
|
||||
from jose import jwt
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any, List
|
||||
from pydantic import BaseModel, Field
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TokenData(BaseModel):
|
||||
"""Date conținute în token"""
|
||||
username: str = Field(description="Numele utilizatorului")
|
||||
user_id: Optional[int] = Field(default=None, description="ID-ul utilizatorului")
|
||||
companies: List[str] = Field(default_factory=list, description="Lista firmelor accesibile")
|
||||
permissions: List[str] = Field(default_factory=list, description="Lista permisiunilor")
|
||||
exp: datetime = Field(description="Data expirării")
|
||||
iat: datetime = Field(description="Data creării")
|
||||
token_type: str = Field(alias="type", description="Tipul token-ului (access/refresh)")
|
||||
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
"""Răspuns pentru token-uri"""
|
||||
access_token: str = Field(description="JWT access token")
|
||||
refresh_token: Optional[str] = Field(default=None, description="JWT refresh token")
|
||||
token_type: str = Field(default="bearer", description="Tipul token-ului")
|
||||
expires_in: int = Field(description="Timpul de expirare în secunde")
|
||||
|
||||
|
||||
class JWTHandler:
|
||||
"""
|
||||
Gestionarea JWT tokens pentru autentificare
|
||||
|
||||
Această clasă oferă funcționalități pentru:
|
||||
- Crearea token-urilor access și refresh
|
||||
- Validarea și decodificarea token-urilor
|
||||
- Gestionarea expirării token-urilor
|
||||
"""
|
||||
|
||||
def __init__(self, secret_key: Optional[str] = None, algorithm: str = "HS256"):
|
||||
"""
|
||||
Inițializează JWT handler
|
||||
|
||||
Args:
|
||||
secret_key: Cheia secretă pentru semnarea token-urilor
|
||||
algorithm: Algoritmul de criptare (default: HS256)
|
||||
"""
|
||||
self.secret_key = secret_key or os.getenv('JWT_SECRET_KEY', 'your-secret-key-change-in-production')
|
||||
self.algorithm = algorithm
|
||||
self.access_token_expire_minutes = int(os.getenv('ACCESS_TOKEN_EXPIRE_MINUTES', 30))
|
||||
self.refresh_token_expire_days = int(os.getenv('REFRESH_TOKEN_EXPIRE_DAYS', 7))
|
||||
|
||||
# Warning pentru development
|
||||
if self.secret_key == 'your-secret-key-change-in-production':
|
||||
logger.warning("Using default JWT secret key! Change JWT_SECRET_KEY in production!")
|
||||
|
||||
def create_access_token(
|
||||
self,
|
||||
username: str,
|
||||
companies: List[str],
|
||||
user_id: Optional[int] = None,
|
||||
permissions: Optional[List[str]] = None
|
||||
) -> str:
|
||||
"""
|
||||
Creează un JWT access token
|
||||
|
||||
Args:
|
||||
username: Numele utilizatorului
|
||||
companies: Lista firmelor la care utilizatorul are acces
|
||||
user_id: ID-ul utilizatorului în baza de date
|
||||
permissions: Lista permisiunilor utilizatorului
|
||||
|
||||
Returns:
|
||||
Token JWT ca string
|
||||
"""
|
||||
now = datetime.utcnow()
|
||||
expire = now + timedelta(minutes=self.access_token_expire_minutes)
|
||||
|
||||
payload = {
|
||||
"username": username,
|
||||
"user_id": user_id,
|
||||
"companies": companies or [],
|
||||
"permissions": permissions or ["read"],
|
||||
"exp": expire,
|
||||
"iat": now,
|
||||
"type": "access"
|
||||
}
|
||||
|
||||
token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
|
||||
logger.debug(f"Created access token for user {username} with companies: {companies}")
|
||||
|
||||
return token
|
||||
|
||||
def create_refresh_token(self, username: str, user_id: Optional[int] = None) -> str:
|
||||
"""
|
||||
Creează un refresh token cu durată mai mare
|
||||
|
||||
Args:
|
||||
username: Numele utilizatorului
|
||||
user_id: ID-ul utilizatorului
|
||||
|
||||
Returns:
|
||||
Refresh token JWT ca string
|
||||
"""
|
||||
now = datetime.utcnow()
|
||||
expire = now + timedelta(days=self.refresh_token_expire_days)
|
||||
|
||||
payload = {
|
||||
"username": username,
|
||||
"user_id": user_id,
|
||||
"exp": expire,
|
||||
"iat": now,
|
||||
"type": "refresh"
|
||||
}
|
||||
|
||||
token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
|
||||
logger.debug(f"Created refresh token for user {username}")
|
||||
|
||||
return token
|
||||
|
||||
def verify_token(self, token: str) -> Optional[TokenData]:
|
||||
"""
|
||||
Verifică și decodează un JWT token
|
||||
|
||||
Args:
|
||||
token: Token-ul JWT de verificat
|
||||
|
||||
Returns:
|
||||
TokenData cu informațiile din token sau None dacă token-ul e invalid
|
||||
"""
|
||||
try:
|
||||
logger.debug(f"Using JWT secret key (first 10 chars): {self.secret_key[:10]}...")
|
||||
payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
|
||||
token_data = TokenData(**payload)
|
||||
logger.debug(f"Token verified successfully for user {token_data.username}")
|
||||
return token_data
|
||||
except jwt.ExpiredSignatureError:
|
||||
logger.warning("Token has expired")
|
||||
return None
|
||||
except jwt.JWTError as e:
|
||||
logger.warning(f"Invalid token: {str(e)}")
|
||||
logger.debug(f"Token that failed verification: {token[:50]}...")
|
||||
return None
|
||||
|
||||
def refresh_access_token(self, refresh_token: str, companies: List[str], permissions: Optional[List[str]] = None) -> Optional[str]:
|
||||
"""
|
||||
Creează un nou access token folosind refresh token-ul
|
||||
|
||||
Args:
|
||||
refresh_token: Refresh token-ul valid
|
||||
companies: Lista actualizată a firmelor (poate fi modificată între refresh-uri)
|
||||
permissions: Lista actualizată a permisiunilor
|
||||
|
||||
Returns:
|
||||
Noul access token sau None dacă refresh token-ul e invalid
|
||||
"""
|
||||
token_data = self.verify_token(refresh_token)
|
||||
|
||||
if not token_data or token_data.token_type != "refresh":
|
||||
logger.warning("Invalid refresh token")
|
||||
return None
|
||||
|
||||
# Creează nou access token cu datele din refresh token
|
||||
return self.create_access_token(
|
||||
username=token_data.username,
|
||||
companies=companies,
|
||||
user_id=token_data.user_id,
|
||||
permissions=permissions
|
||||
)
|
||||
|
||||
def create_token_response(
|
||||
self,
|
||||
username: str,
|
||||
companies: List[str],
|
||||
user_id: Optional[int] = None,
|
||||
permissions: Optional[List[str]] = None,
|
||||
include_refresh: bool = True
|
||||
) -> TokenResponse:
|
||||
"""
|
||||
Creează un răspuns complet cu access și refresh token
|
||||
|
||||
Args:
|
||||
username: Numele utilizatorului
|
||||
companies: Lista firmelor accesibile
|
||||
user_id: ID-ul utilizatorului
|
||||
permissions: Lista permisiunilor
|
||||
include_refresh: Dacă să includă și refresh token
|
||||
|
||||
Returns:
|
||||
TokenResponse cu toate token-urile
|
||||
"""
|
||||
access_token = self.create_access_token(username, companies, user_id, permissions)
|
||||
refresh_token = self.create_refresh_token(username, user_id) if include_refresh else None
|
||||
|
||||
return TokenResponse(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
token_type="bearer",
|
||||
expires_in=self.access_token_expire_minutes * 60
|
||||
)
|
||||
|
||||
def decode_token_payload(self, token: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Decodează token-ul fără verificare (pentru debugging)
|
||||
|
||||
Args:
|
||||
token: Token-ul de decodat
|
||||
|
||||
Returns:
|
||||
Payload-ul token-ului sau None
|
||||
"""
|
||||
try:
|
||||
# Decodare fără verificare - doar pentru debugging
|
||||
payload = jwt.decode(token, key="", algorithms=[self.algorithm], options={"verify_signature": False})
|
||||
return payload
|
||||
except Exception as e:
|
||||
logger.error(f"Error decoding token payload: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
# Instance globală pentru folosire în toate aplicațiile
|
||||
jwt_handler = JWTHandler()
|
||||
373
shared/auth/middleware.py
Normal file
373
shared/auth/middleware.py
Normal file
@@ -0,0 +1,373 @@
|
||||
"""
|
||||
FastAPI Authentication Middleware pentru ROA2WEB
|
||||
|
||||
Acest modul oferă middleware pentru autentificarea automată în aplicațiile FastAPI,
|
||||
incluzând extragerea token-urilor, validarea și injectarea datelor utilizatorului
|
||||
în contextul request-ului.
|
||||
|
||||
Funcționalități:
|
||||
- Extragere automată token JWT din header Authorization
|
||||
- Validare token și user data injection
|
||||
- Rate limiting pentru endpoint-urile de autentificare
|
||||
- Logging pentru securitate și monitoring
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional, Callable, Dict, Any, List, Set
|
||||
from collections import defaultdict, deque
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from fastapi import Request, Response, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from .jwt_handler import jwt_handler, TokenData
|
||||
from .auth_service import auth_service
|
||||
from .models import CurrentUser, AuthError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RateLimiter:
|
||||
"""
|
||||
Rate limiter pentru protejarea endpoint-urilor de autentificare
|
||||
"""
|
||||
|
||||
def __init__(self, max_requests: int = 5, time_window: int = 300):
|
||||
"""
|
||||
Inițializează rate limiter
|
||||
|
||||
Args:
|
||||
max_requests: Numărul maxim de request-uri permise
|
||||
time_window: Fereastra de timp în secunde
|
||||
"""
|
||||
self.max_requests = max_requests
|
||||
self.time_window = time_window
|
||||
self.requests: Dict[str, deque] = defaultdict(deque)
|
||||
|
||||
def is_allowed(self, client_ip: str) -> bool:
|
||||
"""
|
||||
Verifică dacă request-ul este permis pentru acest IP
|
||||
|
||||
Args:
|
||||
client_ip: Adresa IP a clientului
|
||||
|
||||
Returns:
|
||||
True dacă request-ul este permis
|
||||
"""
|
||||
now = time.time()
|
||||
client_requests = self.requests[client_ip]
|
||||
|
||||
# Șterge request-urile vechi
|
||||
while client_requests and client_requests[0] < now - self.time_window:
|
||||
client_requests.popleft()
|
||||
|
||||
# Verifică dacă putem accepta încă un request
|
||||
if len(client_requests) >= self.max_requests:
|
||||
return False
|
||||
|
||||
# Adaugă request-ul curent
|
||||
client_requests.append(now)
|
||||
return True
|
||||
|
||||
def get_reset_time(self, client_ip: str) -> int:
|
||||
"""
|
||||
Returnează timpul când rate limiting se resetează pentru acest IP
|
||||
|
||||
Args:
|
||||
client_ip: Adresa IP a clientului
|
||||
|
||||
Returns:
|
||||
Timestamp când se resetează
|
||||
"""
|
||||
client_requests = self.requests[client_ip]
|
||||
if not client_requests:
|
||||
return int(time.time())
|
||||
|
||||
return int(client_requests[0] + self.time_window)
|
||||
|
||||
|
||||
class AuthenticationMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
Middleware pentru autentificarea automată în FastAPI
|
||||
|
||||
Acest middleware:
|
||||
- Extrage token-ul JWT din header-ul Authorization
|
||||
- Validează token-ul și obține datele utilizatorului
|
||||
- Injectează utilizatorul curent în request.state
|
||||
- Aplică rate limiting pentru endpoint-urile sensibile
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
app,
|
||||
excluded_paths: Optional[List[str]] = None,
|
||||
rate_limit_paths: Optional[List[str]] = None,
|
||||
rate_limiter: Optional[RateLimiter] = None
|
||||
):
|
||||
"""
|
||||
Inițializează middleware-ul
|
||||
|
||||
Args:
|
||||
app: Aplicația FastAPI
|
||||
excluded_paths: Căile care nu necesită autentificare
|
||||
rate_limit_paths: Căile cu rate limiting
|
||||
rate_limiter: Instance de rate limiter personalizat
|
||||
"""
|
||||
super().__init__(app)
|
||||
|
||||
self.excluded_paths = excluded_paths or [
|
||||
"/docs", "/redoc", "/openapi.json", "/health", "/",
|
||||
"/auth/login", "/auth/register"
|
||||
]
|
||||
|
||||
self.rate_limit_paths = rate_limit_paths or [
|
||||
"/auth/login", "/auth/register", "/auth/forgot-password"
|
||||
]
|
||||
|
||||
self.rate_limiter = rate_limiter or RateLimiter(max_requests=5, time_window=300)
|
||||
|
||||
logger.info(f"Authentication middleware initialized with {len(self.excluded_paths)} excluded paths")
|
||||
|
||||
def _get_client_ip(self, request: Request) -> str:
|
||||
"""Obține adresa IP a clientului"""
|
||||
# Verifică header-ele proxy
|
||||
forwarded_for = request.headers.get("X-Forwarded-For")
|
||||
if forwarded_for:
|
||||
return forwarded_for.split(",")[0].strip()
|
||||
|
||||
real_ip = request.headers.get("X-Real-IP")
|
||||
if real_ip:
|
||||
return real_ip
|
||||
|
||||
# Fallback la client IP direct
|
||||
return request.client.host if request.client else "unknown"
|
||||
|
||||
def _should_exclude_path(self, path: str) -> bool:
|
||||
"""Verifică dacă path-ul trebuie exclus de la autentificare"""
|
||||
# Special case for root path to avoid excluding all paths that start with "/"
|
||||
if "/" in self.excluded_paths and path == "/":
|
||||
return True
|
||||
# Check other excluded paths (excluding "/" to avoid matching all paths)
|
||||
excluded_paths_no_root = [p for p in self.excluded_paths if p != "/"]
|
||||
return any(path.startswith(excluded) for excluded in excluded_paths_no_root)
|
||||
|
||||
def _should_rate_limit_path(self, path: str) -> bool:
|
||||
"""Verifică dacă path-ul necesită rate limiting"""
|
||||
return any(path.startswith(limited) for limited in self.rate_limit_paths)
|
||||
|
||||
def _extract_token_from_header(self, request: Request) -> Optional[str]:
|
||||
"""
|
||||
Extrage token-ul JWT în header-ul Authorization
|
||||
|
||||
Args:
|
||||
request: Request-ul HTTP
|
||||
|
||||
Returns:
|
||||
Token-ul JWT sau None
|
||||
"""
|
||||
authorization = request.headers.get("Authorization")
|
||||
if not authorization:
|
||||
return None
|
||||
|
||||
if not authorization.startswith("Bearer "):
|
||||
return None
|
||||
|
||||
return authorization[7:] # Elimină "Bearer "
|
||||
|
||||
async def _create_current_user(self, token_data: TokenData) -> CurrentUser:
|
||||
"""
|
||||
Creează obiectul CurrentUser din token data
|
||||
|
||||
Args:
|
||||
token_data: Datele din token
|
||||
|
||||
Returns:
|
||||
Obiectul CurrentUser
|
||||
"""
|
||||
return CurrentUser(
|
||||
username=token_data.username,
|
||||
user_id=token_data.user_id,
|
||||
companies=token_data.companies,
|
||||
permissions=token_data.permissions,
|
||||
last_login=datetime.now()
|
||||
)
|
||||
|
||||
async def _handle_rate_limiting(self, request: Request, path: str) -> Optional[Response]:
|
||||
"""
|
||||
Gestionează rate limiting pentru căile sensibile
|
||||
|
||||
Args:
|
||||
request: Request-ul HTTP
|
||||
path: Calea request-ului
|
||||
|
||||
Returns:
|
||||
Response cu eroare dacă este rate limited, None altfel
|
||||
"""
|
||||
if not self._should_rate_limit_path(path):
|
||||
return None
|
||||
|
||||
client_ip = self._get_client_ip(request)
|
||||
|
||||
if not self.rate_limiter.is_allowed(client_ip):
|
||||
reset_time = self.rate_limiter.get_reset_time(client_ip)
|
||||
|
||||
logger.warning(f"Rate limit exceeded for IP {client_ip} on path {path}")
|
||||
|
||||
error = AuthError(
|
||||
error="rate_limit_exceeded",
|
||||
error_description="Too many requests. Please try again later.",
|
||||
error_code="RATE_LIMIT_001"
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
||||
content=error.dict(),
|
||||
headers={
|
||||
"X-RateLimit-Limit": str(self.rate_limiter.max_requests),
|
||||
"X-RateLimit-Remaining": "0",
|
||||
"X-RateLimit-Reset": str(reset_time),
|
||||
"Retry-After": str(reset_time - int(time.time()))
|
||||
}
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
||||
"""
|
||||
Procesează request-ul prin middleware
|
||||
|
||||
Args:
|
||||
request: Request-ul HTTP
|
||||
call_next: Următorul handler din pipeline
|
||||
|
||||
Returns:
|
||||
Response-ul HTTP
|
||||
"""
|
||||
print(f"[ORIGINAL MIDDLEWARE] dispatch called for path: {request.url.path}")
|
||||
start_time = time.time()
|
||||
path = request.url.path
|
||||
|
||||
# Rate limiting pentru căile sensibile
|
||||
rate_limit_response = await self._handle_rate_limiting(request, path)
|
||||
if rate_limit_response:
|
||||
return rate_limit_response
|
||||
|
||||
# Skip autentificare pentru căile excluse
|
||||
if self._should_exclude_path(path):
|
||||
request.state.user = None
|
||||
request.state.is_authenticated = False
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
# Extrage token-ul
|
||||
print(f"[MIDDLEWARE DEBUG] Extracting token for path: {path}")
|
||||
token = self._extract_token_from_header(request)
|
||||
print(f"[MIDDLEWARE DEBUG] Extracted token: {token[:30] if token else 'None'}...")
|
||||
|
||||
if not token:
|
||||
# Nu există token - pentru endpoint-urile protejate returnează 401
|
||||
logger.warning(f"No token provided for protected path {path}")
|
||||
|
||||
error = AuthError(
|
||||
error="authentication_required",
|
||||
error_description="Authentication required",
|
||||
error_code="AUTH_003"
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
content=error.dict(),
|
||||
headers={"WWW-Authenticate": "Bearer"}
|
||||
)
|
||||
|
||||
# Validează token-ul
|
||||
print(f"[MIDDLEWARE DEBUG] Validating token: {token[:30]}...")
|
||||
token_data = jwt_handler.verify_token(token)
|
||||
print(f"[MIDDLEWARE DEBUG] Token validation result: {token_data}")
|
||||
|
||||
if not token_data:
|
||||
# Token invalid
|
||||
logger.warning(f"Invalid token used for path {path}")
|
||||
|
||||
error = AuthError(
|
||||
error="invalid_token",
|
||||
error_description="The provided token is invalid or expired.",
|
||||
error_code="AUTH_001"
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
content=error.dict(),
|
||||
headers={"WWW-Authenticate": "Bearer"}
|
||||
)
|
||||
|
||||
# Token valid - creează utilizatorul curent
|
||||
try:
|
||||
current_user = await self._create_current_user(token_data)
|
||||
request.state.user = current_user
|
||||
request.state.is_authenticated = True
|
||||
request.state.token_data = token_data
|
||||
|
||||
logger.debug(f"User {current_user.username} authenticated successfully for path {path}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating current user: {str(e)}")
|
||||
|
||||
error = AuthError(
|
||||
error="authentication_error",
|
||||
error_description="Authentication processing error.",
|
||||
error_code="AUTH_002"
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content=error.dict()
|
||||
)
|
||||
|
||||
# Procesează request-ul
|
||||
response = await call_next(request)
|
||||
|
||||
# Adaugă header-e de securitate
|
||||
response.headers["X-Content-Type-Options"] = "nosniff"
|
||||
response.headers["X-Frame-Options"] = "DENY"
|
||||
response.headers["X-XSS-Protection"] = "1; mode=block"
|
||||
|
||||
# Log timpul de procesare
|
||||
process_time = time.time() - start_time
|
||||
response.headers["X-Process-Time"] = str(process_time)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class HTTPBearerOptional(HTTPBearer):
|
||||
"""
|
||||
Versiune opțională pentru autentificare care nu aruncă excepții
|
||||
dacă token-ul lipsește - utile pentru endpoint-urile care
|
||||
pot funcționa atât cu cât și fără autentificare
|
||||
"""
|
||||
|
||||
async def __call__(self, request: Request) -> Optional[HTTPAuthorizationCredentials]:
|
||||
"""
|
||||
Extrage credențialele de autentificare fără să arunce excepții
|
||||
|
||||
Args:
|
||||
request: Request-ul HTTP
|
||||
|
||||
Returns:
|
||||
Credențialele sau None
|
||||
"""
|
||||
try:
|
||||
return await super().__call__(request)
|
||||
except HTTPException:
|
||||
return None
|
||||
|
||||
|
||||
# Instance predefinite pentru folosire rapidă
|
||||
security_optional = HTTPBearerOptional(auto_error=False)
|
||||
security_required = HTTPBearer()
|
||||
|
||||
# Rate limiter default
|
||||
default_rate_limiter = RateLimiter(max_requests=5, time_window=300)
|
||||
231
shared/auth/models.py
Normal file
231
shared/auth/models.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
Authentication Pydantic Models pentru ROA2WEB
|
||||
|
||||
Acest modul definește toate modelele de date folosite în sistemul de autentificare,
|
||||
incluzând request/response models și modele pentru user data.
|
||||
|
||||
Modelele acoperă:
|
||||
- Login request și response
|
||||
- Token data și management
|
||||
- User information și permisiuni
|
||||
- Company access control
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field, validator, EmailStr
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class PermissionType(str, Enum):
|
||||
"""Tipurile de permisiuni disponibile în sistem"""
|
||||
READ = "read"
|
||||
WRITE = "write"
|
||||
DELETE = "delete"
|
||||
ADMIN = "admin"
|
||||
REPORTS = "reports"
|
||||
EXPORT = "export"
|
||||
|
||||
|
||||
class TokenType(str, Enum):
|
||||
"""Tipurile de token-uri JWT"""
|
||||
ACCESS = "access"
|
||||
REFRESH = "refresh"
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
"""Model pentru request-ul de login"""
|
||||
username: str = Field(
|
||||
...,
|
||||
min_length=3,
|
||||
max_length=50,
|
||||
description="Numele utilizatorului",
|
||||
example="admin"
|
||||
)
|
||||
password: str = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
description="Parola utilizatorului"
|
||||
)
|
||||
remember_me: bool = Field(
|
||||
default=False,
|
||||
description="Dacă să păstreze utilizatorul autentificat mai mult timp"
|
||||
)
|
||||
|
||||
@validator('username')
|
||||
def username_alphanumeric(cls, v):
|
||||
"""Validează că username-ul conține doar caractere permise (inclusiv spații)"""
|
||||
# Permitem litere, cifre, spații, _, și -
|
||||
allowed_chars = v.replace(' ', '').replace('_', '').replace('-', '')
|
||||
if not allowed_chars.isalnum():
|
||||
raise ValueError('Username-ul poate conține doar litere, cifre, spații, _ și -')
|
||||
return v.upper() # Convertim la uppercase pentru consistență cu Oracle
|
||||
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
"""Model pentru răspunsul de autentificare cu token-uri"""
|
||||
access_token: str = Field(description="JWT access token")
|
||||
refresh_token: Optional[str] = Field(
|
||||
default=None,
|
||||
description="JWT refresh token (opțional)"
|
||||
)
|
||||
token_type: str = Field(
|
||||
default="bearer",
|
||||
description="Tipul token-ului (întotdeauna 'bearer')"
|
||||
)
|
||||
expires_in: int = Field(
|
||||
description="Timpul de expirare al access token-ului în secunde"
|
||||
)
|
||||
user: 'CurrentUser' = Field(description="Informațiile utilizatorului autentificat")
|
||||
|
||||
|
||||
class RefreshTokenRequest(BaseModel):
|
||||
"""Model pentru request-ul de refresh token"""
|
||||
refresh_token: str = Field(description="Refresh token-ul valid")
|
||||
|
||||
|
||||
class LogoutRequest(BaseModel):
|
||||
"""Model pentru request-ul de logout"""
|
||||
refresh_token: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Refresh token de invalidat (opțional)"
|
||||
)
|
||||
|
||||
|
||||
class CurrentUser(BaseModel):
|
||||
"""Model pentru utilizatorul curent autentificat"""
|
||||
username: str = Field(description="Numele utilizatorului")
|
||||
user_id: Optional[int] = Field(
|
||||
default=None,
|
||||
description="ID-ul utilizatorului în baza de date"
|
||||
)
|
||||
email: Optional[EmailStr] = Field(
|
||||
default=None,
|
||||
description="Email-ul utilizatorului"
|
||||
)
|
||||
companies: List[str] = Field(
|
||||
default_factory=list,
|
||||
description="Lista codurilor firmelor la care utilizatorul are acces"
|
||||
)
|
||||
permissions: List[PermissionType] = Field(
|
||||
default_factory=lambda: [PermissionType.READ],
|
||||
description="Lista permisiunilor utilizatorului"
|
||||
)
|
||||
is_active: bool = Field(
|
||||
default=True,
|
||||
description="Dacă utilizatorul este activ"
|
||||
)
|
||||
last_login: Optional[datetime] = Field(
|
||||
default=None,
|
||||
description="Data ultimei autentificări"
|
||||
)
|
||||
|
||||
@validator('companies')
|
||||
def companies_not_empty_if_active(cls, v, values):
|
||||
"""Validează că utilizatorii activi au cel puțin o firmă"""
|
||||
if values.get('is_active', True) and not v:
|
||||
raise ValueError('Utilizatorii activi trebuie să aibă acces la cel puțin o firmă')
|
||||
return v
|
||||
|
||||
|
||||
class UserCompany(BaseModel):
|
||||
"""Model pentru o firmă la care utilizatorul are acces"""
|
||||
code: str = Field(description="Codul firmei (schema Oracle)")
|
||||
name: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Numele firmei (dacă este disponibil)"
|
||||
)
|
||||
permissions: List[PermissionType] = Field(
|
||||
default_factory=lambda: [PermissionType.READ],
|
||||
description="Permisiunile utilizatorului pentru această firmă"
|
||||
)
|
||||
is_default: bool = Field(
|
||||
default=False,
|
||||
description="Dacă aceasta este firma implicită pentru utilizator"
|
||||
)
|
||||
|
||||
|
||||
class CompanyAccessRequest(BaseModel):
|
||||
"""Model pentru verificarea accesului la o firmă"""
|
||||
company_code: str = Field(description="Codul firmei de verificat")
|
||||
required_permissions: Optional[List[PermissionType]] = Field(
|
||||
default=None,
|
||||
description="Permisiunile necesare (opțional)"
|
||||
)
|
||||
|
||||
|
||||
class CompanyAccessResponse(BaseModel):
|
||||
"""Model pentru răspunsul de verificare acces firmă"""
|
||||
has_access: bool = Field(description="Dacă utilizatorul are acces")
|
||||
company: Optional[UserCompany] = Field(
|
||||
default=None,
|
||||
description="Detaliile firmei dacă utilizatorul are acces"
|
||||
)
|
||||
missing_permissions: Optional[List[PermissionType]] = Field(
|
||||
default=None,
|
||||
description="Permisiunile lipsă (dacă aplicabil)"
|
||||
)
|
||||
|
||||
|
||||
class AuthError(BaseModel):
|
||||
"""Model pentru erorile de autentificare"""
|
||||
error: str = Field(description="Tipul erorii")
|
||||
error_description: str = Field(description="Descrierea detaliată a erorii")
|
||||
error_code: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Codul de eroare pentru procesare automată"
|
||||
)
|
||||
|
||||
|
||||
class AuthStats(BaseModel):
|
||||
"""Model pentru statisticile de autentificare"""
|
||||
total_users: int = Field(description="Numărul total de utilizatori")
|
||||
active_sessions: int = Field(description="Sesiuni active curente")
|
||||
cache_hit_ratio: float = Field(
|
||||
description="Rata de hit a cache-ului pentru date utilizatori"
|
||||
)
|
||||
last_cleanup: Optional[datetime] = Field(
|
||||
default=None,
|
||||
description="Ultima curățare a cache-ului"
|
||||
)
|
||||
|
||||
|
||||
class PasswordChangeRequest(BaseModel):
|
||||
"""Model pentru schimbarea parolei (pentru viitor)"""
|
||||
current_password: str = Field(description="Parola curentă")
|
||||
new_password: str = Field(
|
||||
min_length=8,
|
||||
description="Noua parolă (minim 8 caractere)"
|
||||
)
|
||||
confirm_password: str = Field(description="Confirmarea noii parole")
|
||||
|
||||
@validator('confirm_password')
|
||||
def passwords_match(cls, v, values):
|
||||
"""Validează că parolele coincid"""
|
||||
if 'new_password' in values and v != values['new_password']:
|
||||
raise ValueError('Parolele nu coincid')
|
||||
return v
|
||||
|
||||
|
||||
class SessionInfo(BaseModel):
|
||||
"""Model pentru informațiile despre sesiune"""
|
||||
session_id: str = Field(description="ID-ul sesiunii")
|
||||
username: str = Field(description="Numele utilizatorului")
|
||||
created_at: datetime = Field(description="Data creării sesiunii")
|
||||
last_activity: datetime = Field(description="Ultima activitate")
|
||||
ip_address: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Adresa IP a utilizatorului"
|
||||
)
|
||||
user_agent: Optional[str] = Field(
|
||||
default=None,
|
||||
description="User agent-ul browserului"
|
||||
)
|
||||
is_active: bool = Field(
|
||||
default=True,
|
||||
description="Dacă sesiunea este încă activă"
|
||||
)
|
||||
|
||||
|
||||
# Update la forward references pentru TokenResponse
|
||||
TokenResponse.model_rebuild()
|
||||
433
shared/auth/routes.py
Normal file
433
shared/auth/routes.py
Normal file
@@ -0,0 +1,433 @@
|
||||
"""
|
||||
Authentication Routes Template pentru ROA2WEB FastAPI Applications
|
||||
|
||||
Acest modul oferă rute predefinite pentru autentificare care pot fi integrate
|
||||
în orice aplicație FastAPI din ecosistemul ROA2WEB.
|
||||
|
||||
Endpoints disponibile:
|
||||
- POST /auth/login - Autentificare utilizator
|
||||
- POST /auth/refresh - Refresh access token
|
||||
- POST /auth/logout - Deconectare utilizator
|
||||
- GET /auth/me - Informații utilizator curent
|
||||
- GET /auth/companies - Firmele utilizatorului
|
||||
- GET /auth/status - Status autentificare
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request, Response
|
||||
from fastapi.security import HTTPAuthorizationCredentials
|
||||
|
||||
from .models import (
|
||||
LoginRequest, TokenResponse, RefreshTokenRequest, LogoutRequest,
|
||||
CurrentUser, UserCompany, CompanyAccessRequest, CompanyAccessResponse,
|
||||
AuthError, AuthStats
|
||||
)
|
||||
from .auth_service import auth_service, AuthenticationError
|
||||
from .jwt_handler import jwt_handler
|
||||
from .dependencies import (
|
||||
get_current_user, get_optional_user,
|
||||
security_required, security_optional
|
||||
)
|
||||
from .middleware import default_rate_limiter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_auth_router(
|
||||
prefix: str = "/auth",
|
||||
tags: Optional[List[str]] = None,
|
||||
include_admin_routes: bool = False
|
||||
) -> APIRouter:
|
||||
"""
|
||||
Creează un router FastAPI cu toate rutele de autentificare
|
||||
|
||||
Args:
|
||||
prefix: Prefix-ul pentru toate rutele
|
||||
tags: Tag-urile pentru documentația OpenAPI
|
||||
include_admin_routes: Dacă să includă rutele de administrare
|
||||
|
||||
Returns:
|
||||
Router-ul FastAPI configurat
|
||||
"""
|
||||
router = APIRouter(prefix=prefix, tags=tags or ["authentication"])
|
||||
|
||||
@router.post("/login", response_model=TokenResponse, status_code=status.HTTP_200_OK)
|
||||
async def login(
|
||||
login_data: LoginRequest,
|
||||
request: Request,
|
||||
response: Response
|
||||
) -> TokenResponse:
|
||||
"""
|
||||
Autentifică un utilizator și returnează token-urile JWT
|
||||
|
||||
Acest endpoint:
|
||||
- Validează credențialele utilizatorului în Oracle
|
||||
- Obține firmele la care utilizatorul are acces
|
||||
- Generează access și refresh token-uri JWT
|
||||
- Aplică rate limiting pentru securitate
|
||||
|
||||
Args:
|
||||
login_data: Datele de autentificare (username, password)
|
||||
request: Request-ul HTTP (pentru rate limiting)
|
||||
response: Response-ul HTTP (pentru header-e)
|
||||
|
||||
Returns:
|
||||
Token-urile JWT și informațiile utilizatorului
|
||||
|
||||
Raises:
|
||||
HTTPException: Pentru credențiale invalide sau erori de sistem
|
||||
"""
|
||||
try:
|
||||
# Log tentativa de autentificare
|
||||
client_ip = request.client.host if request.client else "unknown"
|
||||
logger.info(f"Login attempt for user {login_data.username} from IP {client_ip}")
|
||||
|
||||
# Autentifică și creează token-urile
|
||||
success, token_response, error_message = await auth_service.authenticate_and_create_tokens(
|
||||
login_data.username,
|
||||
login_data.password
|
||||
)
|
||||
|
||||
if not success:
|
||||
logger.warning(f"Failed login attempt for user {login_data.username}: {error_message}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=error_message or "Authentication failed"
|
||||
)
|
||||
|
||||
# Adaugă informațiile utilizatorului în răspuns
|
||||
companies = await auth_service.get_user_companies(login_data.username)
|
||||
current_user = CurrentUser(
|
||||
username=login_data.username,
|
||||
companies=companies,
|
||||
permissions=["read", "reports"], # Permisiuni de bază
|
||||
last_login=datetime.now()
|
||||
)
|
||||
|
||||
token_response.user = current_user
|
||||
|
||||
# Header-e de securitate
|
||||
response.headers["X-Content-Type-Options"] = "nosniff"
|
||||
response.headers["X-Frame-Options"] = "DENY"
|
||||
|
||||
logger.info(f"Successful login for user {login_data.username}")
|
||||
return token_response
|
||||
|
||||
except AuthenticationError as e:
|
||||
logger.error(f"Authentication error for user {login_data.username}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error during login for user {login_data.username}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Internal authentication error"
|
||||
)
|
||||
|
||||
@router.post("/refresh", response_model=TokenResponse, status_code=status.HTTP_200_OK)
|
||||
async def refresh_token(refresh_data: RefreshTokenRequest) -> TokenResponse:
|
||||
"""
|
||||
Reîmprospătează access token-ul folosind refresh token-ul
|
||||
|
||||
Args:
|
||||
refresh_data: Refresh token-ul valid
|
||||
|
||||
Returns:
|
||||
Noul access token și informațiile utilizatorului
|
||||
|
||||
Raises:
|
||||
HTTPException: Pentru refresh token-uri invalide
|
||||
"""
|
||||
try:
|
||||
# Validează refresh token-ul
|
||||
token_data = jwt_handler.verify_token(refresh_data.refresh_token)
|
||||
|
||||
if not token_data or token_data.token_type != "refresh":
|
||||
logger.warning("Invalid refresh token provided")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid refresh token"
|
||||
)
|
||||
|
||||
# Obține datele actualizate ale utilizatorului
|
||||
companies = await auth_service.get_user_companies(token_data.username)
|
||||
permissions = ["read", "reports"] # Poate fi extins în viitor
|
||||
|
||||
# Creează noul access token
|
||||
new_access_token = jwt_handler.create_access_token(
|
||||
username=token_data.username,
|
||||
companies=companies,
|
||||
user_id=token_data.user_id,
|
||||
permissions=permissions
|
||||
)
|
||||
|
||||
# Informațiile utilizatorului
|
||||
current_user = CurrentUser(
|
||||
username=token_data.username,
|
||||
user_id=token_data.user_id,
|
||||
companies=companies,
|
||||
permissions=permissions
|
||||
)
|
||||
|
||||
token_response = TokenResponse(
|
||||
access_token=new_access_token,
|
||||
token_type="bearer",
|
||||
expires_in=jwt_handler.access_token_expire_minutes * 60,
|
||||
user=current_user
|
||||
)
|
||||
|
||||
logger.info(f"Token refreshed for user {token_data.username}")
|
||||
return token_response
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error refreshing token: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Token refresh failed"
|
||||
)
|
||||
|
||||
@router.post("/logout", status_code=status.HTTP_200_OK)
|
||||
async def logout(
|
||||
logout_data: Optional[LogoutRequest] = None,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict:
|
||||
"""
|
||||
Deconectează utilizatorul (invalidează token-urile)
|
||||
|
||||
Note: În implementarea curentă, token-urile JWT sunt stateless,
|
||||
deci nu pot fi invalidate direct. În viitor poate fi implementat
|
||||
un blacklist pentru token-uri.
|
||||
|
||||
Args:
|
||||
logout_data: Date pentru logout (opțional)
|
||||
current_user: Utilizatorul curent autentificat
|
||||
|
||||
Returns:
|
||||
Confirmarea deconectării
|
||||
"""
|
||||
logger.info(f"User {current_user.username} logged out")
|
||||
|
||||
# În viitor, aici se poate implementa:
|
||||
# - Adăugarea token-ului într-un blacklist
|
||||
# - Invalidarea tuturor sesiunilor utilizatorului
|
||||
# - Notificări de securitate
|
||||
|
||||
return {
|
||||
"message": "Successfully logged out",
|
||||
"username": current_user.username,
|
||||
"logout_time": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
@router.get("/me", response_model=CurrentUser)
|
||||
async def get_current_user_info(
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
) -> CurrentUser:
|
||||
"""
|
||||
Returnează informațiile despre utilizatorul curent
|
||||
|
||||
Args:
|
||||
current_user: Utilizatorul curent autentificat
|
||||
|
||||
Returns:
|
||||
Informațiile complete ale utilizatorului
|
||||
"""
|
||||
logger.debug(f"User info requested for {current_user.username}")
|
||||
return current_user
|
||||
|
||||
@router.get("/companies", response_model=List[UserCompany])
|
||||
async def get_user_companies(
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
) -> List[UserCompany]:
|
||||
"""
|
||||
Returnează lista firmelor la care utilizatorul are acces
|
||||
|
||||
Args:
|
||||
current_user: Utilizatorul curent autentificat
|
||||
|
||||
Returns:
|
||||
Lista firmelor cu permisiunile asociate
|
||||
"""
|
||||
try:
|
||||
# Obține firmele actualizate din baza de date
|
||||
companies = await auth_service.get_user_companies(current_user.username)
|
||||
|
||||
user_companies = []
|
||||
for i, company_code in enumerate(companies):
|
||||
# Obține permisiunile pentru fiecare firmă
|
||||
permissions = await auth_service.get_user_permissions(
|
||||
current_user.username,
|
||||
company_code
|
||||
)
|
||||
|
||||
user_company = UserCompany(
|
||||
code=company_code,
|
||||
permissions=permissions,
|
||||
is_default=(i == 0) # Prima firmă ca default
|
||||
)
|
||||
user_companies.append(user_company)
|
||||
|
||||
logger.debug(f"Returned {len(user_companies)} companies for user {current_user.username}")
|
||||
return user_companies
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting companies for user {current_user.username}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Error retrieving user companies"
|
||||
)
|
||||
|
||||
@router.post("/check-company-access", response_model=CompanyAccessResponse)
|
||||
async def check_company_access(
|
||||
access_request: CompanyAccessRequest,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
) -> CompanyAccessResponse:
|
||||
"""
|
||||
Verifică dacă utilizatorul are acces la o firmă specifică
|
||||
|
||||
Args:
|
||||
access_request: Request-ul de verificare acces
|
||||
current_user: Utilizatorul curent autentificat
|
||||
|
||||
Returns:
|
||||
Răspunsul cu informații despre acces
|
||||
"""
|
||||
try:
|
||||
has_access = await auth_service.validate_user_company_access(
|
||||
current_user.username,
|
||||
access_request.company_code
|
||||
)
|
||||
|
||||
if not has_access:
|
||||
return CompanyAccessResponse(
|
||||
has_access=False,
|
||||
company=None,
|
||||
missing_permissions=None
|
||||
)
|
||||
|
||||
# Obține permisiunile pentru firmă
|
||||
permissions = await auth_service.get_user_permissions(
|
||||
current_user.username,
|
||||
access_request.company_code
|
||||
)
|
||||
|
||||
# Verifică permisiunile cerute
|
||||
missing_permissions = []
|
||||
if access_request.required_permissions:
|
||||
missing_permissions = [
|
||||
perm for perm in access_request.required_permissions
|
||||
if perm not in permissions
|
||||
]
|
||||
|
||||
user_company = UserCompany(
|
||||
code=access_request.company_code,
|
||||
permissions=permissions
|
||||
)
|
||||
|
||||
return CompanyAccessResponse(
|
||||
has_access=True,
|
||||
company=user_company,
|
||||
missing_permissions=missing_permissions if missing_permissions else None
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking company access: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Error checking company access"
|
||||
)
|
||||
|
||||
@router.get("/status")
|
||||
async def get_auth_status(
|
||||
current_user: Optional[CurrentUser] = Depends(get_optional_user)
|
||||
) -> dict:
|
||||
"""
|
||||
Returnează statusul de autentificare (endpoint public)
|
||||
|
||||
Args:
|
||||
current_user: Utilizatorul curent (opțional)
|
||||
|
||||
Returns:
|
||||
Statusul de autentificare
|
||||
"""
|
||||
if current_user:
|
||||
return {
|
||||
"authenticated": True,
|
||||
"username": current_user.username,
|
||||
"companies_count": len(current_user.companies),
|
||||
"permissions": current_user.permissions
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"authenticated": False,
|
||||
"username": None,
|
||||
"companies_count": 0,
|
||||
"permissions": []
|
||||
}
|
||||
|
||||
# Rute de administrare (opționale)
|
||||
if include_admin_routes:
|
||||
|
||||
@router.get("/admin/stats", response_model=AuthStats)
|
||||
async def get_auth_stats(
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
) -> AuthStats:
|
||||
"""
|
||||
Returnează statistici despre sistemul de autentificare
|
||||
|
||||
Necesită permisiuni de admin.
|
||||
"""
|
||||
# Verifică permisiuni admin
|
||||
if "admin" not in current_user.permissions:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Admin permissions required"
|
||||
)
|
||||
|
||||
cache_stats = auth_service.get_cache_stats()
|
||||
|
||||
return AuthStats(
|
||||
total_users=1, # Placeholder - poate fi implementat
|
||||
active_sessions=1, # Placeholder - poate fi implementat
|
||||
cache_hit_ratio=cache_stats.get('cache_hit_ratio', 0),
|
||||
last_cleanup=datetime.now()
|
||||
)
|
||||
|
||||
@router.post("/admin/refresh-cache")
|
||||
async def refresh_user_cache(
|
||||
username: Optional[str] = None,
|
||||
current_user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict:
|
||||
"""
|
||||
Reîmprospătează cache-ul utilizatorilor
|
||||
|
||||
Necesită permisiuni de admin.
|
||||
"""
|
||||
if "admin" not in current_user.permissions:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Admin permissions required"
|
||||
)
|
||||
|
||||
if username:
|
||||
success = await auth_service.refresh_user_data(username)
|
||||
return {
|
||||
"message": f"Cache refreshed for user {username}",
|
||||
"success": success
|
||||
}
|
||||
else:
|
||||
auth_service.clear_cache()
|
||||
return {"message": "All user cache cleared"}
|
||||
|
||||
return router
|
||||
|
||||
|
||||
# Router implicit pentru folosire rapidă
|
||||
auth_router = create_auth_router()
|
||||
|
||||
# Router cu rute de admin incluse
|
||||
auth_router_with_admin = create_auth_router(include_admin_routes=True)
|
||||
Reference in New Issue
Block a user