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 @@
|
||||
# Routers package
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
Reference in New Issue
Block a user