Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Supabase Configuration
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_KEY=your-service-key
SUPABASE_ANON_KEY=your-anon-key

# Startup Mode
# Set ALLOW_DEGRADED_STARTUP=1 to allow backend startup even if duplicate/RAG models fail to load
Expand Down
48 changes: 26 additions & 22 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,14 @@

# Initialize Supabase Client (Service Role for backend bypass)
try:
from supabase import create_client, Client
url = os.environ.get("SUPABASE_URL")
key = os.environ.get("SUPABASE_SERVICE_KEY")
if not url or not key:
print("[ERROR] SUPABASE_URL or SUPABASE_SERVICE_KEY not set in backend/.env")
supabase = None
else:
supabase = create_client(url, key)
from backend.supabase_client import get_admin_client, get_user_client, get_anon_client
supabase = get_admin_client()
supabase_anon = get_anon_client()
except (ImportError, Exception) as e:
print(f"[WARNING] Supabase initialization failed: {e}")
supabase = None
Client = None
supabase_anon = None
get_user_client = None

# Ensure project root is on path for imports
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
Expand Down Expand Up @@ -625,22 +621,24 @@ async def log_correction(raw_request: Request):
# Ticket operations (Now via Supabase)
# ---------------------------------------------------------------------------
@app.get("/tickets")
async def get_tickets(company_id: str | None = None):
async def get_tickets(request: Request, company_id: str | None = None):
"""Fetch persistent tickets from Supabase."""
if not supabase:
client = get_user_client(request.headers.get("Authorization")) if get_user_client else None
if not client:
raise HTTPException(status_code=500, detail="Database connection not initialized")

query = supabase.table("tickets").select("*").order("created_at", desc=True)
query = client.table("tickets").select("*").order("created_at", desc=True)
if company_id:
query = query.eq("company_id", company_id)

res = query.execute()
return res.data

@app.get("/tickets/search")
async def search_tickets(q: str | None = None, company_id: str | None = None, limit: int = 50, offset: int = 0):
async def search_tickets(request: Request, q: str | None = None, company_id: str | None = None, limit: int = 50, offset: int = 0):
"""Search tickets using tenant-safe full-text search."""
if not supabase:
client = get_user_client(request.headers.get("Authorization")) if get_user_client else None
if not client:
raise HTTPException(status_code=500, detail="Database connection not initialized")

