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")
}