Skip to content
Merged
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
2 changes: 1 addition & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 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
36 changes: 30 additions & 6 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@
env_path = Path(__file__).parent / '.env'
load_dotenv(dotenv_path=env_path)

# Initialize Supabase Client (Service Role for backend bypass)
# Initialize Supabase Client (Anon Key to respect RLS)
try:
from supabase import create_client, Client
url = os.environ.get("SUPABASE_URL")
key = os.environ.get("SUPABASE_SERVICE_KEY")
key = os.environ.get("SUPABASE_ANON_KEY")
if not url or not key:
print("[ERROR] SUPABASE_URL or SUPABASE_SERVICE_KEY not set in backend/.env")
print("[ERROR] SUPABASE_URL or SUPABASE_ANON_KEY not set in backend/.env")
supabase = None
else:
supabase = create_client(url, key)
Expand Down Expand Up @@ -283,8 +283,8 @@ async def lifespan(app: FastAPI):
"http://localhost:3000",
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
allow_methods=["GET", "POST", "PATCH", "OPTIONS"],
allow_headers=["Content-Type", "Authorization", "Accept"],
)


Expand Down Expand Up @@ -549,7 +549,7 @@ async def get_tickets(company_id: str | None = None):
return res.data

@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.
Expand All @@ -558,6 +558,30 @@ async def save_ticket(request_body: TicketSaveRequest):
raise HTTPException(status_code=500, detail="Supabase connection not initialized.")

logger = logging.getLogger(__name__)

# --- 1000% Perfect Tenant Auth & Spoofing Protection ---
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Missing or invalid Authorization header")

token = auth_header.split(" ")[1]
try:
user_response = supabase.auth.get_user(token)
if not user_response or not getattr(user_response, "user", None):
raise HTTPException(status_code=401, detail="Invalid token")
trusted_user_id = user_response.user.id
except Exception as e:
logger.error(f"Authentication failed: {e}")
raise HTTPException(status_code=401, detail="Authentication failed")

# Enforce trusted user ID from token
if request_body.user_id and request_body.user_id != trusted_user_id:
logger.warning(f"ID Spoofing Attempt: Token user {trusted_user_id} tried to act as {request_body.user_id}")
raise HTTPException(status_code=403, detail="User ID mismatch. Spoofing detected.")

request_body.user_id = trusted_user_id
# --------------------------------------------------------

try:
final_data = request_body.dict()

Expand Down
Loading