Spaces:
Running
Running
Share player and session browsing
Browse files- src/analysis.ts +2 -2
- src/profile-images.ts +1 -5
- src/server.ts +11 -20
src/analysis.ts
CHANGED
|
@@ -62,9 +62,9 @@ export function analysisRouter(db: SupabaseClient, secret: string) {
|
|
| 62 |
});
|
| 63 |
}
|
| 64 |
const { data: player, error: playerError } = await db.from("players")
|
| 65 |
-
.select("player_id").eq("player_id", input.playerId).maybeSingle();
|
| 66 |
if (playerError) return res.status(503).json({ error: "Unable to verify the selected player." });
|
| 67 |
-
if (!player) return res.status(404).json({ error: "Choose a player from
|
| 68 |
const { data, error } = await db.rpc("begin_analysis", {
|
| 69 |
p_session_id: input.clientId, p_user_id: userId,
|
| 70 |
p_player_id: input.playerId,
|
|
|
|
| 62 |
});
|
| 63 |
}
|
| 64 |
const { data: player, error: playerError } = await db.from("players")
|
| 65 |
+
.select("player_id").eq("player_id", input.playerId).eq("user_id", userId).maybeSingle();
|
| 66 |
if (playerError) return res.status(503).json({ error: "Unable to verify the selected player." });
|
| 67 |
+
if (!player) return res.status(404).json({ error: "Choose a player from your own roster before starting analysis." });
|
| 68 |
const { data, error } = await db.rpc("begin_analysis", {
|
| 69 |
p_session_id: input.clientId, p_user_id: userId,
|
| 70 |
p_player_id: input.playerId,
|
src/profile-images.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { randomUUID } from "node:crypto";
|
|
| 2 |
import express, { type Express, type Request, type Response } from "express";
|
| 3 |
import { z } from "zod";
|
| 4 |
import { authenticatedUser } from "./auth.js";
|
| 5 |
-
import { activeSubscriptionFor } from "./subscriptions.js";
|
| 6 |
import { supabaseAdmin } from "./supabase.js";
|
| 7 |
|
| 8 |
const BUCKET = "profile-images";
|
|
@@ -121,12 +120,9 @@ export function registerProfileImageRoutes(app: Express) {
|
|
| 121 |
if (!current) return res.status(401).json({ error: "Authentication required." });
|
| 122 |
const id = playerIdSchema.safeParse(req.params.playerId);
|
| 123 |
if (!id.success) return res.status(404).json({ error: "Player not found." });
|
| 124 |
-
const { data, error } = await supabaseAdmin.from("players").select("
|
| 125 |
if (error) throw new Error(`Unable to load player image: ${error.message}`);
|
| 126 |
if (!data) return res.status(404).json({ error: "Player not found." });
|
| 127 |
-
if (data.user_id !== current.user.id && !await activeSubscriptionFor(current.user.id, { refresh: true })) {
|
| 128 |
-
return res.status(403).json({ error: "An active subscription is required to view shared player profiles.", code: "subscription_required" });
|
| 129 |
-
}
|
| 130 |
return await sendImage(res, safePreviousPath("players", id.data, data?.image_path));
|
| 131 |
} catch (error) { next(error); }
|
| 132 |
});
|
|
|
|
| 2 |
import express, { type Express, type Request, type Response } from "express";
|
| 3 |
import { z } from "zod";
|
| 4 |
import { authenticatedUser } from "./auth.js";
|
|
|
|
| 5 |
import { supabaseAdmin } from "./supabase.js";
|
| 6 |
|
| 7 |
const BUCKET = "profile-images";
|
|
|
|
| 120 |
if (!current) return res.status(401).json({ error: "Authentication required." });
|
| 121 |
const id = playerIdSchema.safeParse(req.params.playerId);
|
| 122 |
if (!id.success) return res.status(404).json({ error: "Player not found." });
|
| 123 |
+
const { data, error } = await supabaseAdmin.from("players").select("image_path").eq("player_id", id.data).maybeSingle();
|
| 124 |
if (error) throw new Error(`Unable to load player image: ${error.message}`);
|
| 125 |
if (!data) return res.status(404).json({ error: "Player not found." });
|
|
|
|
|
|
|
|
|
|
| 126 |
return await sendImage(res, safePreviousPath("players", id.data, data?.image_path));
|
| 127 |
} catch (error) { next(error); }
|
| 128 |
});
|
src/server.ts
CHANGED
|
@@ -202,13 +202,13 @@ app.get("/api/owned-players", async (req, res, next) => {
|
|
| 202 |
try {
|
| 203 |
const current = await authenticatedUser(req, res);
|
| 204 |
if (!current) return res.status(401).json({ error: "Authentication required." });
|
| 205 |
-
const subscribed = Boolean(await activeSubscriptionFor(current.user.id, { refresh: true }));
|
| 206 |
const { data, error } = await supabaseAdmin.from("players")
|
| 207 |
.select("player_id,user_id,fname,lname,position,club,gender,image_path")
|
| 208 |
.order("fname", { ascending: true });
|
| 209 |
if (error) throw new Error(`Unable to load players: ${error.message}`);
|
| 210 |
-
|
| 211 |
-
|
|
|
|
| 212 |
playerId: player.player_id,
|
| 213 |
name: [player.fname, player.lname].filter(Boolean).join(" "),
|
| 214 |
isOwner: player.user_id === current.user.id,
|
|
@@ -312,24 +312,22 @@ app.put("/api/players/:playerId", async (req, res, next) => {
|
|
| 312 |
} catch (error) { next(error); }
|
| 313 |
});
|
| 314 |
|
| 315 |
-
//
|
|
|
|
| 316 |
app.get("/api/players-overview", async (req, res, next) => {
|
| 317 |
try {
|
| 318 |
const current = await authenticatedUser(req, res);
|
| 319 |
if (!current) return res.status(401).json({ error: "Authentication required." });
|
| 320 |
|
| 321 |
-
const
|
| 322 |
-
let playersQuery = supabaseAdmin.from("players")
|
| 323 |
.select("player_id,user_id,fname,lname,bdate,height,weight,position,club,gender,image_path")
|
| 324 |
.order("fname", { ascending: true });
|
| 325 |
-
if (!subscription) playersQuery = playersQuery.eq("user_id", current.user.id);
|
| 326 |
const { data: rows, error } = await playersQuery;
|
| 327 |
if (error) throw new Error(`Unable to load players: ${error.message}`);
|
| 328 |
|
| 329 |
const playerIds = (rows ?? []).map((row) => row.player_id);
|
| 330 |
let sessionsQuery = supabaseAdmin.from("sessions").select("session_id,player_id,name,created_at")
|
| 331 |
.in("player_id", playerIds).order("created_at", { ascending: false });
|
| 332 |
-
if (!subscription) sessionsQuery = sessionsQuery.eq("user_id", current.user.id);
|
| 333 |
const { data: sessions, error: sessionsError } = playerIds.length
|
| 334 |
? await sessionsQuery
|
| 335 |
: { data: [], error: null };
|
|
@@ -396,22 +394,18 @@ app.get("/api/players-overview/:playerId", async (req, res, next) => {
|
|
| 396 |
if (!current) return res.status(401).json({ error: "Authentication required." });
|
| 397 |
const playerId = z.string().uuid().safeParse(req.params.playerId);
|
| 398 |
if (!playerId.success) return res.status(404).json({ error: "Player not found." });
|
| 399 |
-
const
|
| 400 |
-
let playerQuery = supabaseAdmin.from("players").select("player_id,user_id")
|
| 401 |
.eq("player_id", playerId.data);
|
| 402 |
-
if (!subscription) playerQuery = playerQuery.eq("user_id", current.user.id);
|
| 403 |
const { data, error } = await playerQuery.maybeSingle();
|
| 404 |
if (error) throw new Error(`Unable to load player: ${error.message}`);
|
| 405 |
if (!data) return res.status(404).json({ error: "Player not found." });
|
| 406 |
|
| 407 |
-
|
| 408 |
.select("player_id,user_id,fname,lname,bdate,height,weight,position,club,gender,image_path")
|
| 409 |
.order("fname", { ascending: true });
|
| 410 |
-
if (!subscription) overviewQuery = overviewQuery.eq("user_id", current.user.id);
|
| 411 |
const { data: overview, error: overviewError } = await overviewQuery;
|
| 412 |
if (overviewError) throw new Error(`Unable to load players: ${overviewError.message}`);
|
| 413 |
-
|
| 414 |
-
if (!subscription) sessionsQuery = sessionsQuery.eq("user_id", current.user.id);
|
| 415 |
const { data: sessions, error: sessionsError } = await sessionsQuery;
|
| 416 |
if (sessionsError) throw new Error(`Unable to load sessions: ${sessionsError.message}`);
|
| 417 |
const { data: actions, error: actionsError } = (sessions ?? []).length
|
|
@@ -447,8 +441,7 @@ app.get("/api/players-overview/:playerId", async (req, res, next) => {
|
|
| 447 |
const values = sessionActions.map((action) => scoreMap.get(action.action_id)).filter((score): score is number => score != null && Number.isFinite(score));
|
| 448 |
return { sessionId: row.session_id, createdAt: Date.parse(row.created_at) || 0, score: meanPlayerScore(values), actionCount: sessionActions.length, scoredCount: values.length };
|
| 449 |
});
|
| 450 |
-
|
| 451 |
-
if (!subscription) peersQuery = peersQuery.eq("user_id", current.user.id);
|
| 452 |
const { data: peers, error: peersError } = await peersQuery;
|
| 453 |
if (peersError) throw new Error(`Unable to load roster: ${peersError.message}`);
|
| 454 |
const playerScores = new Map<string, number[]>();
|
|
@@ -482,11 +475,9 @@ app.get("/api/sessions-overview", async (req, res, next) => {
|
|
| 482 |
const current = await authenticatedUser(req, res);
|
| 483 |
if (!current) return res.status(401).json({ error: "Authentication required." });
|
| 484 |
|
| 485 |
-
const
|
| 486 |
-
let sessionsQuery = supabaseAdmin.from("sessions")
|
| 487 |
.select("session_id,player_id,user_id,name,path,created_at,status")
|
| 488 |
.order("created_at", { ascending: false });
|
| 489 |
-
if (!subscription) sessionsQuery = sessionsQuery.eq("user_id", current.user.id);
|
| 490 |
const { data: sessions, error } = await sessionsQuery;
|
| 491 |
if (error) throw new Error(`Unable to load sessions: ${error.message}`);
|
| 492 |
const playerIds = [...new Set((sessions ?? []).flatMap((row) => row.player_id ? [row.player_id] : []))];
|
|
|
|
| 202 |
try {
|
| 203 |
const current = await authenticatedUser(req, res);
|
| 204 |
if (!current) return res.status(401).json({ error: "Authentication required." });
|
|
|
|
| 205 |
const { data, error } = await supabaseAdmin.from("players")
|
| 206 |
.select("player_id,user_id,fname,lname,position,club,gender,image_path")
|
| 207 |
.order("fname", { ascending: true });
|
| 208 |
if (error) throw new Error(`Unable to load players: ${error.message}`);
|
| 209 |
+
// Analysis player choices stay scoped to the signed-in user's own roster.
|
| 210 |
+
const ownedPlayers = (data ?? []).filter((player) => player.user_id === current.user.id);
|
| 211 |
+
return res.json({ players: ownedPlayers.map((player) => ({
|
| 212 |
playerId: player.player_id,
|
| 213 |
name: [player.fname, player.lname].filter(Boolean).join(" "),
|
| 214 |
isOwner: player.user_id === current.user.id,
|
|
|
|
| 312 |
} catch (error) { next(error); }
|
| 313 |
});
|
| 314 |
|
| 315 |
+
// Roster overview is shared for viewing; player writes and analysis choices
|
| 316 |
+
// remain limited to the signed-in player's own roster.
|
| 317 |
app.get("/api/players-overview", async (req, res, next) => {
|
| 318 |
try {
|
| 319 |
const current = await authenticatedUser(req, res);
|
| 320 |
if (!current) return res.status(401).json({ error: "Authentication required." });
|
| 321 |
|
| 322 |
+
const playersQuery = supabaseAdmin.from("players")
|
|
|
|
| 323 |
.select("player_id,user_id,fname,lname,bdate,height,weight,position,club,gender,image_path")
|
| 324 |
.order("fname", { ascending: true });
|
|
|
|
| 325 |
const { data: rows, error } = await playersQuery;
|
| 326 |
if (error) throw new Error(`Unable to load players: ${error.message}`);
|
| 327 |
|
| 328 |
const playerIds = (rows ?? []).map((row) => row.player_id);
|
| 329 |
let sessionsQuery = supabaseAdmin.from("sessions").select("session_id,player_id,name,created_at")
|
| 330 |
.in("player_id", playerIds).order("created_at", { ascending: false });
|
|
|
|
| 331 |
const { data: sessions, error: sessionsError } = playerIds.length
|
| 332 |
? await sessionsQuery
|
| 333 |
: { data: [], error: null };
|
|
|
|
| 394 |
if (!current) return res.status(401).json({ error: "Authentication required." });
|
| 395 |
const playerId = z.string().uuid().safeParse(req.params.playerId);
|
| 396 |
if (!playerId.success) return res.status(404).json({ error: "Player not found." });
|
| 397 |
+
const playerQuery = supabaseAdmin.from("players").select("player_id,user_id")
|
|
|
|
| 398 |
.eq("player_id", playerId.data);
|
|
|
|
| 399 |
const { data, error } = await playerQuery.maybeSingle();
|
| 400 |
if (error) throw new Error(`Unable to load player: ${error.message}`);
|
| 401 |
if (!data) return res.status(404).json({ error: "Player not found." });
|
| 402 |
|
| 403 |
+
const overviewQuery = supabaseAdmin.from("players")
|
| 404 |
.select("player_id,user_id,fname,lname,bdate,height,weight,position,club,gender,image_path")
|
| 405 |
.order("fname", { ascending: true });
|
|
|
|
| 406 |
const { data: overview, error: overviewError } = await overviewQuery;
|
| 407 |
if (overviewError) throw new Error(`Unable to load players: ${overviewError.message}`);
|
| 408 |
+
const sessionsQuery = supabaseAdmin.from("sessions").select("session_id,player_id,name,created_at,user_id");
|
|
|
|
| 409 |
const { data: sessions, error: sessionsError } = await sessionsQuery;
|
| 410 |
if (sessionsError) throw new Error(`Unable to load sessions: ${sessionsError.message}`);
|
| 411 |
const { data: actions, error: actionsError } = (sessions ?? []).length
|
|
|
|
| 441 |
const values = sessionActions.map((action) => scoreMap.get(action.action_id)).filter((score): score is number => score != null && Number.isFinite(score));
|
| 442 |
return { sessionId: row.session_id, createdAt: Date.parse(row.created_at) || 0, score: meanPlayerScore(values), actionCount: sessionActions.length, scoredCount: values.length };
|
| 443 |
});
|
| 444 |
+
const peersQuery = supabaseAdmin.from("players").select("player_id");
|
|
|
|
| 445 |
const { data: peers, error: peersError } = await peersQuery;
|
| 446 |
if (peersError) throw new Error(`Unable to load roster: ${peersError.message}`);
|
| 447 |
const playerScores = new Map<string, number[]>();
|
|
|
|
| 475 |
const current = await authenticatedUser(req, res);
|
| 476 |
if (!current) return res.status(401).json({ error: "Authentication required." });
|
| 477 |
|
| 478 |
+
const sessionsQuery = supabaseAdmin.from("sessions")
|
|
|
|
| 479 |
.select("session_id,player_id,user_id,name,path,created_at,status")
|
| 480 |
.order("created_at", { ascending: false });
|
|
|
|
| 481 |
const { data: sessions, error } = await sessionsQuery;
|
| 482 |
if (error) throw new Error(`Unable to load sessions: ${error.message}`);
|
| 483 |
const playerIds = [...new Set((sessions ?? []).flatMap((row) => row.player_id ? [row.player_id] : []))];
|