diff options
| author | Fuwn <[email protected]> | 2026-02-07 01:42:57 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-07 01:42:57 -0800 |
| commit | 5c5b1993edd890a80870ee05607ac5f088191d4e (patch) | |
| tree | a721b76bcd49ba10826c53efc87302c7a689512f /apps/web/app/api/webhook-config/test | |
| download | asa.news-5c5b1993edd890a80870ee05607ac5f088191d4e.tar.xz asa.news-5c5b1993edd890a80870ee05607ac5f088191d4e.zip | |
feat: asa.news RSS reader with developer tier, REST API, and webhooks
Full-stack RSS reader SaaS: Supabase + Next.js + Go worker.
Includes three subscription tiers (free/pro/developer), API key auth,
read-only REST API, webhook push notifications, Stripe billing with
proration, and PWA support.
Diffstat (limited to 'apps/web/app/api/webhook-config/test')
| -rw-r--r-- | apps/web/app/api/webhook-config/test/route.ts | 101 |
1 files changed, 101 insertions, 0 deletions
diff --git a/apps/web/app/api/webhook-config/test/route.ts b/apps/web/app/api/webhook-config/test/route.ts new file mode 100644 index 0000000..684ec0c --- /dev/null +++ b/apps/web/app/api/webhook-config/test/route.ts @@ -0,0 +1,101 @@ +import { NextResponse } from "next/server" +import { createHmac } from "crypto" +import { createSupabaseServerClient } from "@/lib/supabase/server" +import { createSupabaseAdminClient } from "@/lib/supabase/admin" +import { TIER_LIMITS, type SubscriptionTier } from "@asa-news/shared" +import { rateLimit } from "@/lib/rate-limit" + +export async function POST() { + const supabaseClient = await createSupabaseServerClient() + const { + data: { user }, + } = await supabaseClient.auth.getUser() + + if (!user) { + return NextResponse.json({ error: "Not authenticated" }, { status: 401 }) + } + + const rateLimitResult = rateLimit(`webhook-test:${user.id}`, 5, 60_000) + if (!rateLimitResult.success) { + return NextResponse.json({ error: "Too many requests" }, { status: 429 }) + } + + const adminClient = createSupabaseAdminClient() + const { data: profile } = await adminClient + .from("user_profiles") + .select( + "tier, webhook_url, webhook_secret, webhook_enabled" + ) + .eq("id", user.id) + .single() + + if ( + !profile || + !TIER_LIMITS[profile.tier as SubscriptionTier]?.allowsWebhooks + ) { + return NextResponse.json( + { error: "Webhooks require the developer plan" }, + { status: 403 } + ) + } + + if (!profile.webhook_url) { + return NextResponse.json( + { error: "No webhook URL configured" }, + { status: 400 } + ) + } + + const testPayload = { + event: "test", + timestamp: new Date().toISOString(), + entries: [ + { + entryIdentifier: "test-entry-000", + feedIdentifier: "test-feed-000", + title: "Test webhook delivery", + url: "https://asa.news", + author: "asa.news", + summary: "This is a test webhook payload to verify your endpoint.", + publishedAt: new Date().toISOString(), + }, + ], + } + + const payloadString = JSON.stringify(testPayload) + const headers: Record<string, string> = { + "Content-Type": "application/json", + "User-Agent": "asa.news Webhook/1.0", + } + + if (profile.webhook_secret) { + const signature = createHmac("sha256", profile.webhook_secret) + .update(payloadString) + .digest("hex") + headers["X-Asa-Signature-256"] = `sha256=${signature}` + } + + try { + const response = await fetch(profile.webhook_url, { + method: "POST", + headers, + body: payloadString, + signal: AbortSignal.timeout(10_000), + }) + + return NextResponse.json({ + delivered: true, + statusCode: response.status, + }) + } catch (deliveryError) { + const errorMessage = + deliveryError instanceof Error + ? deliveryError.message + : "Unknown error" + + return NextResponse.json({ + delivered: false, + error: errorMessage, + }) + } +} |