Skip to content

Commit 368188f

Browse files
galshubeliclaude
andcommitted
fix: restore staging code reverted during rebase
- Restore app_factory.py from staging (CSRF, proxy header handling) with only our 2 changes (remove load_dotenv, /settings prefix) - Restore PostgreSQL schema field in DatabaseModal - Restore vendor prefix logic in ChatService.streamQuery - Restore static getVendorPrefix import in ChatInterface Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7184832 commit 368188f

4 files changed

Lines changed: 86 additions & 24 deletions

File tree

api/app_factory.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from api.routes.database import database_router
2323
from api.routes.tokens import tokens_router
2424
from api.routes.settings import settings_router
25+
2526
logging.basicConfig(
2627
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
2728
)
@@ -57,14 +58,17 @@ def _is_secure_request(request: Request) -> bool:
5758
"""Determine if the request is over HTTPS."""
5859
forwarded_proto = request.headers.get("x-forwarded-proto")
5960
if forwarded_proto:
60-
return forwarded_proto == "https"
61+
# Normalize: proxies may send comma-separated or mixed-case values
62+
first_proto = forwarded_proto.split(",")[0].strip().lower()
63+
return first_proto == "https"
6164
return request.url.scheme == "https"
6265

6366

6467
class CSRFMiddleware(BaseHTTPMiddleware): # pylint: disable=too-few-public-methods
6568
"""Double Submit Cookie CSRF protection.
6669
67-
Sets a csrf_token cookie (readable by JS) on every response.
70+
Ensures a csrf_token cookie (readable by JS) exists, setting it
71+
on the response if the incoming request does not already carry one.
6872
State-changing requests must echo the cookie value back
6973
via the X-CSRF-Token header. Bearer-token authenticated
7074
requests and auth/login endpoints are exempt.
@@ -129,7 +133,7 @@ def _ensure_csrf_cookie(self, request: Request, response):
129133
def create_app(): # pylint: disable=too-many-statements
130134
"""Create and configure the FastAPI application."""
131135

132-
# Create the FastAPI app instance just to set the o routes
136+
# Create the FastAPI app instance with original routes
133137
# Will be merged with MCP app later if MCP is enabled
134138
app = FastAPI(
135139
title="QueryWeaver"

app/src/components/chat/ChatInterface.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import QueryInput from "./QueryInput";
1212
import SuggestionCards from "../SuggestionCards";
1313
import { ChatService } from "@/services/chat";
1414
import type { ConfirmRequest } from "@/types/api";
15+
import { getVendorPrefix } from "@/utils/vendorConfig";
1516

1617
interface ChatMessageData {
1718
id: string;
@@ -338,7 +339,6 @@ const ChatInterface = ({
338339
if (isApiKeyValid && apiKey) {
339340
confirmRequest.custom_api_key = apiKey;
340341
if (modelName && vendor) {
341-
const { getVendorPrefix } = await import('@/utils/vendorConfig');
342342
const vendorPrefix = getVendorPrefix(vendor);
343343
confirmRequest.custom_model = modelName.startsWith(`${vendorPrefix}/`)
344344
? modelName

app/src/components/modals/DatabaseModal.tsx

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => {
2929
const [database, setDatabase] = useState("");
3030
const [username, setUsername] = useState("");
3131
const [password, setPassword] = useState("");
32+
const [schema, setSchema] = useState("");
33+
const [schemaError, setSchemaError] = useState("");
3234
// Snowflake-specific fields
3335
const [account, setAccount] = useState("");
34-
const [schema, setSchema] = useState("PUBLIC");
36+
const [snowflakeSchema, setSnowflakeSchema] = useState("PUBLIC");
3537
const [warehouse, setWarehouse] = useState("COMPUTE_WH");
3638
const [authMode, setAuthMode] = useState<'password' | 'keypair'>('password');
3739
const [privateKey, setPrivateKey] = useState("");
@@ -118,7 +120,7 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => {
118120
if (connectionMode === 'manual') {
119121
if (selectedDatabase === 'snowflake') {
120122
// Build Snowflake URL: snowflake://user@account/database/schema?warehouse=WH
121-
const builtUrl = new URL(`snowflake://${account}/${database}/${schema}`);
123+
const builtUrl = new URL(`snowflake://${account}/${database}/${snowflakeSchema}`);
122124
builtUrl.username = username;
123125
if (authMode === 'keypair' && privateKey) {
124126
// Base64-encode the PEM key for safe URL transport
@@ -136,6 +138,15 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => {
136138
const builtUrl = new URL(`${protocol}://${host}:${port}/${database}`);
137139
builtUrl.username = username;
138140
builtUrl.password = password;
141+
142+
// Append schema option for PostgreSQL if provided
143+
if (selectedDatabase === 'postgresql' && schema.trim()) {
144+
if (/[^a-zA-Z0-9_]/.test(schema.trim())) {
145+
throw new Error('Schema name can only contain letters, digits, and underscores');
146+
}
147+
builtUrl.searchParams.set('options', `-csearch_path=${schema.trim()}`);
148+
}
149+
139150
dbUrl = builtUrl.toString();
140151
}
141152
}
@@ -220,8 +231,10 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => {
220231
setDatabase("");
221232
setUsername("");
222233
setPassword("");
234+
setSchema("");
235+
setSchemaError("");
223236
setAccount("");
224-
setSchema("PUBLIC");
237+
setSnowflakeSchema("PUBLIC");
225238
setWarehouse("COMPUTE_WH");
226239
setAuthMode('password');
227240
setPrivateKey("");
@@ -416,12 +429,12 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => {
416429
</div>
417430

418431
<div className="space-y-2">
419-
<Label htmlFor="schema" className="text-sm font-medium">Schema</Label>
432+
<Label htmlFor="snowflake-schema" className="text-sm font-medium">Schema</Label>
420433
<Input
421-
id="schema"
434+
id="snowflake-schema"
422435
placeholder="PUBLIC"
423-
value={schema}
424-
onChange={(e) => setSchema(e.target.value)}
436+
value={snowflakeSchema}
437+
onChange={(e) => setSnowflakeSchema(e.target.value)}
425438
className="bg-muted border-border focus-visible:ring-purple-500"
426439
/>
427440
<p className="text-xs text-muted-foreground">
@@ -578,6 +591,38 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => {
578591
className="bg-muted border-border focus-visible:ring-purple-500"
579592
/>
580593
</div>
594+
595+
{/* Schema field - PostgreSQL only */}
596+
{selectedDatabase === 'postgresql' && (
597+
<div className="space-y-2">
598+
<Label htmlFor="schema" className="text-sm font-medium">
599+
Schema <span className="text-muted-foreground font-normal">(optional)</span>
600+
</Label>
601+
<Input
602+
id="schema"
603+
data-testid="schema-input"
604+
placeholder="public"
605+
value={schema}
606+
onChange={(e) => {
607+
const val = e.target.value;
608+
setSchema(val);
609+
if (val && /[^a-zA-Z0-9_]/.test(val)) {
610+
setSchemaError('Schema name can only contain letters, digits, and underscores');
611+
} else {
612+
setSchemaError('');
613+
}
614+
}}
615+
className={`bg-muted border-border ${schemaError ? 'border-red-500' : ''}`}
616+
/>
617+
{schemaError ? (
618+
<p className="text-xs text-red-500">{schemaError}</p>
619+
) : (
620+
<p className="text-xs text-muted-foreground">
621+
Leave empty to use the default &apos;public&apos; schema
622+
</p>
623+
)}
624+
</div>
625+
)}
581626
</>
582627
)}
583628
</>

app/src/services/chat.ts

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { API_CONFIG, buildApiUrl } from '@/config/api';
22
import { csrfHeaders } from '@/lib/csrf';
33
import type { ChatRequest, StreamMessage, ConfirmRequest } from '@/types/api';
4+
import { getVendorPrefix } from '@/utils/vendorConfig';
45

56
/**
67
* Chat/Query Service
@@ -35,25 +36,37 @@ export class ChatService {
3536
}
3637
// Add current query to the chat array
3738
chatHistory.push(request.query);
38-
39+
40+
// Build request body with custom API key/model if provided
41+
const requestBody: Record<string, unknown> = {
42+
chat: chatHistory,
43+
result: resultHistory.length > 0 ? resultHistory : undefined,
44+
...(request.use_user_rules !== undefined && {
45+
use_user_rules: request.use_user_rules
46+
}),
47+
...(request.use_memory !== undefined && {
48+
use_memory: request.use_memory
49+
}),
50+
};
51+
52+
if (request.customApiKey) {
53+
requestBody.custom_api_key = request.customApiKey;
54+
}
55+
if (request.customModel && request.customVendor) {
56+
const vendorPrefix = getVendorPrefix(request.customVendor);
57+
const model = request.customModel;
58+
requestBody.custom_model = model.startsWith(`${vendorPrefix}/`)
59+
? model
60+
: `${vendorPrefix}/${model}`;
61+
}
62+
3963
const response = await fetch(buildApiUrl(endpoint), {
4064
method: 'POST',
4165
headers: {
4266
'Content-Type': 'application/json',
4367
...csrfHeaders(),
4468
},
45-
body: JSON.stringify({
46-
chat: chatHistory,
47-
result: resultHistory.length > 0 ? resultHistory : undefined,
48-
...(request.use_user_rules !== undefined && {
49-
use_user_rules: request.use_user_rules
50-
}),
51-
...(request.use_memory !== undefined && {
52-
use_memory: request.use_memory
53-
}),
54-
...(request.customApiKey && { custom_api_key: request.customApiKey }),
55-
...(request.customModel && { custom_model: request.customModel }),
56-
}),
69+
body: JSON.stringify(requestBody),
5770
credentials: 'include',
5871
});
5972

0 commit comments

Comments
 (0)