import os import logging import aiohttp import base64 from fastapi import APIRouter, HTTPException, Header, Body from pydantic import BaseModel from datetime import datetime from typing import Optional router = APIRouter() # 🔧 Supabase Config SUPABASE_URL = "https://ussxqnifefkgkaumjann.supabase.co" SUPABASE_KEY = os.getenv("SUPA_KEY") SUPABASE_ROLE_KEY = os.getenv("SUPA_SERVICE_KEY") if not SUPABASE_KEY or not SUPABASE_ROLE_KEY: raise ValueError("❌ SUPA_KEY or SUPA_SERVICE_KEY not set in environment!") SUPABASE_HEADERS = { "apikey": SUPABASE_KEY, "Authorization": f"Bearer {SUPABASE_KEY}", "Content-Type": "application/json" } SUPABASE_ROLE_HEADERS = { "apikey": SUPABASE_ROLE_KEY, "Authorization": f"Bearer {SUPABASE_ROLE_KEY}", "Content-Type": "application/json", "Prefer": "return=representation" } # 🛡️ Verificação de token de usuário (sem admin check) async def verify_user_token(user_token: str) -> str: headers = { "Authorization": f"Bearer {user_token}", "apikey": SUPABASE_KEY, "Content-Type": "application/json" } async with aiohttp.ClientSession() as session: async with session.get(f"{SUPABASE_URL}/auth/v1/user", headers=headers) as response: if response.status != 200: raise HTTPException(status_code=401, detail="Token inválido ou expirado") user_data = await response.json() user_id = user_data.get("id") if not user_id: raise HTTPException(status_code=400, detail="ID do usuário não encontrado") return user_id # 📨 Modelo da requisição de ticket class CreateTicketRequest(BaseModel): message: str @router.get("/ticket/user") async def get_user_tickets(user_id: str): async with aiohttp.ClientSession() as session: # 1. Buscar os últimos 50 tickets do usuário async with session.get( f"{SUPABASE_URL}/rest/v1/Tickets?user_id=eq.{user_id}&order=created_at.desc&limit=50", headers=SUPABASE_ROLE_HEADERS ) as ticket_resp: if ticket_resp.status != 200: error_detail = await ticket_resp.text() raise HTTPException(status_code=500, detail=f"Erro ao buscar tickets: {error_detail}") tickets = await ticket_resp.json() # 2. Para cada ticket, buscar a última mensagem ticket_results = [] for ticket in tickets: ticket_id = ticket["id"] async with session.get( f"{SUPABASE_URL}/rest/v1/messages_tickets?ticket_id=eq.{ticket_id}&order=created_at.desc&limit=1", headers=SUPABASE_ROLE_HEADERS ) as msg_resp: if msg_resp.status != 200: continue # apenas ignora caso erro messages = await msg_resp.json() last_message = messages[0] if messages else None ticket_results.append({ "ticket": ticket, "last_message": last_message }) return ticket_results @router.get("/ticket/detail") async def get_ticket_details(ticket_id: int): async with aiohttp.ClientSession() as session: # 1. Buscar dados do ticket async with session.get( f"{SUPABASE_URL}/rest/v1/Tickets?id=eq.{ticket_id}", headers=SUPABASE_ROLE_HEADERS ) as ticket_resp: if ticket_resp.status != 200: raise HTTPException(status_code=404, detail="Ticket não encontrado") ticket_data = await ticket_resp.json() if not ticket_data: raise HTTPException(status_code=404, detail="Ticket inexistente") ticket = ticket_data[0] # 2. Buscar as 50 últimas mensagens async with session.get( f"{SUPABASE_URL}/rest/v1/messages_tickets?ticket_id=eq.{ticket_id}&order=created_at.desc&limit=50", headers=SUPABASE_ROLE_HEADERS ) as msg_resp: if msg_resp.status != 200: raise HTTPException(status_code=500, detail="Erro ao buscar mensagens") messages_raw = await msg_resp.json() # 3. Buscar info dos usuários das mensagens user_cache = {} for msg in messages_raw: user_id = msg["user"] if user_id not in user_cache: async with session.get( f"{SUPABASE_URL}/rest/v1/User?id=eq.{user_id}", headers=SUPABASE_ROLE_HEADERS ) as user_resp: if user_resp.status == 200: user_data = await user_resp.json() if user_data: u = user_data[0] user_cache[user_id] = { "id": u["id"], "name": u.get("name", "Desconhecido"), "avatar": u.get("avatar", None), "type": "support" if u.get("role") == "admin" else "customer" } else: user_cache[user_id] = { "id": user_id, "name": "Desconhecido", "avatar": None, "type": "unknown" } # 4. Substituir o campo user nas mensagens messages = [] for msg in messages_raw: uid = msg["user"] user_info = user_cache.get(uid, { "id": uid, "name": "Desconhecido", "avatar": None, }) # Definir o tipo com base no ticket if uid == ticket["user_id"]: user_info["type"] = "customer" else: user_info["type"] = "support" messages.append({ **msg, "user": user_info }) return { "ticket": ticket, "messages": messages } class RespondTicketRequest(BaseModel): ticket_id: int content: str @router.post("/ticket/respond") async def respond_ticket( ticket_id: int, content: str, user_token: str = Header(None, alias="User-key") ): # 1. Verificar se o usuário está autenticado com o token user_id = await verify_user_token(user_token) created_at = datetime.utcnow().isoformat() # 2. Verificar se o ticket existe async with aiohttp.ClientSession() as session: async with session.get( f"{SUPABASE_URL}/rest/v1/Tickets?id=eq.{ticket_id}", headers=SUPABASE_ROLE_HEADERS ) as ticket_resp: if ticket_resp.status != 200: raise HTTPException(status_code=404, detail="Ticket não encontrado") ticket_data = await ticket_resp.json() if not ticket_data: raise HTTPException(status_code=404, detail="Ticket inexistente") ticket = ticket_data[0] # 3. Criar a mensagem de resposta ao ticket message_payload = { "user": user_id, "content": content, "created_at": created_at, "ticket_id": ticket_id } # 4. Salvar a mensagem no banco async with aiohttp.ClientSession() as session: async with session.post( f"{SUPABASE_URL}/rest/v1/messages_tickets", headers=SUPABASE_ROLE_HEADERS, json=message_payload ) as message_resp: if message_resp.status != 201: error_detail = await message_resp.text() raise HTTPException(status_code=500, detail=f"Erro ao salvar a mensagem: {error_detail}") message_data = await message_resp.json() # 5. Retornar confirmação da resposta return { "status": "response sent successfully", "ticket_id": ticket_id, "message_id": message_data[0]["id"], "message_content": content } @router.post("/ticket/create") async def create_ticket( body: CreateTicketRequest, user_token: str = Header(None, alias="User-key") ): user_id = await verify_user_token(user_token) created_at = datetime.utcnow().isoformat() ticket_payload = { "user_id": user_id, "support_id": None, "created_at": created_at } async with aiohttp.ClientSession() as session: async with session.post( f"{SUPABASE_URL}/rest/v1/Tickets", headers=SUPABASE_ROLE_HEADERS, json=ticket_payload ) as ticket_resp: if ticket_resp.status != 201: error_detail = await ticket_resp.text() raise HTTPException(status_code=500, detail=f"Erro ao criar ticket: {error_detail}") ticket_data = await ticket_resp.json() ticket_id = ticket_data[0]["id"] message_payload = { "user": user_id, "content": body.message, "created_at": created_at, "ticket_id": ticket_id } async with aiohttp.ClientSession() as session: async with session.post( f"{SUPABASE_URL}/rest/v1/messages_tickets", headers=SUPABASE_ROLE_HEADERS, json=message_payload ) as message_resp: if message_resp.status != 201: error_detail = await message_resp.text() raise HTTPException(status_code=500, detail=f"Erro ao criar mensagem: {error_detail}") return {"ticket_id": ticket_id} # 📧 Envio de e-mails com Gmail API GMAIL_CLIENT_ID = "784687789817-3genmmvps11ip3a6fkbkkd8dm3bstgdc.apps.googleusercontent.com" GMAIL_CLIENT_SECRET = "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" GMAIL_REFRESH_TOKEN = "1//04ZOO_chVwlYiCgYIARAAGAQSNwF-L9IrhQO1ij79thk-DTjiMudl_XQshuU5CDTDYtt8rrOTMbz_rL8ECGjNfEN9da6W-mnjhZA" class TicketResponseRequest(BaseModel): ticket_id: int content: str async def get_gmail_access_token() -> str: url = "https://oauth2.googleapis.com/token" data = { "client_id": GMAIL_CLIENT_ID, "client_secret": GMAIL_CLIENT_SECRET, "refresh_token": GMAIL_REFRESH_TOKEN, "grant_type": "refresh_token" } async with aiohttp.ClientSession() as session: async with session.post(url, data=data) as response: if response.status != 200: raise HTTPException(status_code=500, detail="Erro ao obter access_token do Gmail") token_data = await response.json() return token_data["access_token"] def encode_message(raw_message: str) -> str: return base64.urlsafe_b64encode(raw_message.encode("utf-8")).decode("utf-8").replace("=", "") @router.post("/ticket/support/respond") async def respond_ticket( payload: TicketResponseRequest, user_token: str = Header(None, alias="User-key") ): # 1. Verify support agent token support_id = await verify_user_token(user_token) created_at = datetime.utcnow().isoformat() # Get support agent name async with aiohttp.ClientSession() as session: async with session.get( f"{SUPABASE_URL}/rest/v1/User?id=eq.{support_id}", headers=SUPABASE_ROLE_HEADERS ) as support_user_resp: if support_user_resp.status != 200: raise HTTPException(status_code=404, detail="Support agent not found") support_user_data = await support_user_resp.json() if not support_user_data: raise HTTPException(status_code=404, detail="Support agent does not exist") support_name = support_user_data[0].get("name", "Support Team").strip() # 2. Retrieve ticket async with aiohttp.ClientSession() as session: async with session.get( f"{SUPABASE_URL}/rest/v1/Tickets?id=eq.{payload.ticket_id}", headers=SUPABASE_ROLE_HEADERS ) as ticket_resp: if ticket_resp.status != 200: raise HTTPException(status_code=404, detail="Ticket not found") ticket_data = await ticket_resp.json() if not ticket_data: raise HTTPException(status_code=404, detail="Ticket does not exist") ticket = ticket_data[0] user_id = ticket["user_id"] # 3. Get user email and name async with aiohttp.ClientSession() as session: async with session.get( f"{SUPABASE_URL}/rest/v1/User?id=eq.{user_id}", headers=SUPABASE_ROLE_HEADERS ) as user_resp: if user_resp.status != 200: raise HTTPException(status_code=404, detail="User not found") user_data = await user_resp.json() if not user_data: raise HTTPException(status_code=404, detail="User does not exist") user_email = user_data[0]["email"] user_name = user_data[0].get("name", "user").strip() first_name = user_name.split(" ")[0] if user_name else "user" # 4. Send email with clean, elegant HTML template with brand purple gradient access_token = await get_gmail_access_token() subject = f"Customer Support - Case {payload.ticket_id}" email_html = f"""

