diff options
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 }, + } +} |