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
691 changes: 375 additions & 316 deletions src/components/ProjectsNew.tsx

Large diffs are not rendered by default.

35 changes: 14 additions & 21 deletions src/pages/api/donation-complete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,34 @@ import type { APIRoute } from "astro";
import * as Sentry from "@sentry/astro";
import { reportError } from "../../lib/report-error";
import { getEnv } from "../../utils/getEnv";
import {
isAllowedOrigin,
corsHeaders,
ALLOWED_ORIGIN,
type OriginPolicy,
} from "../../utils/origin";

export const prerender = false;

const ALLOWED_ORIGINS = [
"https://techforpalestine.org",
...(import.meta.env.PROD ? [] : ["http://localhost:4321"]),
];

function isAllowedOrigin(origin: string | null): origin is string {
if (!origin) return false;
if (ALLOWED_ORIGINS.includes(origin)) return true;
return /\.website-aun\.pages\.dev$/.test(new URL(origin).hostname);
}
const ORIGIN_POLICY: OriginPolicy = {
allowedOrigins: [ALLOWED_ORIGIN, ...(import.meta.env.PROD ? [] : ["http://localhost:4321"])],
allowedSuffixes: [".website-aun.pages.dev"],
};
const EO_MEMBERS_LIST_URL =
"https://emailoctopus.com/api/1.6/lists/8adc2ed4-f798-11ef-b60f-115427c25a1c/contacts";
const MAX_NAME_LENGTH = 200;

function corsHeaders(origin: string) {
return {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
} as const;
}

export const POST: APIRoute = async ({ request, locals }) => {
const origin = request.headers.get("Origin");
if (!isAllowedOrigin(origin)) {
if (!isAllowedOrigin(origin, ORIGIN_POLICY)) {
return new Response(JSON.stringify({ message: "Forbidden" }), {
status: 403,
headers: { "Content-Type": "application/json" },
});
}

const ctx = (locals as { runtime?: { ctx?: { waitUntil: (p: Promise<unknown>) => void } } }).runtime?.ctx;
const ctx = (locals as { runtime?: { ctx?: { waitUntil: (p: Promise<unknown>) => void } } })
.runtime?.ctx;

let body: Record<string, unknown>;
try {
Expand Down Expand Up @@ -105,7 +98,7 @@ export const POST: APIRoute = async ({ request, locals }) => {

export const OPTIONS: APIRoute = async ({ request }) => {
const origin = request.headers.get("Origin");
if (!isAllowedOrigin(origin)) {
if (!isAllowedOrigin(origin, ORIGIN_POLICY)) {
return new Response(null, { status: 403 });
}
return new Response(null, { status: 200, headers: corsHeaders(origin) });
Expand Down
33 changes: 17 additions & 16 deletions src/pages/api/e4p-pledge-sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import * as Sentry from "@sentry/astro";
import { Client } from "@notionhq/client";
import { getEnv } from "../../utils/getEnv.js";
import { reportError } from "../../lib/report-error";
import { isAllowedOrigin, corsHeaders } from "../../utils/origin";

export const prerender = false;

export const POST: APIRoute = async ({ request, locals }) => {
const ctx = locals.runtime?.ctx;
const origin = request.headers.get("Origin");
if (origin !== "https://techforpalestine.org") {
if (!isAllowedOrigin(origin)) {
return new Response(JSON.stringify({ error: "Forbidden" }), {
status: 403,
headers: { "Content-Type": "application/json" },
Expand Down Expand Up @@ -43,7 +44,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
status: 400,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "https://techforpalestine.org",
"Access-Control-Allow-Origin": origin,
},
}
);
Expand All @@ -54,14 +55,16 @@ export const POST: APIRoute = async ({ request, locals }) => {
if (!emailRx.test(pledgeData.email)) {
return new Response(JSON.stringify({ error: "Invalid email address" }), {
status: 400,
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "https://techforpalestine.org" },
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": origin },
});
}

try { new URL(pledgeData.linkedin); } catch {
try {
new URL(pledgeData.linkedin);
} catch {
return new Response(JSON.stringify({ error: "Invalid LinkedIn URL" }), {
status: 400,
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "https://techforpalestine.org" },
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": origin },
});
}

Expand All @@ -73,10 +76,13 @@ export const POST: APIRoute = async ({ request, locals }) => {
];
for (const [field, value] of textFields) {
if (value.length > MAX) {
return new Response(JSON.stringify({ error: `Field '${field}' exceeds maximum length of ${MAX} characters` }), {
status: 400,
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "https://techforpalestine.org" },
});
return new Response(
JSON.stringify({ error: `Field '${field}' exceeds maximum length of ${MAX} characters` }),
{
status: 400,
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": origin },
}
);
}
}

Expand Down Expand Up @@ -156,12 +162,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
}),
{
status: 201,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "https://techforpalestine.org",
"Access-Control-Allow-Methods": "POST",
"Access-Control-Allow-Headers": "Content-Type",
},
headers: { "Content-Type": "application/json", ...corsHeaders(origin, "POST") },
}
);
} catch (error) {
Expand All @@ -172,7 +173,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
status: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "https://techforpalestine.org",
"Access-Control-Allow-Origin": origin,
},
});
}
Expand Down
39 changes: 21 additions & 18 deletions src/pages/api/endorsement-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import * as Sentry from "@sentry/astro";
import { Client } from "@notionhq/client";
import { getEnv } from "../../utils/getEnv.js";
import { reportError } from "../../lib/report-error";
import { isAllowedOrigin, corsHeaders } from "../../utils/origin";

