Shoraky commited on
Commit
10ace60
·
1 Parent(s): 01b3771

Track subscription usage and gate new analyses

Browse files
Files changed (2) hide show
  1. src/analysis.ts +7 -2
  2. src/server.ts +15 -13
src/analysis.ts CHANGED
@@ -2,6 +2,7 @@ import { createHash, timingSafeEqual } from "node:crypto";
2
  import express from "express";
3
  import type { SupabaseClient } from "@supabase/supabase-js";
4
  import { z } from "zod";
 
5
 
6
  export function stableId(value: string) {
7
  const hex = createHash("sha256").update(value).digest("hex");
@@ -44,10 +45,14 @@ export function analysisRouter(db: SupabaseClient, secret: string) {
44
  router.post("/", async (req, res, next) => {
45
  try {
46
  const input = beginSchema.parse(req.body);
 
 
 
 
47
  const playerId = z.uuid().safeParse(input.playerId);
48
  const { data, error } = await db.rpc("begin_analysis", {
49
- p_session_id: input.clientId, p_user_id: res.locals.userId,
50
- p_player_id: playerId.success ? playerId.data : stableId(`${res.locals.userId}:player:${input.playerId}`),
51
  p_reference: playerId.success ? null : input.playerId,
52
  p_name: input.name || `Session ${input.clientId.slice(0, 8)}`,
53
  });
 
2
  import express from "express";
3
  import type { SupabaseClient } from "@supabase/supabase-js";
4
  import { z } from "zod";
5
+ import { activeSubscriptionFor } from "./subscriptions.js";
6
 
7
  export function stableId(value: string) {
8
  const hex = createHash("sha256").update(value).digest("hex");
 
45
  router.post("/", async (req, res, next) => {
46
  try {
47
  const input = beginSchema.parse(req.body);
48
+ const userId = res.locals.userId as string;
49
+ if (!await activeSubscriptionFor(userId, { refresh: true })) {
50
+ return res.status(402).json({ error: "An active subscription is required to start a new analysis.", code: "subscription_required" });
51
+ }
52
  const playerId = z.uuid().safeParse(input.playerId);
53
  const { data, error } = await db.rpc("begin_analysis", {
54
+ p_session_id: input.clientId, p_user_id: userId,
55
+ p_player_id: playerId.success ? playerId.data : stableId(`${userId}:player:${input.playerId}`),
56
  p_reference: playerId.success ? null : input.playerId,
57
  p_name: input.name || `Session ${input.clientId.slice(0, 8)}`,
58
  });
src/server.ts CHANGED
@@ -114,7 +114,7 @@ async function requireActiveSubscription(req: Request, res: Response) {
114
  res.status(401).json({ error: "Authentication required." });
115
  return null;
116
  }
117
- if (!await activeSubscriptionFor(current.user.id)) {
118
  res.status(402).json({ error: "An active subscription is required to access Sporalize Labs.", code: "subscription_required" });
119
  return null;
120
  }
@@ -153,14 +153,9 @@ app.get("/api/plan-billing", async (req, res, next) => {
153
  const current = await authenticatedUser(req, res);
154
  if (!current) return res.status(401).json({ error: "Authentication required." });
155
  const userId = current.user.id;
156
- const [assignmentResult, playersResult, sessionsResult] = await Promise.all([
157
- supabaseAdmin.from("users_subscriptions").select("subscription_id,startdate,enddate").eq("user_id", userId).maybeSingle(),
158
- supabaseAdmin.from("players").select("player_id", { count: "exact", head: true }).eq("user_id", userId),
159
- supabaseAdmin.from("sessions").select("session_id", { count: "exact", head: true }).eq("user_id", userId),
160
- ]);
161
  if (assignmentResult.error) throw new Error(`Unable to load plan assignment: ${assignmentResult.error.message}`);
162
- if (playersResult.error) throw new Error(`Unable to count players: ${playersResult.error.message}`);
163
- if (sessionsResult.error) throw new Error(`Unable to count sessions: ${sessionsResult.error.message}`);
164
  const assignment = assignmentResult.data;
165
  let subscription = null;
166
  if (assignment?.subscription_id) {
@@ -180,7 +175,7 @@ app.get("/api/plan-billing", async (req, res, next) => {
180
  }
181
  }
182
  res.set("Cache-Control", "no-store");
183
- return res.json({ subscription, usage: { players: playersResult.count ?? 0, sessions: sessionsResult.count ?? 0 }, billingHistoryAvailable: false });
184
  } catch (error) { next(error); }
185
  });
186
 
@@ -421,8 +416,8 @@ app.get("/api/sessions-overview", async (req, res, next) => {
421
 
422
  app.get("/api/sessions/:sessionId/reviews", async (req, res, next) => {
423
  try {
424
- const current = await requireActiveSubscription(req, res);
425
- if (!current) return;
426
  const sessionId = z.uuid().safeParse(req.params.sessionId);
427
  if (!sessionId.success) return res.sendStatus(404);
428
 
@@ -732,8 +727,15 @@ app.delete("/api/auth/account", async (req, res, next) => {
732
  });
733
 
734
  async function proxyAnalysisRequest(req: Request, res: Response) {
735
- const current = await requireActiveSubscription(req, res);
736
- if (!current) return;
 
 
 
 
 
 
 
737
 
738
  const target = new URL(req.originalUrl, env.ANALYSIS_API_URL);
739
  const headers = new Headers();
 
114
  res.status(401).json({ error: "Authentication required." });
115
  return null;
116
  }
117
+ if (!await activeSubscriptionFor(current.user.id, { refresh: true })) {
118
  res.status(402).json({ error: "An active subscription is required to access Sporalize Labs.", code: "subscription_required" });
119
  return null;
120
  }
 
153
  const current = await authenticatedUser(req, res);
154
  if (!current) return res.status(401).json({ error: "Authentication required." });
155
  const userId = current.user.id;
156
+ const assignmentResult = await supabaseAdmin.from("users_subscriptions")
157
+ .select("subscription_id,startdate,enddate,num_players,num_sessions").eq("user_id", userId).maybeSingle();
 
 
 
158
  if (assignmentResult.error) throw new Error(`Unable to load plan assignment: ${assignmentResult.error.message}`);
 
 
159
  const assignment = assignmentResult.data;
160
  let subscription = null;
161
  if (assignment?.subscription_id) {
 
175
  }
176
  }
177
  res.set("Cache-Control", "no-store");
178
+ return res.json({ subscription, usage: { players: assignment?.num_players ?? 0, sessions: assignment?.num_sessions ?? 0 }, billingHistoryAvailable: false });
179
  } catch (error) { next(error); }
180
  });
181
 
 
416
 
417
  app.get("/api/sessions/:sessionId/reviews", async (req, res, next) => {
418
  try {
419
+ const current = await authenticatedUser(req, res);
420
+ if (!current) return res.status(401).json({ error: "Authentication required." });
421
  const sessionId = z.uuid().safeParse(req.params.sessionId);
422
  if (!sessionId.success) return res.sendStatus(404);
423
 
 
727
  });
728
 
729
  async function proxyAnalysisRequest(req: Request, res: Response) {
730
+ const current = await authenticatedUser(req, res);
731
+ if (!current) return res.status(401).json({ error: "Authentication required." });
732
+ // Archived sessions and results remain available after expiry. Only starting
733
+ // a new analysis is gated here; the internal begin-analysis route repeats
734
+ // this check before creating its player/session rows.
735
+ if (req.method === "POST" && req.path === "/analyze") {
736
+ const active = await activeSubscriptionFor(current.user.id, { refresh: true });
737
+ if (!active) return res.status(402).json({ error: "An active subscription is required to create or change Labs data.", code: "subscription_required" });
738
+ }
739
 
740
  const target = new URL(req.originalUrl, env.ANALYSIS_API_URL);
741
  const headers = new Headers();