Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions apps/ui/src/components/dialogs/board-background-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 2 additions & 3 deletions apps/ui/src/components/ui/description-image-dropzone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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',
Expand Down
34 changes: 34 additions & 0 deletions apps/ui/src/lib/api-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,37 @@ export async function apiDeleteRaw(
): Promise<Response> {
return apiFetch(endpoint, 'DELETE', options);
}

/**
* Build an authenticated image URL for use in <img> 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()}`;
}
14 changes: 2 additions & 12 deletions apps/ui/tests/context/add-context-image.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions scripts/lint-lockfile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ 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.');
console.error('Run: git config --global url."https://github.com/".insteadOf "git@github.com:"');
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) {
Expand Down