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/billing/create-portal-session | |
| 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/billing/create-portal-session')
| -rw-r--r-- | apps/web/app/api/billing/create-portal-session/route.ts | 51 |
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 }) +} |