Files
leocrm/app/routes/guest_auth.py
T

182 lines
5.6 KiB
Python

"""Guest Auth routes — login, logout for guest users."""
from __future__ import annotations
import uuid
from datetime import UTC, datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
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, verify_password
from app.core.db import get_db
from app.deps import get_current_guest
from app.models.guest_user import GuestUser
router = APIRouter(prefix="/api/v1/guest", tags=["guest-auth"])
settings = get_settings()
@router.post("/login")
async def guest_login(
request: Request,
body: dict,
db: AsyncSession = Depends(get_db),
):
"""Guest login with email+password. Sets guest session cookie."""
email = body.get("email", "")
password = body.get("password", "")
tenant_slug = body.get("tenant_slug", "")
if not email or not password:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail={"detail": "Email and password required", "code": "missing_fields"},
)
# Find guest user by email
from app.models.tenant import Tenant
if tenant_slug:
tenant_q = await db.execute(
select(Tenant).where(Tenant.slug == tenant_slug)
)
tenant = tenant_q.scalar_one_or_none()
if not tenant:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={"detail": "Invalid credentials", "code": "invalid_credentials"},
)
tenant_id = tenant.id
else:
# Try to find guest by email across all tenants (less secure but simpler)
guest_q = await db.execute(
select(GuestUser).where(GuestUser.email == email).where(GuestUser.status == "active")
)
guest = guest_q.scalar_one_or_none()
if not guest:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={"detail": "Invalid credentials", "code": "invalid_credentials"},
)
tenant_id = guest.tenant_id
# Find guest with tenant context
guest_q = await db.execute(
select(GuestUser)
.where(GuestUser.email == email)
.where(GuestUser.tenant_id == tenant_id)
.where(GuestUser.status == "active")
)
guest = guest_q.scalar_one_or_none()
if not guest or not guest.password_hash:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={"detail": "Invalid credentials", "code": "invalid_credentials"},
)
if not verify_password(password, guest.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={"detail": "Invalid credentials", "code": "invalid_credentials"},
)
# Check expiration
if guest.expires_at and guest.expires_at < datetime.now(UTC):
guest.status = "expired"
await db.commit()
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail={"detail": "Guest access expired", "code": "guest_expired"},
)
# Create guest session in Redis
redis = get_redis()
session_id = str(uuid.uuid4())
csrf_token = str(uuid.uuid4())
session_data = {
"guest_user_id": str(guest.id),
"tenant_id": str(tenant_id),
"email": guest.email,
"name": guest.name,
"csrf_token": csrf_token,
"is_guest": True,
}
import json
await redis.setex(
f"guest_session:{session_id}",
1800, # 30 min TTL
json.dumps(session_data),
)
# Track session in guest index for revocation (P1.6 fix)
await redis.sadd(f"guest_sessions:{guest.id}", session_id)
await redis.expire(f"guest_sessions:{guest.id}", 1800)
from fastapi.responses import JSONResponse
resp = JSONResponse(
status_code=status.HTTP_200_OK,
content={
"guest_user_id": str(guest.id),
"email": guest.email,
"name": guest.name,
"tenant_id": str(tenant_id),
"csrf_token": csrf_token,
},
)
resp.set_cookie(
key="guest_session",
value=session_id,
httponly=True,
secure=settings.session_cookie_secure,
samesite=settings.session_cookie_samesite,
max_age=1800,
path="/",
)
return resp
@router.post("/logout")
async def guest_logout(
request: Request,
):
"""Logout — invalidate guest session, clear cookie."""
session_id = request.cookies.get("guest_session")
if session_id:
redis = get_redis()
# Remove from guest sessions index (P1.6 fix)
guest_data = await redis.get(f"guest_session:{session_id}")
if guest_data:
import json
data = json.loads(guest_data)
gid = data.get("guest_user_id")
if gid:
await redis.srem(f"guest_sessions:{gid}", session_id)
await redis.delete(f"guest_session:{session_id}")
from fastapi.responses import JSONResponse
resp = JSONResponse(
status_code=status.HTTP_200_OK,
content={"message": "Logged out"},
)
resp.delete_cookie("guest_session", path="/")
return resp
@router.get("/me")
async def guest_me(
current_guest: dict = Depends(get_current_guest),
):
"""Get current guest user info."""
return {
"guest_user_id": current_guest.get("guest_user_id"),
"email": current_guest.get("email"),
"name": current_guest.get("name"),
"tenant_id": current_guest.get("tenant_id"),
}