export const prerender = false;

export const POST: APIRoute = async ({ request, locals }) => {
const ctx = locals.runtime?.ctx;
const origin = request.headers.get("Origin");
if (origin !== "https://techforpalestine.org") {
if (!isAllowedOrigin(origin)) {
return new Response(JSON.stringify({ error: "Forbidden" }), {
status: 403,
headers: { "Content-Type": "application/json" },
Expand Down Expand Up @@ -48,7 +49,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
status: 400,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "https://techforpalestine.org",
"Access-Control-Allow-Origin": origin,
},
});
}
Expand All @@ -58,21 +59,25 @@ export const POST: APIRoute = async ({ request, locals }) => {
if (!emailRx.test(endorsementData.contactEmail)) {
return new Response(JSON.stringify({ error: "Invalid email address" }), {
status: 400,
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "https://techforpalestine.org" },
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": origin },
});
}

try { new URL(endorsementData.organizationWebsite); } catch {
try {
new URL(endorsementData.organizationWebsite);
} catch {
return new Response(JSON.stringify({ error: "Invalid organization website URL" }), {
status: 400,
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "https://techforpalestine.org" },
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": origin },
});
}

try { new URL(endorsementData.campaignLink); } catch {
try {
new URL(endorsementData.campaignLink);
} catch {
return new Response(JSON.stringify({ error: "Invalid campaign link URL" }), {
status: 400,
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "https://techforpalestine.org" },
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": origin },
});
}

Expand All @@ -87,10 +92,13 @@ export const POST: APIRoute = async ({ request, locals }) => {
];
for (const [field, value] of textFields) {
if (value.length > MAX) {
return new Response(JSON.stringify({ error: `Field '${field}' exceeds maximum length of ${MAX} characters` }), {
status: 400,
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "https://techforpalestine.org" },
});
return new Response(
JSON.stringify({ error: `Field '${field}' exceeds maximum length of ${MAX} characters` }),
{
status: 400,
headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": origin },
}
);
}
}

Expand Down Expand Up @@ -191,12 +199,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
}),
{
status: 201,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "https://techforpalestine.org",
"Access-Control-Allow-Methods": "POST",
"Access-Control-Allow-Headers": "Content-Type",
},
headers: { "Content-Type": "application/json", ...corsHeaders(origin, "POST") },
}
);
} catch (error) {
Expand All @@ -207,7 +210,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
status: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "https://techforpalestine.org",
"Access-Control-Allow-Origin": origin,
},
});
}
Expand Down
43 changes: 20 additions & 23 deletions src/pages/api/membership-complete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,34 @@ import type { APIRoute } from "astro";
import * as Sentry from "@sentry/astro";
import { reportError } from "../../lib/report-error";
import { getEnv } from "../../utils/getEnv";
import {
isAllowedOrigin,
corsHeaders,
ALLOWED_ORIGIN,
type OriginPolicy,
} from "../../utils/origin";

export const prerender = false;

const ALLOWED_ORIGINS = [
"https://techforpalestine.org",
...(import.meta.env.PROD ? [] : ["http://localhost:4321"]),
];