Hello {first_name},

{payload.content}

Best regards,

{support_name}

Customer Support

ClosetCoach © 2025
""" raw_message = f"""To: {user_email} From: "ClosetCoach" Subject: {subject} Content-Type: text/html; charset="UTF-8" {email_html} """ encoded_message = encode_message(raw_message) async with aiohttp.ClientSession() as session: async with session.post( "https://gmail.googleapis.com/gmail/v1/users/me/messages/send", headers={ "Authorization": f"Bearer {access_token}", "Content-Type": "application/json" }, json={"raw": encoded_message} ) as gmail_resp: if gmail_resp.status != 200: error_detail = await gmail_resp.text() raise HTTPException(status_code=500, detail=f"Error sending email: {error_detail}") gmail_data = await gmail_resp.json() # 5. Save response in messages_tickets message_payload = { "user": support_id, "content": payload.content, "created_at": created_at, "ticket_id": payload.ticket_id } async with aiohttp.ClientSession() as session: async with session.post( f"{SUPABASE_URL}/rest/v1/messages_tickets", headers=SUPABASE_ROLE_HEADERS, json=message_payload ) as msg_resp: if msg_resp.status != 201: error_detail = await msg_resp.text() raise HTTPException(status_code=500, detail=f"Error recording response: {error_detail}") return { "status": "response sent successfully", "ticket_id": payload.ticket_id, "email_to": user_email, "message_id": gmail_data.get("id") }