if not q:
Expand All @@ -649,7 +647,7 @@ async def search_tickets(q: str | None = None, company_id: str | None = None, li
raise HTTPException(status_code=400, detail="company_id is required for tenant-safe search")

try:
result = supabase.rpc(
result = client.rpc(
"search_tickets",
{
"query_text": q,
Expand All @@ -663,13 +661,16 @@ async def search_tickets(q: str | None = None, company_id: str | None = None, li
raise HTTPException(status_code=500, detail=f"Search failed: {e}")

@app.post("/tickets/save")
async def save_ticket(request_body: TicketSaveRequest):
async def save_ticket(request_body: TicketSaveRequest, request: Request):
"""
OFFICIAL PERSISTENCE: Saves the analyzed ticket to Supabase.
This is called AFTER the user confirms the analysis results.
"""
if not supabase:
client = get_user_client(request.headers.get("Authorization")) if get_user_client else None
if not client:
raise HTTPException(status_code=500, detail="Supabase connection not initialized.")
# For some admin validations in this endpoint, we might still use the global admin `supabase` client,
# but the final insert will be done using `client` (user-scoped).

logger = logging.getLogger(__name__)
try:
Expand All @@ -679,6 +680,8 @@ async def save_ticket(request_body: TicketSaveRequest):
profile = {}
if request_body.user_id:
try:
# We use the admin client (`supabase`) here to ensure we can read profiles
# even if RLS is tricky for self-healing, though `client` could also work.
profile_res = (
supabase.table("profiles")
.select("company_id, company")
Expand Down Expand Up @@ -792,7 +795,7 @@ async def save_ticket(request_body: TicketSaveRequest):
# Strip keys not accepted by the DB schema
insert_data = {k: v for k, v in final_data.items() if k in VALID_TICKET_COLUMNS}

res = supabase.table("tickets").insert(insert_data).execute()
res = client.table("tickets").insert(insert_data).execute()

if not res.data:
raise Exception("Failed to insert ticket into database.")
Expand All @@ -818,7 +821,7 @@ async def save_ticket(request_body: TicketSaveRequest):
if final_data["auto_resolve"]:
msg = "AI Auto-Resolution active: A verified solution has been identified. Please review the attached resolution steps."

supabase.table("ticket_messages").insert({
client.table("ticket_messages").insert({
"ticket_id": ticket_id,
"sender_id": "00000000-0000-0000-0000-000000000000", # System ID
"sender_name": "AI Assistant",
Expand All @@ -842,12 +845,13 @@ async def save_ticket(request_body: TicketSaveRequest):
raise HTTPException(status_code=500, detail=str(e))

@app.get("/tickets/{ticket_id}")
async def get_ticket_by_id(ticket_id: str):
async def get_ticket_by_id(ticket_id: str, request: Request):
"""Fetch single persistent ticket."""
if not supabase:
client = get_user_client(request.headers.get("Authorization")) if get_user_client else None
if not client:
raise HTTPException(status_code=500, detail="Database connection not initialized")

res = supabase.table("tickets").select("*").eq("id", ticket_id).single().execute()
res = client.table("tickets").select("*").eq("id", ticket_id).single().execute()
if not res.data:
raise HTTPException(status_code=404, detail="Ticket not found")
return res.data
Expand Down
8 changes: 2 additions & 6 deletions backend/services/auto_close_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@
import logging
from datetime import datetime, timedelta, timezone
from typing import Optional, Dict, List

from supabase import create_client
from backend.supabase_client import get_admin_client
from dotenv import load_dotenv

load_dotenv()
Expand All @@ -34,10 +33,7 @@ class AutoCloseService:

def __init__(self):
"""Initialize the auto-close service with Supabase client."""
self.supabase = create_client(
os.getenv("SUPABASE_URL"),
os.getenv("SUPABASE_SERVICE_ROLE_KEY")
)
self.supabase = get_admin_client()
self.enabled = os.getenv("AUTO_CLOSE_ENABLED", "true").lower() == "true"
self.default_auto_close_days = int(os.getenv("AUTO_CLOSE_DAYS", "7"))
self.cron_schedule = os.getenv("AUTO_CLOSE_CRON_SCHEDULE", "0 2 * * *") # 2 AM UTC daily
Expand Down
7 changes: 2 additions & 5 deletions backend/services/notification_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from typing import Optional, Dict
from enum import Enum

from supabase import create_client
from backend.supabase_client import get_admin_client
from dotenv import load_dotenv

load_dotenv()
Expand Down Expand Up @@ -51,10 +51,7 @@ class NotificationRoutingMiddleware:

def __init__(self):
"""Initialize the notification routing middleware."""
self.supabase = create_client(
os.getenv("SUPABASE_URL"),
os.getenv("SUPABASE_SERVICE_ROLE_KEY")
)
self.supabase = get_admin_client()
self._settings_cache: Dict[str, Dict] = {}
self.log_level = os.getenv("NOTIFICATION_ROUTING_LOG_LEVEL", "info").lower()

Expand Down
10 changes: 4 additions & 6 deletions backend/services/rag_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,10 @@ def __init__(self):
self._loaded = False
self._load_failed = False

load_dotenv()
url = os.environ.get("SUPABASE_URL")
key = os.environ.get("SUPABASE_SERVICE_KEY")
if url and key:
self.supabase: Client = create_client(url, key)
else:
try:
from backend.supabase_client import get_admin_client
self.supabase = get_admin_client()
except ImportError:
self.supabase = None

def is_available(self) -> bool:
Expand Down
8 changes: 2 additions & 6 deletions backend/services/sla_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,12 +269,8 @@ def load(supabase_client: Any = None, notification_router: Any = None) -> SlaEsc
global _instance
if _instance is None:
if supabase_client is None:
from supabase import create_client

supabase_client = create_client(
os.getenv("SUPABASE_URL"),
os.getenv("SUPABASE_SERVICE_ROLE_KEY") or os.getenv("SUPABASE_SERVICE_KEY"),
)
from backend.supabase_client import get_admin_client
supabase_client = get_admin_client()
_instance = SlaEscalationService(
supabase_client,
notification_router=notification_router,
Expand Down
39 changes: 39 additions & 0 deletions backend/supabase_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import os
from supabase import create_client, Client, ClientOptions

def get_admin_client() -> Client | None:
"""Returns a Supabase client initialized with the service-role key (bypasses RLS)."""
url = os.environ.get("SUPABASE_URL")
# Some services might look for SUPABASE_SERVICE_ROLE_KEY or SUPABASE_SERVICE_KEY
key = os.environ.get("SUPABASE_SERVICE_ROLE_KEY") or os.environ.get("SUPABASE_SERVICE_KEY")
if not url or not key:
print("[ERROR] SUPABASE_URL or SUPABASE_SERVICE_KEY not set in backend/.env")
return None
return create_client(url, key)

def get_anon_client() -> Client | None:
"""Returns a Supabase client initialized with the anonymous key."""
url = os.environ.get("SUPABASE_URL")
key = os.environ.get("SUPABASE_ANON_KEY")
if not url or not key:
print("[WARNING] SUPABASE_URL or SUPABASE_ANON_KEY not set in backend/.env")
return None
return create_client(url, key)

def get_user_client(auth_header: str | None) -> Client | None:
"""
Returns a Supabase client initialized with the anon key and scoped to the user
by injecting the JWT token from the Authorization header.
"""
url = os.environ.get("SUPABASE_URL")
key = os.environ.get("SUPABASE_ANON_KEY")
if not url or not key:
return None

if auth_header and auth_header.lower().startswith("bearer "):
# Inject the authorization header so requests are executed as the authenticated user
options = ClientOptions(headers={"Authorization": auth_header})
return create_client(url, key, options=options)

# Fallback to anon client if no valid auth header
return get_anon_client()