rhythia-api 229.0.0 → 231.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.
@@ -1,78 +1,94 @@
1
- import { NextResponse } from "next/server";
2
- import z from "zod";
3
- import { protectedApi, validUser } from "../utils/requestUtils";
4
- import { supabase } from "../utils/supabase";
5
- import { getUserBySession } from "../utils/getUserBySession";
6
- import { User } from "@supabase/supabase-js";
7
-
8
- export const Schema = {
9
- input: z.strictObject({
10
- session: z.string(),
11
- mapId: z.number(),
12
- }),
13
- output: z.object({
14
- error: z.string().optional(),
15
- }),
16
- };
17
-
18
- export async function POST(request: Request) {
19
- return protectedApi({
20
- request,
21
- schema: Schema,
22
- authorization: validUser,
23
- activity: handler,
24
- });
25
- }
26
-
27
- export async function handler(data: (typeof Schema)["input"]["_type"]) {
28
- const user = (await getUserBySession(data.session)) as User;
29
- let { data: queryUserData, error: userError } = await supabase
30
- .from("profiles")
31
- .select("*")
32
- .eq("uid", user.id)
33
- .single();
34
-
35
- if (!queryUserData) {
36
- return NextResponse.json({ error: "Can't find user" });
37
- }
38
-
39
- const tags = (queryUserData?.badges || []) as string[];
40
-
41
- if (!tags.includes("MMT")) {
42
- return NextResponse.json({ error: "Only MMTs can approve maps!" });
43
- }
44
-
45
- const { data: mapData, error } = await supabase
46
- .from("beatmapPages")
47
- .select("id,nominations,owner")
48
- .eq("id", data.mapId)
49
- .single();
50
-
51
- if (!mapData) {
52
- return NextResponse.json({ error: "Bad map" });
53
- }
54
-
55
- if (mapData.owner == queryUserData.id) {
56
- return NextResponse.json({ error: "Can't approve own map" });
57
- }
58
-
59
- if ((mapData.nominations as number[])!.length < 2) {
60
- return NextResponse.json({
61
- error: "Maps can get approved only if they have 2 nominations",
62
- });
63
- }
64
-
65
- if ((mapData.nominations as number[]).includes(queryUserData.id)) {
66
- return NextResponse.json({
67
- error: "Can't nominate and approve",
68
- });
69
- }
70
-
71
- await supabase.from("beatmapPages").upsert({
72
- id: data.mapId,
73
- status: "RANKED",
74
- ranked_at: Date.now(),
75
- });
76
-
77
- return NextResponse.json({});
78
- }
1
+ import { NextResponse } from "next/server";
2
+ import z from "zod";
3
+ import { protectedApi, validUser } from "../utils/requestUtils";
4
+ import { supabase } from "../utils/supabase";
5
+ import { getUserBySession } from "../utils/getUserBySession";
6
+ import { User } from "@supabase/supabase-js";
7
+
8
+ const VETO_BADGES = ["Team Ranked"];
9
+
10
+ export const Schema = {
11
+ input: z.strictObject({
12
+ session: z.string(),
13
+ mapId: z.number(),
14
+ reason: z.string().min(1).max(1000),
15
+ }),
16
+ output: z.object({
17
+ error: z.string().optional(),
18
+ }),
19
+ };
20
+
21
+ export async function POST(request: Request) {
22
+ return protectedApi({
23
+ request,
24
+ schema: Schema,
25
+ authorization: validUser,
26
+ activity: handler,
27
+ });
28
+ }
29
+
30
+ export async function handler(data: (typeof Schema)["input"]["_type"]) {
31
+ const user = (await getUserBySession(data.session)) as User;
32
+ const { data: queryUserData } = await supabase
33
+ .from("profiles")
34
+ .select("*")
35
+ .eq("uid", user.id)
36
+ .single();
37
+
38
+ if (!queryUserData) {
39
+ return NextResponse.json({ error: "Can't find user" });
40
+ }
41
+
42
+ const tags = (queryUserData?.badges || []) as string[];
43
+ const hasVetoAccess = VETO_BADGES.some((badge) => tags.includes(badge));
44
+
45
+ if (!hasVetoAccess) {
46
+ return NextResponse.json({ error: "Only management can veto maps!" });
47
+ }
48
+
49
+ const { data: mapData } = await supabase
50
+ .from("beatmapPages")
51
+ .select("id,qualified")
52
+ .eq("id", data.mapId)
53
+ .single();
54
+
55
+ if (!mapData) {
56
+ return NextResponse.json({ error: "Bad map" });
57
+ }
58
+
59
+ if (!mapData.qualified) {
60
+ return NextResponse.json({
61
+ error: "Only qualified maps can be vetoed",
62
+ });
63
+ }
64
+
65
+ const { data: vetoData, error: vetoError } = await supabase
66
+ .from("vetos")
67
+ .insert({
68
+ beatmapPage: data.mapId,
69
+ user: queryUserData.id,
70
+ veto_reason: data.reason,
71
+ })
72
+ .select("id")
73
+ .single();
74
+
75
+ if (vetoError) {
76
+ return NextResponse.json({ error: vetoError.message });
77
+ }
78
+
79
+ const { error: updateError } = await supabase.from("beatmapPages").upsert({
80
+ id: data.mapId,
81
+ qualified: false,
82
+ qualifiedAt: null,
83
+ });
84
+
85
+ if (updateError) {
86
+ if (vetoData?.id) {
87
+ await supabase.from("vetos").delete().eq("id", vetoData.id);
88
+ }
89
+
90
+ return NextResponse.json({ error: updateError.message });
91
+ }
92
+
93
+ return NextResponse.json({});
94
+ }
package/handleApi.ts CHANGED
@@ -2,20 +2,16 @@ import { z } from "zod";
2
2
  let env = "development";
