rhythia-api 237.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.
- package/api/editProfile.ts +136 -36
- package/api/executeAdminOperation.ts +296 -1
- package/api/getProfile.ts +60 -25
- package/index.ts +21 -22
- package/package.json +1 -1
- package/types/database.ts +1385 -1327
package/api/editProfile.ts
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
+
}
|
|
@@ -7,6 +7,56 @@ import { getUserBySession } from "../utils/getUserBySession";
|
|
|
7
7
|
import { User } from "@supabase/supabase-js";
|
|
8
8
|
import { invalidateCache, invalidateCachePrefix } from "../utils/cache";
|
|
9
9
|
|
|
10
|
+
type AdminProfileSummary = Pick<
|
|
11
|
+
Database["public"]["Tables"]["profiles"]["Row"],
|
|
12
|
+
"id" | "username" | "avatar_url" | "flag" | "badges" | "ban"
|
|
13
|
+
>;
|
|
14
|
+
|
|
15
|
+
type InvestigationDateRange = {
|
|
16
|
+
firstSeenAt: string | null;
|
|
17
|
+
lastSeenAt: string | null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type LinkedAccountAggregate = AdminProfileSummary &
|
|
21
|
+
InvestigationDateRange & {
|
|
22
|
+
occurrences: number;
|
|
23
|
+
sharedHwids: Set<string>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type LinkedProfileAggregate = AdminProfileSummary &
|
|
27
|
+
InvestigationDateRange & {
|
|
28
|
+
occurrences: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const MULTIACCOUNT_PROFILE_SELECT =
|
|
32
|
+
"id, username, avatar_url, flag, badges, ban";
|
|
33
|
+
|
|
34
|
+
const SCORE_CACHE_READ_OPERATIONS = new Set([
|
|
35
|
+
"getScoresPaginated",
|
|
36
|
+
"getMultiaccountInvestigation",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
function timestampOrZero(value: string | null) {
|
|
40
|
+
return value ? new Date(value).getTime() : 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function updateDateRange(
|
|
44
|
+
range: InvestigationDateRange,
|
|
45
|
+
createdAt: string | null
|
|
46
|
+
) {
|
|
47
|
+
if (!createdAt) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!range.firstSeenAt || createdAt < range.firstSeenAt) {
|
|
52
|
+
range.firstSeenAt = createdAt;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!range.lastSeenAt || createdAt > range.lastSeenAt) {
|
|
56
|
+
range.lastSeenAt = createdAt;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
10
60
|
// Define supported admin operations and their parameter types
|
|
11
61
|
const adminOperations = {
|
|
12
62
|
deleteUser: z.object({ userId: z.number() }),
|
|
@@ -28,6 +78,7 @@ const adminOperations = {
|
|
|
28
78
|
userId: z.number().optional(),
|
|
29
79
|
includeAdditionalData: z.boolean().default(true),
|
|
30
80
|
}),
|
|
81
|
+
getMultiaccountInvestigation: z.object({ userId: z.number() }),
|
|
31
82
|
} as const;
|
|
32
83
|
|
|
33
84
|
// Create a discriminated union type for operation parameters
|
|
@@ -88,6 +139,10 @@ const OperationParam = z.discriminatedUnion("operation", [
|
|
|
88
139
|
operation: z.literal("getScoresPaginated"),
|
|
89
140
|
params: adminOperations.getScoresPaginated,
|
|
90
141
|
}),
|
|
142
|
+
z.object({
|
|
143
|
+
operation: z.literal("getMultiaccountInvestigation"),
|
|
144
|
+
params: adminOperations.getMultiaccountInvestigation,
|
|
145
|
+
}),
|
|
91
146
|
]);
|
|
92
147
|
|
|
93
148
|
export const Schema = {
|
|
@@ -346,6 +401,242 @@ export async function handler(
|
|
|
346
401
|
};
|
|
347
402
|
}
|
|
348
403
|
break;
|
|
404
|
+
|
|
405
|
+
case "getMultiaccountInvestigation":
|
|
406
|
+
const { data: investigatedUser, error: investigatedUserError } =
|
|
407
|
+
await supabase
|
|
408
|
+
.from("profiles")
|
|
409
|
+
.select(MULTIACCOUNT_PROFILE_SELECT)
|
|
410
|
+
.eq("id", params.userId)
|
|
411
|
+
.maybeSingle();
|
|
412
|
+
|
|
413
|
+
if (investigatedUserError) {
|
|
414
|
+
result = { error: investigatedUserError, data: null };
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!investigatedUser) {
|
|
419
|
+
result = {
|
|
420
|
+
error: { message: "User not found" },
|
|
421
|
+
data: null,
|
|
422
|
+
};
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const { data: investigatedUserHwids, error: investigatedUserHwidsError } =
|
|
427
|
+
await supabase
|
|
428
|
+
.from("user_hwids")
|
|
429
|
+
.select("id, hwid, created_at")
|
|
430
|
+
.eq("id", params.userId)
|
|
431
|
+
.order("created_at", { ascending: false });
|
|
432
|
+
|
|
433
|
+
if (investigatedUserHwidsError) {
|
|
434
|
+
result = { error: investigatedUserHwidsError, data: null };
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const targetRows = investigatedUserHwids || [];
|
|
439
|
+
const uniqueHwids = Array.from(
|
|
440
|
+
new Set(targetRows.map((row) => row.hwid).filter(Boolean))
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const dateRange: InvestigationDateRange = {
|
|
444
|
+
firstSeenAt: null,
|
|
445
|
+
lastSeenAt: null,
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const hwidAggregates = new Map<
|
|
449
|
+
string,
|
|
450
|
+
InvestigationDateRange & {
|
|
451
|
+
hwid: string;
|
|
452
|
+
occurrences: number;
|
|
453
|
+
linkedProfiles: Map<number, LinkedProfileAggregate>;
|
|
454
|
+
}
|
|
455
|
+
>();
|
|
456
|
+
|
|
457
|
+
for (const row of targetRows) {
|
|
458
|
+
updateDateRange(dateRange, row.created_at);
|
|
459
|
+
|
|
460
|
+
const existing = hwidAggregates.get(row.hwid) || {
|
|
461
|
+
hwid: row.hwid,
|
|
462
|
+
occurrences: 0,
|
|
463
|
+
firstSeenAt: null,
|
|
464
|
+
lastSeenAt: null,
|
|
465
|
+
linkedProfiles: new Map<number, LinkedProfileAggregate>(),
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
existing.occurrences += 1;
|
|
469
|
+
updateDateRange(existing, row.created_at);
|
|
470
|
+
hwidAggregates.set(row.hwid, existing);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (uniqueHwids.length === 0) {
|
|
474
|
+
result = {
|
|
475
|
+
data: {
|
|
476
|
+
user: investigatedUser,
|
|
477
|
+
summary: {
|
|
478
|
+
totalRows: 0,
|
|
479
|
+
uniqueHwids: 0,
|
|
480
|
+
sharedHwids: 0,
|
|
481
|
+
linkedAccounts: 0,
|
|
482
|
+
firstSeenAt: null,
|
|
483
|
+
lastSeenAt: null,
|
|
484
|
+
},
|
|
485
|
+
accounts: [],
|
|
486
|
+
hwids: [],
|
|
487
|
+
},
|
|
488
|
+
error: null,
|
|
489
|
+
};
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const { data: relatedRows, error: relatedRowsError } = await supabase
|
|
494
|
+
.from("user_hwids")
|
|
495
|
+
.select("id, hwid, created_at")
|
|
496
|
+
.in("hwid", uniqueHwids)
|
|
497
|
+
.order("created_at", { ascending: false });
|
|
498
|
+
|
|
499
|
+
if (relatedRowsError) {
|
|
500
|
+
result = { error: relatedRowsError, data: null };
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const relatedProfileIds = Array.from(
|
|
505
|
+
new Set(
|
|
506
|
+
(relatedRows || [])
|
|
507
|
+
.map((row) => row.id)
|
|
508
|
+
.filter((id) => id !== params.userId)
|
|
509
|
+
)
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
const { data: relatedProfiles, error: relatedProfilesError } =
|
|
513
|
+
relatedProfileIds.length > 0
|
|
514
|
+
? await supabase
|
|
515
|
+
.from("profiles")
|
|
516
|
+
.select(MULTIACCOUNT_PROFILE_SELECT)
|
|
517
|
+
.in("id", relatedProfileIds)
|
|
518
|
+
: { data: [], error: null };
|
|
519
|
+
|
|
520
|
+
if (relatedProfilesError) {
|
|
521
|
+
result = { error: relatedProfilesError, data: null };
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const profileMap = new Map<number, AdminProfileSummary>([
|
|
526
|
+
[investigatedUser.id, investigatedUser],
|
|
527
|
+
...((relatedProfiles || []) as AdminProfileSummary[]).map((profile) => [
|
|
528
|
+
profile.id,
|
|
529
|
+
profile,
|
|
530
|
+
]),
|
|
531
|
+
]);
|
|
532
|
+
|
|
533
|
+
const linkedAccountAggregates = new Map<number, LinkedAccountAggregate>();
|
|
534
|
+
|
|
535
|
+
for (const row of relatedRows || []) {
|
|
536
|
+
if (row.id === params.userId) {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const hwidAggregate = hwidAggregates.get(row.hwid);
|
|
541
|
+
const relatedProfile = profileMap.get(row.id);
|
|
542
|
+
|
|
543
|
+
if (!hwidAggregate || !relatedProfile) {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const linkedProfile =
|
|
548
|
+
hwidAggregate.linkedProfiles.get(row.id) || {
|
|
549
|
+
...relatedProfile,
|
|
550
|
+
occurrences: 0,
|
|
551
|
+
firstSeenAt: null,
|
|
552
|
+
lastSeenAt: null,
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
linkedProfile.occurrences += 1;
|
|
556
|
+
updateDateRange(linkedProfile, row.created_at);
|
|
557
|
+
hwidAggregate.linkedProfiles.set(row.id, linkedProfile);
|
|
558
|
+
|
|
559
|
+
const linkedAccount = linkedAccountAggregates.get(row.id) || {
|
|
560
|
+
...relatedProfile,
|
|
561
|
+
occurrences: 0,
|
|
562
|
+
sharedHwids: new Set<string>(),
|
|
563
|
+
firstSeenAt: null,
|
|
564
|
+
lastSeenAt: null,
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
linkedAccount.occurrences += 1;
|
|
568
|
+
linkedAccount.sharedHwids.add(row.hwid);
|
|
569
|
+
updateDateRange(linkedAccount, row.created_at);
|
|
570
|
+
linkedAccountAggregates.set(row.id, linkedAccount);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const accounts = Array.from(linkedAccountAggregates.values())
|
|
574
|
+
.map((account) => ({
|
|
575
|
+
...account,
|
|
576
|
+
sharedHwids: Array.from(account.sharedHwids).sort(),
|
|
577
|
+
sharedHwidCount: account.sharedHwids.size,
|
|
578
|
+
}))
|
|
579
|
+
.sort((a, b) => {
|
|
580
|
+
if (b.sharedHwidCount !== a.sharedHwidCount) {
|
|
581
|
+
return b.sharedHwidCount - a.sharedHwidCount;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (b.occurrences !== a.occurrences) {
|
|
585
|
+
return b.occurrences - a.occurrences;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return timestampOrZero(b.lastSeenAt) - timestampOrZero(a.lastSeenAt);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
const hwids = Array.from(hwidAggregates.values())
|
|
592
|
+
.map((aggregate) => ({
|
|
593
|
+
hwid: aggregate.hwid,
|
|
594
|
+
occurrences: aggregate.occurrences,
|
|
595
|
+
firstSeenAt: aggregate.firstSeenAt,
|
|
596
|
+
lastSeenAt: aggregate.lastSeenAt,
|
|
597
|
+
linkedAccountCount: aggregate.linkedProfiles.size,
|
|
598
|
+
linkedProfiles: Array.from(aggregate.linkedProfiles.values()).sort(
|
|
599
|
+
(a, b) => {
|
|
600
|
+
if (b.occurrences !== a.occurrences) {
|
|
601
|
+
return b.occurrences - a.occurrences;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return (
|
|
605
|
+
timestampOrZero(b.lastSeenAt) - timestampOrZero(a.lastSeenAt)
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
),
|
|
609
|
+
}))
|
|
610
|
+
.sort((a, b) => {
|
|
611
|
+
if (b.linkedAccountCount !== a.linkedAccountCount) {
|
|
612
|
+
return b.linkedAccountCount - a.linkedAccountCount;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (b.occurrences !== a.occurrences) {
|
|
616
|
+
return b.occurrences - a.occurrences;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return timestampOrZero(b.lastSeenAt) - timestampOrZero(a.lastSeenAt);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
result = {
|
|
623
|
+
data: {
|
|
624
|
+
user: investigatedUser,
|
|
625
|
+
summary: {
|
|
626
|
+
totalRows: targetRows.length,
|
|
627
|
+
uniqueHwids: uniqueHwids.length,
|
|
628
|
+
sharedHwids: hwids.filter((hwid) => hwid.linkedAccountCount > 0)
|
|
629
|
+
.length,
|
|
630
|
+
linkedAccounts: accounts.length,
|
|
631
|
+
firstSeenAt: dateRange.firstSeenAt,
|
|
632
|
+
lastSeenAt: dateRange.lastSeenAt,
|
|
633
|
+
},
|
|
634
|
+
accounts,
|
|
635
|
+
hwids,
|
|
636
|
+
},
|
|
637
|
+
error: null,
|
|
638
|
+
};
|
|
639
|
+
break;
|
|
349
640
|
}
|
|
350
641
|
|
|
351
642
|
// Log the admin action
|
|
@@ -366,7 +657,11 @@ export async function handler(
|
|
|
366
657
|
);
|
|
367
658
|
}
|
|
368
659
|
|
|
369
|
-
if (
|
|
660
|
+
if (
|
|
661
|
+
targetUserId !== null &&
|
|
662
|
+
!result?.error &&
|
|
663
|
+
!SCORE_CACHE_READ_OPERATIONS.has(operation)
|
|
664
|
+
) {
|
|
370
665
|
await invalidateCachePrefix(`userscore:${targetUserId}`);
|
|
371
666
|
}
|
|
372
667
|
|
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
|
-
|
|
15
|
-
|
|
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
|
}),
|
|
@@ -40,11 +52,19 @@ export const Schema = {
|
|
|
40
52
|
country_position: z.number().nullable(),
|
|
41
53
|
activity_status: z.enum(["active", "inactive"]),
|
|
42
54
|
is_online: z.boolean(),
|
|
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
|
+
),
|
|
43
63
|
clans: z
|
|
44
64
|
.object({
|
|
45
65
|
id: z.number(),
|
|
46
|
-
acronym: z.string(),
|
|
47
|
-
})
|
|
66
|
+
acronym: z.string(),
|
|
67
|
+
})
|
|
48
68
|
.optional()
|
|
49
69
|
.nullable(),
|
|
50
70
|
})
|
|
@@ -122,11 +142,17 @@ export async function handler(
|
|
|
122
142
|
|
|
123
143
|
const activityStatus = await getActivityStatusForUserId(user.id);
|
|
124
144
|
|
|
125
|
-
const { data: activityData } = await supabase
|
|
126
|
-
.from("profileActivities")
|
|
127
|
-
.select("
|
|
128
|
-
.eq("uid", user.uid || "")
|
|
129
|
-
.single();
|
|
145
|
+
const { data: activityData } = await supabase
|
|
146
|
+
.from("profileActivities")
|
|
147
|
+
.select("last_activity")
|
|
148
|
+
.eq("uid", user.uid || "")
|
|
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 });
|
|
130
156
|
|
|
131
157
|
//last 30 minutes
|
|
132
158
|
if (activityData && activityData.last_activity) {
|
|
@@ -164,23 +190,32 @@ export async function handler(
|
|
|
164
190
|
: null;
|
|
165
191
|
}
|
|
166
192
|
|
|
167
|
-
if (user.verificationDeadline < Date.now()) {
|
|
168
|
-
await supabase
|
|
169
|
-
.from("profiles")
|
|
170
|
-
.upsert({
|
|
193
|
+
if (user.verificationDeadline < Date.now()) {
|
|
194
|
+
await supabase
|
|
195
|
+
.from("profiles")
|
|
196
|
+
.upsert({
|
|
171
197
|
id: user.id,
|
|
172
198
|
verified: false,
|
|
173
|
-
})
|
|
174
|
-
.select();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
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: {
|
|
179
211
|
...user,
|
|
180
212
|
position,
|
|
181
213
|
country_position: countryPosition,
|
|
182
214
|
activity_status: activityStatus,
|
|
183
215
|
is_online: isOnline,
|
|
216
|
+
last_active_timestamp: activityData?.last_activity ?? null,
|
|
217
|
+
next_username_change_at: getNextUsernameChangeAt(latestUsernameChangeAt),
|
|
218
|
+
previous_usernames: previousUsernames,
|
|
184
219
|
},
|
|
185
220
|
});
|
|
186
221
|
}
|