Add profile photo uploads and custom difficulty support
This commit is contained in:
@@ -21,6 +21,11 @@ import { createHash } from "node:crypto";
|
||||
import { recoveryCookie, verifyRecoveryGrant } from "./password-recovery";
|
||||
import { requestOrigin } from "./request-origin";
|
||||
import { issueInitialPassword } from "@/shared/initial-password";
|
||||
import {
|
||||
profileAvatarBucket,
|
||||
profileAvatarMaxBytes,
|
||||
profileAvatarMimeTypes,
|
||||
} from "@/shared/profile-appearance";
|
||||
|
||||
type Context = {
|
||||
jsonBody: (r: NextRequest) => Promise<unknown>;
|
||||
@@ -102,6 +107,41 @@ export async function accountApi(
|
||||
{ error: "Earn a username glow before choosing its color" },
|
||||
403,
|
||||
);
|
||||
if (
|
||||
body.avatarPath !== undefined &&
|
||||
body.avatarPath !== null &&
|
||||
body.avatarPath !== current.avatar_path
|
||||
) {
|
||||
const [ownerId, filename] = body.avatarPath.split("/");
|
||||
if (
|
||||
ownerId !== account.id ||
|
||||
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.(jpg|png|webp)$/i.test(
|
||||
filename ?? "",
|
||||
)
|
||||
)
|
||||
return json({ error: "Choose a valid profile photo" }, 400);
|
||||
const uploaded = await admin()
|
||||
.storage.from(profileAvatarBucket)
|
||||
.info(body.avatarPath);
|
||||
if (uploaded.error || !uploaded.data)
|
||||
return json(
|
||||
{ error: "Upload your profile photo again" },
|
||||
400,
|
||||
);
|
||||
if (
|
||||
!profileAvatarMimeTypes.includes(
|
||||
uploaded.data.contentType as (typeof profileAvatarMimeTypes)[number],
|
||||
) ||
|
||||
!uploaded.data.size ||
|
||||
uploaded.data.size > profileAvatarMaxBytes
|
||||
) {
|
||||
await admin().storage.from(profileAvatarBucket).remove([body.avatarPath]);
|
||||
return json(
|
||||
{ error: "Choose an image no larger than 5 MB" },
|
||||
400,
|
||||
);
|
||||
}
|
||||
}
|
||||
const profile = required(
|
||||
await admin()
|
||||
.from("profiles")
|
||||
@@ -112,13 +152,25 @@ export async function accountApi(
|
||||
...(body.glowColor !== undefined
|
||||
? { glow_color: body.glowColor }
|
||||
: {}),
|
||||
...(body.avatarPath !== undefined
|
||||
? { avatar_path: body.avatarPath }
|
||||
: {}),
|
||||
})
|
||||
.eq("id", account.id)
|
||||
.select(
|
||||
"id,display_name,role,name_glow,glow_color,bug_finder,custom_prefix,custom_prefix_icon",
|
||||
"id,display_name,avatar_path,role,name_glow,glow_color,bug_finder,custom_prefix,custom_prefix_icon",
|
||||
)
|
||||
.single(),
|
||||
);
|
||||
if (
|
||||
body.avatarPath !== undefined &&
|
||||
body.avatarPath !== current.avatar_path
|
||||
) {
|
||||
if (current.avatar_path?.startsWith(`${account.id}/`))
|
||||
await admin()
|
||||
.storage.from(profileAvatarBucket)
|
||||
.remove([current.avatar_path]);
|
||||
}
|
||||
return json({ profile });
|
||||
}
|
||||
return json({ error: "Method not allowed" }, 405);
|
||||
|
||||
38
lib/api.ts
38
lib/api.ts
@@ -7,7 +7,7 @@ import { accountControlsApi } from "./account-controls-api";
|
||||
import {
|
||||
datasetSchema,
|
||||
faces,
|
||||
difficulties,
|
||||
customDifficultySchema,
|
||||
parseUploadManifest,
|
||||
panoramaPath,
|
||||
viewpointSchema,
|
||||
@@ -144,6 +144,36 @@ export async function handle(request: NextRequest, path: string[]) {
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
});
|
||||
}
|
||||
if (path[0] === "difficulty-presets" && path.length === 1) {
|
||||
if (method === "GET") {
|
||||
const presets = required(
|
||||
await client
|
||||
.from("difficulty_presets")
|
||||
.select("id,name,seconds,zoom")
|
||||
.order("name", { ascending: true }),
|
||||
);
|
||||
return NextResponse.json({ presets });
|
||||
}
|
||||
if (method === "POST") {
|
||||
const body = customDifficultySchema.parse(await jsonBody(request));
|
||||
const address =
|
||||
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
|
||||
"unknown";
|
||||
await limit("difficulty-presets:" + hash(address), 8, 3600);
|
||||
const { data, error } = await client
|
||||
.from("difficulty_presets")
|
||||
.insert(body)
|
||||
.select("id,name,seconds,zoom")
|
||||
.single();
|
||||
if (error?.code === "23505")
|
||||
throw new ServiceError(
|
||||
"A difficulty with that name already exists",
|
||||
409,
|
||||
);
|
||||
checkServiceError(error);
|
||||
return NextResponse.json({ preset: data });
|
||||
}
|
||||
}
|
||||
if (path[0] === "health" && path.length === 1 && method === "GET") {
|
||||
const { error } = await client
|
||||
.from("catalog_servers")
|
||||
@@ -172,7 +202,10 @@ export async function handle(request: NextRequest, path: string[]) {
|
||||
const body = z
|
||||
.object({
|
||||
datasetId: idSchema,
|
||||
difficulty: z.enum(["easy", "normal", "hard"]),
|
||||
difficulty: z.union([
|
||||
z.enum(["easy", "normal", "hard"]),
|
||||
idSchema,
|
||||
]),
|
||||
})
|
||||
.parse(await jsonBody(request));
|
||||
const secret = cookieSecret(request) || randomBytes(32).toString("hex");
|
||||
@@ -231,6 +264,7 @@ export async function handle(request: NextRequest, path: string[]) {
|
||||
{
|
||||
id: s.id,
|
||||
difficulty: s.difficulty,
|
||||
difficultySettings: s.difficulty_settings,
|
||||
round: s.current_round,
|
||||
bounds: manifest.bounds,
|
||||
name: manifest.name,
|
||||
|
||||
@@ -64,7 +64,7 @@ export async function catalog() {
|
||||
await client
|
||||
.from("profiles")
|
||||
.select(
|
||||
"id,display_name,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
|
||||
"id,display_name,avatar_path,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
|
||||
)
|
||||
.in("id", [
|
||||
...new Set(
|
||||
@@ -106,7 +106,7 @@ export async function getMap(id: string) {
|
||||
? admin()
|
||||
.from("profiles")
|
||||
.select(
|
||||
"display_name,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
|
||||
"display_name,avatar_path,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
|
||||
)
|
||||
.eq("id", map.owner_id)
|
||||
.maybeSingle()
|
||||
|
||||
@@ -102,11 +102,20 @@ async function announcementSource(
|
||||
);
|
||||
hostName = profile.display_name;
|
||||
}
|
||||
const difficultySettings =
|
||||
room.difficulty_settings &&
|
||||
typeof room.difficulty_settings === "object" &&
|
||||
!Array.isArray(room.difficulty_settings)
|
||||
? room.difficulty_settings
|
||||
: null;
|
||||
return {
|
||||
id: room.id,
|
||||
name: map.data.name,
|
||||
owner: hostName,
|
||||
difficulty: room.difficulty,
|
||||
difficulty:
|
||||
typeof difficultySettings?.name === "string"
|
||||
? difficultySettings.name
|
||||
: room.difficulty,
|
||||
players: players.length,
|
||||
limit: room.player_limit,
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ export async function leaderboard({
|
||||
await admin()
|
||||
.from("profiles")
|
||||
.select(
|
||||
"id,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
|
||||
"id,avatar_path,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
|
||||
)
|
||||
.in("id", ids),
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function multiplayerAppearance(room: string, ids: string[]) {
|
||||
await db
|
||||
.from("profiles")
|
||||
.select(
|
||||
"id,display_name,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
|
||||
"id,display_name,avatar_path,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
|
||||
)
|
||||
.in("id", accounts),
|
||||
);
|
||||
|
||||
@@ -21,7 +21,7 @@ export async function accountProfile(account: User) {
|
||||
await admin()
|
||||
.from("profiles")
|
||||
.select(
|
||||
"id,display_name,role,name_glow,glow_color,bug_finder,custom_prefix,custom_prefix_icon,account_status,account_reason,account_changed_at",
|
||||
"id,display_name,avatar_path,role,name_glow,glow_color,bug_finder,custom_prefix,custom_prefix_icon,account_status,account_reason,account_changed_at",
|
||||
)
|
||||
.eq("id", account.id)
|
||||
.single(),
|
||||
|
||||
Reference in New Issue
Block a user