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
+145
View File
@@ -0,0 +1,145 @@
"""
quiXzoom Academy API endpoints for LandveX Admin
"""
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime
from app.core.security import get_current_user
from app.models.user import User
router = APIRouter(prefix="/academy", tags=["academy"])
# Models
class LessonCompleteRequest(BaseModel):
lesson_id: str
module_id: str
score: Optional[int] = None
class ProgressResponse(BaseModel):
user_id: str
completed_lessons: List[str]
current_module: int
total_progress: float
certificate_earned: bool
certificate_date: Optional[str] = None
class CertificateResponse(BaseModel):
pdf_url: str
verification_code: str
issue_date: str
# In-memory storage (replace with DB in production)
academy_progress = {}
LESSONS_PER_MODULE = {
"module1": 4,
"module2": 4,
"module3": 3,
"module4": 3,
"module5": 3,
"module6": 4
}
TOTAL_LESSONS = sum(LESSONS_PER_MODULE.values())
@router.get("/progress", response_model=ProgressResponse)
async def get_progress(current_user: User = Depends(get_current_user)):
"""Get user's academy progress"""
user_id = str(current_user.id)
progress = academy_progress.get(user_id, {
"completed_lessons": [],
"current_module": 1
})
completed = len(progress["completed_lessons"])
total_progress = (completed / TOTAL_LESSONS) * 100
certificate_earned = completed >= TOTAL_LESSONS
return ProgressResponse(
user_id=user_id,
completed_lessons=progress["completed_lessons"],
current_module=progress["current_module"],
total_progress=round(total_progress, 1),
certificate_earned=certificate_earned,
certificate_date=datetime.utcnow().isoformat() if certificate_earned else None
)
@router.post("/complete", status_code=status.HTTP_200_OK)
async def complete_lesson(
request: LessonCompleteRequest,
current_user: User = Depends(get_current_user)
):
"""Mark a lesson as completed"""
user_id = str(current_user.id)
if user_id not in academy_progress:
academy_progress[user_id] = {
"completed_lessons": [],
"current_module": 1
}
progress = academy_progress[user_id]
if request.lesson_id not in progress["completed_lessons"]:
progress["completed_lessons"].append(request.lesson_id)
# Update current module
module_num = int(request.module_id.replace("module", ""))
module_lessons = [f"m{module_num}l{i}" for i in range(1, LESSONS_PER_MODULE[request.module_id] + 1)]
if all(l in progress["completed_lessons"] for l in module_lessons):
progress["current_module"] = min(module_num + 1, 6)
return {
"success": True,
"lesson_id": request.lesson_id,
"progress": len(progress["completed_lessons"]) / TOTAL_LESSONS * 100
}
@router.get("/certificate", response_model=CertificateResponse)
async def get_certificate(current_user: User = Depends(get_current_user)):
"""Generate certificate PDF"""
user_id = str(current_user.id)
progress = academy_progress.get(user_id, {"completed_lessons": []})
if len(progress["completed_lessons"]) < TOTAL_LESSONS:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Certificate not yet earned. Complete all lessons first."
)
# Generate verification code
verification = f"QX-{user_id[:8].upper()}-{datetime.utcnow().strftime('%Y%m%d')}"
return CertificateResponse(
pdf_url=f"/api/academy/certificate/{user_id}/download",
verification_code=verification,
issue_date=datetime.utcnow().isoformat()
)
@router.get("/stats")
async def get_academy_stats(current_user: User = Depends(get_current_user)):
"""Get academy statistics (admin only)"""
if not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin access required"
)
total_users = len(academy_progress)
completed_users = sum(
1 for p in academy_progress.values()
if len(p["completed_lessons"]) >= TOTAL_LESSONS
)
return {
"total_enrolled": total_users,
"completed_all": completed_users,
"completion_rate": round(completed_users / total_users * 100, 1) if total_users > 0 else 0,
"average_progress": round(
sum(len(p["completed_lessons"]) for p in academy_progress.values()) / total_users / TOTAL_LESSONS * 100, 1
) if total_users > 0 else 0
}
+83
View File
@@ -0,0 +1,83 @@
"""API dependencies for authentication, authorization, and tenant resolution."""
from typing import Annotated
from uuid import UUID
from fastapi import Depends, HTTPException, Header, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
from app.database import get_db
from app.core.security import decode_token
from app.models.user import User
from app.models.tenant import Tenant
from app.core.permissions import has_permission
security = HTTPBearer()
async def get_current_user(
credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)],
db: Session = Depends(get_db),
) -> User:
"""Validate JWT and return current user."""
try:
payload = decode_token(credentials.credentials)
user_id = UUID(payload["sub"])
except Exception:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
user = db.query(User).filter(User.id == user_id).first()
if not user or user.status != "active":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found or inactive",
)
return user
async def get_current_tenant(
x_tenant_id: Annotated[UUID, Header(..., alias="X-Tenant-ID")],
user: User = Depends(get_current_user),
) -> Tenant:
"""Validate user has access to the specified tenant."""
if user.tenant_id != x_tenant_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to this tenant",
)
return user.tenant
class RequirePermission:
"""Dependency factory for permission checking."""
def __init__(self, permission: str):
self.permission = permission
async def __call__(self, user: User = Depends(get_current_user)) -> User:
user_perms = set()
for role in user.roles:
user_perms.update(role.permissions)
if not has_permission(user_perms, self.permission):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Missing permission: {self.permission}",
)
return user
# Common permission dependencies
RequireAdmin = Depends(RequirePermission("users.update"))
RequireUserRead = Depends(RequirePermission("users.read"))
RequireTenantManage = Depends(RequirePermission("tenants.manage"))
RequireBillingRead = Depends(RequirePermission("billing.read"))
RequireAuditRead = Depends(RequirePermission("audit.read"))
RequireReportCreate = Depends(RequirePermission("reports.create"))
RequireApiKeyCreate = Depends(RequirePermission("api_keys.create"))