3
3
  import { profanity, CensorType } from "@2toad/profanity";
4
4
  export function setEnvironment(
5
- stage: "development" | "testing" | "production" | "local"
5
+ stage: "development" | "testing" | "production"
6
6
  ) {
7
7
  env = stage;
8
8
  }
9
9
  export function handleApi<
10
- T extends { url: string; input: z.ZodObject<any>; output: z.ZodObject<any> },
10
+ T extends { url: string; input: z.ZodObject<any>; output: z.ZodObject<any> }
11
11
  >(apiSchema: T) {
12
12
  profanity.whitelist.addWords(["willy"]);
13
13
  return async (input: T["input"]["_type"]): Promise<T["output"]["_type"]> => {
14
- let url = `https://${env}.rhythia.com${apiSchema.url}`;
15
- if (env == "local") {
16
- url = `http://localhost:3000${apiSchema.url}`;
17
- }
18
- const response = await fetch(url, {
14
+ const response = await fetch(`https://${env}.rhythia.com${apiSchema.url}`, {
19
15
  method: "POST",
20
16
  body: JSON.stringify(input),
21
17
  });
package/index.ts CHANGED
@@ -33,22 +33,6 @@ import { Schema as AddCollectionMap } from "./api/addCollectionMap"
33
33
  export { Schema as SchemaAddCollectionMap } from "./api/addCollectionMap"
34
34
  export const addCollectionMap = handleApi({url:"/api/addCollectionMap",...AddCollectionMap})
35
35
 
36
- // ./api/approveMap.ts API
37
-
38
- /*
39
- export const Schema = {
40
- input: z.strictObject({
41
- session: z.string(),
42
- mapId: z.number(),
43
- }),
44
- output: z.object({
45
- error: z.string().optional(),
46
- }),
47
- };*/
48
- import { Schema as ApproveMap } from "./api/approveMap"
49
- export { Schema as SchemaApproveMap } from "./api/approveMap"
50
- export const approveMap = handleApi({url:"/api/approveMap",...ApproveMap})
51
-
52
36
  // ./api/chartPublicStats.ts API
53
37
 
54
38
  /*
@@ -60,6 +44,22 @@ import { Schema as ChartPublicStats } from "./api/chartPublicStats"
60
44
  export { Schema as SchemaChartPublicStats } from "./api/chartPublicStats"
61
45
  export const chartPublicStats = handleApi({url:"/api/chartPublicStats",...ChartPublicStats})
62
46
 
47
+ // ./api/checkQualified.ts API
48
+
49
+ /*
50
+ export const Schema = {
51
+ input: z.strictObject({
52
+ secret: z.string(),
53
+ }),
54
+ output: z.object({
55
+ error: z.string().optional(),
56
+ updated: z.number(),
57
+ }),
58
+ };*/
59
+ import { Schema as CheckQualified } from "./api/checkQualified"
60
+ export { Schema as SchemaCheckQualified } from "./api/checkQualified"
61
+ export const checkQualified = handleApi({url:"/api/checkQualified",...CheckQualified})
62
+
63
63
  // ./api/createBeatmap.ts API
64
64
 
65
65
  /*
@@ -469,16 +469,30 @@ export const Schema = {
469
469
  image: z.string().nullable().optional(),
470
470
  imageLarge: z.string().nullable().optional(),
471
471
  starRating: z.number().nullable().optional(),
472
- owner: z.number().nullable().optional(),
473
- ownerUsername: z.string().nullable().optional(),
474
- ownerAvatar: z.string().nullable().optional(),
475
- status: z.string().nullable().optional(),
476
- description: z.string().nullable().optional(),
477
- tags: z.string().nullable().optional(),
478
- videoUrl: z.string().nullable().optional(),
479
- })
480
- .optional(),
481
- }),
472
+ owner: z.number().nullable().optional(),
473
+ ownerUsername: z.string().nullable().optional(),
474
+ ownerAvatar: z.string().nullable().optional(),
475
+ status: z.string().nullable().optional(),
476
+ qualified: z.boolean().nullable().optional(),
477
+ qualifiedAt: z.string().nullable().optional(),
478
+ description: z.string().nullable().optional(),
479
+ tags: z.string().nullable().optional(),
480
+ videoUrl: z.string().nullable().optional(),
481
+ vetos: z
482
+ .array(
483
+ z.object({
484
+ id: z.number(),
485
+ userId: z.number().nullable().optional(),
486
+ username: z.string().nullable().optional(),
487
+ avatar_url: z.string().nullable().optional(),
488
+ veto_reason: z.string().nullable().optional(),
489
+ created_at: z.string().nullable().optional(),
490
+ })
491
+ )
492
+ .optional(),
493
+ })
494
+ .optional(),
495
+ }),
482
496
  };*/
483
497
  import { Schema as GetBeatmapPage } from "./api/getBeatmapPage"
484
498
  export { Schema as SchemaGetBeatmapPage } from "./api/getBeatmapPage"
@@ -528,14 +542,28 @@ export const Schema = {
528
542
  beatmapFile: z.string().nullable().optional(),
529
543
  image: z.string().nullable().optional(),
530
544
  starRating: z.number().nullable().optional(),
531
- owner: z.number().nullable().optional(),
532
- ownerUsername: z.string().nullable().optional(),
533
- ownerAvatar: z.string().nullable().optional(),
534
- status: z.string().nullable().optional(),
535
- videoUrl: z.string().nullable(),
536
- })
537
- .optional(),
538
- }),
545
+ owner: z.number().nullable().optional(),
546
+ ownerUsername: z.string().nullable().optional(),
547
+ ownerAvatar: z.string().nullable().optional(),
548
+ status: z.string().nullable().optional(),
549
+ qualified: z.boolean().nullable().optional(),
550
+ qualifiedAt: z.string().nullable().optional(),
551
+ videoUrl: z.string().nullable(),
552
+ vetos: z
553
+ .array(
554
+ z.object({
555
+ id: z.number(),
556
+ userId: z.number().nullable().optional(),
557
+ username: z.string().nullable().optional(),
558
+ avatar_url: z.string().nullable().optional(),
559
+ veto_reason: z.string().nullable().optional(),
560
+ created_at: z.string().nullable().optional(),
561
+ })
562
+ )
563
+ .optional(),
564
+ })
565
+ .optional(),
566
+ }),
539
567
  };*/
