fix: harden security, reduce duplication, and improve robustness
- Fix SQL injection in data.rs by wrapping get_table_data in READ ONLY transaction - Fix SQL injection in docker.rs CREATE DATABASE via escape_ident - Fix command injection in docker.rs by validating pg_version/container_name and escaping shell-interpolated values - Fix UTF-8 panic on stderr truncation with char_indices - Wrap delete_rows in a transaction for atomicity - Replace .expect() with proper error propagation in lib.rs - Cache AI settings in AppState to avoid repeated disk reads - Cap JSONB column discovery at 50 to prevent unbounded queries - Fix ERD colorMode to respect system theme via useTheme() - Extract AppState::get_pool() replacing ~19 inline pool patterns - Extract shared AiSettingsFields component (DRY popover + sheet) - Make get_connections_path pub(crate) and reuse from docker.rs - Deduplicate check_docker by delegating to check_docker_internal Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
82
src/components/ai/AiSettingsFields.tsx
Normal file
82
src/components/ai/AiSettingsFields.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useOllamaModels } from "@/hooks/use-ai";
|
||||
import { RefreshCw, Loader2 } from "lucide-react";
|
||||
|
||||
interface Props {
|
||||
ollamaUrl: string;
|
||||
onOllamaUrlChange: (url: string) => void;
|
||||
model: string;
|
||||
onModelChange: (model: string) => void;
|
||||
}
|
||||
|
||||
export function AiSettingsFields({
|
||||
ollamaUrl,
|
||||
onOllamaUrlChange,
|
||||
model,
|
||||
onModelChange,
|
||||
}: Props) {
|
||||
const {
|
||||
data: models,
|
||||
isLoading: modelsLoading,
|
||||
isError: modelsError,
|
||||
refetch: refetchModels,
|
||||
} = useOllamaModels(ollamaUrl);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs text-muted-foreground">Ollama URL</label>
|
||||
<Input
|
||||
value={ollamaUrl}
|
||||
onChange={(e) => onOllamaUrlChange(e.target.value)}
|
||||
placeholder="http://localhost:11434"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs text-muted-foreground">Model</label>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-5 w-5 p-0"
|
||||
onClick={() => refetchModels()}
|
||||
disabled={modelsLoading}
|
||||
title="Refresh models"
|
||||
>
|
||||
{modelsLoading ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{modelsError ? (
|
||||
<p className="text-xs text-destructive">Cannot connect to Ollama</p>
|
||||
) : (
|
||||
<Select value={model} onValueChange={onModelChange}>
|
||||
<SelectTrigger className="h-8 w-full text-xs">
|
||||
<SelectValue placeholder="Select a model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{models?.map((m) => (
|
||||
<SelectItem key={m.name} value={m.name}>
|
||||
{m.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -5,17 +5,10 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useAiSettings, useSaveAiSettings, useOllamaModels } from "@/hooks/use-ai";
|
||||
import { Settings, RefreshCw, Loader2 } from "lucide-react";
|
||||
import { useAiSettings, useSaveAiSettings } from "@/hooks/use-ai";
|
||||
import { Settings } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { AiSettingsFields } from "./AiSettingsFields";
|
||||
|
||||
export function AiSettingsPopover() {
|
||||
const { data: settings } = useAiSettings();
|
||||
@@ -27,16 +20,9 @@ export function AiSettingsPopover() {
|
||||
const currentUrl = url ?? settings?.ollama_url ?? "http://localhost:11434";
|
||||
const currentModel = model ?? settings?.model ?? "";
|
||||
|
||||
const {
|
||||
data: models,
|
||||
isLoading: modelsLoading,
|
||||
isError: modelsError,
|
||||
refetch: refetchModels,
|
||||
} = useOllamaModels(currentUrl);
|
||||
|
||||
const handleSave = () => {
|
||||
saveMutation.mutate(
|
||||
{ ollama_url: currentUrl, model: currentModel },
|
||||
{ provider: "ollama", ollama_url: currentUrl, model: currentModel },
|
||||
{
|
||||
onSuccess: () => toast.success("AI settings saved"),
|
||||
onError: (err) =>
|
||||
@@ -63,53 +49,12 @@ export function AiSettingsPopover() {
|
||||
<div className="flex flex-col gap-3">
|
||||
<h4 className="text-sm font-medium">Ollama Settings</h4>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs text-muted-foreground">Ollama URL</label>
|
||||
<Input
|
||||
value={currentUrl}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
placeholder="http://localhost:11434"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs text-muted-foreground">Model</label>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-5 w-5 p-0"
|
||||
onClick={() => refetchModels()}
|
||||
disabled={modelsLoading}
|
||||
title="Refresh models"
|
||||
>
|
||||
{modelsLoading ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{modelsError ? (
|
||||
<p className="text-xs text-destructive">
|
||||
Cannot connect to Ollama
|
||||
</p>
|
||||
) : (
|
||||
<Select value={currentModel} onValueChange={setModel}>
|
||||
<SelectTrigger className="h-8 w-full text-xs">
|
||||
<SelectValue placeholder="Select a model" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{models?.map((m) => (
|
||||
<SelectItem key={m.name} value={m.name}>
|
||||
{m.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
<AiSettingsFields
|
||||
ollamaUrl={currentUrl}
|
||||
onOllamaUrlChange={setUrl}
|
||||
model={currentModel}
|
||||
onModelChange={setModel}
|
||||
/>
|
||||
|
||||
<Button size="sm" className="h-7 text-xs" onClick={handleSave}>
|
||||
Save
|
||||
|
||||
Reference in New Issue
Block a user