rhythia-api 241.0.0 → 243.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/createBeatmap.ts +57 -39
- package/api/editProfile.ts +4 -56
- package/api/editProfileFriend.ts +122 -0
- package/api/executeAdminOperation.ts +1030 -681
- package/api/getAvatarUploadUrl.ts +90 -85
- package/api/getBeatmapPage.ts +2 -0
- package/api/getBeatmapPageById.ts +2 -0
- package/api/getBeatmaps.ts +110 -197
- package/api/getCollection.ts +44 -31
- package/api/getFriends.ts +154 -0
- package/api/getMapUploadUrl.ts +90 -93
- package/api/getProfile.ts +195 -127
- package/api/getScore.ts +2 -0
- package/api/getVideoUploadUrl.ts +90 -85
- package/api/submitScoreInternal.ts +506 -461
- package/api/updateBeatmapPage.ts +6 -0
- package/beatmap-file-urls.json +29398 -0
- package/handleApi.ts +24 -21
- package/index.ts +183 -137
- package/package.json +5 -2
- package/queries/admin_delete_user.sql +42 -39
- package/queries/admin_remove_all_scores.sql +6 -3
- package/queries/admin_remove_score.sql +107 -0
- package/queries/admin_update_profile.sql +22 -0
- package/queries/get_beatmaps_v2.sql +48 -0
- package/queries/get_top_scores_for_beatmap3.sql +47 -38
- package/queries/profile_update_guards.sql +66 -0
- package/types/database.ts +1525 -1414
- package/utils/beatmapFiles.ts +102 -0
- package/utils/beatmapHash.ts +336 -0
- package/utils/beatmapTopScores.ts +68 -84
- package/utils/getUserBySession.ts +3 -1
- package/utils/profileUpdateValidation.ts +51 -0
- package/utils/redis.ts +24 -0
- package/utils/rhrReplay.ts +122 -0
- package/utils/star-calc/sspmParser.ts +294 -160
- package/worker.ts +195 -191
package/api/getProfile.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { geolocation } from "../utils/requestGeo";
|
|
2
|
-
import { NextResponse } from "../utils/response";
|
|
3
|
-
import z from "zod";
|
|
4
|
-
import { Database } from "../types/database";
|
|
5
|
-
import { protectedApi } from "../utils/requestUtils";
|
|
6
|
-
import { supabase } from "../utils/supabase";
|
|
7
|
-
import { getUserBySession } from "../utils/getUserBySession";
|
|
1
|
+
import { geolocation } from "../utils/requestGeo";
|
|
2
|
+
import { NextResponse } from "../utils/response";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
import { Database } from "../types/database";
|
|
5
|
+
import { protectedApi } from "../utils/requestUtils";
|
|
6
|
+
import { supabase } from "../utils/supabase";
|
|
7
|
+
import { getUserBySession } from "../utils/getUserBySession";
|
|
8
8
|
import { User } from "@supabase/supabase-js";
|
|
9
9
|
import {
|
|
10
10
|
getActivityStatusForUserId,
|
|
11
11
|
getScoreActivityCutoffIso,
|
|
12
12
|
} from "../utils/activityStatus";
|
|
13
13
|
|
|
14
|
+
const friendStateSchema = z.enum(["none", "added", "mutual"]);
|
|
15
|
+
|
|
14
16
|
function getNextUsernameChangeAt(
|
|
15
17
|
lastChangedAt: string | null | undefined
|
|
16
18
|
): string | null {
|
|
@@ -23,28 +25,73 @@ function getNextUsernameChangeAt(
|
|
|
23
25
|
return nextAllowedAt.toISOString();
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
async function getViewerProfileId(session: string) {
|
|
29
|
+
if (!session) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const viewer = await getUserBySession(session);
|
|
34
|
+
|
|
35
|
+
if (!viewer) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { data: viewerProfile } = await supabase
|
|
40
|
+
.from("profiles")
|
|
41
|
+
.select("id")
|
|
42
|
+
.eq("uid", viewer.id)
|
|
43
|
+
.single();
|
|
44
|
+
|
|
45
|
+
return viewerProfile?.id ?? null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function getFriendState(viewerProfileId: number | null, profileId: number) {
|
|
49
|
+
if (viewerProfileId === null || viewerProfileId === profileId) {
|
|
50
|
+
return "none" as const;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const [{ count: addedCount }, { count: mutualCount }] = await Promise.all([
|
|
54
|
+
supabase
|
|
55
|
+
.from("profileFriends")
|
|
56
|
+
.select("*", { count: "exact", head: true })
|
|
57
|
+
.eq("profile_id", viewerProfileId)
|
|
58
|
+
.eq("friend_id", profileId),
|
|
59
|
+
supabase
|
|
60
|
+
.from("profileFriends")
|
|
61
|
+
.select("*", { count: "exact", head: true })
|
|
62
|
+
.eq("profile_id", profileId)
|
|
63
|
+
.eq("friend_id", viewerProfileId),
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
if (!addedCount) {
|
|
67
|
+
return "none" as const;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return mutualCount ? ("mutual" as const) : ("added" as const);
|
|
71
|
+
}
|
|
72
|
+
|
|
26
73
|
export const Schema = {
|
|
27
74
|
input: z.strictObject({
|
|
28
|
-
session: z.string(),
|
|
29
|
-
id: z.number().nullable().optional(),
|
|
30
|
-
}),
|
|
31
|
-
output: z.object({
|
|
32
|
-
error: z.string().optional(),
|
|
33
|
-
user: z
|
|
34
|
-
.object({
|
|
35
|
-
about_me: z.string().nullable(),
|
|
36
|
-
avatar_url: z.string().nullable(),
|
|
37
|
-
profile_image: z.string().nullable(),
|
|
38
|
-
badges: z.any().nullable(),
|
|
39
|
-
created_at: z.number().nullable(),
|
|
40
|
-
flag: z.string().nullable(),
|
|
41
|
-
id: z.number(),
|
|
42
|
-
uid: z.string().nullable(),
|
|
43
|
-
ban: z.string().nullable(),
|
|
44
|
-
username: z.string().nullable(),
|
|
45
|
-
verified: z.boolean().nullable(),
|
|
46
|
-
verificationDeadline: z.number().nullable(),
|
|
47
|
-
play_count: z.number().nullable(),
|
|
75
|
+
session: z.string(),
|
|
76
|
+
id: z.number().nullable().optional(),
|
|
77
|
+
}),
|
|
78
|
+
output: z.object({
|
|
79
|
+
error: z.string().optional(),
|
|
80
|
+
user: z
|
|
81
|
+
.object({
|
|
82
|
+
about_me: z.string().nullable(),
|
|
83
|
+
avatar_url: z.string().nullable(),
|
|
84
|
+
profile_image: z.string().nullable(),
|
|
85
|
+
badges: z.any().nullable(),
|
|
86
|
+
created_at: z.number().nullable(),
|
|
87
|
+
flag: z.string().nullable(),
|
|
88
|
+
id: z.number(),
|
|
89
|
+
uid: z.string().nullable(),
|
|
90
|
+
ban: z.string().nullable(),
|
|
91
|
+
username: z.string().nullable(),
|
|
92
|
+
verified: z.boolean().nullable(),
|
|
93
|
+
verificationDeadline: z.number().nullable(),
|
|
94
|
+
play_count: z.number().nullable(),
|
|
48
95
|
skill_points: z.number().nullable(),
|
|
49
96
|
squares_hit: z.number().nullable(),
|
|
50
97
|
total_score: z.number().nullable(),
|
|
@@ -53,6 +100,8 @@ export const Schema = {
|
|
|
53
100
|
activity_status: z.enum(["active", "inactive"]),
|
|
54
101
|
is_online: z.boolean(),
|
|
55
102
|
last_active_timestamp: z.number().nullable(),
|
|
103
|
+
friend_count: z.number(),
|
|
104
|
+
friend_state: friendStateSchema,
|
|
56
105
|
can_change_flag: z.boolean(),
|
|
57
106
|
next_username_change_at: z.string().nullable(),
|
|
58
107
|
previous_usernames: z.array(
|
|
@@ -66,105 +115,122 @@ export const Schema = {
|
|
|
66
115
|
id: z.number(),
|
|
67
116
|
acronym: z.string(),
|
|
68
117
|
})
|
|
69
|
-
.optional()
|
|
70
|
-
.nullable(),
|
|
71
|
-
})
|
|
72
|
-
.optional(),
|
|
73
|
-
}),
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
export async function POST(request: Request): Promise<NextResponse> {
|
|
77
|
-
return protectedApi({
|
|
78
|
-
request,
|
|
79
|
-
schema: Schema,
|
|
80
|
-
authorization: () => {},
|
|
81
|
-
activity: handler,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export async function handler(
|
|
86
|
-
data: (typeof Schema)["input"]["_type"],
|
|
87
|
-
req: Request
|
|
88
|
-
): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
|
|
89
|
-
let profiles: Database["public"]["Tables"]["profiles"]["Row"][] = [];
|
|
90
|
-
let isOnline = false;
|
|
91
|
-
// Fetch by id
|
|
92
|
-
if (data.id !== undefined && data.id !== null) {
|
|
93
|
-
let { data: queryData, error } = await supabase
|
|
94
|
-
.from("profiles")
|
|
95
|
-
.select(`*,clans:clan(id,acronym)`)
|
|
96
|
-
.eq("id", data.id);
|
|
97
|
-
|
|
98
|
-
console.log(profiles, error);
|
|
99
|
-
|
|
100
|
-
if (!queryData?.length) {
|
|
101
|
-
return NextResponse.json(
|
|
102
|
-
{
|
|
103
|
-
error: "User not found",
|
|
104
|
-
},
|
|
105
|
-
{ status: 404 }
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
profiles = queryData;
|
|
110
|
-
} else {
|
|
111
|
-
// Fetch by session id
|
|
112
|
-
const user = (await getUserBySession(data.session)) as User;
|
|
113
|
-
|
|
114
|
-
if (user) {
|
|
115
|
-
let { data: queryData, error } = await supabase
|
|
116
|
-
.from("profiles")
|
|
117
|
-
.select("*")
|
|
118
|
-
.eq("uid", user.id);
|
|
119
|
-
|
|
120
|
-
if (!queryData?.length) {
|
|
121
|
-
const geo = geolocation(req);
|
|
122
|
-
const data = await supabase.from("profiles").upsert({
|
|
123
|
-
uid: user.id,
|
|
124
|
-
about_me: "",
|
|
125
|
-
avatar_url:
|
|
126
|
-
"https://rhthia-avatars.s3.eu-central-003.backblazeb2.com/user-avatar-1725309193296-72002e6b-321c-4f60-a692-568e0e75147d",
|
|
127
|
-
badges: [],
|
|
128
|
-
username: `user${Math.round(Math.random() * 900000 + 100000)}`,
|
|
129
|
-
computedUsername: `user${Math.round(Math.random() * 900000 + 100000)}`.toLowerCase(),
|
|
130
|
-
flag: (geo.country || "US").toUpperCase(),
|
|
131
|
-
created_at: Date.now(),
|
|
132
|
-
}).select(`
|
|
133
|
-
*,clans:clan(id,acronym)`);
|
|
134
|
-
|
|
135
|
-
profiles = data.data!;
|
|
136
|
-
} else {
|
|
137
|
-
profiles = queryData;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const user = profiles[0];
|
|
143
|
-
|
|
144
|
-
const activityStatus = await getActivityStatusForUserId(user.id);
|
|
145
|
-
|
|
146
|
-
const { data: activityData } = await supabase
|
|
147
|
-
.from("profileActivities")
|
|
148
|
-
.select("last_activity")
|
|
149
|
-
.eq("uid", user.uid || "")
|
|
150
|
-
.single();
|
|
118
|
+
.optional()
|
|
119
|
+
.nullable(),
|
|
120
|
+
})
|
|
121
|
+
.optional(),
|
|
122
|
+
}),
|
|
123
|
+
};
|
|
151
124
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
125
|
+
export async function POST(request: Request): Promise<NextResponse> {
|
|
126
|
+
return protectedApi({
|
|
127
|
+
request,
|
|
128
|
+
schema: Schema,
|
|
129
|
+
authorization: () => {},
|
|
130
|
+
activity: handler,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
157
133
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
134
|
+
export async function handler(
|
|
135
|
+
data: (typeof Schema)["input"]["_type"],
|
|
136
|
+
req: Request
|
|
137
|
+
): Promise<NextResponse<(typeof Schema)["output"]["_type"]>> {
|
|
138
|
+
let profiles: Database["public"]["Tables"]["profiles"]["Row"][] = [];
|
|
139
|
+
let isOnline = false;
|
|
140
|
+
let viewerProfileId =
|
|
141
|
+
data.id !== undefined && data.id !== null
|
|
142
|
+
? await getViewerProfileId(data.session)
|
|
143
|
+
: null;
|
|
144
|
+
// Fetch by id
|
|
145
|
+
if (data.id !== undefined && data.id !== null) {
|
|
146
|
+
let { data: queryData, error } = await supabase
|
|
147
|
+
.from("profiles")
|
|
148
|
+
.select(`*,clans:clan(id,acronym)`)
|
|
149
|
+
.eq("id", data.id);
|
|
150
|
+
|
|
151
|
+
console.log(profiles, error);
|
|
152
|
+
|
|
153
|
+
if (!queryData?.length) {
|
|
154
|
+
return NextResponse.json(
|
|
155
|
+
{
|
|
156
|
+
error: "User not found",
|
|
157
|
+
},
|
|
158
|
+
{ status: 404 }
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
profiles = queryData;
|
|
163
|
+
} else {
|
|
164
|
+
// Fetch by session id
|
|
165
|
+
const user = (await getUserBySession(data.session)) as User;
|
|
166
|
+
|
|
167
|
+
if (user) {
|
|
168
|
+
let { data: queryData, error } = await supabase
|
|
169
|
+
.from("profiles")
|
|
170
|
+
.select("*")
|
|
171
|
+
.eq("uid", user.id);
|
|
172
|
+
|
|
173
|
+
if (!queryData?.length) {
|
|
174
|
+
const geo = geolocation(req);
|
|
175
|
+
const data = await supabase.from("profiles").upsert({
|
|
176
|
+
uid: user.id,
|
|
177
|
+
about_me: "",
|
|
178
|
+
avatar_url:
|
|
179
|
+
"https://rhthia-avatars.s3.eu-central-003.backblazeb2.com/user-avatar-1725309193296-72002e6b-321c-4f60-a692-568e0e75147d",
|
|
180
|
+
badges: [],
|
|
181
|
+
username: `user${Math.round(Math.random() * 900000 + 100000)}`,
|
|
182
|
+
computedUsername: `user${Math.round(Math.random() * 900000 + 100000)}`.toLowerCase(),
|
|
183
|
+
flag: (geo.country || "US").toUpperCase(),
|
|
184
|
+
created_at: Date.now(),
|
|
185
|
+
}).select(`
|
|
186
|
+
*,clans:clan(id,acronym)`);
|
|
187
|
+
|
|
188
|
+
profiles = data.data!;
|
|
189
|
+
} else {
|
|
190
|
+
profiles = queryData;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
viewerProfileId = profiles[0]?.id ?? null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const user = profiles[0];
|
|
198
|
+
|
|
199
|
+
const activityStatus = await getActivityStatusForUserId(user.id);
|
|
200
|
+
|
|
201
|
+
const [
|
|
202
|
+
{ data: activityData },
|
|
203
|
+
{ data: usernameHistoryData },
|
|
204
|
+
{ data: flagHistoryData },
|
|
205
|
+
{ count: friendCount },
|
|
206
|
+
friendState,
|
|
207
|
+
] = await Promise.all([
|
|
208
|
+
supabase
|
|
209
|
+
.from("profileActivities")
|
|
210
|
+
.select("last_activity")
|
|
211
|
+
.eq("uid", user.uid || "")
|
|
212
|
+
.single(),
|
|
213
|
+
supabase
|
|
214
|
+
.from("profileUsernames")
|
|
215
|
+
.select("username,changed_at")
|
|
216
|
+
.eq("profile_id", user.id)
|
|
217
|
+
.order("changed_at", { ascending: false }),
|
|
218
|
+
supabase
|
|
219
|
+
.from("profileFlags")
|
|
220
|
+
.select("id")
|
|
221
|
+
.eq("profile_id", user.id)
|
|
222
|
+
.limit(1),
|
|
223
|
+
supabase
|
|
224
|
+
.from("profileFriends")
|
|
225
|
+
.select("*", { count: "exact", head: true })
|
|
226
|
+
.eq("friend_id", user.id),
|
|
227
|
+
getFriendState(viewerProfileId, user.id),
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
//last 30 minutes
|
|
231
|
+
if (activityData && activityData.last_activity) {
|
|
232
|
+
isOnline = Date.now() - activityData.last_activity < 1800000;
|
|
233
|
+
}
|
|
168
234
|
|
|
169
235
|
let position: number | null = null;
|
|
170
236
|
let countryPosition: number | null = null;
|
|
@@ -196,13 +262,13 @@ export async function handler(
|
|
|
196
262
|
? (countryPlayersWithMorePoints || 0) + 1
|
|
197
263
|
: null;
|
|
198
264
|
}
|
|
199
|
-
|
|
265
|
+
|
|
200
266
|
if (user.verificationDeadline < Date.now()) {
|
|
201
267
|
await supabase
|
|
202
268
|
.from("profiles")
|
|
203
269
|
.upsert({
|
|
204
|
-
id: user.id,
|
|
205
|
-
verified: false,
|
|
270
|
+
id: user.id,
|
|
271
|
+
verified: false,
|
|
206
272
|
})
|
|
207
273
|
.select();
|
|
208
274
|
}
|
|
@@ -221,6 +287,8 @@ export async function handler(
|
|
|
221
287
|
activity_status: activityStatus,
|
|
222
288
|
is_online: isOnline,
|
|
223
289
|
last_active_timestamp: activityData?.last_activity ?? null,
|
|
290
|
+
friend_count: friendCount || 0,
|
|
291
|
+
friend_state: friendState,
|
|
224
292
|
can_change_flag: !flagHistoryData?.length,
|
|
225
293
|
next_username_change_at: getNextUsernameChangeAt(latestUsernameChangeAt),
|
|
226
294
|
previous_usernames: previousUsernames,
|
package/api/getScore.ts
CHANGED
|
@@ -26,6 +26,7 @@ export const Schema = {
|
|
|
26
26
|
username: z.string().optional().nullable(),
|
|
27
27
|
speed: z.number().optional().nullable(),
|
|
28
28
|
spin: z.boolean().optional().nullable(),
|
|
29
|
+
replay_url: z.string().optional().nullable(),
|
|
29
30
|
})
|
|
30
31
|
.optional(),
|
|
31
32
|
}),
|
|
@@ -80,6 +81,7 @@ export async function handler(
|
|
|
80
81
|
username: score.profiles?.username,
|
|
81
82
|
speed: score.speed,
|
|
82
83
|
spin: score.spin,
|
|
84
|
+
replay_url: score.replay_url,
|
|
83
85
|
},
|
|
84
86
|
});
|
|
85
87
|
}
|
package/api/getVideoUploadUrl.ts
CHANGED
|
@@ -1,85 +1,90 @@
|
|
|
1
|
-
import { NextResponse } from "../utils/response";
|
|
2
|
-
import z from "zod";
|
|
3
|
-
import { protectedApi, validUser } from "../utils/requestUtils";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
1
|
+
import { NextResponse } from "../utils/response";
|
|
2
|
+
import z from "zod";
|
|
3
|
+
import { protectedApi, validUser } from "../utils/requestUtils";
|
|
4
|
+
|
|
5
|
+
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
6
|
+
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
7
|
+
import { validateIntrinsicToken } from "../utils/validateToken";
|
|
8
|
+
import { getUserBySession } from "../utils/getUserBySession";
|
|
9
|
+
import { User } from "@supabase/supabase-js";
|
|
10
|
+
|
|
11
|
+
const allowedContentTypes = new Set(["video/mp4", "video/webm", "video/ogg"]);
|
|
12
|
+
|
|
13
|
+
const s3Client = new S3Client({
|
|
14
|
+
region: "auto",
|
|
15
|
+
endpoint: "https://s3.eu-central-003.backblazeb2.com",
|
|
16
|
+
credentials: {
|
|
17
|
+
secretAccessKey: process.env.SECRET_BUCKET || "",
|
|
18
|
+
accessKeyId: process.env.ACCESS_BUCKET || "",
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const Schema = {
|
|
23
|
+
input: z.strictObject({
|
|
24
|
+
session: z.string(),
|
|
25
|
+
contentLength: z.number(),
|
|
26
|
+
contentType: z.string(),
|
|
27
|
+
intrinsicToken: z.string(),
|
|
28
|
+
}),
|
|
29
|
+
output: z.strictObject({
|
|
30
|
+
error: z.string().optional(),
|
|
31
|
+
url: z.string().optional(),
|
|
32
|
+
objectKey: z.string().optional(),
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export async function POST(request: Request): Promise<NextResponse> {
|
|
37
|
+
return protectedApi({
|
|
38
|
+
request,
|
|
39
|
+
schema: Schema,
|
|
40
|
+
authorization: validUser,
|
|
41
|
+
activity: handler,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function handler({
|
|
46
|
+
session,
|
|
47
|
+
contentLength,
|
|
48
|
+
contentType,
|
|
49
|
+
intrinsicToken,
|
|
50
|
+
}: (typeof Schema)["input"]["_type"]): Promise<
|
|
51
|
+
NextResponse<(typeof Schema)["output"]["_type"]>
|
|
52
|
+
> {
|
|
53
|
+
const user = (await getUserBySession(session)) as User;
|
|
54
|
+
|
|
55
|
+
if (!validateIntrinsicToken(intrinsicToken)) {
|
|
56
|
+
return NextResponse.json({
|
|
57
|
+
error: "Invalid intrinsic token",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (contentLength > 25000000) {
|
|
62
|
+
return NextResponse.json({
|
|
63
|
+
error: "Max content length exceeded.",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!allowedContentTypes.has(contentType)) {
|
|
68
|
+
return NextResponse.json({
|
|
69
|
+
error: "Unacceptable format",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const key = `beatmap-video-${Date.now()}-${user.id}`;
|
|
74
|
+
const command = new PutObjectCommand({
|
|
75
|
+
Bucket: "rhthia-avatars",
|
|
76
|
+
Key: key,
|
|
77
|
+
ContentLength: contentLength,
|
|
78
|
+
ContentType: contentType,
|
|
79
|
+
ContentDisposition: "attachment",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const presigned = await getSignedUrl(s3Client, command, {
|
|
83
|
+
expiresIn: 3600,
|
|
84
|
+
signableHeaders: new Set(["content-type"]),
|
|
85
|
+
});
|
|
86
|
+
return NextResponse.json({
|
|
87
|
+
url: presigned,
|
|
88
|
+
objectKey: key,
|
|
89
|
+
});
|
|
90
|
+
}
|