File size: 3,665 Bytes
d1ceac4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57b63b8
 
d1ceac4
 
 
57b63b8
 
 
 
 
d1ceac4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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;
}