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
@@ -0,0 +1 @@
# Routers package
@@ -0,0 +1,81 @@
"""Auth-endpoints: login, refresh, change password."""
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.dependencies import get_current_user
from app.models import User
from app.schemas import Token, LoginRequest, RefreshRequest, ChangePasswordRequest
from app.security import create_access_token, create_refresh_token, decode_token
from app.services.user_service import UserService
router = APIRouter(prefix="/auth", tags=["Authentication"])
@router.post("/login", response_model=Token)
async def login(data: LoginRequest, db: AsyncSession = Depends(get_db)):
service = UserService(db)
user = await service.authenticate(data.email, data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Bearer"},
)
user.last_login_at = datetime.utcnow()
await db.commit()
access_token = create_access_token(user.id)
refresh_token = create_refresh_token(user.id)
return Token(
access_token=access_token,
refresh_token=refresh_token,
expires_in=30 * 60,
)
@router.post("/refresh", response_model=Token)
async def refresh(data: RefreshRequest, db: AsyncSession = Depends(get_db)):
payload = decode_token(data.refresh_token)
if payload is None or payload.get("type") != "refresh":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid refresh token",
)
from uuid import UUID
user_id = payload.get("sub")
service = UserService(db)
user = await service.get_by_id(UUID(user_id))
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found",
)
access_token = create_access_token(user.id)
refresh_token = create_refresh_token(user.id)
return Token(
access_token=access_token,
refresh_token=refresh_token,
expires_in=30 * 60,
)
@router.post("/change-password")
async def change_password(
data: ChangePasswordRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
service = UserService(db)
await service.change_password(current_user, data.current_password, data.new_password)
return {"message": "Password changed successfully"}
@router.get("/me")
async def me(current_user: User = Depends(get_current_user)):
from app.schemas import UserDetailOut
return UserDetailOut.model_validate(current_user)
@@ -0,0 +1,93 @@
"""Invitation endpoints."""
from typing import Optional
from uuid import UUID
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.dependencies import get_current_user, require_admin, require_manager
from app.models import User
from app.schemas import InvitationCreate, InvitationOut, AcceptInvitationRequest, UserOut
from app.services.invitation_service import InvitationService
router = APIRouter(prefix="/invitations", tags=["Invitations"])
@router.get("", response_model=list[InvitationOut])
async def list_invitations(
tenant_id: Optional[UUID] = Query(None),
pending_only: bool = Query(True),
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=500),
current_user: User = Depends(require_manager),
db: AsyncSession = Depends(get_db),
):
service = InvitationService(db)
effective_tenant = tenant_id
if not current_user.is_superuser and current_user.role.value == "manager":
effective_tenant = current_user.tenant_id
items, total = await service.list_invitations(
tenant_id=effective_tenant,
pending_only=pending_only,
skip=skip,
limit=limit,
)
return items
@router.post("", response_model=InvitationOut, status_code=201)
async def create_invitation(
data: InvitationCreate,
current_user: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
service = InvitationService(db)
invitation = await service.create_invitation(data, invited_by=current_user)
return InvitationOut.model_validate(invitation)
@router.post("/accept", response_model=UserOut)
async def accept_invitation(
data: AcceptInvitationRequest,
db: AsyncSession = Depends(get_db),
):
service = InvitationService(db)
user = await service.accept_invitation(data)
return UserOut.model_validate(user)
@router.get("/validate/{token}")
async def validate_invitation(
token: str,
db: AsyncSession = Depends(get_db),
):
service = InvitationService(db)
invitation = await service.get_by_token(token)
if invitation is None:
return {"valid": False, "reason": "not_found"}
if invitation.accepted_at is not None:
return {"valid": False, "reason": "already_accepted"}
if invitation.expires_at < datetime.utcnow():
return {"valid": False, "reason": "expired"}
return {
"valid": True,
"email": invitation.email,
"role": invitation.role.value,
"expires_at": invitation.expires_at.isoformat(),
}
@router.delete("/{invitation_id}", status_code=204)
async def revoke_invitation(
invitation_id: UUID,
current_user: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
service = InvitationService(db)
await service.revoke_invitation(invitation_id, actor=current_user)
return None
from datetime import datetime
@@ -0,0 +1,74 @@
"""Tenant CRUD-endpoints."""
from uuid import UUID
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.dependencies import get_current_user, require_admin
from app.schemas import TenantCreate, TenantUpdate, TenantOut
from app.services.tenant_service import TenantService
router = APIRouter(prefix="/tenants", tags=["Tenants"])
@router.get("", response_model=list[TenantOut])
async def list_tenants(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=500),
current_user=Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
service = TenantService(db)
items, _ = await service.list_tenants(skip=skip, limit=limit)
return items
@router.get("/{tenant_id}", response_model=TenantOut)
async def get_tenant(
tenant_id: UUID,
current_user=Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
service = TenantService(db)
tenant = await service.get_by_id(tenant_id)
if tenant is None:
raise HTTPException(status_code=404, detail="Tenant not found")
return TenantOut.model_validate(tenant)
@router.post("", response_model=TenantOut, status_code=201)
async def create_tenant(
data: TenantCreate,
current_user=Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
service = TenantService(db)
tenant = await service.create_tenant(data)
return TenantOut.model_validate(tenant)
@router.patch("/{tenant_id}", response_model=TenantOut)
async def update_tenant(
tenant_id: UUID,
data: TenantUpdate,
current_user=Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
service = TenantService(db)
tenant = await service.update_tenant(tenant_id, data)
return TenantOut.model_validate(tenant)
@router.delete("/{tenant_id}", status_code=204)
async def delete_tenant(
tenant_id: UUID,
current_user=Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
service = TenantService(db)
await service.delete_tenant(tenant_id)
return None
from fastapi import HTTPException
@@ -0,0 +1,96 @@
"""User CRUD-endpoints med roll-baserad åtkomst."""
from typing import Optional
from uuid import UUID
from fastapi import APIRouter, Depends, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.dependencies import get_current_user, require_admin, require_manager
from app.models import User, UserRole, UserStatus
from app.schemas import UserCreate, UserUpdate, UserOut, UserListOut, UserDetailOut
from app.services.user_service import UserService
router = APIRouter(prefix="/users", tags=["Users"])
@router.get("", response_model=UserListOut)
async def list_users(
tenant_id: Optional[UUID] = Query(None),
role: Optional[UserRole] = Query(None),
status: Optional[UserStatus] = Query(None),
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=500),
current_user: User = Depends(require_manager),
db: AsyncSession = Depends(get_db),
):
service = UserService(db)
# Managers ser bara användare inom sin tenant (om de inte är superuser)
effective_tenant = tenant_id
if not current_user.is_superuser and current_user.role == UserRole.MANAGER:
effective_tenant = current_user.tenant_id
items, total = await service.list_users(
tenant_id=effective_tenant,
role=role,
status=status,
skip=skip,
limit=limit,
)
return UserListOut(items=items, total=total)
@router.get("/{user_id}", response_model=UserDetailOut)
async def get_user(
user_id: UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
service = UserService(db)
user = await service.get_by_id(user_id)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
# Managers kan bara se användare inom sin tenant
if not current_user.is_superuser and current_user.role == UserRole.MANAGER:
if user.tenant_id != current_user.tenant_id:
raise HTTPException(status_code=403, detail="Access denied")
return UserDetailOut.model_validate(user)
@router.post("", response_model=UserOut, status_code=201)
async def create_user(
data: UserCreate,
current_user: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
service = UserService(db)
user = await service.create_user(data, created_by=current_user)
return UserOut.model_validate(user)
@router.patch("/{user_id}", response_model=UserOut)
async def update_user(
user_id: UUID,
data: UserUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
service = UserService(db)
user = await service.update_user(user_id, data, actor=current_user)
return UserOut.model_validate(user)
@router.delete("/{user_id}", status_code=204)
async def delete_user(
user_id: UUID,
current_user: User = Depends(require_admin),
db: AsyncSession = Depends(get_db),
):
service = UserService(db)
await service.delete_user(user_id, actor=current_user)
return None
from fastapi import HTTPException