summaryrefslogtreecommitdiff
path: root/apps/web/app/api/billing/create-portal-session
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-07 01:42:57 -0800
committerFuwn <[email protected]>2026-02-07 01:42:57 -0800
commit5c5b1993edd890a80870ee05607ac5f088191d4e (patch)
treea721b76bcd49ba10826c53efc87302c7a689512f /apps/web/app/api/billing/create-portal-session
downloadasa.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/billing/create-portal-session')
-rw-r--r--apps/web/app/api/billing/create-portal-session/route.ts51
1 files changed, 51 insertions, 0 deletions
diff --git a/apps/web/app/api/billing/create-portal-session/route.ts b/apps/web/app/api/billing/create-portal-session/route.ts
new file mode 100644
index 0000000..3832c0d
--- /dev/null
+++ b/apps/web/app/api/billing/create-portal-session/route.ts
@@ -0,0 +1,51 @@
+import { NextResponse } from "next/server"
+import { headers } from "next/headers"
+import { createSupabaseServerClient } from "@/lib/supabase/server"
+import { getStripe } from "@/lib/stripe"
+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(`portal:${user.id}`, 10, 60_000)
+ if (!rateLimitResult.success) {
+ return NextResponse.json({ error: "Too many requests" }, { status: 429 })
+ }
+
+ const { data: profile, error: profileError } = await supabaseClient
+ .from("user_profiles")
+ .select("stripe_customer_identifier")
+ .eq("id", user.id)
+ .single()
+
+ if (profileError || !profile) {
+ return NextResponse.json(
+ { error: "Failed to load profile" },
+ { status: 500 }
+ )
+ }
+
+ if (!profile.stripe_customer_identifier) {
+ return NextResponse.json(
+ { error: "No billing account found" },
+ { status: 400 }
+ )
+ }
+
+ const headersList = await headers()
+ const origin = headersList.get("origin") || "http://localhost:3000"
+
+ const portalSession = await getStripe().billingPortal.sessions.create({
+ customer: profile.stripe_customer_identifier,
+ return_url: `${origin}/reader/settings`,
+ })
+
+ return NextResponse.json({ url: portalSession.url })
+}