rhythia-api 238.0.0 → 239.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.
@@ -2,25 +2,57 @@ import { NextResponse } from "../utils/response";
2
2
  import z from "zod";
3
3
  import { Database } from "../types/database";
4
4
  import { protectedApi, validUser } from "../utils/requestUtils";
5
- import { supabase } from "../utils/supabase";
6
- import { getUserBySession } from "../utils/getUserBySession";
7
- import { User } from "@supabase/supabase-js";
8
- import validator from "validator";
9
- import removeZeroWidth from "zero-width";
10
-
11
- export const Schema = {
12
- input: z.strictObject({
13
- session: z.string(),
5
+ import { supabase } from "../utils/supabase";
6
+ import { getUserBySession } from "../utils/getUserBySession";
7
+ import { User } from "@supabase/supabase-js";
8
+ import validator from "validator";
9
+ import removeZeroWidth from "zero-width";
10
+
11
+ const USERNAME_CHANGE_COOLDOWN_ERROR =
12
+ "Username can only be changed once every 6 months";
13
+
14
+ function getNextUsernameChangeAt(
15
+ lastChangedAt: string | null | undefined
16
+ ): string | null {
17
+ if (!lastChangedAt) {
18
+ return null;
19
+ }
20
+
21
+ const nextAllowedAt = new Date(lastChangedAt);
22
+ nextAllowedAt.setUTCMonth(nextAllowedAt.getUTCMonth() + 6);
23
+ return nextAllowedAt.toISOString();
24
+ }
25
+
26
+ function formatUsernameChangeDate(date: string): string {
27
+ return new Intl.DateTimeFormat("en-US", {
28
+ year: "numeric",
29
+ month: "short",
30
+ day: "numeric",
31
+ }).format(new Date(date));
32
+ }
33
+
34
+ function isUsernameChangeLocked(
35
+ lastChangedAt: string | null | undefined,
36
+ now = Date.now()
37
+ ): boolean {
38
+ const nextAllowedAt = getNextUsernameChangeAt(lastChangedAt);
39
+ return nextAllowedAt !== null && new Date(nextAllowedAt).getTime() > now;
40
+ }
41
+
42
+ export const Schema = {
43
+ input: z.strictObject({
44
+ session: z.string(),
14
45
  data: z.object({
15
46
  avatar_url: z.string().optional(),
16
47
  profile_image: z.string().optional(),
17
48
  username: z.string().optional(),
18
49
  }),
19
- }),
20
- output: z.object({
21
- error: z.string().optional(),
22
- }),
23
- };
50
+ }),
51
+ output: z.object({
52
+ error: z.string().optional(),
53
+ next_username_change_at: z.string().nullable().optional(),
54
+ }),
55
+ };
24
56
 
25
57
  export async function POST(request: Request): Promise<NextResponse> {
26
58
  return protectedApi({
@@ -70,9 +102,9 @@ export async function handler(
70
102
  },
71
103
  { status: 404 }
72
104
  );
73
- }
74
-
75
- data.data.username = removeZeroWidth(data.data.username || "");
105
+ }
106
+
107
+ data.data.username = removeZeroWidth(data.data.username || "");
76
108
 
77
109
  const user = (await getUserBySession(data.session)) as User;
78
110
 
@@ -96,24 +128,62 @@ export async function handler(
96
128
  userData = queryUserData[0];
97
129
  }
98
130
 
99
- if (
100
- userData.ban == "excluded" ||
101
- userData.ban == "restricted" ||
102
- userData.ban == "silenced"
103
- ) {
131
+ if (
132
+ userData.ban == "excluded" ||
133
+ userData.ban == "restricted" ||
134
+ userData.ban == "silenced"
135
+ ) {
104
136
  return NextResponse.json(
105
137
  {
106
138
  error:
107
139
  "Silenced, restricted or excluded players can't update their profile.",
108
140
  },
109
141
  { status: 404 }
110
- );
111
- }
112
-
113
- const upsertPayload: Database["public"]["Tables"]["profiles"]["Update"] = {
114
- id: userData.id,
115
- computedUsername: data.data.username?.toLowerCase(),
116
- ...data.data,
142
+ );
143
+ }
144
+
145
+ const usernameHasChanged =
146
+ data.data.username !== undefined && data.data.username !== userData.username;
147
+
148
+ let nextUsernameChangeAt: string | null = null;
149
+
150
+ if (usernameHasChanged) {
151
+ const { data: usernameHistory, error: usernameHistoryError } = await supabase
152
+ .from("profileUsernames")
153
+ .select("changed_at")
154
+ .eq("profile_id", userData.id!)
155
+ .order("changed_at", { ascending: false })
156
+ .limit(1);
157
+
158
+ if (usernameHistoryError) {
159
+ return NextResponse.json(
160
+ {
161
+ error: "Could not verify username change availability.",
162
+ },
163
+ { status: 500 }
164
+ );
165
+ }
166
+
167
+ const lastChangedAt = usernameHistory?.[0]?.changed_at ?? null;
168
+ nextUsernameChangeAt = getNextUsernameChangeAt(lastChangedAt);
169
+
170
+ if (isUsernameChangeLocked(lastChangedAt)) {
171
+ return NextResponse.json(
172
+ {
173
+ error: `${USERNAME_CHANGE_COOLDOWN_ERROR}. Next change available ${formatUsernameChangeDate(
174
+ nextUsernameChangeAt!
175
+ )}.`,
176
+ next_username_change_at: nextUsernameChangeAt,
177
+ },
178
+ { status: 404 }
179
+ );
180
+ }
181
+ }
182
+
183
+ const upsertPayload: Database["public"]["Tables"]["profiles"]["Update"] = {
184
+ id: userData.id,
185
+ computedUsername: data.data.username?.toLowerCase(),
186
+ ...data.data,
117
187
  };
