rhythia-api 216.0.0 → 225.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env ADDED
@@ -0,0 +1,12 @@
1
+ REDIS_PORT=18304
2
+ REDIS_USERNAME=default
3
+ REDIS_PASSWORD=yg7hYF9LPhGJRmORVIjMB8FWP2axPGto
4
+ REDIS_HOST=redis-18304.c55.eu-central-1-1.ec2.cloud.redislabs.com
5
+ CF_NAMESPACE_ID=571cf88650514eeba649517a75ede74a
6
+ CF_API_TOKEN=Ldsh-UKJZw0feHmZOvhSCxaurwJBVW12LgnV_v38
7
+ CF_ACCOUNT_ID=571cf88650514eeba649517a75ede74a
8
+ TOKEN_SECRET=fa686bfdffd3758f6377abbc23bf3d9bdc1a0dda4a6e7f8dbdd579fa1ff6d7e2
9
+ ACCESS_BUCKET=003c245e893e8060000000003
10
+ SECRET_BUCKET=K003LpAu8X+2lJ09EB8NtPL/OZXV8ts
11
+ ADMIN_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBma2FqbmdibGxjYmR6b3lscnZwIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTcyOTAxNTMwNSwiZXhwIjoyMDQ0NTkxMzA1fQ.dKg8Wq3zZEBAH63V7q1D8a8N-d6qkB5Inm524Vmso3k
12
+ PROD_DO_NOT_WRITE_PG=postgresql://postgres:huaUpTz3d3p7w6VU@db.pfkajngbllcbdzoylrvp.supabase.co:5432/postgres
package/.yarnrc ADDED
@@ -0,0 +1 @@
1
+ registry: https://registry.npmjs.org/
package/README.md CHANGED
@@ -7,3 +7,5 @@ Install dependencies with Bun, then reach for the scripts in `package.json`; `bu
7
7
  Runtime secrets live in environment variables. Upload endpoints expect `ACCESS_BUCKET` and `SECRET_BUCKET` for S3, the purchase flow checks `BUY_SECRET`, auth helpers derive tokens from `TOKEN_SECRET`, and the Supabase admin calls need `ADMIN_KEY`. The deploy script also looks for `GIT_USER`, `GIT_KEY`, `SOURCE_BRANCH`, and `TARGET_BRANCH` when the CI job mirrors changes upstream.
8
8
 
9
9
  If you need to point at a different stack, call `setEnvironment` with `development`, `testing`, or `production` before making requests so the helper talks to the right Rhythia host.
10
+
11
+ //
@@ -102,7 +102,7 @@ export async function handler({
102
102
  .single();
103
103
 
104
104
  if (!userData) {
105
- return NextResponse.json({ error: "Bad user" });
105
+ return NextResponse.json({ error: "Bad user!" });
106
106
  }
107
107
 
108
108
  if (
@@ -4,6 +4,7 @@ import { protectedApi, validUser } from "../utils/requestUtils";
4
4
  import { supabase } from "../utils/supabase";
5
5
  import { getUserBySession } from "../utils/getUserBySession";
6
6
  import { User } from "@supabase/supabase-js";
7
+ import { invalidateCache, invalidateCachePrefix } from "../utils/cache";
7
8
 
8
9
  export const Schema = {
9
10
  input: z.strictObject({
@@ -54,12 +55,13 @@ export async function handler({
54
55
  if (!userData) return NextResponse.json({ error: "No user." });
55
56
  if (!beatmapData) return NextResponse.json({ error: "No beatmap." });
56
57
 
57
- if (userData.id !== pageData.owner) {
58
- const isDev =
59
- (userData.badges as string[]).includes("Developer") ||
60
- (userData.badges as string[]).includes("Global Moderator");
58
+ const badges = (userData.badges || []) as string[];
59
+ const hasDeletionRole = badges.includes("RCT") || badges.includes("MMT");
61
60
 
62
- if (!isDev) return NextResponse.json({ error: "Non-authz user." });
61
+ if (!hasDeletionRole) {
62
+ return NextResponse.json({
63
+ error: "Only RCT or MMT members can delete beatmaps.",
64
+ });
63
65
  }
64
66
 
65
67
  if (pageData.status !== "UNRANKED")
@@ -73,5 +75,10 @@ export async function handler({
73
75
  .delete()
74
76
  .eq("beatmapHash", beatmapData.beatmapHash);
75
77
 
78
+ await invalidateCache(`beatmap-comments:${id}`);
79
+ if (pageData.latestBeatmapHash) {
80
+ await invalidateCachePrefix(`beatmap-scores:${pageData.latestBeatmapHash}`);
81
+ }
82
+
76
83
  return NextResponse.json({});
77
84
  }
@@ -5,6 +5,7 @@ import { protectedApi, validUser } from "../utils/requestUtils";
5
5
  import { supabase } from "../utils/supabase";
6
6
  import { getUserBySession } from "../utils/getUserBySession";
7
7
  import { User } from "@supabase/supabase-js";
8
+ import { invalidateCache, invalidateCachePrefix } from "../utils/cache";
8
9
 
9
10
  // Define supported admin operations and their parameter types
10
11
  const adminOperations = {
@@ -148,9 +149,13 @@ export async function handler(
148
149
 
149
150
  // Execute the requested admin operation
150
151
  try {
151
- let result;
152
+ let result: { data?: any; error?: any } | null = null;
152
153
  const operation = data.data.operation;
153
154
  const params = data.data.params as any;
155
+ const targetUserId =
156
+ "userId" in params && typeof params.userId === "number"
157
+ ? params.userId
158
+ : null;
154
159
 
155
160
  switch (operation) {
156
161
  case "deleteUser":
@@ -226,6 +231,8 @@ export async function handler(
226
231
  badges: JSON.parse(params.badges),
227
232
  })
228
233
  .select();
234
+ } else {
235
+ result = { data: null, error: { message: "Unauthorized" } };
229
236
  }
230
237
  break;
231
238
 
@@ -252,6 +259,8 @@ export async function handler(
252
259
  } else {
253
260
  result = { data: targetUser, error: null };
254
261
  }
262
+ } else {
263
+ result = { data: null, error: { message: "Unauthorized" } };
255
264
  }
256
265
  break;
257
266
 
@@ -275,6 +284,8 @@ export async function handler(
275
284
  badges: updatedBadges,
276
285
  })
277
286
  .select();
287
+ } else {
288
+ result = { data: null, error: { message: "Unauthorized" } };
278
289
  }
279
290
  break;
280
291
 
@@ -345,7 +356,7 @@ export async function handler(
345
356
  details: { params },
346
357
  });
347
358
 
348
- if (result.error) {
359
+ if (result?.error) {
349
360
  return NextResponse.json(
350
361
  {
351
362
  success: false,
@@ -355,9 +366,34 @@ export async function handler(
355
366
  );
356
367
  }
357
368
 
369
+ if (targetUserId !== null && !result?.error) {
370
+ await invalidateCachePrefix(`userscore:${targetUserId}`);
371
+
372
+ if (
373
+ operation === "removeAllScores" ||
374
+ operation === "invalidateRankedScores" ||
375
+ operation === "deleteUser"
376
+ ) {
377
+ const { data: beatmapHashes } = await supabase
378
+ .from("scores")
379
+ .select("beatmapHash")
380
+ .eq("userId", targetUserId);
381
+
382
+ const uniqueHashes = new Set(
383
+ (beatmapHashes || [])
384
+ .map((row) => row.beatmapHash)
385
+ .filter((hash): hash is string => Boolean(hash))
386
+ );
387
+
388
+ for (const hash of uniqueHashes) {
389
+ await invalidateCachePrefix(`beatmap-scores:${hash}`);
390
+ }
391
+ }
392
+ }
393
+
358
394
  return NextResponse.json({
359
395
  success: true,
360
- result: result.data,
396
+ result: result?.data,
361
397
  });
362
398
  } catch (err: any) {
363
399
  return NextResponse.json(
@@ -1,10 +1,12 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import z from "zod";
3
3
  import { supabase } from "../utils/supabase";
4
+ import { getActiveProfileIdSet } from "../utils/activityStatus";
4
5
 
5
6
  export const Schema = {
6
7
  input: z.strictObject({
7
8
  limit: z.number().min(1).max(100).optional().default(100),
9
+ include_inactive: z.boolean().optional().default(false),
8
10
  }),
9
11
  output: z.object({
10
12
  leaderboard: z.array(
@@ -21,13 +23,20 @@ export const Schema = {
21
23
  };
22
24
 
23
25
  export async function POST(request: Request): Promise<NextResponse> {
24
- return handler({ limit: 100 });
26
+ let body: unknown = {};
27
+ try {
28
+ body = await request.json();
29
+ } catch {}
30
+
31
+ return handler(Schema.input.parse(body));
25
32
  }
26
33
 
27
34
  export async function handler({
28
35
  limit = 100,
36
+ include_inactive = false,
29
37
  }: {
30
38
  limit?: number;
39
+ include_inactive?: boolean;
31
40
  }): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
32
41
  try {
33
42
  const { data: leaderboard, error } = await supabase.rpc(
@@ -49,9 +58,22 @@ export async function handler({
49
58
  );
50
59
  }
51
60
 
61
+ const entries = leaderboard || [];
62
+
63
+ if (include_inactive) {
64
+ return NextResponse.json({
65
+ leaderboard: entries,
66
+ total_count: entries.length,
67
+ });
68
+ }
69
+
70
+ const activeIds = await getActiveProfileIdSet(entries.map((entry) => entry.id));
71
+
72
+ const filteredLeaderboard = entries.filter((entry) => activeIds.has(entry.id));
73
+
52
74
  return NextResponse.json({
53
- leaderboard: leaderboard || [],
54
- total_count: leaderboard?.length || 0,
75
+ leaderboard: filteredLeaderboard,
76
+ total_count: filteredLeaderboard.length,
55
77
  });
56
78
  } catch (error) {
57
79
  console.error("Badge leaderboard exception:", error);
@@ -40,7 +40,8 @@ export async function handler(
40
40
  export async function getLeaderboard(badge: string) {
41
41
  let { data: queryData, error } = await supabase
42
42
  .from("profiles")
43
- .select("flag,id,username,badges");
43
+ .select("flag,id,username,badges,scores!inner(id)")
44
+ .limit(1, { foreignTable: "scores" });
44
45
 
45
46
  const users = queryData?.filter((e) =>
46
47
  ((e.badges || []) as string[]).includes(badge)
@@ -1,6 +1,7 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import z from "zod";
3
3
  import { protectedApi, validUser } from "../utils/requestUtils";
4
+ import { getCacheValue, setCacheValue } from "../utils/cache";
4
5
  import { supabase } from "../utils/supabase";
5
6
 
6
7
  export const Schema = {
@@ -39,6 +40,15 @@ export async function handler({
39
40
  }: (typeof Schema)["input"]["_type"]): Promise<
40
41
  NextResponse<(typeof Schema)["output"]["_type"]>
41
42
  > {
43
+ const cacheKey = `beatmap-comments:${page}`;
44
+ const cachedComments = await getCacheValue<
45
+ (typeof Schema)["output"]["_type"]["comments"]
46
+ >(cacheKey);
47
+
48
+ if (cachedComments) {
49
+ return NextResponse.json({ comments: cachedComments });
50
+ }
51
+
42
52
  let { data: userData, error: userError } = await supabase
43
53
  .from("beatmapPageComments")
44
54
  .select(
@@ -53,5 +63,9 @@ export async function handler({
53
63
  )
54
64
  .eq("beatmapPage", page);
55
65
 
66
+ if (userData) {
67
+ await setCacheValue(cacheKey, userData);
68
+ }
69
+
56
70
  return NextResponse.json({ comments: userData! });
57
71
  }
@@ -1,12 +1,14 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import z from "zod";
3
3
  import { protectedApi } from "../utils/requestUtils";
4
+ import { getCacheValue, setCacheValue } from "../utils/cache";
4
5
  import { supabase } from "../utils/supabase";
5
6
 
6
7
  export const Schema = {
7
8
  input: z.strictObject({
8
9
  session: z.string(),
9
10
  id: z.number(),
11
+ limit: z.number().min(1).max(200).default(50),
10
12
  }),
11
13
  output: z.object({
12
14
  error: z.string().optional(),
@@ -70,6 +72,8 @@ export async function handler(
70
72
  data: (typeof Schema)["input"]["_type"],
71
73
  req: Request
72
74
  ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
75
+ const limit = data.limit ?? 50;
76
+
73
77
  let { data: beatmapPage, error: errorlast } = await supabase
74
78
  .from("beatmapPages")
75
79
  .select(
@@ -97,19 +101,38 @@ export async function handler(
97
101
  .eq("id", data.id)
98
102
  .single();
99
103
 
100
- const { data: scoreData, error } = await supabase.rpc(
101
- "get_top_scores_for_beatmap",
102
- { beatmap_hash: beatmapPage?.latestBeatmapHash || "" }
103
- );
104
+ if (!beatmapPage) return NextResponse.json({});
105
+
106
+ const beatmapHash = beatmapPage?.latestBeatmapHash || "";
107
+ const isCacheable =
108
+ beatmapPage?.status === "RANKED" || beatmapPage?.status === "APPROVED";
109
+ const cacheKey = `beatmap-scores:${beatmapHash}:limit=${limit}`;
104
110
 
105
- if (error) {
106
- return NextResponse.json({ error: JSON.stringify(error) });
107
- }
111
+ // let scoreData: any[] | null = null;
108
112
 
109
- if (!beatmapPage) return NextResponse.json({});
113
+ // if (isCacheable && beatmapHash) {
114
+ // scoreData = await getCacheValue<any[]>(cacheKey);
115
+ // }
116
+
117
+ // if (!scoreData) {
118
+ // const { data: rpcScores, error } = await supabase.rpc(
119
+ // "get_top_scores_for_beatmap",
120
+ // { beatmap_hash: beatmapHash }
121
+ // );
122
+
123
+ // if (error) {
124
+ // return NextResponse.json({ error: JSON.stringify(error) });
125
+ // }
126
+
127
+ // scoreData = (rpcScores || []).slice(0, limit);
128
+
129
+ // if (isCacheable && beatmapHash) {
130
+ // await setCacheValue(cacheKey, scoreData);
131
+ // }
132
+ // }
110
133
 
111
134
  return NextResponse.json({
112
- scores: scoreData.map((score: any) => ({
135
+ scores: [].map((score: any) => ({
113
136
  id: score.id,
114
137
  awarded_sp: score.awarded_sp,
115
138
  created_at: score.created_at,
@@ -1,12 +1,15 @@
1
1
  import { NextResponse } from "next/server";
2
2
  import z from "zod";
3
3
  import { protectedApi } from "../utils/requestUtils";
4
+ import { getCacheValue, setCacheValue } from "../utils/cache";
4
5
  import { supabase } from "../utils/supabase";
6
+ import { getActiveProfileIdSet } from "../utils/activityStatus";
5
7
 
6
8
  export const Schema = {
7
9
  input: z.strictObject({
8
10
  session: z.string(),
9
11
  mapId: z.string(),
12
+ limit: z.number().min(1).max(200).default(50),
10
13
  }),
11
14
  output: z.object({
12
15
  error: z.string().optional(),
@@ -66,6 +69,8 @@ export async function handler(
66
69
  data: (typeof Schema)["input"]["_type"],
67
70
  req: Request
68
71
  ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
72
+ const limit = data.limit ?? 50;
73
+
69
74
  let { data: beatmapPage, error: errorlast } = await supabase
70
75
  .from("beatmapPages")
71
76
  .select(
@@ -94,17 +99,44 @@ export async function handler(
94
99
 
95
100
  if (!beatmapPage) return NextResponse.json({});
96
101
 
97
- const { data: scoreData, error } = await supabase.rpc(
98
- "get_top_scores_for_beatmap",
99
- { beatmap_hash: beatmapPage?.latestBeatmapHash || "" }
100
- );
102
+ const beatmapHash = beatmapPage?.latestBeatmapHash || "";
103
+ const isCacheable =
104
+ beatmapPage?.status === "RANKED" || beatmapPage?.status === "APPROVED";
105
+ const cacheKey = `beatmap-scores:${beatmapHash}`;
106
+
107
+ let scoreData: any[] | null = null;
108
+
109
+ if (isCacheable && beatmapHash) {
110
+ scoreData = await getCacheValue<any[]>(cacheKey);
111
+ }
101
112
 
102
- if (error) {
103
- return NextResponse.json({ error: JSON.stringify(error) });
113
+ if (!scoreData) {
114
+ const { data: rpcScores, error } = await supabase.rpc(
115
+ "get_top_scores_for_beatmap",
116
+ { beatmap_hash: beatmapHash }
117
+ );
118
+
119
+ if (error) {
120
+ return NextResponse.json({ error: JSON.stringify(error) });
121
+ }
122
+
123
+ scoreData = (rpcScores || []).slice(0, 200);
124
+
125
+ if (isCacheable && beatmapHash) {
126
+ await setCacheValue(cacheKey, scoreData);
127
+ }
104
128
  }
105
129
 
130
+ const userIds = Array.from(
131
+ new Set((scoreData || []).map((score) => score.userid).filter(Boolean))
132
+ );
133
+ const activeUserIds = await getActiveProfileIdSet(userIds);
134
+ const visibleScores = (scoreData || [])
135
+ .filter((score) => activeUserIds.has(score.userid))
136
+ .slice(0, limit);
137
+
106
138
  return NextResponse.json({
107
- scores: scoreData.map((score: any) => ({
139
+ scores: visibleScores.map((score: any) => ({
108
140
  id: score.id,
109
141
  awarded_sp: score.awarded_sp,
110
142
  created_at: score.created_at,