fix: ensure all API requests use user ID and fix missing data migration for existing users
Some checks failed
Release Binaries / release (push) Has been cancelled

This commit is contained in:
Gemini AI
2025-12-29 01:58:15 +04:00
Unverified
parent 39d1e03785
commit 57720e6c5b
5 changed files with 182 additions and 124 deletions

View File

@@ -124,6 +124,26 @@ export function ensureDefaultUsers(): UserRecord {
roman.updatedAt = nowIso() roman.updatedAt = nowIso()
writeStore(store) writeStore(store)
} }
// NEW: Check if roman needs data migration (e.g. if he was created before migration logic was robust)
const userDir = getUserDir(roman.id)
const configPath = join(userDir, "config.json")
let needsMigration = !existsSync(configPath)
if (!needsMigration) {
try {
const config = JSON.parse(readFileSync(configPath, "utf-8"))
if (!config.recentFolders || config.recentFolders.length === 0) {
needsMigration = true
}
} catch (e) {
needsMigration = true
}
}
if (needsMigration) {
console.log(`[UserStore] Roman exists but seems to have missing data. Triggering migration to ${userDir}...`)
migrateLegacyData(userDir)
}
} }
if (store.users.length > 0) { if (store.users.length > 0) {

View File

@@ -11,8 +11,6 @@ import { RemoteAccessOverlay } from "./components/remote-access-overlay"
import { InstanceMetadataProvider } from "./lib/contexts/instance-metadata-context" import { InstanceMetadataProvider } from "./lib/contexts/instance-metadata-context"
import { initMarkdown } from "./lib/markdown" import { initMarkdown } from "./lib/markdown"
import QwenOAuthCallback from "./pages/QwenOAuthCallback" import QwenOAuthCallback from "./pages/QwenOAuthCallback"
import LoginView from "./components/auth/LoginView"
import { isLoggedIn, initializeUserContext } from "./lib/user-context"
import { useTheme } from "./lib/theme" import { useTheme } from "./lib/theme"
import { useCommands } from "./lib/hooks/use-commands" import { useCommands } from "./lib/hooks/use-commands"
@@ -102,11 +100,6 @@ const App: Component = () => {
}) })
onMount(() => { onMount(() => {
// Initialize user context from Electron IPC
import("./lib/user-context").then(({ initializeUserContext }) => {
initializeUserContext()
})
updateInstanceTabBarHeight() updateInstanceTabBarHeight()
const handleResize = () => updateInstanceTabBarHeight() const handleResize = () => updateInstanceTabBarHeight()
window.addEventListener("resize", handleResize) window.addEventListener("resize", handleResize)
@@ -393,111 +386,106 @@ const App: Component = () => {
</div> </div>
</Dialog.Portal> </Dialog.Portal>
</Dialog> </Dialog>
<Show <div class="h-screen w-screen flex flex-col">
when={isLoggedIn()} <Show
fallback={<LoginView onLoginSuccess={() => initializeUserContext()} />} when={shouldShowFolderSelection()}
> fallback={
<div class="h-screen w-screen flex flex-col"> <>
<Show <InstanceTabs
when={shouldShowFolderSelection()} instances={instances()}
fallback={ activeInstanceId={activeInstanceId()}
<> onSelect={setActiveInstanceId}
<InstanceTabs onClose={handleCloseInstance}
instances={instances()} onNew={handleNewInstanceRequest}
activeInstanceId={activeInstanceId()} onOpenRemoteAccess={() => setRemoteAccessOpen(true)}
onSelect={setActiveInstanceId} />
onClose={handleCloseInstance}
onNew={handleNewInstanceRequest}
onOpenRemoteAccess={() => setRemoteAccessOpen(true)}
/>
<For each={Array.from(instances().values())}> <For each={Array.from(instances().values())}>
{(instance) => { {(instance) => {
const isActiveInstance = () => activeInstanceId() === instance.id const isActiveInstance = () => activeInstanceId() === instance.id
const isVisible = () => isActiveInstance() && !showFolderSelection() const isVisible = () => isActiveInstance() && !showFolderSelection()
return ( return (
<div class="flex-1 min-h-0 overflow-hidden" style={{ display: isVisible() ? "flex" : "none" }}> <div class="flex-1 min-h-0 overflow-hidden" style={{ display: isVisible() ? "flex" : "none" }}>
<InstanceMetadataProvider instance={instance}> <InstanceMetadataProvider instance={instance}>
<InstanceShell <InstanceShell
instance={instance} instance={instance}
escapeInDebounce={escapeInDebounce()} escapeInDebounce={escapeInDebounce()}
paletteCommands={paletteCommands} paletteCommands={paletteCommands}
onCloseSession={(sessionId) => handleCloseSession(instance.id, sessionId)} onCloseSession={(sessionId) => handleCloseSession(instance.id, sessionId)}
onNewSession={() => handleNewSession(instance.id)} onNewSession={() => handleNewSession(instance.id)}
handleSidebarAgentChange={(sessionId, agent) => handleSidebarAgentChange(instance.id, sessionId, agent)} handleSidebarAgentChange={(sessionId, agent) => handleSidebarAgentChange(instance.id, sessionId, agent)}
handleSidebarModelChange={(sessionId, model) => handleSidebarModelChange(instance.id, sessionId, model)} handleSidebarModelChange={(sessionId, model) => handleSidebarModelChange(instance.id, sessionId, model)}
onExecuteCommand={executeCommand} onExecuteCommand={executeCommand}
tabBarOffset={instanceTabBarHeight()} tabBarOffset={instanceTabBarHeight()}
/> />
</InstanceMetadataProvider> </InstanceMetadataProvider>
</div> </div>
) )
}} }}
</For> </For>
</> </>
} }
> >
<FolderSelectionView <FolderSelectionView
onSelectFolder={handleSelectFolder} onSelectFolder={handleSelectFolder}
isLoading={isSelectingFolder()} isLoading={isSelectingFolder()}
advancedSettingsOpen={isAdvancedSettingsOpen()} advancedSettingsOpen={isAdvancedSettingsOpen()}
onAdvancedSettingsOpen={() => setIsAdvancedSettingsOpen(true)} onAdvancedSettingsOpen={() => setIsAdvancedSettingsOpen(true)}
onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)} onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)}
onOpenRemoteAccess={() => setRemoteAccessOpen(true)} onOpenRemoteAccess={() => setRemoteAccessOpen(true)}
/>
</Show>
<Show when={showFolderSelection()}>
<div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
<div class="w-full h-full relative">
<button
onClick={() => {
setShowFolderSelection(false)
setShowFolderSelectionOnStart(false)
setIsAdvancedSettingsOpen(false)
clearLaunchError()
}}
class="absolute top-4 right-4 z-10 p-2 bg-white dark:bg-gray-800 rounded-lg shadow-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
title="Close (Esc)"
>
<svg class="w-5 h-5 text-gray-600 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<FolderSelectionView
onSelectFolder={handleSelectFolder}
isLoading={isSelectingFolder()}
advancedSettingsOpen={isAdvancedSettingsOpen()}
onAdvancedSettingsOpen={() => setIsAdvancedSettingsOpen(true)}
onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)}
/>
</div>
</div>
</Show>
<RemoteAccessOverlay open={remoteAccessOpen()} onClose={() => setRemoteAccessOpen(false)} />
<AlertDialog />
<Toaster
position="top-right"
gutter={8}
containerClassName=""
containerStyle={{}}
toastOptions={{
className: "",
duration: 5000,
style: {
background: "#363636",
color: "#fff",
},
}}
/> />
</div> </Show>
</Show>
<Show when={showFolderSelection()}>
<div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
<div class="w-full h-full relative">
<button
onClick={() => {
setShowFolderSelection(false)
setShowFolderSelectionOnStart(false)
setIsAdvancedSettingsOpen(false)
clearLaunchError()
}}
class="absolute top-4 right-4 z-10 p-2 bg-white dark:bg-gray-800 rounded-lg shadow-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
title="Close (Esc)"
>
<svg class="w-5 h-5 text-gray-600 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<FolderSelectionView
onSelectFolder={handleSelectFolder}
isLoading={isSelectingFolder()}
advancedSettingsOpen={isAdvancedSettingsOpen()}
onAdvancedSettingsOpen={() => setIsAdvancedSettingsOpen(true)}
onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)}
/>
</div>
</div>
</Show>
<RemoteAccessOverlay open={remoteAccessOpen()} onClose={() => setRemoteAccessOpen(false)} />
<AlertDialog />
<Toaster
position="top-right"
gutter={8}
containerClassName=""
containerStyle={{}}
toastOptions={{
className: "",
duration: 5000,
style: {
background: "#363636",
color: "#fff",
},
}}
/>
</div>
</> </>
) )
} }

