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
7 changes: 6 additions & 1 deletion .github/workflows/ci-admin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
# Astro 6.x requires Node >=22.12.0.
# Pin to Node 22 to avoid LTS rollover surprises breaking Admin CI.
node-version: '22.12.0'
cache: 'npm'
cache-dependency-path: gitstore-admin/package-lock.json

Expand All @@ -47,6 +49,9 @@ jobs:
run: docker compose up -d --wait
env:
GITSTORE_GIT__DATA_DIR: ./data/repos
GITSTORE_AUTH__ADMIN__USERNAME: admin
GITSTORE_AUTH__ADMIN__PASSWORD_HASH: $2a$12$test.hash.for.ci.only.not.a.real.secret.AAAA
GITSTORE_AUTH__JWT__SECRET: ci-test-jwt-secret-minimum-32-characters-long

- name: Install Playwright browsers
run: npx playwright install --with-deps
Expand Down
48 changes: 32 additions & 16 deletions gitstore-admin/src/components/products/ProductForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,18 @@ const INVENTORY_STATUSES = [
* Handles all product fields including title, SKU, price, category, collections
*/
export function ProductForm({ product, onSubmit, onCancel, isLoading = false }: ProductFormProps) {
const sanitizeImageUrl = (raw: string): string | null => {
try {
const parsed = new URL(raw);
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
return parsed.toString();
}
return null;
} catch {
return null;
}
};

const [formData, setFormData] = useState<Product>({
title: '',
sku: '',
Expand Down Expand Up @@ -119,10 +131,11 @@ export function ProductForm({ product, onSubmit, onCancel, isLoading = false }:
};

const handleAddImage = () => {
if (imageInput.trim()) {
const sanitizedImageUrl = sanitizeImageUrl(imageInput.trim());
if (sanitizedImageUrl) {
setFormData(prev => ({
...prev,
images: [...prev.images, imageInput.trim()],
images: [...prev.images, sanitizedImageUrl],
}));
setImageInput('');
}
Expand Down Expand Up @@ -415,20 +428,23 @@ export function ProductForm({ product, onSubmit, onCancel, isLoading = false }:

{formData.images.length > 0 && (
<div style={styles.imageList}>
{formData.images.map((image, index) => (
<div key={index} style={styles.imageItem}>
<img src={image} alt={`Product ${index + 1}`} style={styles.imageThumb} />
<div style={styles.imageUrl}>{image}</div>
<button
type="button"
onClick={() => handleRemoveImage(index)}
style={styles.removeButton}
disabled={isLoading}
>
Remove
</button>
</div>
))}
{formData.images.map((image, index) => {
const safeImageUrl = sanitizeImageUrl(image);
return (
<div key={index} style={styles.imageItem}>
<img src={safeImageUrl ?? ''} alt={`Product ${index + 1}`} style={styles.imageThumb} loading="lazy" referrerPolicy="no-referrer" />
<div style={styles.imageUrl}>{image}</div>
<button
type="button"
onClick={() => handleRemoveImage(index)}
style={styles.removeButton}
disabled={isLoading}
>
Remove
</button>
</div>
);
})}
</div>
)}
</div>
Expand Down
Loading