landvex: Fixar och tester klara för alla komponenter

- Datafabrik: Dockerfile fix, agentorkestrering fungerar
- Vision: Identify-modell, FAISS, OCR alla testade
- API: Alla 7 integrationstester passerade
- Upplösare: Entitetsupplösning verifierad
This commit is contained in:
Bernt
2026-07-05 06:41:32 +00:00
parent f4f853d94b
commit aee0f09db8
19583 changed files with 1450867 additions and 1153 deletions
+53
View File
@@ -0,0 +1,53 @@
"""
LandveX Admin Backend — Configuration
"""
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import List, Optional
class Settings(BaseSettings):
"""Application settings loaded from environment variables."""
model_config = SettingsConfigDict(
extra="allow",
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=True
)
# Application
APP_NAME: str = "LandveX Admin Backend"
APP_VERSION: str = "1.0.0"
DEBUG: bool = False
# Database
DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/landvex_admin"
DATABASE_POOL_SIZE: int = 20
DATABASE_MAX_OVERFLOW: int = 10
# Schema isolation
DEFAULT_TENANT_SCHEMA: str = "public"
TENANT_SCHEMA_PREFIX: str = "tenant_"
# Security
SECRET_KEY: str = "change-me-in-production-landvex-secret-key-2026"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60
REFRESH_TOKEN_EXPIRE_DAYS: int = 7
ALGORITHM: str = "HS256"
# CORS
CORS_ORIGINS: List[str] = ["*"]
CORS_ALLOW_CREDENTIALS: bool = True
CORS_ALLOW_METHODS: List[str] = ["*"]
CORS_ALLOW_HEADERS: List[str] = ["*"]
# Audit logging
AUDIT_LOG_TABLE: str = "audit_logs"
AUDIT_RETENTION_DAYS: int = 365
# Pagination
DEFAULT_PAGE_SIZE: int = 20
MAX_PAGE_SIZE: int = 100
settings = Settings()
@@ -0,0 +1,100 @@
"""
LandveX Admin Backend — Custom Exceptions
"""
from fastapi import HTTPException, status
from typing import Optional, Dict, Any
class LandvexException(Exception):
"""Base exception for LandveX application."""
def __init__(
self,
message: str,
status_code: int = 500,
details: Optional[Dict[str, Any]] = None
):
self.message = message
self.status_code = status_code
self.details = details or {}
super().__init__(self.message)
class TenantNotFoundException(LandvexException):
"""Raised when a tenant is not found."""
def __init__(self, tenant_id: str):
super().__init__(
message=f"Tenant not found: {tenant_id}",
status_code=status.HTTP_404_NOT_FOUND,
details={"tenant_id": tenant_id}
)
class TenantAlreadyExistsException(LandvexException):
"""Raised when attempting to create a tenant that already exists."""
def __init__(self, identifier: str, field: str = "slug"):
super().__init__(
message=f"Tenant with {field} '{identifier}' already exists",
status_code=status.HTTP_409_CONFLICT,
details={"field": field, "value": identifier}
)
class TenantValidationException(LandvexException):
"""Raised when tenant data validation fails."""
def __init__(self, message: str, field: Optional[str] = None):
details = {}
if field:
details["field"] = field
super().__init__(
message=message,
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
details=details
)
class TenantSchemaException(LandvexException):
"""Raised when tenant schema operations fail."""
def __init__(self, tenant_id: str, operation: str, original_error: str):
super().__init__(
message=f"Schema operation '{operation}' failed for tenant {tenant_id}: {original_error}",
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
details={"tenant_id": tenant_id, "operation": operation}
)
class UnauthorizedTenantAccessException(LandvexException):
"""Raised when a user tries to access a tenant they don't have permission for."""
def __init__(self, tenant_id: str, user_id: str):
super().__init__(
message=f"User {user_id} is not authorized to access tenant {tenant_id}",
status_code=status.HTTP_403_FORBIDDEN,
details={"tenant_id": tenant_id, "user_id": user_id}
)
class AuditLogException(LandvexException):
"""Raised when audit logging fails."""
def __init__(self, operation: str, original_error: str):
super().__init__(
message=f"Audit logging failed for operation '{operation}': {original_error}",
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
details={"operation": operation}
)
def http_exception_handler(exc: LandvexException) -> HTTPException:
"""Convert LandvexException to FastAPI HTTPException."""
return HTTPException(
status_code=exc.status_code,
detail={
"error": exc.message,
"details": exc.details
}
)
+130
View File
@@ -0,0 +1,130 @@
"""
LandveX Admin Backend — Security utilities
JWT tokens, password hashing, and auth helpers
"""
from datetime import datetime, timedelta, timezone
from typing import Optional, Dict, Any
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
security = HTTPBearer(auto_error=False)
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a plain password against a hashed password."""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""Hash a password."""
return pwd_context.hash(password)
def create_access_token(
data: Dict[str, Any],
expires_delta: Optional[timedelta] = None
) -> str:
"""Create a JWT access token."""
to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire, "type": "access"})
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
def create_refresh_token(data: Dict[str, Any]) -> str:
"""Create a JWT refresh token."""
to_encode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
to_encode.update({"exp": expire, "type": "refresh"})
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
def decode_token(token: str) -> Optional[Dict[str, Any]]:
"""Decode and validate a JWT token."""
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
return payload
except JWTError:
return None
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security)
) -> Dict[str, Any]:
"""Get the current authenticated user from the JWT token."""
if credentials is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required",
headers={"WWW-Authenticate": "Bearer"},
)
payload = decode_token(credentials.credentials)
if payload is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
headers={"WWW-Authenticate": "Bearer"},
)
user_id = payload.get("sub")
if user_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token payload",
headers={"WWW-Authenticate": "Bearer"},
)
return {
"id": user_id,
"email": payload.get("email"),
"role": payload.get("role", "user"),
"tenant_id": payload.get("tenant_id"),
"permissions": payload.get("permissions", []),
}
async def require_admin(
current_user: Dict[str, Any] = Depends(get_current_user)
) -> Dict[str, Any]:
"""Require admin role."""
if current_user.get("role") != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin access required"
)
return current_user
async def require_tenant_access(
tenant_id: str,
current_user: Dict[str, Any] = Depends(get_current_user)
) -> Dict[str, Any]:
"""Require access to a specific tenant."""
user_role = current_user.get("role", "")
user_tenant = current_user.get("tenant_id")
# Admin can access all tenants
if user_role == "admin":
return current_user
# Users can only access their own tenant
if user_tenant != tenant_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Access denied for tenant {tenant_id}"
)
return current_user