summaryrefslogtreecommitdiff
path: root/apps/web/app/api/account
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/account
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/account')
-rw-r--r--apps/web/app/api/account/data/route.ts96
-rw-r--r--apps/web/app/api/account/route.ts27
2 files changed, 123 insertions, 0 deletions
diff --git a/apps/web/app/api/account/data/route.ts b/apps/web/app/api/account/data/route.ts
new file mode 100644
index 0000000..dbee725
--- /dev/null
+++ b/apps/web/app/api/account/data/route.ts
@@ -0,0 +1,96 @@
+import { NextResponse } from "next/server"
+import { createSupabaseServerClient } from "@/lib/supabase/server"
+
+export async function GET() {
+ const supabaseClient = await createSupabaseServerClient()
+ const {
+ data: { user },
+ } = await supabaseClient.auth.getUser()
+
+ if (!user) {
+ return NextResponse.json({ error: "Not authenticated" }, { status: 401 })
+ }
+
+ const [
+ profileResult,
+ subscriptionsResult,
+ foldersResult,
+ mutedKeywordsResult,
+ customFeedsResult,
+ entryStatesResult,
+ highlightsResult,
+ sharedEntriesResult,
+ savedEntriesResult,
+ ] = await Promise.all([
+ supabaseClient
+ .from("user_profiles")
+ .select("id, display_name, tier, created_at")
+ .eq("id", user.id)
+ .single(),
+ supabaseClient
+ .from("subscriptions")
+ .select("id, feed_id, folder_id, custom_title, created_at, feeds(title, url)")
+ .eq("user_id", user.id),
+ supabaseClient
+ .from("folders")
+ .select("id, name, position, created_at")
+ .eq("user_id", user.id),
+ supabaseClient
+ .from("muted_keywords")
+ .select("id, keyword, created_at")
+ .eq("user_id", user.id),
+ supabaseClient
+ .from("custom_feeds")
+ .select("id, name, query, position, created_at")
+ .eq("user_id", user.id),
+ supabaseClient
+ .from("user_entry_states")
+ .select("entry_id, read, saved, updated_at")
+ .eq("user_id", user.id),
+ supabaseClient
+ .from("user_highlights")
+ .select(
+ "id, entry_id, highlighted_text, note, color, text_offset, text_length, created_at, entries(title, url)"
+ )
+ .eq("user_id", user.id),
+ supabaseClient
+ .from("shared_entries")
+ .select("id, entry_id, share_token, created_at, entries(title, url)")
+ .eq("user_id", user.id),
+ supabaseClient
+ .from("user_entry_states")
+ .select(
+ "entries(id, title, url, author, summary, published_at, feeds(title, url))"
+ )
+ .eq("user_id", user.id)
+ .eq("saved", true),
+ ])
+
+ const exportData = {
+ exportedAt: new Date().toISOString(),
+ account: {
+ emailAddress: user.email,
+ ...profileResult.data,
+ },
+ subscriptions: subscriptionsResult.data ?? [],
+ folders: foldersResult.data ?? [],
+ mutedKeywords: mutedKeywordsResult.data ?? [],
+ customFeeds: customFeedsResult.data ?? [],
+ entryStates: entryStatesResult.data ?? [],
+ highlights: highlightsResult.data ?? [],
+ sharedEntries: sharedEntriesResult.data ?? [],
+ savedEntries:
+ (savedEntriesResult.data ?? []).map(
+ (row) => (row as Record<string, unknown>).entries
+ ) ?? [],
+ }
+
+ const jsonString = JSON.stringify(exportData, null, 2)
+
+ return new Response(jsonString, {
+ headers: {
+ "Content-Type": "application/json",
+ "Content-Disposition": `attachment; filename="asa-news-gdpr-export-${new Date().toISOString().slice(0, 10)}.json"`,
+ },
+ })
+}
diff --git a/apps/web/app/api/account/route.ts b/apps/web/app/api/account/route.ts
new file mode 100644
index 0000000..6b1bc2d
--- /dev/null
+++ b/apps/web/app/api/account/route.ts
@@ -0,0 +1,27 @@
+import { NextResponse } from "next/server"
+import { createSupabaseServerClient } from "@/lib/supabase/server"
+import { createSupabaseAdminClient } from "@/lib/supabase/admin"
+
+export async function DELETE() {
+ const supabaseClient = await createSupabaseServerClient()
+ const {
+ data: { user },
+ } = await supabaseClient.auth.getUser()
+
+ if (!user) {
+ return NextResponse.json({ error: "Not authenticated" }, { status: 401 })
+ }
+
+ const adminClient = createSupabaseAdminClient()
+
+ const { error } = await adminClient.auth.admin.deleteUser(user.id)
+
+ if (error) {
+ return NextResponse.json(
+ { error: "Failed to delete account" },
+ { status: 500 }
+ )
+ }
+
+ return new Response(null, { status: 204 })
+}