Spaces:
Running
Running
Download src/profile-images.ts from Shoraky/Sporalize-api: direct link, hf CLI and curl.
- Browser
- Download file 9.58 kB
-
https://huggingface.co/spaces/Shoraky/Sporalize-api/resolve/main/src/profile-images.ts
- Command line
-
hf download hf://spaces/Shoraky/Sporalize-api/src/profile-images.ts
-
curl -L -o profile-images.ts https://huggingface.co/spaces/Shoraky/Sporalize-api/resolve/main/src/profile-images.ts
9.58 kB
| import { randomUUID } from "node:crypto"; | |
| import express, { type Express, type Request, type Response } from "express"; | |
| import { z } from "zod"; | |
| import { authenticatedUser } from "./auth.js"; | |
| import { supabaseAdmin } from "./supabase.js"; | |
| const BUCKET = "profile-images"; | |
| const MAX_BYTES = 4 * 1024 * 1024; | |
| const uploadBody = express.raw({ type: ["image/jpeg", "image/png", "image/webp"], limit: MAX_BYTES }); | |
| const playerIdSchema = z.string().uuid(); | |
| type Owner = "players" | "users"; | |
| type ImageType = { mime: string; extension: string }; | |
| async function canAccessPlayer(userId: string, playerId: string) { | |
| const { data: user, error: userError } = await supabaseAdmin.from("users").select("account_role").eq("user_id", userId).maybeSingle(); | |
| if (userError) throw new Error(`Unable to verify account permissions: ${userError.message}`); | |
| if (String(user?.account_role ?? "user").toLowerCase() === "admin") return true; | |
| const { data: player, error } = await supabaseAdmin.from("players").select("player_id").eq("player_id", playerId).eq("user_id", userId).maybeSingle(); | |
| if (error) throw new Error(`Unable to verify player ownership: ${error.message}`); | |
| return Boolean(player); | |
| } | |
| function inspectImage(bytes: Buffer): ImageType | null { | |
| if (bytes.length < 12 || bytes.length > MAX_BYTES) return null; | |
| if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return { mime: "image/jpeg", extension: "jpg" }; | |
| if (bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) return { mime: "image/png", extension: "png" }; | |
| if (bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP") return { mime: "image/webp", extension: "webp" }; | |
| return null; | |
| } | |
| function safePreviousPath(owner: Owner, id: string, path: string | null | undefined) { | |
| const prefix = `${owner}/${id}/`; | |
| if (!path?.startsWith(prefix)) return null; | |
| const filename = path.slice(prefix.length); | |
| return /^[0-9a-f-]{36}\.(jpg|png|webp)$/.test(filename) ? path : null; | |
| } | |
| async function removeStoredImage(path: string) { | |
| const { error } = await supabaseAdmin.storage.from(BUCKET).remove([path]); | |
| if (error) throw new Error(`Unable to remove profile image: ${error.message}`); | |
| } | |
| async function sendImage(res: Response, path: string | null | undefined) { | |
| if (!path) return res.status(404).json({ error: "Profile image not found." }); | |
| const { data, error } = await supabaseAdmin.storage.from(BUCKET).download(path); | |
| if (error || !data) return res.status(404).json({ error: "Profile image not found." }); | |
| const type = path.endsWith(".png") ? "image/png" : path.endsWith(".webp") ? "image/webp" : "image/jpeg"; | |
| res.set({ | |
| "Content-Type": type, | |
| // Each upload gets a new UUID path, so this response never needs revalidation. | |
| "Cache-Control": "private, max-age=31536000, immutable", | |
| "Cross-Origin-Resource-Policy": "cross-origin", | |
| "X-Content-Type-Options": "nosniff", | |
| }); | |
| return res.send(Buffer.from(await data.arrayBuffer())); | |
| } | |
| async function uploadImage(req: Request, res: Response, owner: Owner, id: string, previousPath: string | null | undefined) { | |
| const bytes = req.body; | |
| if (!Buffer.isBuffer(bytes)) return res.status(415).json({ error: "Choose a JPG, PNG or WEBP image." }); | |
| const type = inspectImage(bytes); | |
| if (!type || req.headers["content-type"]?.split(";")[0] !== type.mime) { | |
| return res.status(415).json({ error: "Choose a valid JPG, PNG or WEBP image up to 4 MB." }); | |
| } | |
| const path = `${owner}/${id}/${randomUUID()}.${type.extension}`; | |
| const { error: uploadError } = await supabaseAdmin.storage.from(BUCKET).upload(path, bytes, { | |
| contentType: type.mime, | |
| cacheControl: "31536000", | |
| upsert: false, | |
| }); | |
| if (uploadError) throw new Error(`Unable to upload profile image: ${uploadError.message}`); | |
| const table = owner === "players" ? "players" : "users"; | |
| const idColumn = owner === "players" ? "player_id" : "user_id"; | |
| const { data: saved, error: saveError } = await supabaseAdmin.from(table).update({ image_path: path }).eq(idColumn, id).select(idColumn).maybeSingle(); | |
| if (saveError || !saved) { | |
| await removeStoredImage(path).catch((cleanupError) => console.error(cleanupError)); | |
| throw new Error(`Unable to save profile image: ${saveError?.message ?? "Profile not found."}`); | |
| } | |
| const oldPath = safePreviousPath(owner, id, previousPath); | |
| if (oldPath) await removeStoredImage(oldPath).catch((cleanupError) => console.error(cleanupError)); | |
| return res.json({ imagePath: path }); | |
| } | |
| async function deleteImage(res: Response, owner: Owner, id: string, previousPath: string | null | undefined) { | |
| const table = owner === "players" ? "players" : "users"; | |
| const idColumn = owner === "players" ? "player_id" : "user_id"; | |
| const { data: saved, error } = await supabaseAdmin.from(table).update({ image_path: null }).eq(idColumn, id).select(idColumn).maybeSingle(); | |
| if (error || !saved) throw new Error(`Unable to remove profile image: ${error?.message ?? "Profile not found."}`); | |
| const oldPath = safePreviousPath(owner, id, previousPath); | |
| if (oldPath) await removeStoredImage(oldPath).catch((cleanupError) => console.error(cleanupError)); | |
| return res.json({ imagePath: null }); | |
| } | |
| export function registerProfileImageRoutes(app: Express) { | |
| app.get("/api/auth/image", async (req, res, next) => { | |
| try { | |
| const current = await authenticatedUser(req, res); | |
| if (!current) return res.status(401).json({ error: "Authentication required." }); | |
| const { data, error } = await supabaseAdmin.from("users").select("image_path").eq("user_id", current.user.id).maybeSingle(); | |
| if (error) throw new Error(`Unable to load user image: ${error.message}`); | |
| return await sendImage(res, safePreviousPath("users", current.user.id, data?.image_path)); | |
| } catch (error) { next(error); } | |
| }); | |
| app.post("/api/auth/image", uploadBody, async (req, res, next) => { | |
| try { | |
| const current = await authenticatedUser(req, res); | |
| if (!current) return res.status(401).json({ error: "Authentication required." }); | |
| const { data, error } = await supabaseAdmin.from("users").select("image_path").eq("user_id", current.user.id).maybeSingle(); | |
| if (error || !data) return res.status(404).json({ error: "User profile not found." }); | |
| return await uploadImage(req, res, "users", current.user.id, data.image_path); | |
| } catch (error) { next(error); } | |
| }); | |
| app.delete("/api/auth/image", async (req, res, next) => { | |
| try { | |
| const current = await authenticatedUser(req, res); | |
| if (!current) return res.status(401).json({ error: "Authentication required." }); | |
| const { data, error } = await supabaseAdmin.from("users").select("image_path").eq("user_id", current.user.id).maybeSingle(); | |
| if (error || !data) return res.status(404).json({ error: "User profile not found." }); | |
| return await deleteImage(res, "users", current.user.id, data.image_path); | |
| } catch (error) { next(error); } | |
| }); | |
| app.get("/api/players/:playerId/image", async (req, res, next) => { | |
| try { | |
| const current = await authenticatedUser(req, res); | |
| if (!current) return res.status(401).json({ error: "Authentication required." }); | |
| const id = playerIdSchema.safeParse(req.params.playerId); | |
| if (!id.success) return res.status(404).json({ error: "Player not found." }); | |
| if (!await canAccessPlayer(current.user.id, id.data)) return res.status(404).json({ error: "Player not found." }); | |
| const { data, error } = await supabaseAdmin.from("players").select("image_path").eq("player_id", id.data).maybeSingle(); | |
| if (error) throw new Error(`Unable to load player image: ${error.message}`); | |
| if (!data) return res.status(404).json({ error: "Player not found." }); | |
| return await sendImage(res, safePreviousPath("players", id.data, data?.image_path)); | |
| } catch (error) { next(error); } | |
| }); | |
| app.post("/api/players/:playerId/image", uploadBody, async (req, res, next) => { | |
| try { | |
| const current = await authenticatedUser(req, res); | |
| if (!current) return res.status(401).json({ error: "Authentication required." }); | |
| const id = playerIdSchema.safeParse(req.params.playerId); | |
| if (!id.success) return res.status(404).json({ error: "Player not found." }); | |
| if (!await canAccessPlayer(current.user.id, id.data)) return res.status(404).json({ error: "Player not found." }); | |
| const { data, error } = await supabaseAdmin.from("players").select("image_path").eq("player_id", id.data).maybeSingle(); | |
| if (error || !data) return res.status(404).json({ error: "Player not found." }); | |
| return await uploadImage(req, res, "players", id.data, data.image_path); | |
| } catch (error) { next(error); } | |
| }); | |
| app.delete("/api/players/:playerId/image", async (req, res, next) => { | |
| try { | |
| const current = await authenticatedUser(req, res); | |
| if (!current) return res.status(401).json({ error: "Authentication required." }); | |
| const id = playerIdSchema.safeParse(req.params.playerId); | |
| if (!id.success) return res.status(404).json({ error: "Player not found." }); | |
| if (!await canAccessPlayer(current.user.id, id.data)) return res.status(404).json({ error: "Player not found." }); | |
| const { data, error } = await supabaseAdmin.from("players").select("image_path").eq("player_id", id.data).maybeSingle(); | |
| if (error || !data) return res.status(404).json({ error: "Player not found." }); | |
| return await deleteImage(res, "players", id.data, data.image_path); | |
| } catch (error) { next(error); } | |
| }); | |
| } | |