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:
@@ -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
|
||||
}
|
||||
@@ -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"))
|
||||
Reference in New Issue
Block a user