rhythia-api 239.0.0 → 241.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,15 +1,18 @@
1
- import { NextResponse } from "../utils/response";
2
- import z from "zod";
3
- import { Database } from "../types/database";
4
- import { protectedApi, validUser } from "../utils/requestUtils";
1
+ import { NextResponse } from "../utils/response";
2
+ import z from "zod";
3
+ import { Database } from "../types/database";
4
+ import { protectedApi, validUser } from "../utils/requestUtils";
5
5
  import { supabase } from "../utils/supabase";
6
6
  import { getUserBySession } from "../utils/getUserBySession";
7
+ import { COUNTRY_LIST } from "../utils/countryList";
7
8
  import { User } from "@supabase/supabase-js";
8
9
  import validator from "validator";
9
10
  import removeZeroWidth from "zero-width";
10
11
 
11
12
  const USERNAME_CHANGE_COOLDOWN_ERROR =
12
13
  "Username can only be changed once every 6 months";
14
+ const FLAG_CHANGE_ERROR = "Flag can only be changed once";
15
+ const countryCodes = new Set(COUNTRY_LIST);
13
16
 
14
17
  function getNextUsernameChangeAt(
15
18
  lastChangedAt: string | null | undefined
@@ -42,110 +45,130 @@ function isUsernameChangeLocked(
42
45
  export const Schema = {
43
46
  input: z.strictObject({
44
47
  session: z.string(),
45
- data: z.object({
46
- avatar_url: z.string().optional(),
47
- profile_image: z.string().optional(),
48
- username: z.string().optional(),
49
- }),
48
+ data: z.object({
49
+ avatar_url: z.string().optional(),
50
+ flag: z.string().optional(),
51
+ profile_image: z.string().optional(),
52
+ username: z.string().optional(),
53
+ }),
50
54
  }),
51
55
  output: z.object({
52
56
  error: z.string().optional(),
57
+ can_change_flag: z.boolean().optional(),
53
58
  next_username_change_at: z.string().nullable().optional(),
54
59
  }),
55
60
  };
56
-
57
- export async function POST(request: Request): Promise<NextResponse> {
58
- return protectedApi({
59
- request,
60
- schema: Schema,
61
- authorization: validUser,
62
- activity: handler,
63
- });
64
- }
65
-
66
- export async function handler(
67
- data: (typeof Schema)["input"]["_type"]
68
- ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
69
- if (data.data.username !== undefined) {
70
- if (data.data.username.length < 3) {
71
- return NextResponse.json(
72
- {
73
- error: "Username must be at least 3 characters long",
74
- },
75
- { status: 404 }
76
- );
77
- }
78
-
79
- if (data.data.username.length > 20) {
80
- return NextResponse.json(
81
- {
82
- error: "Username too long.",
83
- },
84
- { status: 404 }
85
- );
86
- }
87
-
88
- if (!/^[a-z0-9]+$/i.test(data.data.username)) {
89
- return NextResponse.json(
90
- {
91
- error: "Username can only contain letters (a-z) and numbers (0-9)",
92
- },
93
- { status: 404 }
94
- );
95
- }
96
- }
97
-
98
- if (validator.trim(data.data.username || "") !== (data.data.username || "")) {
99
- return NextResponse.json(
100
- {
101
- error: "Username can't start or end with spaces.",
102
- },
103
- { status: 404 }
104
- );
61
+
62
+ export async function POST(request: Request): Promise<NextResponse> {
63
+ return protectedApi({
64
+ request,
65
+ schema: Schema,
66
+ authorization: validUser,
67
+ activity: handler,
68
+ });
69
+ }
70
+
71
+ export async function handler(
72
+ data: (typeof Schema)["input"]["_type"]
73
+ ): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
74
+ if (data.data.username !== undefined) {
75
+ if (data.data.username.length < 3) {
76
+ return NextResponse.json(
77
+ {
78
+ error: "Username must be at least 3 characters long",
79
+ },
80
+ { status: 404 }
81
+ );
82
+ }
83
+
84
+ if (data.data.username.length > 20) {
85
+ return NextResponse.json(
86
+ {
87
+ error: "Username too long.",
88
+ },
89
+ { status: 404 }
90
+ );
91
+ }
92
+
93
+ if (!/^[a-z0-9]+$/i.test(data.data.username)) {
94
+ return NextResponse.json(
95
+ {
96
+ error: "Username can only contain letters (a-z) and numbers (0-9)",
97
+ },
98
+ { status: 404 }
99
+ );
100
+ }
101
+ }
102
+
103
+ if (validator.trim(data.data.username || "") !== (data.data.username || "")) {
104
+ return NextResponse.json(
105
+ {
106
+ error: "Username can't start or end with spaces.",
107
+ },
108
+ { status: 404 }
109
+ );
105
110
  }
106
111
 
107
112
  data.data.username = removeZeroWidth(data.data.username || "");
108
-
109
- const user = (await getUserBySession(data.session)) as User;
110
-
111
- let userData: Database["public"]["Tables"]["profiles"]["Update"];
112
-
113
- // Find user's entry
114
- {
115
- let { data: queryUserData, error } = await supabase
116
- .from("profiles")
117
- .select("*")
118
- .eq("uid", user.id);
119
-
120
- if (!queryUserData?.length) {
121
- return NextResponse.json(
122
- {
123
- error: "User cannot be retrieved from session",
124
- },
125
- { status: 404 }
126
- );
127
- }
128
- userData = queryUserData[0];
129
- }
130
-
113
+
114
+ if (data.data.flag !== undefined) {
115
+ const normalizedFlag = validator.trim(data.data.flag).toUpperCase();
116
+
117
+ if (!countryCodes.has(normalizedFlag as (typeof COUNTRY_LIST)[number])) {
118
+ return NextResponse.json(
119
+ {
120
+ error: "Flag must be a valid country code.",
121
+ },
122
+ { status: 404 }
123
+ );
124
+ }
125
+
126
+ data.data.flag = normalizedFlag;
127
+ }
128
+
129
+ const user = (await getUserBySession(data.session)) as User;
130
+
131
+ let userData: Database["public"]["Tables"]["profiles"]["Update"];
132
+
133
+ // Find user's entry
134
+ {
135
+ let { data: queryUserData, error } = await supabase
136
+ .from("profiles")
137
+ .select("*")
138
+ .eq("uid", user.id);
139
+
140
+ if (!queryUserData?.length) {
141
+ return NextResponse.json(
142
+ {
143
+ error: "User cannot be retrieved from session",
144
+ },
145
+ { status: 404 }
146
+ );
147
+ }
148
+ userData = queryUserData[0];
149
+ }
150
+
131
151
  if (
132
152
  userData.ban == "excluded" ||
133
153
  userData.ban == "restricted" ||
134
154
  userData.ban == "silenced"
135
155
  ) {
136
- return NextResponse.json(
137
- {
138
- error:
139
- "Silenced, restricted or excluded players can't update their profile.",
140
- },
141
- { status: 404 }
156
+ return NextResponse.json(
157
+ {
158
+ error:
159
+ "Silenced, restricted or excluded players can't update their profile.",
160
+ },
161
+ { status: 404 }
142
162
  );
143
163
  }
144
164
 
145
165
  const usernameHasChanged =
146
166
  data.data.username !== undefined && data.data.username !== userData.username;
167
+ const flagHasChanged =
168
+ data.data.flag !== undefined && data.data.flag !== userData.flag;
147
169
 
148
170
  let nextUsernameChangeAt: string | null = null;
171
+ let canChangeFlag = true;
149
172
 
150
173
  if (usernameHasChanged) {
151
174
  const { data: usernameHistory, error: usernameHistoryError } = await supabase
@@ -180,17 +203,46 @@ export async function handler(
180
203
  }
181
204
  }
182
205
 
206
+ if (flagHasChanged) {
207
+ const { data: flagHistory, error: flagHistoryError } = await supabase
208
+ .from("profileFlags")
209
+ .select("id")
210
+ .eq("profile_id", userData.id!)
211
+ .limit(1);
212
+
213
+ if (flagHistoryError) {
214
+ return NextResponse.json(
215
+ {
216
+ error: "Could not verify flag change availability.",
217
+ },
218
+ { status: 500 }
219
+ );
220
+ }
221
+
222
+ canChangeFlag = !flagHistory?.length;
223
+
224
+ if (!canChangeFlag) {
225
+ return NextResponse.json(
226
+ {
227
+ error: FLAG_CHANGE_ERROR,
228
+ can_change_flag: false,
229
+ },
230
+ { status: 404 }
231
+ );
232
+ }
233
+ }
234
+
183
235
  const upsertPayload: Database["public"]["Tables"]["profiles"]["Update"] = {
184
236
  id: userData.id,
185
237
  computedUsername: data.data.username?.toLowerCase(),
186
238
  ...data.data,
187
- };
188
-
189
- const upsertResult = await supabase
190
- .from("profiles")
191
- .upsert(upsertPayload)
192
- .select();
193
-
239
+ };
240
+
241
+ const upsertResult = await supabase
242
+ .from("profiles")
243
+ .upsert(upsertPayload)
244
+ .select();
245
+
194
246
  if (upsertResult.error) {
195
247
  if (
196
248
  usernameHasChanged &&
@@ -222,13 +274,23 @@ export async function handler(
222
274
  );
223
275
  }
224
276
 
277
+ if (flagHasChanged && upsertResult.error.message.includes(FLAG_CHANGE_ERROR)) {
278
+ return NextResponse.json(
279
+ {
280
+ error: FLAG_CHANGE_ERROR,
281
+ can_change_flag: false,
282
+ },
283
+ { status: 404 }
284
+ );
285
+ }
286
+
225
287
  return NextResponse.json(
226
288
  {
227
289
  error: "Can't update, username might be used by someone else!",
228
- },
229
- { status: 404 }
230
- );
231
- }
290
+ },
291
+ { status: 404 }
292
+ );
293
+ }
232
294
 
233
295
  return NextResponse.json({});
234
296
  }