View File

@@ -28,6 +28,7 @@ import type {
PortAvailabilityResponse, PortAvailabilityResponse,
} from "../../../server/src/api-types" } from "../../../server/src/api-types"
import { getLogger } from "./logger" import { getLogger } from "./logger"
import { getUserHeaders } from "./user-context"
const FALLBACK_API_BASE = "http://127.0.0.1:9898" const FALLBACK_API_BASE = "http://127.0.0.1:9898"
const RUNTIME_BASE = typeof window !== "undefined" ? window.location?.origin : undefined const RUNTIME_BASE = typeof window !== "undefined" ? window.location?.origin : undefined
@@ -87,8 +88,10 @@ function logHttp(message: string, context?: Record<string, unknown>) {
async function request<T>(path: string, init?: RequestInit): Promise<T> { async function request<T>(path: string, init?: RequestInit): Promise<T> {
const url = API_BASE_ORIGIN ? new URL(path, API_BASE_ORIGIN).toString() : path const url = API_BASE_ORIGIN ? new URL(path, API_BASE_ORIGIN).toString() : path
const userHeaders = getUserHeaders()
const headers: HeadersInit = { const headers: HeadersInit = {
"Content-Type": "application/json", "Content-Type": "application/json",
...userHeaders,
...(init?.headers ?? {}), ...(init?.headers ?? {}),
} }

View File

@@ -45,22 +45,55 @@ export function getUserHeaders(): Record<string, string> {
*/ */
export function withUserHeaders(options: RequestInit = {}): RequestInit { export function withUserHeaders(options: RequestInit = {}): RequestInit {
const userHeaders = getUserHeaders() const userHeaders = getUserHeaders()
if (Object.keys(userHeaders).length === 0) return options
const headers = new Headers(options.headers || {})
for (const [key, value] of Object.entries(userHeaders)) {
headers.set(key, value)
}
return { return {
...options, ...options,
headers: { headers,
...options.headers,
...userHeaders,
},
} }
} }
/** /**
* Fetch wrapper that automatically includes user headers * Fetch wrapper that automatically includes user headers
*/ */
export async function userFetch(url: string, options: RequestInit = {}): Promise<Response> { export async function userFetch(url: string | URL | Request, options: RequestInit = {}): Promise<Response> {
return fetch(url, withUserHeaders(options)) return fetch(url, withUserHeaders(options))
} }
/**
* Globally patch fetch to include user headers for all internal /api/* requests
* This ensures compatibility with legacy code and 3rd party libraries.
*/
export function patchFetch(): void {
if ((window as any)._codenomad_fetch_patched) return
(window as any)._codenomad_fetch_patched = true
const originalFetch = window.fetch
window.fetch = async function (input: RequestInfo | URL, init?: RequestInit) {
let url = ""
if (typeof input === "string") {
url = input
} else if (input instanceof URL) {
url = input.toString()
} else if (input instanceof Request) {
url = input.url
}
// Only inject headers for internal API calls
if (url.startsWith("/api/") || url.includes(window.location.origin + "/api/")) {
return originalFetch(input, withUserHeaders(init))
}
return originalFetch(input, init)
}
console.log("[UserContext] Global fetch patched for /api/* requests")
}
/** /**
* Initialize user context from Electron IPC * Initialize user context from Electron IPC
* Call this on app startup * Call this on app startup

View File

@@ -1,9 +1,12 @@
import { render } from "solid-js/web" import { render } from "solid-js/web"
import { Show, onMount } from "solid-js"
import App from "./App" import App from "./App"
import { ThemeProvider } from "./lib/theme" import { ThemeProvider } from "./lib/theme"
import { ConfigProvider } from "./stores/preferences" import { ConfigProvider } from "./stores/preferences"
import { InstanceConfigProvider } from "./stores/instance-config" import { InstanceConfigProvider } from "./stores/instance-config"
import { runtimeEnv } from "./lib/runtime-env" import { runtimeEnv } from "./lib/runtime-env"
import LoginView from "./components/auth/LoginView"
import { isLoggedIn, initializeUserContext, patchFetch } from "./lib/user-context"
import "./index.css" import "./index.css"
import "@git-diff-view/solid/styles/diff-view-pure.css" import "@git-diff-view/solid/styles/diff-view-pure.css"
@@ -18,15 +21,26 @@ if (typeof document !== "undefined") {
document.documentElement.dataset.runtimePlatform = runtimeEnv.platform document.documentElement.dataset.runtimePlatform = runtimeEnv.platform
} }
render( const Root = () => {
() => ( onMount(() => {
<ConfigProvider> patchFetch()
<InstanceConfigProvider> initializeUserContext()
<ThemeProvider> })
<App />
</ThemeProvider> return (
</InstanceConfigProvider> <Show
</ConfigProvider> when={isLoggedIn()}
), fallback={<LoginView onLoginSuccess={() => initializeUserContext()} />}
root, >
) <ConfigProvider>
<InstanceConfigProvider>
<ThemeProvider>
<App />
</ThemeProvider>
</InstanceConfigProvider>
</ConfigProvider>
</Show>
)
}
render(() => <Root />, root)