rhythia-api 197.0.0 → 198.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 +205 -0
- package/api/getVerified.ts +1 -1
- package/index.ts +18 -0
- package/package.json +1 -1
- package/types/database.ts +137 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
import { Database } from "../types/database";
|
|
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
|
+
|
|
9
|
+
// Define supported admin operations and their parameter types
|
|
10
|
+
const adminOperations = {
|
|
11
|
+
deleteUser: z.object({ userId: z.number() }),
|
|
12
|
+
excludeUser: z.object({ userId: z.number() }),
|
|
13
|
+
restrictUser: z.object({ userId: z.number() }),
|
|
14
|
+
silenceUser: z.object({ userId: z.number() }),
|
|
15
|
+
searchUsers: z.object({ searchText: z.string() }),
|
|
16
|
+
removeAllScores: z.object({ userId: z.number() }),
|
|
17
|
+
invalidateRankedScores: z.object({ userId: z.number() }),
|
|
18
|
+
unbanUser: z.object({ userId: z.number() }),
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
// Create a discriminated union type for operation parameters
|
|
22
|
+
const OperationParam = z.discriminatedUnion("operation", [
|
|
23
|
+
z.object({
|
|
24
|
+
operation: z.literal("deleteUser"),
|
|
25
|
+
params: adminOperations.deleteUser,
|
|
26
|
+
}),
|
|
27
|
+
z.object({
|
|
28
|
+
operation: z.literal("excludeUser"),
|
|
29
|
+
params: adminOperations.excludeUser,
|
|
30
|
+
}),
|
|
31
|
+
z.object({
|
|
32
|
+
operation: z.literal("restrictUser"),
|
|
33
|
+
params: adminOperations.restrictUser,
|
|
34
|
+
}),
|
|
35
|
+
z.object({
|
|
36
|
+
operation: z.literal("silenceUser"),
|
|
37
|
+
params: adminOperations.silenceUser,
|
|
38
|
+
}),
|
|
39
|
+
z.object({
|
|
40
|
+
operation: z.literal("searchUsers"),
|
|
41
|
+
params: adminOperations.searchUsers,
|
|
42
|
+
}),
|
|
43
|
+
z.object({
|
|
44
|
+
operation: z.literal("removeAllScores"),
|
|
45
|
+
params: adminOperations.removeAllScores,
|
|
46
|
+
}),
|
|
47
|
+
z.object({
|
|
48
|
+
operation: z.literal("invalidateRankedScores"),
|
|
49
|
+
params: adminOperations.invalidateRankedScores,
|
|
50
|
+
}),
|
|
51
|
+
z.object({
|
|
52
|
+
operation: z.literal("unbanUser"),
|
|
53
|
+
params: adminOperations.unbanUser,
|
|
54
|
+
}),
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
export const Schema = {
|
|
58
|
+
input: z.strictObject({
|
|
59
|
+
session: z.string(),
|
|
60
|
+
data: OperationParam,
|
|
61
|
+
}),
|
|
62
|
+
output: z.object({
|
|
63
|
+
success: z.boolean(),
|
|
64
|
+
result: z.any().optional(),
|
|
65
|
+
error: z.string().optional(),
|
|
66
|
+
}),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export async function POST(request: Request): Promise<NextResponse> {
|
|
70
|
+
return protectedApi({
|
|
71
|
+
request,
|
|
72
|
+
schema: Schema,
|
|
73
|
+
authorization: () => {},
|
|
74
|
+
activity: handler,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function handler(
|
|
79
|
+
data: (typeof Schema)["input"]["_type"]
|
|
80
|
+
): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
|
|
81
|
+
// Get user from session
|
|
82
|
+
const user = (await getUserBySession(data.session)) as User;
|
|
83
|
+
|
|
84
|
+
// Get user's profile data
|
|
85
|
+
const { data: queryUserData, error: userError } = await supabase
|
|
86
|
+
.from("profiles")
|
|
87
|
+
.select("*")
|
|
88
|
+
.eq("uid", user.id)
|
|
89
|
+
.single();
|
|
90
|
+
|
|
91
|
+
if (userError || !queryUserData) {
|
|
92
|
+
return NextResponse.json(
|
|
93
|
+
{
|
|
94
|
+
success: false,
|
|
95
|
+
error: "User cannot be retrieved from session",
|
|
96
|
+
},
|
|
97
|
+
{ status: 404 }
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check if user has "Global Moderator" badge
|
|
102
|
+
const badges = queryUserData.badges as Record<string, any> | null;
|
|
103
|
+
const isGlobalModerator =
|
|
104
|
+
badges &&
|
|
105
|
+
Array.isArray(badges.badges) &&
|
|
106
|
+
badges.badges.some((badge: any) => badge === "Global Moderator");
|
|
107
|
+
|
|
108
|
+
if (!isGlobalModerator) {
|
|
109
|
+
return NextResponse.json(
|
|
110
|
+
{
|
|
111
|
+
success: false,
|
|
112
|
+
error: "Unauthorized. Only Global Moderators can perform this action.",
|
|
113
|
+
},
|
|
114
|
+
{ status: 403 }
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Execute the requested admin operation
|
|
119
|
+
try {
|
|
120
|
+
let result;
|
|
121
|
+
const operation = data.data.operation;
|
|
122
|
+
const params = data.data.params as any;
|
|
123
|
+
|
|
124
|
+
switch (operation) {
|
|
125
|
+
case "deleteUser":
|
|
126
|
+
result = await supabase.rpc("admin_delete_user", {
|
|
127
|
+
user_id: params.userId,
|
|
128
|
+
});
|
|
129
|
+
break;
|
|
130
|
+
|
|
131
|
+
case "excludeUser":
|
|
132
|
+
result = await supabase.rpc("admin_exclude_user", {
|
|
133
|
+
user_id: params.userId,
|
|
134
|
+
});
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case "restrictUser":
|
|
138
|
+
result = await supabase.rpc("admin_restrict_user", {
|
|
139
|
+
user_id: params.userId,
|
|
140
|
+
});
|
|
141
|
+
break;
|
|
142
|
+
|
|
143
|
+
case "silenceUser":
|
|
144
|
+
result = await supabase.rpc("admin_silence_user", {
|
|
145
|
+
user_id: params.userId,
|
|
146
|
+
});
|
|
147
|
+
break;
|
|
148
|
+
|
|
149
|
+
case "searchUsers":
|
|
150
|
+
result = await supabase.rpc("admin_search_users", {
|
|
151
|
+
search_text: params.searchText,
|
|
152
|
+
});
|
|
153
|
+
break;
|
|
154
|
+
|
|
155
|
+
case "removeAllScores":
|
|
156
|
+
result = await supabase.rpc("admin_remove_all_scores", {
|
|
157
|
+
user_id: params.userId,
|
|
158
|
+
});
|
|
159
|
+
break;
|
|
160
|
+
|
|
161
|
+
case "invalidateRankedScores":
|
|
162
|
+
result = await supabase.rpc("admin_invalidate_ranked_scores", {
|
|
163
|
+
user_id: params.userId,
|
|
164
|
+
});
|
|
165
|
+
break;
|
|
166
|
+
|
|
167
|
+
case "unbanUser":
|
|
168
|
+
result = await supabase.rpc("admin_unban_user", {
|
|
169
|
+
user_id: params.userId,
|
|
170
|
+
});
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Log the admin action
|
|
175
|
+
await supabase.rpc("admin_log_action", {
|
|
176
|
+
admin_id: queryUserData.id,
|
|
177
|
+
action_type: operation,
|
|
178
|
+
target_id: "userId" in params ? params.userId : null,
|
|
179
|
+
details: { params },
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (result.error) {
|
|
183
|
+
return NextResponse.json(
|
|
184
|
+
{
|
|
185
|
+
success: false,
|
|
186
|
+
error: result.error.message,
|
|
187
|
+
},
|
|
188
|
+
{ status: 500 }
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return NextResponse.json({
|
|
193
|
+
success: true,
|
|
194
|
+
result: result.data,
|
|
195
|
+
});
|
|
196
|
+
} catch (err: any) {
|
|
197
|
+
return NextResponse.json(
|
|
198
|
+
{
|
|
199
|
+
success: false,
|
|
200
|
+
error: err.message || "An error occurred during the operation",
|
|
201
|
+
},
|
|
202
|
+
{ status: 500 }
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
package/api/getVerified.ts
CHANGED
package/index.ts
CHANGED
|
@@ -308,6 +308,24 @@ import { Schema as EditProfile } from "./api/editProfile"
|
|
|
308
308
|
export { Schema as SchemaEditProfile } from "./api/editProfile"
|
|
309
309
|
export const editProfile = handleApi({url:"/api/editProfile",...EditProfile})
|
|
310
310
|
|
|
311
|
+
// ./api/executeAdminOperation.ts API
|
|
312
|
+
|
|
313
|
+
/*
|
|
314
|
+
export const Schema = {
|
|
315
|
+
input: z.strictObject({
|
|
316
|
+
session: z.string(),
|
|
317
|
+
data: OperationParam,
|
|
318
|
+
}),
|
|
319
|
+
output: z.object({
|
|
320
|
+
success: z.boolean(),
|
|
321
|
+
result: z.any().optional(),
|
|
322
|
+
error: z.string().optional(),
|
|
323
|
+
}),
|
|
324
|
+
};*/
|
|
325
|
+
import { Schema as ExecuteAdminOperation } from "./api/executeAdminOperation"
|
|
326
|
+
export { Schema as SchemaExecuteAdminOperation } from "./api/executeAdminOperation"
|
|
327
|
+
export const executeAdminOperation = handleApi({url:"/api/executeAdminOperation",...ExecuteAdminOperation})
|
|
328
|
+
|
|
311
329
|
// ./api/getAvatarUploadUrl.ts API
|
|
312
330
|
|
|
313
331
|
/*
|
package/package.json
CHANGED
package/types/database.ts
CHANGED
|
@@ -9,6 +9,62 @@ export type Json =
|
|
|
9
9
|
export type Database = {
|
|
10
10
|
public: {
|
|
11
11
|
Tables: {
|
|
12
|
+
admin_actions: {
|
|
13
|
+
Row: {
|
|
14
|
+
action_type: string
|
|
15
|
+
admin_id: number
|
|
16
|
+
created_at: string | null
|
|
17
|
+
details: Json | null
|
|
18
|
+
id: number
|
|
19
|
+
target_id: number | null
|
|
20
|
+
}
|
|
21
|
+
Insert: {
|
|
22
|
+
action_type: string
|
|
23
|
+
admin_id: number
|
|
24
|
+
created_at?: string | null
|
|
25
|
+
details?: Json | null
|
|
26
|
+
id?: number
|
|
27
|
+
target_id?: number | null
|
|
28
|
+
}
|
|
29
|
+
Update: {
|
|
30
|
+
action_type?: string
|
|
31
|
+
admin_id?: number
|
|
32
|
+
created_at?: string | null
|
|
33
|
+
details?: Json | null
|
|
34
|
+
id?: number
|
|
35
|
+
target_id?: number | null
|
|
36
|
+
}
|
|
37
|
+
Relationships: []
|
|
38
|
+
}
|
|
39
|
+
admin_operations: {
|
|
40
|
+
Row: {
|
|
41
|
+
action_type: string | null
|
|
42
|
+
details: Json
|
|
43
|
+
id: number
|
|
44
|
+
target_id: string | null
|
|
45
|
+
}
|
|
46
|
+
Insert: {
|
|
47
|
+
action_type?: string | null
|
|
48
|
+
details: Json
|
|
49
|
+
id?: number
|
|
50
|
+
target_id?: string | null
|
|
51
|
+
}
|
|
52
|
+
Update: {
|
|
53
|
+
action_type?: string | null
|
|
54
|
+
details?: Json
|
|
55
|
+
id?: number
|
|
56
|
+
target_id?: string | null
|
|
57
|
+
}
|
|
58
|
+
Relationships: [
|
|
59
|
+
{
|
|
60
|
+
foreignKeyName: "admin_operations_id_fkey"
|
|
61
|
+
columns: ["id"]
|
|
62
|
+
isOneToOne: true
|
|
63
|
+
referencedRelation: "profiles"
|
|
64
|
+
referencedColumns: ["id"]
|
|
65
|
+
},
|
|
66
|
+
]
|
|
67
|
+
}
|
|
12
68
|
beatmapCollections: {
|
|
13
69
|
Row: {
|
|
14
70
|
created_at: string
|
|
@@ -541,6 +597,86 @@ export type Database = {
|
|
|
541
597
|
[_ in never]: never
|
|
542
598
|
}
|
|
543
599
|
Functions: {
|
|
600
|
+
admin_delete_user: {
|
|
601
|
+
Args: {
|
|
602
|
+
user_id: number
|
|
603
|
+
}
|
|
604
|
+
Returns: boolean
|
|
605
|
+
}
|
|
606
|
+
admin_exclude_user: {
|
|
607
|
+
Args: {
|
|
608
|
+
user_id: number
|
|
609
|
+
}
|
|
610
|
+
Returns: boolean
|
|
611
|
+
}
|
|
612
|
+
admin_invalidate_ranked_scores: {
|
|
613
|
+
Args: {
|
|
614
|
+
user_id: number
|
|
615
|
+
}
|
|
616
|
+
Returns: number
|
|
617
|
+
}
|
|
618
|
+
admin_log_action: {
|
|
619
|
+
Args: {
|
|
620
|
+
admin_id: number
|
|
621
|
+
action_type: string
|
|
622
|
+
target_id: string
|
|
623
|
+
details?: Json
|
|
624
|
+
}
|
|
625
|
+
Returns: undefined
|
|
626
|
+
}
|
|
627
|
+
admin_remove_all_scores: {
|
|
628
|
+
Args: {
|
|
629
|
+
user_id: number
|
|
630
|
+
}
|
|
631
|
+
Returns: number
|
|
632
|
+
}
|
|
633
|
+
admin_restrict_user: {
|
|
634
|
+
Args: {
|
|
635
|
+
user_id: number
|
|
636
|
+
}
|
|
637
|
+
Returns: boolean
|
|
638
|
+
}
|
|
639
|
+
admin_search_users: {
|
|
640
|
+
Args: {
|
|
641
|
+
search_text: string
|
|
642
|
+
}
|
|
643
|
+
Returns: {
|
|
644
|
+
about_me: string | null
|
|
645
|
+
avatar_url: string | null
|
|
646
|
+
badges: Json | null
|
|
647
|
+
ban: Database["public"]["Enums"]["banTypes"] | null
|
|
648
|
+
bannedAt: number | null
|
|
649
|
+
clan: number | null
|
|
650
|
+
computedUsername: string | null
|
|
651
|
+
created_at: number | null
|
|
652
|
+
flag: string | null
|
|
653
|
+
id: number
|
|
654
|
+
mu_rank: number
|
|
655
|
+
play_count: number | null
|
|
656
|
+
profile_image: string | null
|
|
657
|
+
sigma_rank: number | null
|
|
658
|
+
skill_points: number | null
|
|
659
|
+
spin_skill_points: number
|
|
660
|
+
squares_hit: number | null
|
|
661
|
+
total_score: number | null
|
|
662
|
+
uid: string | null
|
|
663
|
+
username: string | null
|
|
664
|
+
verificationDeadline: number
|
|
665
|
+
verified: boolean | null
|
|
666
|
+
}[]
|
|
667
|
+
}
|
|
668
|
+
admin_silence_user: {
|
|
669
|
+
Args: {
|
|
670
|
+
user_id: number
|
|
671
|
+
}
|
|
672
|
+
Returns: boolean
|
|
673
|
+
}
|
|
674
|
+
admin_unban_user: {
|
|
675
|
+
Args: {
|
|
676
|
+
user_id: number
|
|
677
|
+
}
|
|
678
|
+
Returns: boolean
|
|
679
|
+
}
|
|
544
680
|
get_clan_leaderboard: {
|
|
545
681
|
Args: {
|
|
546
682
|
page_number?: number
|
|
@@ -755,6 +891,7 @@ export type Database = {
|
|
|
755
891
|
userid: number
|
|
756
892
|
username: string
|
|
757
893
|
avatar_url: string
|
|
894
|
+
accuracy: number
|
|
758
895
|
}[]
|
|
759
896
|
}
|
|
760
897
|
get_top_scores_for_beatmap2: {
|