118
188
 
119
189
  const upsertResult = await supabase
@@ -121,14 +191,44 @@ export async function handler(
121
191
  .upsert(upsertPayload)
122
192
  .select();
123
193
 
124
- if (upsertResult.error) {
125
- return NextResponse.json(
126
- {
127
- error: "Can't update, username might be used by someone else!",
194
+ if (upsertResult.error) {
195
+ if (
196
+ usernameHasChanged &&
197
+ upsertResult.error.message.includes(USERNAME_CHANGE_COOLDOWN_ERROR)
198
+ ) {
199
+ if (!nextUsernameChangeAt) {
200
+ const { data: usernameHistory } = await supabase
201
+ .from("profileUsernames")
202
+ .select("changed_at")
203
+ .eq("profile_id", userData.id!)
204
+ .order("changed_at", { ascending: false })
205
+ .limit(1);
206
+
207
+ nextUsernameChangeAt = getNextUsernameChangeAt(
208
+ usernameHistory?.[0]?.changed_at ?? null
209
+ );
210
+ }
211
+
212
+ return NextResponse.json(
213
+ {
214
+ error: `${USERNAME_CHANGE_COOLDOWN_ERROR}. Next change available ${
215
+ nextUsernameChangeAt
216
+ ? formatUsernameChangeDate(nextUsernameChangeAt)
217
+ : "later"
218
+ }.`,
219
+ next_username_change_at: nextUsernameChangeAt,
220
+ },
221
+ { status: 404 }
222
+ );
223
+ }
224
+
225
+ return NextResponse.json(
226
+ {
227
+ error: "Can't update, username might be used by someone else!",
128
228
  },
129
229
  { status: 404 }
130
230
  );
131
231
  }
132
-
133
- return NextResponse.json({});
134
- }
232
+
233
+ return NextResponse.json({});
234
+ }
package/api/getProfile.ts CHANGED
@@ -5,14 +5,26 @@ import { Database } from "../types/database";
5
5
  import { protectedApi } from "../utils/requestUtils";
6
6
  import { supabase } from "../utils/supabase";
7
7
  import { getUserBySession } from "../utils/getUserBySession";
8
- import { User } from "@supabase/supabase-js";
9
- import {
10
- getActivityStatusForUserId,
11
- getScoreActivityCutoffIso,
12
- } from "../utils/activityStatus";
13
-
14
- export const Schema = {
15
- input: z.strictObject({
8
+ import { User } from "@supabase/supabase-js";
9
+ import {
10
+ getActivityStatusForUserId,
11
+ getScoreActivityCutoffIso,
12
+ } from "../utils/activityStatus";
13
+
14
+ function getNextUsernameChangeAt(
15
+ lastChangedAt: string | null | undefined
16
+ ): string | null {
17
+ if (!lastChangedAt) {
18
+ return null;
19
+ }
20
+
21
+ const nextAllowedAt = new Date(lastChangedAt);
22
+ nextAllowedAt.setUTCMonth(nextAllowedAt.getUTCMonth() + 6);
23
+ return nextAllowedAt.toISOString();
24
+ }
25
+
26
+ export const Schema = {
27
+ input: z.strictObject({
16
28
  session: z.string(),
17
29
  id: z.number().nullable().optional(),
18
30
  }),
@@ -41,6 +53,13 @@ export const Schema = {
41
53
  activity_status: z.enum(["active", "inactive"]),
42
54
  is_online: z.boolean(),
43
55
  last_active_timestamp: z.number().nullable(),
56
+ next_username_change_at: z.string().nullable(),
57
+ previous_usernames: z.array(
58
+ z.object({
59
+ username: z.string(),
60
+ changed_at: z.string(),
61
+ })
62
+ ),
44
63
  clans: z
45
64
  .object({
46
65
  id: z.number(),
@@ -128,6 +147,12 @@ export async function handler(
128
147
  .select("last_activity")
129
148
  .eq("uid", user.uid || "")
130
149
  .single();
150
+
151
+ const { data: usernameHistoryData } = await supabase
152
+ .from("profileUsernames")
153
+ .select("username,changed_at")
154
+ .eq("profile_id", user.id)
155
+ .order("changed_at", { ascending: false });
131
156
 
132
157
  //last 30 minutes
133
158
  if (activityData && activityData.last_activity) {
@@ -165,24 +190,32 @@ export async function handler(
165
190
  : null;
166
191
  }
167
192
 
168
- if (user.verificationDeadline < Date.now()) {
169
- await supabase
170
- .from("profiles")
171
- .upsert({
193
+ if (user.verificationDeadline < Date.now()) {
194
+ await supabase
195
+ .from("profiles")
196
+ .upsert({
172
197
  id: user.id,
173
198
  verified: false,
174
- })
175
- .select();
176
- }
177
-
178
- return NextResponse.json({
179
- user: {
199
+ })
200
+ .select();
201
+ }
202
+
203
+ const previousUsernames = (usernameHistoryData || []).filter((entry) => {
204
+ return entry.username.toLowerCase() !== (user.username || "").toLowerCase();
205
+ });
206
+
207
+ const latestUsernameChangeAt = usernameHistoryData?.[0]?.changed_at ?? null;
208
+
209
+ return NextResponse.json({
210
+ user: {
180
211
  ...user,
181
212
  position,
182
213
  country_position: countryPosition,
183
214
  activity_status: activityStatus,
184
215
  is_online: isOnline,
185
216
  last_active_timestamp: activityData?.last_activity ?? null,
217
+ next_username_change_at: getNextUsernameChangeAt(latestUsernameChangeAt),
218
+ previous_usernames: previousUsernames,
186
219
  },
187
220
  });
188
221
  }
package/index.ts CHANGED
@@ -301,18 +301,19 @@ export const editCollection = handleApi({url:"/api/editCollection",...EditCollec
301
301
  // ./api/editProfile.ts API
302
302
 
303
303
  /*
304
- export const Schema = {
305
- input: z.strictObject({
306
- session: z.string(),
304
+ export const Schema = {
305
+ input: z.strictObject({
306
+ session: z.string(),
307
307
  data: z.object({
308
308
  avatar_url: z.string().optional(),
309
309
  profile_image: z.string().optional(),
310
310
  username: z.string().optional(),
311
311
  }),
312
- }),
313
- output: z.object({
314
- error: z.string().optional(),
315
- }),
312
+ }),
313
+ output: z.object({
314
+ error: z.string().optional(),
315
+ next_username_change_at: z.string().nullable().optional(),
316
+ }),
316
317
  };*/
317
318
  import { Schema as EditProfile } from "./api/editProfile"
318
319
  export { Schema as SchemaEditProfile } from "./api/editProfile"
@@ -962,8 +963,8 @@ export const getPassToken = handleApi({url:"/api/getPassToken",...GetPassToken})
962
963
  // ./api/getProfile.ts API
963
964
 
964
965
  /*
965
- export const Schema = {
966
- input: z.strictObject({
966
+ export const Schema = {
967
+ input: z.strictObject({
967
968
  session: z.string(),
968
969
  id: z.number().nullable().optional(),
969
970
  }),
@@ -992,6 +993,13 @@ export const Schema = {
992
993
  activity_status: z.enum(["active", "inactive"]),
993
994
  is_online: z.boolean(),
994
995
  last_active_timestamp: z.number().nullable(),
996
+ next_username_change_at: z.string().nullable(),
997
+ previous_usernames: z.array(
998
+ z.object({
999
+ username: z.string(),
1000
+ changed_at: z.string(),
1001
+ })
1002
+ ),
995
1003
  clans: z
996
1004
  .object({
997
1005
  id: z.number(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rhythia-api",
3
- "version": "238.0.0",
3
+ "version": "239.0.0",
4
4
  "main": "index.ts",
5
5
  "author": "online-contributors-cunev",
6
6
  "scripts": {
package/types/database.ts CHANGED
@@ -558,6 +558,35 @@ export type Database = {
558
558
  },
559
559
  ]
560
560
  }
561
+ profileUsernames: {
562
+ Row: {
563
+ changed_at: string
564
+ id: number
565
+ profile_id: number
566
+ username: string
567
+ }
568
+ Insert: {
569
+ changed_at?: string
570
+ id?: number
571
+ profile_id: number
572
+ username: string
573
+ }
574
+ Update: {
575
+ changed_at?: string
576
+ id?: number
577
+ profile_id?: number
578
+ username?: string
579
+ }
580
+ Relationships: [
581
+ {
582
+ foreignKeyName: "profileUsernames_profile_id_fkey"
583
+ columns: ["profile_id"]
584
+ isOneToOne: false
585
+ referencedRelation: "profiles"
586
+ referencedColumns: ["id"]
587
+ },
588
+ ]
589
+ }
561
590
  profiles: {
562
591
  Row: {
563
592
  about_me: string | null