Skip to content

Commit 3a862f8

Browse files
committed
fix: address secondary PR review feedback
1 parent cde4a4f commit 3a862f8

7 files changed

Lines changed: 44 additions & 23 deletions

File tree

‎apps/dashboard-api/src/controllers/ai.controller.js‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ const queryBuilder = async (req, res, next) => {
1010
const { projectId } = req.params;
1111
const { collectionName, prompt } = req.body;
1212

13+
const mongoose = require('mongoose');
14+
if (!mongoose.Types.ObjectId.isValid(projectId)) {
15+
throw new AppError(400, "Invalid project ID");
16+
}
17+
1318
if (typeof collectionName !== 'string' || typeof prompt !== 'string') {
1419
throw new AppError(400, "Collection name and prompt must be strings");
1520
}

‎apps/dashboard-api/test_python.js‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,19 @@ const crypto = require('crypto');
44
const run = async () => {
55
const timestamp = Date.now().toString();
66
const payload = JSON.stringify({ prompt: "test", schema_fields: [] });
7-
const secret = "test-secret";
7+
8+
const secret = process.env.TEST_SECRET || process.env.INTERNAL_SECRET;
9+
const apiUrl = process.env.AI_API_URL || 'http://127.0.0.1:8000/ai/query-builder';
10+
11+
if (!secret) {
12+
console.error("Missing TEST_SECRET or INTERNAL_SECRET in environment variables.");
13+
process.exit(1);
14+
}
815

916
const signature = crypto.createHmac('sha256', secret).update(`${timestamp}.${payload}`).digest('hex');
1017

1118
try {
12-
const res = await axios.post('http://127.0.0.1:8000/ai/query-builder', payload, {
19+
const res = await axios.post(apiUrl, payload, {
1320
headers: {
1421
'X-Internal-Signature': signature,
1522
'X-Timestamp': timestamp,

‎apps/python-service/config.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
from pydantic import Field
12
from pydantic_settings import BaseSettings, SettingsConfigDict
23

34
class Settings(BaseSettings):
4-
INTERNAL_SECRET: str
5+
INTERNAL_SECRET: str = Field(min_length=32)
56
GROQ_API_KEY: str = "" # Default empty, can be supplied via BYOK later
67
REDIS_URL: str = "redis://localhost:6379"
78

‎apps/python-service/dependencies.py‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ async def verify_signature(request: Request):
1717

1818
try:
1919
timestamp = int(timestamp_str)
20-
except ValueError:
21-
raise HTTPException(status_code=403, detail="Invalid timestamp format")
20+
except ValueError as e:
21+
raise HTTPException(status_code=403, detail="Invalid timestamp format") from e
2222

2323
# Replay attack protection — 30s window
2424
current_time_ms = int(time.time() * 1000)
@@ -38,11 +38,8 @@ async def verify_signature(request: Request):
3838
if not hmac.compare_digest(expected_mac, signature):
3939
raise HTTPException(status_code=403, detail="Invalid signature")
4040

41-
# Redis Nonce check for exact replay attack prevention
41+
# Redis Nonce check for exact replay attack prevention (Atomic SET NX)
4242
nonce_key = f"internal:nonce:{timestamp}:{signature}"
43-
exists = await redis_client.get(nonce_key)
44-
if exists:
43+
success = await redis_client.set(nonce_key, "1", ex=30, nx=True)
44+
if not success:
4545
raise HTTPException(status_code=403, detail="Replay attack detected")
46-
47-
# Store the nonce with a 30-second TTL
48-
await redis_client.set(nonce_key, "1", ex=30)

‎apps/python-service/routers/ai.py‎

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import asyncio
2+
import logging
13
from fastapi import APIRouter, HTTPException, Depends
24
from pydantic import BaseModel, Field
35
from typing import List, Union
@@ -7,6 +9,7 @@
79
from dependencies import verify_signature
810

911
router = APIRouter(prefix="/ai", tags=["ai"], dependencies=[Depends(verify_signature)])
12+
logger = logging.getLogger(__name__)
1013

1114
class FilterItem(BaseModel):
1215
field: str = Field(description="The exact field name from the schema")
@@ -19,7 +22,7 @@ class QueryResult(BaseModel):
1922

2023
class QueryBuilderRequest(BaseModel):
2124
prompt: str
22-
schema_fields: list
25+
schema_fields: List[dict]
2326

2427
@router.post("/query-builder", response_model=QueryResult)
2528
async def query_builder(request: QueryBuilderRequest):
@@ -55,13 +58,20 @@ async def query_builder(request: QueryBuilderRequest):
5558
# Create the LangChain chain
5659
chain = prompt | structured_llm
5760

58-
# Invoke the chain
59-
result = await chain.ainvoke({
60-
"schema": str(request.schema_fields),
61-
"user_prompt": request.prompt
62-
})
61+
# Invoke the chain with a timeout to prevent hanging requests
62+
result = await asyncio.wait_for(
63+
chain.ainvoke({
64+
"schema": str(request.schema_fields),
65+
"user_prompt": request.prompt
66+
}),
67+
timeout=15.0
68+
)
6369

6470
return result
6571

66-
except Exception:
67-
raise HTTPException(status_code=500, detail="Failed to generate query")
72+
except asyncio.TimeoutError as e:
73+
logger.error("AI Query Builder timeout", exc_info=True)
74+
raise HTTPException(status_code=504, detail="AI request timed out") from e
75+
except Exception as e:
76+
logger.error("AI Query Builder failed", exc_info=True)
77+
raise HTTPException(status_code=500, detail="Internal server error") from e

‎apps/python-service/tests/test_auth.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def generate_valid_signature(timestamp: int, payload: str) -> str:
3838
def mock_redis():
3939
with patch('dependencies.redis_client.get', new_callable=AsyncMock) as mock_get, \
4040
patch('dependencies.redis_client.set', new_callable=AsyncMock) as mock_set:
41-
mock_get.return_value = None # Simulate nonce does not exist (not a replay attack)
41+
mock_set.return_value = True # Simulate nonce does not exist (SET NX succeeds)
4242
yield mock_get, mock_set
4343

4444
def test_missing_headers():
@@ -93,9 +93,9 @@ def test_valid_request():
9393
assert response.json()["message"] == "success"
9494

9595
def test_replay_attack(mock_redis):
96-
mock_get, mock_set = mock_redis
97-
# Simulate that Redis says this nonce already exists
98-
mock_get.return_value = "1"
96+
_, mock_set = mock_redis
97+
# Simulate that Redis says this nonce already exists (SET NX fails)
98+
mock_set.return_value = None
9999

100100
timestamp = int(time.time() * 1000)
101101
payload = '{"foo": "bar"}'

‎apps/web-dashboard/src/components/Database/AiQueryBar.jsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const AiQueryBar = ({ projectId, activeCollection, onFiltersGenerated }) => {
6161
value={prompt}
6262
onChange={(e) => setPrompt(e.target.value)}
6363
placeholder="Ask AI to filter data..."
64+
aria-label="Ask AI to filter data"
6465
disabled={isLoading || !activeCollection || !projectId}
6566
style={{
6667
background: 'transparent',

0 commit comments

Comments
 (0)