"""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 pydantic import BaseModel, EmailStr 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 from app.models.tenant import Tenant router = APIRouter(prefix="/api/v1/guest", tags=["guest-auth"]) settings = get_settings() class GuestLoginRequest(BaseModel): """Schema for guest login request.""" email: EmailStr password: str tenant_slug: str @router.post("/login") async def guest_login( request: Request, body: GuestLoginRequest, db: AsyncSession = Depends(get_db), ): """Guest login with email+password. Sets guest session cookie.""" email = body.email password = body.password tenant_slug = body.tenant_slug # Find guest user by email — tenant_slug is required to prevent cross-tenant enumeration 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 # 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"), }