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
Some checks failed
Release Binaries / release (push) Has been cancelled
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 ?? {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user