From dafeff7cc1f43f23d222fcee5591bca6dc4536c3 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 09:36:26 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=20Fix:=20move=20Systeme.io=20API?= =?UTF-8?q?=20call=20to=20server-side=20endpoint=20to=20hide=20API=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: lsb11 <269203137+lsb11@users.noreply.github.com> --- functions/api/submit-guide.js | 55 +++++++++++++ functions/api/submit-guide.test.js | 95 +++++++++++++++++++++++ src/pages/shopify-automation-guides.astro | 6 +- 3 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 functions/api/submit-guide.js create mode 100644 functions/api/submit-guide.test.js diff --git a/functions/api/submit-guide.js b/functions/api/submit-guide.js new file mode 100644 index 0000000..f1be0b7 --- /dev/null +++ b/functions/api/submit-guide.js @@ -0,0 +1,55 @@ +export async function onRequestPost(context) { + const { request, env } = context; + + const cors = { + "Access-Control-Allow-Origin": "https://stackarchitect.xyz", + "Content-Type": "application/json", + }; + + let body; + try { + body = await request.json(); + } catch { + return new Response(JSON.stringify({ error: "Invalid JSON" }), { status: 400, headers: cors }); + } + + const { email, tags } = body; + if (!email || typeof email !== 'string' || !email.includes('@')) { + return new Response(JSON.stringify({ error: "Valid email is required" }), { status: 400, headers: cors }); + } + + const apiKey = env.SYSTEME_IO_API_KEY; + if (!apiKey) { + return new Response(JSON.stringify({ error: "Server misconfiguration" }), { status: 500, headers: cors }); + } + + try { + const res = await fetch('https://api.systeme.io/api/contacts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': apiKey, + }, + body: JSON.stringify({ email, tags: tags || [{ name: 'guides-capture' }] }), + }); + + if (!res.ok) { + // Return a generic error to avoid leaking systeme.io specifics, but forward status if helpful + return new Response(JSON.stringify({ error: "Failed to submit" }), { status: 500, headers: cors }); + } + + return new Response(JSON.stringify({ ok: true }), { status: 200, headers: cors }); + } catch (err) { + return new Response(JSON.stringify({ error: "Internal server error" }), { status: 500, headers: cors }); + } +} + +export async function onRequestOptions() { + return new Response(null, { + headers: { + "Access-Control-Allow-Origin": "https://stackarchitect.xyz", + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + }, + }); +} diff --git a/functions/api/submit-guide.test.js b/functions/api/submit-guide.test.js new file mode 100644 index 0000000..2d715b8 --- /dev/null +++ b/functions/api/submit-guide.test.js @@ -0,0 +1,95 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { onRequestPost, onRequestOptions } from './submit-guide.js'; + +describe('submit-guide API endpoint', () => { + let mockEnv; + + beforeEach(() => { + mockEnv = { + SYSTEME_IO_API_KEY: 'test-api-key', + }; + global.fetch = vi.fn(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + const createMockContext = (body) => ({ + env: mockEnv, + request: { + json: vi.fn().mockResolvedValue(body), + }, + }); + + it('should return 400 for missing or invalid email', async () => { + const context = createMockContext({}); + const response = await onRequestPost(context); + expect(response.status).toBe(400); + const data = await response.json(); + expect(data.error).toBe("Valid email is required"); + + const context2 = createMockContext({ email: "invalidemail" }); + const response2 = await onRequestPost(context2); + expect(response2.status).toBe(400); + }); + + it('should return 500 if API key is missing', async () => { + mockEnv.SYSTEME_IO_API_KEY = undefined; + const context = createMockContext({ email: "test@example.com" }); + const response = await onRequestPost(context); + expect(response.status).toBe(500); + const data = await response.json(); + expect(data.error).toBe("Server misconfiguration"); + }); + + it('should forward request to systeme.io and return 200 on success', async () => { + global.fetch.mockResolvedValueOnce({ ok: true }); + + const context = createMockContext({ email: "test@example.com" }); + const response = await onRequestPost(context); + + expect(global.fetch).toHaveBeenCalledWith('https://api.systeme.io/api/contacts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': 'test-api-key', + }, + body: JSON.stringify({ email: "test@example.com", tags: [{ name: 'guides-capture' }] }), + }); + + expect(response.status).toBe(200); + const data = await response.json(); + expect(data.ok).toBe(true); + }); + + it('should use custom tags if provided', async () => { + global.fetch.mockResolvedValueOnce({ ok: true }); + + const context = createMockContext({ email: "test@example.com", tags: [{ name: 'custom-tag' }] }); + const response = await onRequestPost(context); + + expect(global.fetch).toHaveBeenCalledWith('https://api.systeme.io/api/contacts', expect.objectContaining({ + body: JSON.stringify({ email: "test@example.com", tags: [{ name: 'custom-tag' }] }), + })); + + expect(response.status).toBe(200); + }); + + it('should return 500 if upstream fails', async () => { + global.fetch.mockResolvedValueOnce({ ok: false }); + + const context = createMockContext({ email: "test@example.com" }); + const response = await onRequestPost(context); + + expect(response.status).toBe(500); + const data = await response.json(); + expect(data.error).toBe("Failed to submit"); + }); + + it('OPTIONS should return CORS headers', async () => { + const response = await onRequestOptions(); + expect(response.headers.get('Access-Control-Allow-Methods')).toContain('POST'); + expect(response.headers.get('Access-Control-Allow-Origin')).toBe('https://stackarchitect.xyz'); + }); +}); diff --git a/src/pages/shopify-automation-guides.astro b/src/pages/shopify-automation-guides.astro index 901a787..31ae200 100644 --- a/src/pages/shopify-automation-guides.astro +++ b/src/pages/shopify-automation-guides.astro @@ -1588,10 +1588,10 @@ document.getElementById('guides-submit')?.addEventListener('click', function () return; } el.style.borderColor = ''; - fetch('https://api.systeme.io/api/contacts', { + fetch('/api/submit-guide', { method: 'POST', - headers: { 'Content-Type': 'application/json', 'X-API-Key': 'kdsymoc8fwhwwc604vrofp4jt0rb5bjosmzs5dqd4r8suo4avx4xsik4ixqvwgo4' }, - body: JSON.stringify({ email, tags: [{ name: 'guides-capture' }] }) + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }) }).catch(() => {}); if (typeof gtag !== 'undefined') { gtag('event', 'guides_email_capture', { event_category: 'conversion', event_label: 'guides_hub' });