package/api/getProfile.ts CHANGED
@@ -53,6 +53,7 @@ export const Schema = {
53
53
  activity_status: z.enum(["active", "inactive"]),
54
54
  is_online: z.boolean(),
55
55
  last_active_timestamp: z.number().nullable(),
56
+ can_change_flag: z.boolean(),
56
57
  next_username_change_at: z.string().nullable(),
57
58
  previous_usernames: z.array(
58
59
  z.object({
@@ -153,6 +154,12 @@ export async function handler(
153
154
  .select("username,changed_at")
154
155
  .eq("profile_id", user.id)
155
156
  .order("changed_at", { ascending: false });
157
+
158
+ const { data: flagHistoryData } = await supabase
159
+ .from("profileFlags")
160
+ .select("id")
161
+ .eq("profile_id", user.id)
162
+ .limit(1);
156
163
 
157
164
  //last 30 minutes
158
165
  if (activityData && activityData.last_activity) {
@@ -214,6 +221,7 @@ export async function handler(
214
221
  activity_status: activityStatus,
215
222
  is_online: isOnline,
216
223
  last_active_timestamp: activityData?.last_activity ?? null,
224
+ can_change_flag: !flagHistoryData?.length,
217
225
  next_username_change_at: getNextUsernameChangeAt(latestUsernameChangeAt),
218
226
  previous_usernames: previousUsernames,
219
227
  },
package/index.ts CHANGED
@@ -304,14 +304,16 @@ export const editCollection = handleApi({url:"/api/editCollection",...EditCollec
304
304
  export const Schema = {
305
305
  input: z.strictObject({
306
306
  session: z.string(),
307
- data: z.object({
308
- avatar_url: z.string().optional(),
309
- profile_image: z.string().optional(),
310
- username: z.string().optional(),
311
- }),
307
+ data: z.object({
308
+ avatar_url: z.string().optional(),
309
+ flag: z.string().optional(),
310
+ profile_image: z.string().optional(),
311
+ username: z.string().optional(),
312
+ }),
312
313
  }),
313
314
  output: z.object({
314
315
  error: z.string().optional(),
316
+ can_change_flag: z.boolean().optional(),
315
317
  next_username_change_at: z.string().nullable().optional(),
316
318
  }),
317
319
  };*/
@@ -993,6 +995,7 @@ export const Schema = {
993
995
  activity_status: z.enum(["active", "inactive"]),
994
996
  is_online: z.boolean(),
995
997
  last_active_timestamp: z.number().nullable(),
998
+ can_change_flag: z.boolean(),
996
999
  next_username_change_at: z.string().nullable(),
997
1000
  previous_usernames: z.array(
998
1001
  z.object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rhythia-api",
3
- "version": "239.0.0",
3
+ "version": "241.0.0",
4
4
  "main": "index.ts",
5
5
  "author": "online-contributors-cunev",
6
6
  "scripts": {
@@ -10,6 +10,7 @@
10
10
  "test": "tsx ./scripts/test.ts",
11
11
  "cache:clear-user-scores": "bun run scripts/clear-user-score-cache.ts",
12
12
  "db:optimize-user-scores": "bun run scripts/optimize-user-scores-indexes.ts",
13
+ "db:create-profile-flags": "node scripts/create-profile-flags-table.ts",
13
14
  "db:create-profile-reports": "node scripts/create-profile-reports-table.ts",
14
15
  "query-pull": "bun run scripts/pull-queries.ts",
15
16
  "query-push": "bun run scripts/deploy-queries.ts",
package/types/database.ts CHANGED
@@ -558,6 +558,35 @@ export type Database = {
558
558
  },
559
559
  ]
560
560
  }
561
+ profileFlags: {
562
+ Row: {
563
+ changed_at: string
564
+ flag: string | null
565
+ id: number
566
+ profile_id: number
567
+ }
568
+ Insert: {
569
+ changed_at?: string
570
+ flag?: string | null
571
+ id?: number
572
+ profile_id: number
573
+ }
574
+ Update: {
575
+ changed_at?: string
576
+ flag?: string | null
577
+ id?: number
578
+ profile_id?: number
579
+ }
580
+ Relationships: [
581
+ {
582
+ foreignKeyName: "profileFlags_profile_id_fkey"
583
+ columns: ["profile_id"]
584
+ isOneToOne: false
585
+ referencedRelation: "profiles"
586
+ referencedColumns: ["id"]
587
+ },
588
+ ]
589
+ }
561
590
  profileUsernames: {
562
591
  Row: {
563
592
  changed_at: string
@@ -0,0 +1,141 @@
1
+ export const COUNTRY_LIST = [
2
+ "US",
3
+ "AE",
4
+ "AM",
5
+ "AR",
6
+ "AT",
7
+ "AU",
8
+ "AZ",
9
+ "BA",
10
+ "BB",
11
+ "BD",
12
+ "BE",
13
+ "BG",
14
+ "BH",
15
+ "BN",
16
+ "BO",
17
+ "BR",
18
+ "BT",
19
+ "BW",
20
+ "BY",
21
+ "CA",
22
+ "CH",
23
+ "CI",
24
+ "CL",
25
+ "CN",
26
+ "CO",
27
+ "CR",
28
+ "CU",
29
+ "CW",
30
+ "CY",
31
+ "CZ",
32
+ "DE",
33
+ "DJ",
34
+ "DK",
35
+ "DO",
36
+ "DZ",
37
+ "EC",
38
+ "EE",
39
+ "EG",
40
+ "ES",
41
+ "ET",
42
+ "FI",
43
+ "FJ",
44
+ "FO",
45
+ "FR",
46
+ "GA",
47
+ "GB",
48
+ "GE",
49
+ "GH",
50
+ "GR",
51
+ "GT",
52
+ "GU",
53
+ "HK",
54
+ "HN",
55
+ "HR",
56
+ "HU",
57
+ "ID",
58
+ "IE",
59
+ "IL",
60
+ "IM",
61
+ "IN",
62
+ "IQ",
63
+ "IR",
64
+ "IS",
65
+ "IT",
66
+ "JE",
67
+ "JM",
68
+ "JO",
69
+ "JP",
70
+ "KE",
71
+ "KG",
72
+ "KH",
73
+ "KR",
74
+ "KW",
75
+ "LI",
76
+ "LK",
77
+ "LT",
78
+ "LU",
79
+ "LV",
80
+ "MA",
81
+ "MC",
82
+ "MD",
83
+ "MG",
84
+ "MK",
85
+ "MM",
86
+ "MN",
87
+ "MT",
88
+ "MU",
89
+ "MV",
90
+ "MX",
91
+ "MY",
92
+ "NA",
93
+ "NC",
94
+ "NG",
95
+ "NL",
96
+ "NO",
97
+ "NP",
98
+ "NZ",
99
+ "OM",
100
+ "PA",
101
+ "PE",
102
+ "PG",
103
+ "PH",
104
+ "PK",
105
+ "PL",
106
+ "PR",
107
+ "PS",
108
+ "PT",
109
+ "PY",
110
+ "QA",
111
+ "RE",
112
+ "RO",
113
+ "RU",
114
+ "SA",
115
+ "SD",
116
+ "SE",
117
+ "SG",
118
+ "SI",
119
+ "SK",
120
+ "SL",
121
+ "SN",
122
+ "SR",
123
+ "SV",
124
+ "SY",
125
+ "TG",
126
+ "TH",
127
+ "TN",
128
+ "TR",
129
+ "TT",
130
+ "TW",
131
+ "TZ",
132
+ "UA",
133
+ "UY",
134
+ "UZ",
135
+ "KZ",
136
+ "VE",
137
+ "VN",
138
+ "XK",
139
+ "ZA",
140
+ "ZW",
141
+ ] as const;