diff --git a/apps/ui/src/components/dialogs/board-background-modal.tsx b/apps/ui/src/components/dialogs/board-background-modal.tsx index c1acdfd9..ae6bd714 100644 --- a/apps/ui/src/components/dialogs/board-background-modal.tsx +++ b/apps/ui/src/components/dialogs/board-background-modal.tsx @@ -13,7 +13,8 @@ import { Label } from '@/components/ui/label'; import { Checkbox } from '@/components/ui/checkbox'; import { cn } from '@/lib/utils'; import { useAppStore, defaultBackgroundSettings } from '@/store/app-store'; -import { getHttpApiClient, getServerUrlSync } from '@/lib/http-api-client'; +import { getHttpApiClient } from '@/lib/http-api-client'; +import { getAuthenticatedImageUrl } from '@/lib/api-fetch'; import { useBoardBackgroundSettings } from '@/hooks/use-board-background-settings'; import { toast } from 'sonner'; import { @@ -62,12 +63,13 @@ export function BoardBackgroundModal({ open, onOpenChange }: BoardBackgroundModa // Update preview image when background settings change useEffect(() => { if (currentProject && backgroundSettings.imagePath) { - const serverUrl = import.meta.env.VITE_SERVER_URL || getServerUrlSync(); // Add cache-busting query parameter to force browser to reload image - const cacheBuster = imageVersion ? `&v=${imageVersion}` : `&v=${Date.now()}`; - const imagePath = `${serverUrl}/api/fs/image?path=${encodeURIComponent( - backgroundSettings.imagePath - )}&projectPath=${encodeURIComponent(currentProject.path)}${cacheBuster}`; + const cacheBuster = imageVersion ?? Date.now().toString(); + const imagePath = getAuthenticatedImageUrl( + backgroundSettings.imagePath, + currentProject.path, + cacheBuster + ); setPreviewImage(imagePath); } else { setPreviewImage(null); diff --git a/apps/ui/src/components/ui/description-image-dropzone.tsx b/apps/ui/src/components/ui/description-image-dropzone.tsx index 9df5e0e6..78fe0346 100644 --- a/apps/ui/src/components/ui/description-image-dropzone.tsx +++ b/apps/ui/src/components/ui/description-image-dropzone.tsx @@ -3,7 +3,7 @@ import { cn } from '@/lib/utils'; import { ImageIcon, X, Loader2, FileText } from 'lucide-react'; import { Textarea } from '@/components/ui/textarea'; import { getElectronAPI } from '@/lib/electron'; -import { getServerUrlSync } from '@/lib/http-api-client'; +import { getAuthenticatedImageUrl } from '@/lib/api-fetch'; import { useAppStore, type FeatureImagePath, type FeatureTextFilePath } from '@/store/app-store'; import { sanitizeFilename, @@ -94,9 +94,8 @@ export function DescriptionImageDropZone({ // Construct server URL for loading saved images const getImageServerUrl = useCallback( (imagePath: string): string => { - const serverUrl = import.meta.env.VITE_SERVER_URL || getServerUrlSync(); const projectPath = currentProject?.path || ''; - return `${serverUrl}/api/fs/image?path=${encodeURIComponent(imagePath)}&projectPath=${encodeURIComponent(projectPath)}`; + return getAuthenticatedImageUrl(imagePath, projectPath); }, [currentProject?.path] ); diff --git a/apps/ui/src/components/views/board-view/hooks/use-board-background.ts b/apps/ui/src/components/views/board-view/hooks/use-board-background.ts index 5bd5f4f2..e61ba44c 100644 --- a/apps/ui/src/components/views/board-view/hooks/use-board-background.ts +++ b/apps/ui/src/components/views/board-view/hooks/use-board-background.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react'; import { useAppStore, defaultBackgroundSettings } from '@/store/app-store'; -import { getServerUrlSync } from '@/lib/http-api-client'; +import { getAuthenticatedImageUrl } from '@/lib/api-fetch'; interface UseBoardBackgroundProps { currentProject: { path: string; id: string } | null; @@ -22,14 +22,14 @@ export function useBoardBackground({ currentProject }: UseBoardBackgroundProps) return {}; } + const imageUrl = getAuthenticatedImageUrl( + backgroundSettings.imagePath, + currentProject.path, + backgroundSettings.imageVersion + ); + return { - backgroundImage: `url(${ - import.meta.env.VITE_SERVER_URL || getServerUrlSync() - }/api/fs/image?path=${encodeURIComponent( - backgroundSettings.imagePath - )}&projectPath=${encodeURIComponent(currentProject.path)}${ - backgroundSettings.imageVersion ? `&v=${backgroundSettings.imageVersion}` : '' - })`, + backgroundImage: `url(${imageUrl})`, backgroundSize: 'cover', backgroundPosition: 'center', backgroundRepeat: 'no-repeat', diff --git a/apps/ui/src/lib/api-fetch.ts b/apps/ui/src/lib/api-fetch.ts index fc76c266..f3df93bf 100644 --- a/apps/ui/src/lib/api-fetch.ts +++ b/apps/ui/src/lib/api-fetch.ts @@ -153,3 +153,37 @@ export async function apiDeleteRaw( ): Promise { return apiFetch(endpoint, 'DELETE', options); } + +/** + * Build an authenticated image URL for use in tags or CSS background-image + * Adds authentication via query parameter since headers can't be set for image loads + * + * @param path - Image path + * @param projectPath - Project path + * @param version - Optional cache-busting version + * @returns Full URL with auth credentials + */ +export function getAuthenticatedImageUrl( + path: string, + projectPath: string, + version?: string | number +): string { + const serverUrl = getServerUrl(); + const params = new URLSearchParams({ + path, + projectPath, + }); + + if (version !== undefined) { + params.set('v', String(version)); + } + + // Add auth credential as query param (needed for image loads that can't set headers) + const apiKey = getApiKey(); + if (apiKey) { + params.set('apiKey', apiKey); + } + // Note: Session token auth relies on cookies which are sent automatically by the browser + + return `${serverUrl}/api/fs/image?${params.toString()}`; +} diff --git a/apps/ui/tests/context/add-context-image.spec.ts b/apps/ui/tests/context/add-context-image.spec.ts index bc40ec31..2159b42b 100644 --- a/apps/ui/tests/context/add-context-image.spec.ts +++ b/apps/ui/tests/context/add-context-image.spec.ts @@ -5,6 +5,7 @@ */ import { test, expect } from '@playwright/test'; +import { Buffer } from 'buffer'; import * as fs from 'fs'; import * as path from 'path'; import { @@ -118,21 +119,10 @@ test.describe('Add Context Image', () => { test('should import an image file to context', async ({ page }) => { await setupProjectWithFixture(page, getFixturePath()); - + await authenticateForTests(page); await page.goto('/'); await waitForNetworkIdle(page); - // Check if we're on the login screen and authenticate if needed - const loginInput = page.locator('input[type="password"][placeholder*="API key"]'); - const isLoginScreen = await loginInput.isVisible({ timeout: 2000 }).catch(() => false); - if (isLoginScreen) { - const apiKey = process.env.AUTOMAKER_API_KEY || 'test-api-key-for-e2e-tests'; - await loginInput.fill(apiKey); - await page.locator('button:has-text("Login")').click(); - await page.waitForURL('**/', { timeout: 5000 }); - await waitForNetworkIdle(page); - } - await navigateToContext(page); // Wait for the file input to be attached to the DOM before setting files diff --git a/scripts/lint-lockfile.mjs b/scripts/lint-lockfile.mjs index b33c9780..d658905b 100644 --- a/scripts/lint-lockfile.mjs +++ b/scripts/lint-lockfile.mjs @@ -12,7 +12,7 @@ const lockfilePath = join(process.cwd(), 'package-lock.json'); try { const content = readFileSync(lockfilePath, 'utf8'); - + // Check for git+ssh:// URLs if (content.includes('git+ssh://')) { console.error('Error: package-lock.json contains git+ssh:// URLs.'); @@ -20,7 +20,7 @@ try { console.error('Or run: npm run fix:lockfile'); process.exit(1); } - + console.log('✓ No git+ssh:// URLs found in package-lock.json'); process.exit(0); } catch (error) {