540
568
  import { Schema as GetBeatmapPageById } from "./api/getBeatmapPageById"
541
569
  export { Schema as SchemaGetBeatmapPageById } from "./api/getBeatmapPageById"
@@ -1116,11 +1144,12 @@ export const Schema = {
1116
1144
  awarded_sp: z.number().nullable(),
1117
1145
  beatmapHash: z.string().nullable(),
1118
1146
  created_at: z.string(),
1119
- id: z.number(),
1120
- misses: z.number().nullable(),
1121
- passed: z.boolean().nullable(),
1122
- songId: z.string().nullable(),
1123
- userId: z.number().nullable(),
1147
+ id: z.number(),
1148
+ misses: z.number().nullable(),
1149
+ passed: z.boolean().nullable(),
1150
+ replay_url: z.string().nullable().optional(),
1151
+ songId: z.string().nullable(),
1152
+ userId: z.number().nullable(),
1124
1153
  beatmapDifficulty: z.number().optional().nullable(),
1125
1154
  beatmapNotes: z.number().optional().nullable(),
1126
1155
  beatmapTitle: z.string().optional().nullable(),
@@ -1135,11 +1164,12 @@ export const Schema = {
1135
1164
  id: z.number(),
1136
1165
  awarded_sp: z.number().nullable(),
1137
1166
  created_at: z.string(),
1138
- misses: z.number().nullable(),
1139
- mods: z.record(z.unknown()),
1140
- passed: z.boolean().nullable(),
1141
- songId: z.string().nullable(),
1142
- speed: z.number().nullable(),
1167
+ misses: z.number().nullable(),
1168
+ mods: z.record(z.unknown()),
1169
+ passed: z.boolean().nullable(),
1170
+ replay_url: z.string().nullable().optional(),
1171
+ songId: z.string().nullable(),
1172
+ speed: z.number().nullable(),
1143
1173
  spin: z.boolean(),
1144
1174
  beatmapHash: z.string().nullable(),
1145
1175
  beatmapTitle: z.string().nullable(),
@@ -1154,11 +1184,12 @@ export const Schema = {
1154
1184
  awarded_sp: z.number().nullable(),
1155
1185
  beatmapHash: z.string().nullable(),
1156
1186
  created_at: z.string(),
1157
- id: z.number(),
1158
- misses: z.number().nullable(),
1159
- passed: z.boolean().nullable(),
1160
- rank: z.string().nullable(),
1161
- songId: z.string().nullable(),
1187
+ id: z.number(),
1188
+ misses: z.number().nullable(),
1189
+ passed: z.boolean().nullable(),
1190
+ replay_url: z.string().nullable().optional(),
1191
+ rank: z.string().nullable(),
1192
+ songId: z.string().nullable(),
1162
1193
  userId: z.number().nullable(),
1163
1194
  beatmapDifficulty: z.number().optional().nullable(),
1164
1195
  beatmapNotes: z.number().optional().nullable(),
@@ -1213,22 +1244,6 @@ import { Schema as GetVideoUploadUrl } from "./api/getVideoUploadUrl"
1213
1244
  export { Schema as SchemaGetVideoUploadUrl } from "./api/getVideoUploadUrl"
1214
1245
  export const getVideoUploadUrl = handleApi({url:"/api/getVideoUploadUrl",...GetVideoUploadUrl})
1215
1246
 
1216
- // ./api/nominateMap.ts API
1217
-
1218
- /*
1219
- export const Schema = {
1220
- input: z.strictObject({
1221
- session: z.string(),
1222
- mapId: z.number(),
1223
- }),
1224
- output: z.object({
1225
- error: z.string().optional(),
1226
- }),
1227
- };*/
1228
- import { Schema as NominateMap } from "./api/nominateMap"
1229
- export { Schema as SchemaNominateMap } from "./api/nominateMap"
1230
- export const nominateMap = handleApi({url:"/api/nominateMap",...NominateMap})
1231
-
1232
1247
  // ./api/postBeatmapComment.ts API
1233
1248
 
1234
1249
  /*
@@ -1246,6 +1261,23 @@ import { Schema as PostBeatmapComment } from "./api/postBeatmapComment"
1246
1261
  export { Schema as SchemaPostBeatmapComment } from "./api/postBeatmapComment"
1247
1262
  export const postBeatmapComment = handleApi({url:"/api/postBeatmapComment",...PostBeatmapComment})
1248
1263
 
1264
+ // ./api/qualifyMap.ts API
1265
+
1266
+ /*
1267
+ export const Schema = {
1268
+ input: z.strictObject({
1269
+ session: z.string(),
1270
+ mapId: z.number(),
1271
+ }),
1272
+ output: z.object({
1273
+ error: z.string().optional(),
1274
+ qualifiedAt: z.string().optional(),
1275
+ }),
1276
+ };*/
1277
+ import { Schema as QualifyMap } from "./api/qualifyMap"
1278
+ export { Schema as SchemaQualifyMap } from "./api/qualifyMap"
1279
+ export const qualifyMap = handleApi({url:"/api/qualifyMap",...QualifyMap})
1280
+
1249
1281
  // ./api/rankMapsArchive.ts API
1250
1282
 
1251
1283
  /*
@@ -1335,26 +1367,26 @@ export const submitScore = handleApi({url:"/api/submitScore",...SubmitScore})
1335
1367
  // ./api/submitScoreInternal.ts API
1336
1368
 
1337
1369
  /*
1338
- export const Schema = {
1339
- input: z.strictObject({
1340
- session: z.string(),
1341
- secret: z.string(),
1342
- token: z.string(),
1343
- data: z.strictObject({
1344
- onlineMapId: z.number(),
1345
- misses: z.number(),
1346
- hits: z.number(),
1347
- speed: z.number(),
1348
- mods: z.array(z.string()),
1349
- spin: z.boolean(),
1350
- replayBytes: z.string().nullable().optional(),
1351
- pauses: z.number().nullable().optional(),
1352
- failTime: z.number().nullable().optional(),
1353
- }),
1354
- }),
1355
- output: z.object({
1356
- error: z.string().optional(),
1357
- }),
1370
+ export const Schema = {
1371
+ input: z.strictObject({
1372
+ session: z.string(),
1373
+ secret: z.string(),
1374
+ token: z.string(),
1375
+ data: z.strictObject({
1376
+ onlineMapId: z.number(),
1377
+ misses: z.number(),
1378
+ hits: z.number(),
1379
+ speed: z.number(),
1380
+ mods: z.array(z.string()),
1381
+ spin: z.boolean(),
1382
+ replayBytes: z.string().nullable().optional(),
1383
+ pauses: z.number().nullable().optional(),
1384
+ failTime: z.number().nullable().optional(),
1385
+ }),
1386
+ }),
1387
+ output: z.object({
1388
+ error: z.string().optional(),
1389
+ }),
1358
1390
  };*/
1359
1391
  import { Schema as SubmitScoreInternal } from "./api/submitScoreInternal"
1360
1392
  export { Schema as SchemaSubmitScoreInternal } from "./api/submitScoreInternal"
@@ -1379,4 +1411,21 @@ export const Schema = {
1379
1411
  import { Schema as UpdateBeatmapPage } from "./api/updateBeatmapPage"
1380
1412
  export { Schema as SchemaUpdateBeatmapPage } from "./api/updateBeatmapPage"
1381
1413
  export const updateBeatmapPage = handleApi({url:"/api/updateBeatmapPage",...UpdateBeatmapPage})
1414
+
1415
+ // ./api/vetoMap.ts API
1416
+
1417
+ /*
1418
+ export const Schema = {
1419
+ input: z.strictObject({
1420
+ session: z.string(),
1421
+ mapId: z.number(),
1422
+ reason: z.string().min(1).max(1000),
1423
+ }),
1424
+ output: z.object({
1425
+ error: z.string().optional(),
1426
+ }),
1427
+ };*/
1428
+ import { Schema as VetoMap } from "./api/vetoMap"
1429
+ export { Schema as SchemaVetoMap } from "./api/vetoMap"
1430
+ export const vetoMap = handleApi({url:"/api/vetoMap",...VetoMap})
1382
1431
  export { handleApi } from "./handleApi"
package/package.json CHANGED
@@ -1,12 +1,18 @@
1
1
  {
2
2
  "name": "rhythia-api",
3
- "version": "229.0.0",
3
+ "version": "231.0.0",
4
4
  "main": "index.ts",
5
5
  "author": "online-contributors-cunev",
6
6
  "scripts": {
7
7
  "update": "bun ./scripts/update.ts",
8
8
  "ci-deploy": "tsx ./scripts/ci-deploy.ts",
9
9
  "test": "tsx ./scripts/test.ts",
10
+ "cache:clear-user-scores": "bun run scripts/clear-user-score-cache.ts",
11
+ "db:optimize-user-scores": "bun run scripts/optimize-user-scores-indexes.ts",
12
+ "query-pull": "bun run scripts/pull-queries.ts",
13
+ "query-push": "bun run scripts/deploy-queries.ts",
14
+ "queries:pull": "bun run scripts/pull-queries.ts",
15
+ "queries:deploy": "bun run scripts/deploy-queries.ts",
10
16
  "sync": "npx supabase gen types typescript --project-id \"pfkajngbllcbdzoylrvp\" --schema public > types/database.ts",
11
17
  "pipeline:build-api": "tsx ./scripts/build.ts",
12
18
  "pipeline:deploy-testing": "tsx ./scripts/build.ts"
@@ -24,6 +30,7 @@
24
30
  "@types/bun": "^1.1.6",
25
31
  "@types/lodash": "^4.17.7",
26
32
  "@types/node": "^22.2.0",
33
+ "@types/pg": "^8.15.5",
27
34
  "@types/validator": "^13.12.2",
28
35
  "@vercel/edge": "^1.1.2",
29
36
  "@vercel/node": "^3.2.8",
@@ -40,12 +47,13 @@
40
47
  "osu-classes": "^3.1.0",
41
48
  "osu-parsers": "^4.1.7",
42
49
  "osu-standard-stable": "^5.0.0",
50
+ "pg": "^8.18.0",
43
51
  "redis": "^5.10.0",
44
52
  "remote-cloudflare-kv": "^1.0.1",
45
53
  "sharp": "^0.33.5",
46
54
  "short-uuid": "^5.2.0",
47
55
  "simple-git": "^3.25.0",
48
- "supabase": "^2.76.15",
56
+ "supabase": "^2.76.16",
49
57
  "tsx": "^4.17.0",
50
58
  "utf-8-validate": "^6.0.4",
51
59
  "uuid": "^11.1.0",
@@ -0,0 +1,39 @@
1
+ CREATE OR REPLACE FUNCTION public.admin_delete_user(user_id integer)
2
+ RETURNS boolean
3
+ LANGUAGE plpgsql
4
+ SECURITY DEFINER
5
+ AS $function$
6
+ DECLARE
7
+ success BOOLEAN;
8
+ BEGIN
9
+ -- Delete scores first due to foreign key constraints
10
+ DELETE FROM public.scores WHERE "userId" = user_id;
11
+
12
+ -- Delete beatmapPageComments
13
+ DELETE FROM public."beatmapPageComments" WHERE owner = user_id;
14
+
15
+ -- Delete beatmapPages owned by user
16
+ DELETE FROM public."beatmapPages" WHERE owner = user_id;
17
+
18
+ -- Delete collections owned by user
19
+ DELETE FROM public."beatmapCollections" WHERE owner = user_id;
20
+
21
+ -- Delete collection relations related to user's collections
22
+ -- (this is handled by cascade if you have it set up)
23
+
24
+ -- Delete passkeys
25
+ DELETE FROM public.passkeys WHERE id = user_id;
26
+
27
+ -- Delete profile activity
28
+ DELETE FROM public."profileActivities"
29
+ WHERE uid = (SELECT uid FROM public.profiles WHERE id = user_id);
30
+
31
+ -- Finally, delete the profile
32
+ DELETE FROM public.profiles WHERE id = user_id;
33
+
34
+ -- Check if deletion was successful
35
+ success := NOT EXISTS (SELECT 1 FROM public.profiles WHERE id = user_id);
36
+
37
+ RETURN success;
38
+ END;
39
+ $function$
@@ -0,0 +1,21 @@
1
+ CREATE OR REPLACE FUNCTION public.admin_exclude_user(user_id integer)
2
+ RETURNS boolean
3
+ LANGUAGE plpgsql
4
+ SECURITY DEFINER
5
+ AS $function$
6
+ DECLARE
7
+ success BOOLEAN;
8
+ BEGIN
9
+ UPDATE public.profiles
10
+ SET ban = 'excluded'::"banTypes",
11
+ "bannedAt" = EXTRACT(EPOCH FROM NOW())::bigint
12
+ WHERE id = user_id;
13
+
14
+ success := EXISTS (
15
+ SELECT 1 FROM public.profiles
16
+ WHERE id = user_id AND ban = 'excluded'::"banTypes"
17
+ );
18
+
19
+ RETURN success;
20
+ END;
21
+ $function$
@@ -0,0 +1,18 @@
1
+ CREATE OR REPLACE FUNCTION public.admin_invalidate_ranked_scores(user_id integer)
2
+ RETURNS integer
3
+ LANGUAGE plpgsql
4
+ SECURITY DEFINER
5
+ AS $function$
6
+ DECLARE
7
+ updated_count INTEGER;
8
+ BEGIN
9
+ -- Update user stats to reflect score invalidation
10
+ UPDATE public.profiles
11
+ SET
12
+ skill_points = 0,
13
+ spin_skill_points = 0
14
+ WHERE id = user_id;
15
+
16
+ RETURN updated_count;
17
+ END;
18
+ $function$
@@ -0,0 +1,10 @@
1
+ CREATE OR REPLACE FUNCTION public.admin_log_action(admin_id integer, action_type text, target_id integer, details jsonb DEFAULT NULL::jsonb)
2
+ RETURNS void
3
+ LANGUAGE plpgsql
4
+ SECURITY DEFINER
5
+ AS $function$
6
+ BEGIN
7
+ INSERT INTO public.admin_actions (admin_id, action_type, target_id, details)
8
+ VALUES (admin_id, action_type, target_id, details);
9
+ END;
10
+ $function$
@@ -0,0 +1,29 @@
1
+ CREATE OR REPLACE FUNCTION public.admin_profanity_clear(user_id integer)
2
+ RETURNS boolean
3
+ LANGUAGE plpgsql
4
+ SECURITY DEFINER
5
+ AS $function$
6
+ DECLARE
7
+ success BOOLEAN;
8
+ random_username TEXT;
9
+ BEGIN
10
+ -- Generate a random username (Player + random number between 10000-99999)
11
+ random_username := 'Player' || (10000 + floor(random() * 90000)::int)::text;
12
+
13
+ -- Update the profile
14
+ UPDATE public.profiles
15
+ SET
16
+ username = random_username,
17
+ "computedUsername" = LOWER(random_username),
18
+ about_me = NULL
19
+ WHERE id = user_id;
20
+
21
+ -- Check if update was successful
22
+ success := EXISTS (
23
+ SELECT 1 FROM public.profiles
24
+ WHERE id = user_id AND username = random_username AND about_me IS NULL
25
+ );
26
+
27
+ RETURN success;
28
+ END;
29
+ $function$