function isAllowedOrigin(origin: string | null): origin is string {
if (!origin) return false;
if (ALLOWED_ORIGINS.includes(origin)) return true;
return /\.website-aun\.pages\.dev$/.test(new URL(origin).hostname);
}
const ORIGIN_POLICY: OriginPolicy = {
allowedOrigins: [ALLOWED_ORIGIN, ...(import.meta.env.PROD ? [] : ["http://localhost:4321"])],
allowedSuffixes: [".website-aun.pages.dev"],
};
const EO_MEMBERS_LIST_URL =
"https://emailoctopus.com/api/1.6/lists/8adc2ed4-f798-11ef-b60f-115427c25a1c/contacts";
const MAX_NAME_LENGTH = 200;

function corsHeaders(origin: string) {
return {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
} as const;
}

export const POST: APIRoute = async ({ request, locals }) => {
const origin = request.headers.get("Origin");
if (!isAllowedOrigin(origin)) {
if (!isAllowedOrigin(origin, ORIGIN_POLICY)) {
return new Response(JSON.stringify({ message: "Forbidden" }), {
status: 403,
headers: { "Content-Type": "application/json" },
});
}

const ctx = (locals as { runtime?: { ctx?: { waitUntil: (p: Promise<unknown>) => void } } }).runtime?.ctx;
const ctx = (locals as { runtime?: { ctx?: { waitUntil: (p: Promise<unknown>) => void } } })
.runtime?.ctx;

let body: Record<string, unknown>;
try {
Expand Down Expand Up @@ -84,7 +77,9 @@ export const POST: APIRoute = async ({ request, locals }) => {
});
}
})
.catch((err) => reportError(err, { context: "membership-complete hub", email: redacted }))
.catch((err) =>
reportError(err, { context: "membership-complete hub", email: redacted })
)
: Promise.resolve(),

eoApiKey
Expand All @@ -110,7 +105,9 @@ export const POST: APIRoute = async ({ request, locals }) => {
});
}
})
.catch((err) => reportError(err, { context: "membership-complete eo", email: redacted }))
.catch((err) =>
reportError(err, { context: "membership-complete eo", email: redacted })
)
: Promise.resolve(),
]);

Expand All @@ -131,7 +128,7 @@ export const POST: APIRoute = async ({ request, locals }) => {

export const OPTIONS: APIRoute = async ({ request }) => {
const origin = request.headers.get("Origin");
if (!isAllowedOrigin(origin)) {
if (!isAllowedOrigin(origin, ORIGIN_POLICY)) {
return new Response(null, { status: 403 });
}
return new Response(null, { status: 200, headers: corsHeaders(origin) });
Expand Down
17 changes: 9 additions & 8 deletions src/pages/api/pipe.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import type { APIRoute } from "astro";
import * as Sentry from "@sentry/astro";
import { reportError } from "../../lib/report-error";
import { isAllowedOrigin, type OriginPolicy } from "../../utils/origin";

const ALLOWED_ORIGIN = "https://techforpalestine.org";
const PREVIEW_ORIGIN_SUFFIX = ".pages.dev";
const PLAUSIBLE_API = "https://plausible.io/api/event";
const CONVERSION_EVENTS = new Set(["Monthly-donate", "One-time-donate", "Membership-complete"]);

function isAllowedOrigin(origin: string): boolean {
return origin === ALLOWED_ORIGIN || origin.endsWith(PREVIEW_ORIGIN_SUFFIX);
}
const ORIGIN_POLICY: OriginPolicy = {
allowedSuffixes: [".pages.dev"],
allowMissingOrigin: true,
};

function parseEventName(body: string): string {
try {
Expand All @@ -23,7 +22,7 @@ function parseEventName(body: string): string {
export const POST: APIRoute = async ({ request, locals }) => {
const ctx = locals.runtime?.ctx;
const origin = request.headers.get("origin");
if (origin && !isAllowedOrigin(origin)) {
if (!isAllowedOrigin(origin, ORIGIN_POLICY)) {
return new Response("Forbidden", { status: 403 });
}

Expand Down Expand Up @@ -73,7 +72,9 @@ export const POST: APIRoute = async ({ request, locals }) => {
const key = `dropped:${date}:${time}:${id}`;

let parsed: Record<string, unknown> = {};
try { parsed = JSON.parse(body); } catch {}
try {
parsed = JSON.parse(body);
} catch {}

const value = JSON.stringify({
eventName,
Expand Down
Loading
Loading