Files
leocrm/app/routes/guests.py
T

203 lines
6.1 KiB
Python

"""Guest management routes — invite, list, delete guests (admin only).
⚠️ Guest-System umgebaut — Guests sind jetzt reguläre User mit role=guest
Guests are now regular users with role='guest' in user_tenants. They authenticate
via the normal login flow and are managed through the standard user system.
This router provides admin endpoints for inviting and managing guest users.
"""
from __future__ import annotations
import hashlib
import secrets
import uuid
from fastapi import APIRouter, Depends, HTTPException, Request, status
from pydantic import BaseModel, EmailStr, Field
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import get_settings
from app.core.auth import get_redis, hash_password
from app.core.db import get_db
from app.deps import require_admin
from app.models.user import User, UserTenant
router = APIRouter(prefix="/api/v1/guests", tags=["guests"])
settings = get_settings()
class GuestInviteRequest(BaseModel):
"""Invite a guest user."""
email: EmailStr
name: str = Field(..., min_length=1, max_length=200)
def _hash_token(token: str) -> str:
"""Hash a token using SHA-256."""
return hashlib.sha256(token.encode()).hexdigest()
@router.post("/invite")
async def invite_guest(
request: Request,
body: GuestInviteRequest,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin),
):
"""Invite a guest user. Admin only. Creates a regular user with role='guest'.
⚠️ Guest-System umgebaut — Guests sind jetzt reguläre User mit role=guest
"""
email = body.email
name = body.name
tenant_id = uuid.UUID(current_user["tenant_id"])
# Check if user already exists by email
user_q = await db.execute(
select(User).where(User.email == email)
)
existing_user = user_q.scalar_one_or_none()
if existing_user:
# Check if already a member of this tenant
ut_q = await db.execute(
select(UserTenant).where(
UserTenant.user_id == existing_user.id,
UserTenant.tenant_id == tenant_id,
)
)
existing_ut = ut_q.scalar_one_or_none()
if existing_ut:
if existing_ut.status == "active" and existing_ut.role == "guest":
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail={"detail": "Guest already active", "code": "guest_exists"},
)
# Re-invite: update existing membership
existing_ut.role = "guest"
existing_ut.status = "invited"
await db.flush()
else:
# Create new tenant membership with guest role
ut = UserTenant(
user_id=existing_user.id,
tenant_id=tenant_id,
is_default=False,
role="guest",
status="invited",
)
db.add(ut)
await db.flush()
else:
# Create new user with a random password (will be set on acceptance)
raw_token = secrets.token_urlsafe(32)
new_user = User(
email=email,
name=name,
password_hash=hash_password(raw_token), # Temporary password
is_active=True,
)
db.add(new_user)
await db.flush()
# Create tenant membership with guest role
ut = UserTenant(
user_id=new_user.id,
tenant_id=tenant_id,
is_default=False,
role="guest",
status="invited",
)
db.add(ut)
await db.commit()
await db.refresh(new_user)
return {
"email": email,
"name": name,
"status": "invited",
"role": "guest",
"message": "Guest invited — they can now log in via the normal login flow",
}
@router.get("")
async def list_guests(
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin),
):
"""List all guest users for the current tenant."""
tenant_id = uuid.UUID(current_user["tenant_id"])
# Query user_tenants with role='guest' and join users
result = await db.execute(
select(UserTenant, User)
.join(User, UserTenant.user_id == User.id)
.where(UserTenant.tenant_id == tenant_id)
.where(UserTenant.role == "guest")
.order_by(UserTenant.created_at.desc())
)
rows = result.all()
return [
{
"id": str(ut.user_id),
"email": user.email,
"name": user.name,
"status": ut.status,
"role": "guest",
"created_at": ut.created_at.isoformat() if ut.created_at else None,
}
for ut, user in rows
]
@router.delete("/{guest_id}")
async def delete_guest(
guest_id: str,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(require_admin),
):
"""Revoke a guest user's tenant membership and invalidate sessions."""
tenant_id = uuid.UUID(current_user["tenant_id"])
try:
gid = uuid.UUID(guest_id)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"detail": "Invalid guest ID", "code": "invalid_id"},
) from None
# Find the user_tenants entry for this guest
ut_q = await db.execute(
select(UserTenant)
.where(UserTenant.user_id == gid)
.where(UserTenant.tenant_id == tenant_id)
.where(UserTenant.role == "guest")
)
ut = ut_q.scalar_one_or_none()
if not ut:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail={"detail": "Guest not found", "code": "not_found"},
)
# Revoke: set status to disabled
ut.status = "disabled"
await db.commit()
# Invalidate all active sessions for this user
redis = get_redis()
if redis:
try:
from app.core.auth import invalidate_all_user_sessions
await invalidate_all_user_sessions(redis, gid)
except Exception:
pass
return {"message": "Guest revoked, all sessions invalidated", "status": "disabled"}