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/lib/api-auth.ts | |
| 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/lib/api-auth.ts')
| -rw-r--r-- | apps/web/lib/api-auth.ts | 80 |
1 files changed, 80 insertions, 0 deletions
diff --git a/apps/web/lib/api-auth.ts b/apps/web/lib/api-auth.ts new file mode 100644 index 0000000..309fbe9 --- /dev/null +++ b/apps/web/lib/api-auth.ts @@ -0,0 +1,80 @@ +import { createSupabaseAdminClient } from "@/lib/supabase/admin" +import { hashApiKey } from "@/lib/api-key" +import { rateLimit } from "@/lib/rate-limit" + +interface AuthenticatedApiUser { + userIdentifier: string + tier: string +} + +export async function authenticateApiRequest( + request: Request +): Promise< + | { authenticated: true; user: AuthenticatedApiUser } + | { authenticated: false; status: number; error: string } +> { + const authorizationHeader = request.headers.get("authorization") + + if (!authorizationHeader?.startsWith("Bearer ")) { + return { + authenticated: false, + status: 401, + error: "Missing or invalid Authorization header", + } + } + + const apiKey = authorizationHeader.slice(7) + + if (!apiKey.startsWith("asn_")) { + return { authenticated: false, status: 401, error: "Invalid API key format" } + } + + const keyHash = hashApiKey(apiKey) + const adminClient = createSupabaseAdminClient() + + const { data: keyRow } = await adminClient + .from("api_keys") + .select("user_id") + .eq("key_hash", keyHash) + .is("revoked_at", null) + .single() + + if (!keyRow) { + return { authenticated: false, status: 401, error: "Invalid or revoked API key" } + } + + const { data: userProfile } = await adminClient + .from("user_profiles") + .select("tier") + .eq("id", keyRow.user_id) + .single() + + if (!userProfile || userProfile.tier !== "developer") { + return { + authenticated: false, + status: 403, + error: "API access requires the developer plan", + } + } + + const rateLimitResult = rateLimit(`api:${keyRow.user_id}`, 100, 60_000) + + if (!rateLimitResult.success) { + return { + authenticated: false, + status: 429, + error: `Rate limit exceeded. ${rateLimitResult.remaining} requests remaining.`, + } + } + + adminClient + .from("api_keys") + .update({ last_used_at: new Date().toISOString() }) + .eq("key_hash", keyHash) + .then(() => {}) + + return { + authenticated: true, + user: { userIdentifier: keyRow.user_id, tier: userProfile.tier }, + } +} |