Add public profiles and cosmetic shop
All checks were successful
Deploy MineGeo to Dokploy / deploy (push) Successful in 7m10s
All checks were successful
Deploy MineGeo to Dokploy / deploy (push) Successful in 7m10s
This commit is contained in:
@@ -181,7 +181,7 @@ export async function accountApi(
|
||||
.update({ avatar_path: avatarPath })
|
||||
.eq("id", account.id)
|
||||
.select(
|
||||
"id,display_name,avatar_path,role,name_glow,glow_color,bug_finder,custom_prefix,custom_prefix_icon",
|
||||
"id,display_name,bio,avatar_path,profile_effect,role,name_glow,glow_color,bug_finder,custom_prefix,custom_prefix_icon",
|
||||
)
|
||||
.single(),
|
||||
);
|
||||
@@ -269,10 +269,11 @@ export async function accountApi(
|
||||
...(body.avatarPath !== undefined
|
||||
? { avatar_path: body.avatarPath }
|
||||
: {}),
|
||||
...(body.bio !== undefined ? { bio: body.bio } : {}),
|
||||
})
|
||||
.eq("id", account.id)
|
||||
.select(
|
||||
"id,display_name,avatar_path,role,name_glow,glow_color,bug_finder,custom_prefix,custom_prefix_icon",
|
||||
"id,display_name,bio,avatar_path,profile_effect,role,name_glow,glow_color,bug_finder,custom_prefix,custom_prefix_icon",
|
||||
)
|
||||
.single(),
|
||||
);
|
||||
|
||||
@@ -18,6 +18,7 @@ import { requestOrigin, verifyOrigin } from "./request-origin";
|
||||
import { browserAccount } from "./request-security";
|
||||
import { jsonBody } from "./json-body";
|
||||
import { accountApi } from "./account-api";
|
||||
import { cosmeticsApi } from "./cosmetics-api";
|
||||
import { multiplayerApi } from "./multiplayer-api";
|
||||
import { bugReportApi } from "./bug-report-api";
|
||||
import { moderatorUsersApi } from "./moderator-users-api";
|
||||
@@ -131,6 +132,8 @@ export async function handle(request: NextRequest, path: string[]) {
|
||||
return await accountControlsApi(request, path, { jsonBody, limit });
|
||||
if (path[0] === "auth" || path[0] === "profile")
|
||||
return await accountApi(request, path, { jsonBody, limit });
|
||||
if (path[0] === "cosmetics")
|
||||
return await cosmeticsApi(request, path, { jsonBody, limit });
|
||||
// Check signed-in identities before any guest-cookie fallback (games/rooms).
|
||||
if (path[0] !== "health") await user();
|
||||
if (path[0] === "rooms")
|
||||
|
||||
@@ -64,7 +64,7 @@ export async function catalog() {
|
||||
await client
|
||||
.from("profiles")
|
||||
.select(
|
||||
"id,display_name,avatar_path,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
|
||||
"id,display_name,avatar_path,profile_effect,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,avatar_path,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
|
||||
"display_name,avatar_path,profile_effect,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
|
||||
)
|
||||
.eq("id", map.owner_id)
|
||||
.maybeSingle()
|
||||
|
||||
52
lib/cosmetics-api.ts
Normal file
52
lib/cosmetics-api.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import "server-only";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { profileCosmeticActionSchema } from "@/shared/profile-cosmetics";
|
||||
import { admin, required, user } from "./supabase";
|
||||
import { browserAccount } from "./request-security";
|
||||
import { profileCosmeticsState } from "./profile-cosmetics";
|
||||
|
||||
type Context = {
|
||||
jsonBody: (request: NextRequest) => Promise<unknown>;
|
||||
limit: (key: string, count: number, seconds: number) => Promise<void>;
|
||||
};
|
||||
|
||||
const json = (body: unknown, status = 200) =>
|
||||
NextResponse.json(body, { status, headers: { "Cache-Control": "no-store" } });
|
||||
|
||||
export async function cosmeticsApi(
|
||||
request: NextRequest,
|
||||
path: string[],
|
||||
context: Context,
|
||||
) {
|
||||
if (path.length !== 1) return json({ error: "Not found" }, 404);
|
||||
const account = await browserAccount(request, user);
|
||||
if (request.method === "GET")
|
||||
return json({ shop: await profileCosmeticsState(account.id) });
|
||||
if (request.method !== "POST")
|
||||
return json({ error: "Method not allowed" }, 405);
|
||||
|
||||
const body = profileCosmeticActionSchema.parse(
|
||||
await context.jsonBody(request),
|
||||
);
|
||||
if (body.action === "purchase") {
|
||||
await context.limit("cosmetic-purchase:" + account.id, 8, 60);
|
||||
return json({
|
||||
shop: required(
|
||||
await admin().rpc("buy_profile_cosmetic", {
|
||||
p_user: account.id,
|
||||
p_cosmetic_id: body.cosmeticId,
|
||||
}),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
await context.limit("cosmetic-equip:" + account.id, 30, 60);
|
||||
return json({
|
||||
shop: required(
|
||||
await admin().rpc("equip_profile_cosmetic", {
|
||||
p_user: account.id,
|
||||
p_cosmetic_id: body.cosmeticId,
|
||||
}),
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -35,7 +35,7 @@ export async function leaderboard({
|
||||
await admin()
|
||||
.from("profiles")
|
||||
.select(
|
||||
"id,avatar_path,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
|
||||
"id,avatar_path,profile_effect,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,avatar_path,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
|
||||
"id,display_name,avatar_path,profile_effect,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon",
|
||||
)
|
||||
.in("id", accounts),
|
||||
);
|
||||
|
||||
15
lib/profile-cosmetics.ts
Normal file
15
lib/profile-cosmetics.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import "server-only";
|
||||
import { admin, required } from "./supabase";
|
||||
import {
|
||||
cosmeticShopStateSchema,
|
||||
type CosmeticShopState,
|
||||
} from "@/shared/profile-cosmetics";
|
||||
|
||||
export async function profileCosmeticsState(
|
||||
userId: string,
|
||||
): Promise<CosmeticShopState> {
|
||||
const result = required(
|
||||
await admin().rpc("cosmetics_shop_state", { p_user: userId }),
|
||||
);
|
||||
return cosmeticShopStateSchema.parse(result);
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export async function accountProfile(account: User) {
|
||||
await admin()
|
||||
.from("profiles")
|
||||
.select(
|
||||
"id,display_name,avatar_path,role,name_glow,glow_color,bug_finder,custom_prefix,custom_prefix_icon,account_status,account_reason,account_changed_at",
|
||||
"id,display_name,bio,avatar_path,profile_effect,role,name_glow,glow_color,bug_finder,custom_prefix,custom_prefix_icon,account_status,account_reason,account_changed_at",
|
||||
)
|
||||
.eq("id", account.id)
|
||||
.single(),
|
||||
|
||||
41
lib/public-profile.ts
Normal file
41
lib/public-profile.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import "server-only";
|
||||
import { z } from "zod";
|
||||
import { admin } from "./supabase";
|
||||
|
||||
const uuidSchema = z.string().uuid();
|
||||
|
||||
export async function publicProfile(userId: string) {
|
||||
const validId = uuidSchema.safeParse(userId);
|
||||
if (!validId.success) return null;
|
||||
|
||||
const result = await admin()
|
||||
.from("profiles")
|
||||
.select(
|
||||
"id,display_name,bio,avatar_path,profile_effect,name_glow,glow_color,role,bug_finder,custom_prefix,custom_prefix_icon,created_at",
|
||||
)
|
||||
.eq("id", validId.data)
|
||||
.eq("account_status", "active")
|
||||
.maybeSingle();
|
||||
if (result.error) throw new Error(result.error.message);
|
||||
if (!result.data) return null;
|
||||
|
||||
const [games, maps] = await Promise.all([
|
||||
admin()
|
||||
.from("game_sessions")
|
||||
.select("id", { count: "exact", head: true })
|
||||
.eq("owner_id", validId.data)
|
||||
.eq("current_round", 5),
|
||||
admin()
|
||||
.from("datasets")
|
||||
.select("id", { count: "exact", head: true })
|
||||
.eq("owner_id", validId.data)
|
||||
.eq("state", "published"),
|
||||
]);
|
||||
if (games.error) throw new Error(games.error.message);
|
||||
if (maps.error) throw new Error(maps.error.message);
|
||||
return {
|
||||
...result.data,
|
||||
completedGames: games.count ?? 0,
|
||||
publishedMaps: maps.count ?? 0,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user