summaryrefslogtreecommitdiff
path: root/apps/web/app/api/webhook-config/test/route.ts
blob: 81c394221693c7129b4743d930adc7e73502a445 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import { NextResponse } from "next/server"
import { createHmac } from "crypto"
import { createSupabaseServerClient } from "@/lib/supabase/server"
import { TIER_LIMITS, type SubscriptionTier } from "@asa-news/shared"
import { rateLimit } from "@/lib/rate-limit"
import { validateWebhookUrl } from "@/lib/validate-webhook-url"
import { checkBotId } from "botid/server"

export async function POST() {
  const botVerification = await checkBotId()
  if (botVerification.isBot) {
    return NextResponse.json({ error: "access denied" }, { status: 403 })
  }

  const supabaseClient = await createSupabaseServerClient()
  const {
    data: { user },
  } = await supabaseClient.auth.getUser()

  if (!user) {
    return NextResponse.json({ error: "not authenticated" }, { status: 401 })
  }

  const rateLimitResult = await rateLimit(`webhook-test:${user.id}`, 5, 60_000)
  if (!rateLimitResult.success) {
    return NextResponse.json({ error: "too many requests" }, { status: 429 })
  }

  const { data: profile } = await supabaseClient
    .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 validationResult = await validateWebhookUrl(profile.webhook_url)
  if (!validationResult.valid) {
    return NextResponse.json(
      { error: validationResult.error },
      { 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,
    })
  }
}