Spaces:
Running
Running
Download src/subscriptions.ts from Shoraky/Sporalize-api: direct link, hf CLI and curl.
- Browser
- Download file 3.67 kB
-
https://huggingface.co/spaces/Shoraky/Sporalize-api/resolve/main/src/subscriptions.ts
- Command line
-
hf download hf://spaces/Shoraky/Sporalize-api/src/subscriptions.ts
-
curl -L -o subscriptions.ts https://huggingface.co/spaces/Shoraky/Sporalize-api/resolve/main/src/subscriptions.ts
3.67 kB
| import { supabaseAdmin } from "./supabase.js"; | |
| type AssignSubscriptionInput = { | |
| userId: string; | |
| subscriptionId: string; | |
| startDate: string; | |
| endDate: string; | |
| }; | |
| /** | |
| * Persist a paid subscription after a payment provider's webhook has been | |
| * authenticated and its payment has been confirmed. Never call this directly | |
| * from a browser request. | |
| */ | |
| export async function assignPaidSubscription(input: AssignSubscriptionInput) { | |
| const { userId, subscriptionId, startDate, endDate } = input; | |
| const isoDate = /^\d{4}-\d{2}-\d{2}$/; | |
| if (!isoDate.test(startDate) || !isoDate.test(endDate) || endDate < startDate) { | |
| throw new Error("Invalid subscription period."); | |
| } | |
| const { data: plan, error: planError } = await supabaseAdmin | |
| .from("subscriptions") | |
| .select("subscription_id") | |
| .eq("subscription_id", subscriptionId) | |
| .maybeSingle(); | |
| if (planError) throw new Error(`Unable to validate subscription plan: ${planError.message}`); | |
| if (!plan) throw new Error("Subscription plan was not found."); | |
| const { data, error } = await supabaseAdmin | |
| .from("users_subscriptions") | |
| .upsert({ user_id: userId, subscription_id: subscriptionId, startdate: startDate, enddate: endDate }, { onConflict: "user_id" }) | |
| .select("user_id,subscription_id,startdate,enddate") | |
| .single(); | |
| if (error) throw new Error(`Unable to assign paid subscription: ${error.message}`); | |
| invalidateSubscriptionCache(userId); | |
| return data; | |
| } | |
| async function loadActiveSubscription(userId: string) { | |
| const today = new Date().toISOString().slice(0, 10); | |
| const { data: assignment, error } = await supabaseAdmin.from("users_subscriptions") | |
| .select("subscription_id,startdate,enddate,plan:subscriptions(subscription_id,plan,price,player_limit,session_limit)") | |
| .eq("user_id", userId).maybeSingle(); | |
| if (error) throw new Error(`Unable to check subscription: ${error.message}`); | |
| if (!assignment?.subscription_id || assignment.startdate > today || assignment.enddate < today) return null; | |
| // The FK is many-to-one; untyped Supabase clients infer embedded rows as arrays. | |
| const plan = assignment.plan as unknown as { | |
| subscription_id: string; plan: string; price: number; | |
| player_limit: number | null; session_limit: number | null; | |
| } | null; | |
| return plan ? { ...plan, startdate: assignment.startdate, enddate: assignment.enddate } : null; | |
| } | |
| // Brief, per-user cache shared by polling, media and session requests. | |
| const accessCache = new Map<string, { value: Awaited<ReturnType<typeof loadActiveSubscription>>; expires: number }>(); | |
| const accessRequests = new Map<string, Promise<Awaited<ReturnType<typeof loadActiveSubscription>>>>(); | |
| export function invalidateSubscriptionCache(userId: string) { | |
| accessCache.delete(userId); | |
| accessRequests.delete(userId); | |
| } | |
| export async function activeSubscriptionFor(userId: string, options: { refresh?: boolean } = {}) { | |
| const cached = accessCache.get(userId); | |
| if (!options.refresh && cached && cached.expires > Date.now()) return cached.value; | |
| const existing = accessRequests.get(userId); | |
| if (existing) return existing; | |
| const pending = loadActiveSubscription(userId).then((value) => { | |
| if (accessRequests.get(userId) === pending) { | |
| if (accessCache.size >= 1000) accessCache.delete(accessCache.keys().next().value!); | |
| const tomorrow = new Date(); tomorrow.setUTCHours(24, 0, 0, 0); | |
| accessCache.set(userId, { value, expires: Math.min(Date.now() + 30_000, tomorrow.getTime()) }); | |
| } | |
| return value; | |
| }).finally(() => { if (accessRequests.get(userId) === pending) accessRequests.delete(userId); }); | |
| accessRequests.set(userId, pending); | |
| return pending; | |
| } | |