Codex review of #36 + #37 surfaced that my prior `escape="\\"` only declared the escape character — the pattern literals themselves still contained unescaped `_` and `%`, so `parent_path="foo_bar"` continued to match `fooXbar/...`, `foo2bar/...`, etc. My earlier ESCAPE-clause assertions were tautological: they verified the SQL rendered the ESCAPE keyword without ever checking that the pattern was actually escaped. The tests passed; the leak persisted. Fix in three layers: 1. Real escape: `backend/src/backend/scripts.py` gains `_escape_like_pattern(value)` that escapes `\` → `\\`, `%` → `\%`, `_` → `\_` (in that order — the escape char MUST be escaped first). `_build_list_scripts_descendant_prefix` now returns the escaped prefix. `list_workspace_directories` and `delete_workspace_directory` also escape their server-built prefixes. `count_scripts` escapes the user subtree prefix. 2. Same bug elsewhere: `backend/src/backend/resources.py:404` had the identical `DataResources.resource_name.like(f"%{keyword}%")` pattern; a search for "100%" would match everything. Now escaped too. 3. Count endpoint scope: `count_scripts` was workspace-wide and skipped the StorageObjects JOIN. Now INNER JOINs StorageObjects (drops orphans whose current_object_id is dangling) and filters by `workspace/{user_id}/` subtree so the result matches what `list_scripts(parent_path="")` would return. Multi-member workspaces no longer over-report, and orphan rows no longer inflate the count. Frontend: `DashboardRoute` is not keyed by workspace/user (only ScriptsPage is), so without a workspace_id dep the previous workspace's count persisted across navigation. useEffect now depends on `currentWorkspace?.workspace_id`; `loadScriptCount` clears the count to null at the start of the fetch so the dashboard doesn't flash a stale number. Tests — backend/tests/test_list_scripts_parent_path.py - Rewritten with three layers of coverage: * Pure helper tests for `_escape_like_pattern` (7 cases including backslash-escape-first ordering). * SQL-contract tests asserting the COMPILED PATTERN contains the escaped form (lowercased to neutralise SQLAlchemy keyword casing). * BEHAVIORAL tests on SQLite in-memory with the same LIKE semantics — proves the fix actually prevents the wildcard leak. Includes a negative test (without escape, siblings DO match) so the fixture is verified to exercise the bug. Tests — backend/tests/test_count_scripts.py - Updated to assert the JOIN + user-scope filter. New test verifies two different users in the same workspace get different subtrees. Verified: - pytest backend/tests: 65 passed (43 baseline + 12 list_scripts + 4 count + 6 helper/SQLite behavioral) - pnpm typecheck: clean - Raw SQL on MySQL (live DB) confirms `LIKE 'workspace/.../foo\_bar/%%' ESCAPE '\\'`.
34 lines
1.2 KiB
TypeScript
34 lines
1.2 KiB
TypeScript
import { useEffect } from "react";
|
|
import { useNavigate } from "react-router";
|
|
|
|
import { useAuth } from "~/context/AuthContext";
|
|
import { DashboardPage } from "../../components/admin/DashboardPage";
|
|
import { useScriptWorkspaceStore } from "./state/scriptWorkspaceStore";
|
|
|
|
export default function DashboardRoute() {
|
|
const scripts = useScriptWorkspaceStore((s) => s.scripts);
|
|
const scriptCount = useScriptWorkspaceStore((s) => s.scriptCount);
|
|
const loadScriptCount = useScriptWorkspaceStore((s) => s.loadScriptCount);
|
|
const apiOnline = useScriptWorkspaceStore((s) => s.apiOnline);
|
|
const workspaceId = useAuth().currentWorkspace?.workspace_id;
|
|
const navigate = useNavigate();
|
|
|
|
// Reload on workspace switch — DashboardRoute is not keyed by
|
|
// workspace/user (only ScriptsPage is), so without this dep the
|
|
// previous workspace's count would persist.
|
|
useEffect(() => {
|
|
void loadScriptCount();
|
|
}, [loadScriptCount, workspaceId]);
|
|
|
|
return (
|
|
<DashboardPage
|
|
scriptCount={scriptCount ?? scripts.length}
|
|
online={apiOnline}
|
|
onNavigate={(page) => {
|
|
if (page === "scripts") navigate("/scripts");
|
|
else if (page === "schedules") navigate("/schedules");
|
|
else navigate("/system");
|
|
}}
|
|
/>
|
|
);
|
|
} |