rhythia-api 237.0.0 → 238.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/executeAdminOperation.ts +296 -1
- package/api/getProfile.ts +9 -7
- package/index.ts +4 -13
- package/package.json +1 -1
- package/types/database.ts +1356 -1327
|
@@ -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
|
@@ -40,11 +40,12 @@ export const Schema = {
|
|
|
40
40
|
country_position: z.number().nullable(),
|
|
41
41
|
activity_status: z.enum(["active", "inactive"]),
|
|
42
42
|
is_online: z.boolean(),
|
|
43
|
+
last_active_timestamp: z.number().nullable(),
|
|
43
44
|
clans: z
|
|
44
45
|
.object({
|
|
45
46
|
id: z.number(),
|
|
46
|
-
acronym: z.string(),
|
|
47
|
-
})
|
|
47
|
+
acronym: z.string(),
|
|
48
|
+
})
|
|
48
49
|
.optional()
|
|
49
50
|
.nullable(),
|
|
50
51
|
})
|
|
@@ -122,11 +123,11 @@ export async function handler(
|
|
|
122
123
|
|
|
123
124
|
const activityStatus = await getActivityStatusForUserId(user.id);
|
|
124
125
|
|
|
125
|
-
const { data: activityData } = await supabase
|
|
126
|
-
.from("profileActivities")
|
|
127
|
-
.select("
|
|
128
|
-
.eq("uid", user.uid || "")
|
|
129
|
-
.single();
|
|
126
|
+
const { data: activityData } = await supabase
|
|
127
|
+
.from("profileActivities")
|
|
128
|
+
.select("last_activity")
|
|
129
|
+
.eq("uid", user.uid || "")
|
|
130
|
+
.single();
|
|
130
131
|
|
|
131
132
|
//last 30 minutes
|
|
132
133
|
if (activityData && activityData.last_activity) {
|
|
@@ -181,6 +182,7 @@ export async function handler(
|
|
|
181
182
|
country_position: countryPosition,
|
|
182
183
|
activity_status: activityStatus,
|
|
183
184
|
is_online: isOnline,
|
|
185
|
+
last_active_timestamp: activityData?.last_activity ?? null,
|
|
184
186
|
},
|
|
185
187
|
});
|
|
186
188
|
}
|
package/index.ts
CHANGED
|
@@ -362,17 +362,7 @@ export const enhancedSearch = handleApi({url:"/api/enhancedSearch",...EnhancedSe
|
|
|
362
362
|
// ./api/executeAdminOperation.ts API
|
|
363
363
|
|
|
364
364
|
/*
|
|
365
|
-
|
|
366
|
-
input: z.strictObject({
|
|
367
|
-
session: z.string(),
|
|
368
|
-
data: OperationParam,
|
|
369
|
-
}),
|
|
370
|
-
output: z.object({
|
|
371
|
-
success: z.boolean(),
|
|
372
|
-
result: z.any().optional(),
|
|
373
|
-
error: z.string().optional(),
|
|
374
|
-
}),
|
|
375
|
-
};*/
|
|
365
|
+
*/
|
|
376
366
|
import { Schema as ExecuteAdminOperation } from "./api/executeAdminOperation"
|
|
377
367
|
export { Schema as SchemaExecuteAdminOperation } from "./api/executeAdminOperation"
|
|
378
368
|
export const executeAdminOperation = handleApi({url:"/api/executeAdminOperation",...ExecuteAdminOperation})
|
|
@@ -1001,11 +991,12 @@ export const Schema = {
|
|
|
1001
991
|
country_position: z.number().nullable(),
|
|
1002
992
|
activity_status: z.enum(["active", "inactive"]),
|
|
1003
993
|
is_online: z.boolean(),
|
|
994
|
+
last_active_timestamp: z.number().nullable(),
|
|
1004
995
|
clans: z
|
|
1005
996
|
.object({
|
|
1006
997
|
id: z.number(),
|
|
1007
|
-
acronym: z.string(),
|
|
1008
|
-
})
|
|
998
|
+
acronym: z.string(),
|
|
999
|
+
})
|
|
1009
1000
|
.optional()
|
|
1010
1001
|
.nullable(),
|
|
1011
1002
|
})
|