diff --git a/app/(public)/join/[code]/JoinButton.tsx b/app/(public)/join/[code]/JoinButton.tsx index 9882448..d9a2645 100644 --- a/app/(public)/join/[code]/JoinButton.tsx +++ b/app/(public)/join/[code]/JoinButton.tsx @@ -77,19 +77,36 @@ export default function JoinButton({ if (!board) throw new Error("Invalid invite code"); + const { data: existingMembership } = await supabase + .from("leaderboard_members") + .select("id") + .eq("leaderboard_id", board.id) + .eq("user_id", user.id) + .maybeSingle(); + + if (existingMembership) { + return { alreadyMember: true }; + } const { error } = await supabase.from("leaderboard_members").insert({ leaderboard_id: board.id, user_id: user.id, }); if (error) throw error; - return board; + return { alreadyMember: false }; })(); try { await toast.promise(joinPromise, { pending: "Joining leaderboard...", - success: "You're in! Welcome to the leaderboard.", + success: { + render({ data }) { + const payload = data as { alreadyMember: boolean }; + return payload.alreadyMember + ? "You're already a member. Opening leaderboard..." + : "You're in! Welcome to the leaderboard."; + }, + }, error: { render({ data }) { const err = data as Error; diff --git a/app/(public)/join/page.tsx b/app/(public)/join/page.tsx index 25e8498..52e97a7 100644 --- a/app/(public)/join/page.tsx +++ b/app/(public)/join/page.tsx @@ -149,8 +149,8 @@ export default async function JoinPage({ searchParams }: Props) { .select("id") .eq("leaderboard_id", leaderboard.id) .eq("user_id", user.id) - .single(); - alreadyMember = !!membership; + .maybeSingle(); + alreadyMember = user.id === leaderboard.owner_id || !!membership; } return ( diff --git a/app/components/Chat.tsx b/app/components/Chat.tsx index 325e88f..c8552d6 100644 --- a/app/components/Chat.tsx +++ b/app/components/Chat.tsx @@ -343,6 +343,40 @@ export default function Chat({ user }: { user: User }) { return messages.filter((m) => (m.text || "").toLowerCase().includes(lowerSearch)); }, [messages, messageSearch]); + const handleUnsendMessage = async (messageId: string) => { + if (!conversationId) return; + if (messageId.startsWith("temp-") || messageId.startsWith("live-")) { + toast.info("Please wait a moment and try again."); + return; + } + + const removedMessage = messages.find((message) => message.id === messageId); + if (!removedMessage) return; + + setMessages((prev) => prev.filter((message) => message.id !== messageId)); + + const { error } = await supabase + .from("messages") + .delete() + .eq("id", messageId) + .eq("sender_id", user.id); + + if (error) { + setMessages((prev) => { + if (prev.some((message) => message.id === messageId)) return prev; + const next = [...prev, removedMessage]; + next.sort( + (a, b) => + new Date(a.created_at).getTime() - new Date(b.created_at).getTime(), + ); + return next; + }); + toast.error(error.message || "Unable to unsend message."); + return; + } + + toast.success("Message unsent."); + }; return ( <> diff --git a/app/components/chat/Messages.tsx b/app/components/chat/Messages.tsx index dd12ad2..5d1a350 100644 --- a/app/components/chat/Messages.tsx +++ b/app/components/chat/Messages.tsx @@ -8,14 +8,18 @@ import { Conversation, Message } from "../Chat"; import { timeAgo } from "@/app/utils/time"; import { type BadgeInfo, getBadgeInfoFromHours } from "@/app/utils/badge"; import { useEffect, useMemo, useRef, useState } from "react"; +import { createPortal } from "react-dom"; import Image from "next/image"; import { + faCopy, faFile, faPause, faPlay, + faTrashCan, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import MediaViewerModal, { type MediaViewerPayload } from "./MediaViewerModal"; +import { toast } from "react-toastify"; export default function Messages({ messages, @@ -24,6 +28,7 @@ export default function Messages({ bottomRef, badgesByUserId, onUserProfileClick, + onUnsendMessage, }: { messages: Message[]; user: User; @@ -31,11 +36,21 @@ export default function Messages({ bottomRef: React.RefObject; badgesByUserId?: Record; onUserProfileClick?: (targetUserId: string, targetEmail: string) => void; + onUnsendMessage?: (messageId: string) => Promise; }) { const [showScrollBtn, setShowScrollBtn] = useState(false); const [mediaViewer, setMediaViewer] = useState( null, ); + const [activeDesktopMenuMessageId, setActiveDesktopMenuMessageId] = useState(null); + const desktopMenuOpenTimerRef = useRef(null); + const longPressTimerRef = useRef(null); + const desktopMenuCloseTimerRef = useRef(null); + const [mobileActionMenu, setMobileActionMenu] = useState<{ + messageId: string; + text: string; + isSelf: boolean; + } | null>(null); const fallbackBadge = useMemo(() => getBadgeInfoFromHours(1), []); const allMediaAttachments = useMemo(() => { @@ -57,6 +72,91 @@ export default function Messages({ return () => container.removeEventListener("scroll", handleScroll); }, []); + useEffect(() => { + return () => { + if (longPressTimerRef.current) { + window.clearTimeout(longPressTimerRef.current); + } + if (desktopMenuOpenTimerRef.current) { + window.clearTimeout(desktopMenuOpenTimerRef.current); + } + if (desktopMenuCloseTimerRef.current) { + window.clearTimeout(desktopMenuCloseTimerRef.current); + } + }; + }, []); + + const visibleDesktopMenuMessageId = + activeDesktopMenuMessageId && + messages.some((message) => message.id === activeDesktopMenuMessageId) + ? activeDesktopMenuMessageId + : null; + + const copyMessageText = async (messageText: string) => { + const trimmed = (messageText || "").trim(); + if (!trimmed) { + toast.info("Nothing to copy."); + return; + } + try { + await navigator.clipboard.writeText(trimmed); + toast.success("Message copied."); + } catch { + toast.error("Failed to copy message."); + } + }; + + const clearLongPress = () => { + if (!longPressTimerRef.current) return; + window.clearTimeout(longPressTimerRef.current); + longPressTimerRef.current = null; + }; + + const queueLongPressMenu = ( + messageId: string, + messageText: string, + isSelf: boolean, + ) => { + clearLongPress(); + longPressTimerRef.current = window.setTimeout(() => { + setMobileActionMenu({ + messageId, + text: messageText, + isSelf, + }); + longPressTimerRef.current = null; + }, 450); + }; + + const openDesktopMenu = (messageId: string) => { + if (desktopMenuOpenTimerRef.current) { + window.clearTimeout(desktopMenuOpenTimerRef.current); + } + if (desktopMenuCloseTimerRef.current) { + window.clearTimeout(desktopMenuCloseTimerRef.current); + desktopMenuCloseTimerRef.current = null; + } + // Delay open slightly for cleaner hover UX on desktop. + desktopMenuOpenTimerRef.current = window.setTimeout(() => { + setActiveDesktopMenuMessageId(messageId); + desktopMenuOpenTimerRef.current = null; + }, 180); + }; + + const closeDesktopMenu = (messageId: string) => { + if (desktopMenuOpenTimerRef.current) { + window.clearTimeout(desktopMenuOpenTimerRef.current); + desktopMenuOpenTimerRef.current = null; + } + if (desktopMenuCloseTimerRef.current) { + window.clearTimeout(desktopMenuCloseTimerRef.current); + } + // Small delay prevents accidental close while moving pointer to menu. + desktopMenuCloseTimerRef.current = window.setTimeout(() => { + setActiveDesktopMenuMessageId((prev) => (prev === messageId ? null : prev)); + desktopMenuCloseTimerRef.current = null; + }, 120); + }; return ( <> + {visibleDesktopMenuMessageId && ( +
+ )} {showScrollBtn && ( + )} + {isSelf && onUnsendMessage && ( + + )} + +
+ + )}
{isSelf && ( @@ -208,6 +374,16 @@ export default function Messages({ {msg.text && (
{ + if (!hasDesktopActions) return; + openDesktopMenu(msg.id); + }} + onMouseLeave={() => { + closeDesktopMenu(msg.id); + }} + onTouchStart={() => queueLongPressMenu(msg.id, msg.text || "", isSelf)} + onTouchEnd={clearLongPress} + onTouchMove={clearLongPress} className={`px-5 py-3 text-[14px] leading-relaxed break-words break-all overflow-x-hidden ${ isSelf ? "bg-indigo-600 border border-indigo-500/50 text-white rounded-2xl rounded-br-sm shadow-sm" @@ -249,7 +425,16 @@ export default function Messages({ )} {normalizedAttachments.length > 0 && ( -
+
{ + if (!hasDesktopActions) return; + openDesktopMenu(msg.id); + }} + onMouseLeave={() => { + closeDesktopMenu(msg.id); + }} + className={`${isVoiceOnlyMessage ? "mt-0" : "mt-1.5"} space-y-1.5`} + > {normalizedAttachments.map((att, i) => (
{getAttachments(att, (payload) => setMediaViewer(payload))} @@ -265,6 +450,56 @@ export default function Messages({
+ + {mobileActionMenu && + createPortal( +
setMobileActionMenu(null)} + > +
+
event.stopPropagation()} + > +
+ {!!mobileActionMenu.text.trim() && ( + + )} + {mobileActionMenu.isSelf && onUnsendMessage && ( + + )} +
+
+
, + document.body, + )} ); } diff --git a/app/components/chat/hooks/useActiveConversationStream.ts b/app/components/chat/hooks/useActiveConversationStream.ts index c673ddc..01a949c 100644 --- a/app/components/chat/hooks/useActiveConversationStream.ts +++ b/app/components/chat/hooks/useActiveConversationStream.ts @@ -323,6 +323,23 @@ export function useActiveConversationStream({ }, 100); }, ) + .on( + "postgres_changes", + { + event: "DELETE", + schema: "public", + table: "messages", + filter: `conversation_id=eq.${conversationId}`, + }, + (payload) => { + const deletedMessageId = payload.old.id as string | undefined; + if (!deletedMessageId) return; + + setMessages((prev) => + prev.filter((message) => message.id !== deletedMessageId), + ); + }, + ) .subscribe(); channelRef.current = channel; diff --git a/app/components/dashboard/LeaderbordList.tsx b/app/components/dashboard/LeaderbordList.tsx index 24a3df2..1951774 100644 --- a/app/components/dashboard/LeaderbordList.tsx +++ b/app/components/dashboard/LeaderbordList.tsx @@ -11,7 +11,7 @@ export interface Leaderboard { }; export interface LeaderboardMember { - leaderboards: Leaderboard[]; + leaderboards: Leaderboard | Leaderboard[] | null; }; export default async function LeaderboardsList() { @@ -39,7 +39,15 @@ export default async function LeaderboardsList() { const joined = joinedResult.data || []; const joinedBoards = - joined?.flatMap((j) => j.leaderboards) || []; + joined + ?.flatMap((j) => { + if (!j.leaderboards) return []; + return Array.isArray(j.leaderboards) ? j.leaderboards : [j.leaderboards]; + }) + .filter((board) => board.owner_id !== user.id) + .filter( + (board, index, arr) => arr.findIndex((candidate) => candidate.id === board.id) === index + ) || []; const ownedCount = owned?.length || 0; const joinedCount = joinedBoards.length; diff --git a/package-lock.json b/package-lock.json index 9c86dcf..2cca4dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,7 +82,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -581,7 +580,6 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.2.0.tgz", "integrity": "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q==", "license": "MIT", - "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "7.2.0" }, @@ -1184,6 +1182,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -1415,7 +1414,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -1437,7 +1435,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.6.0.tgz", "integrity": "sha512-L8UyDwqpTcbkIK5cgwDRDYDoEhQoj8wp8BwsO19w3LB1Z41yEQm2VJyNfAi9DrLP/YTqXqWpKHyZfR9/tFYo1Q==", "license": "Apache-2.0", - "peer": true, "engines": { "node": "^18.19.0 || >=20.6.0" }, @@ -1450,7 +1447,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.0.tgz", "integrity": "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -1859,7 +1855,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.0.tgz", "integrity": "sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -1876,7 +1871,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.0.tgz", "integrity": "sha512-g/OZVkqlxllgFM7qMKqbPV9c1DUPhQ7d4n3pgZFcrnrNft9eJXZM2TNHTPYREJBrtNdRytYyvwjgL5geDKl3EQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.6.0", "@opentelemetry/resources": "2.6.0", @@ -1894,7 +1888,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" } @@ -2051,7 +2044,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2969,7 +2961,6 @@ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.98.0.tgz", "integrity": "sha512-Ohc97CtInLwZyiSASz7tT9/Abm/vqnIbO9REp+PivVUII8UZsuI3bngRQnYgJdFoOIwvaEII1fX1qy8x0CyNiw==", "license": "MIT", - "peer": true, "dependencies": { "@supabase/auth-js": "2.98.0", "@supabase/functions-js": "2.98.0", @@ -3365,6 +3356,7 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -3375,6 +3367,7 @@ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -3487,7 +3480,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3587,7 +3579,6 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -4118,6 +4109,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -4127,25 +4119,29 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -4156,13 +4152,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -4175,6 +4173,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "license": "MIT", + "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -4184,6 +4183,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -4192,13 +4192,15 @@ "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -4215,6 +4217,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -4228,6 +4231,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -4240,6 +4244,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -4254,6 +4259,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -4263,20 +4269,21 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4298,6 +4305,7 @@ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" }, @@ -4349,6 +4357,7 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -4366,6 +4375,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4381,7 +4391,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/ansi-styles": { "version": "4.3.0", @@ -4712,7 +4723,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4731,7 +4741,8 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/call-bind": { "version": "1.0.8", @@ -4885,6 +4896,7 @@ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.0" } @@ -4950,7 +4962,8 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/commondir": { "version": "1.0.1", @@ -5510,7 +5523,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -5610,7 +5624,6 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5796,7 +5809,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6061,6 +6073,7 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.x" } @@ -6227,7 +6240,8 @@ "url": "https://opencollective.com/fastify" } ], - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/fastq": { "version": "1.20.1", @@ -6536,7 +6550,8 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/glob/node_modules/balanced-match": { "version": "4.0.4", @@ -7478,6 +7493,7 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -7492,6 +7508,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -7554,7 +7571,8 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -7908,6 +7926,7 @@ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.11.5" }, @@ -8171,7 +8190,8 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/merge2": { "version": "1.4.1", @@ -8644,6 +8664,7 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -8653,6 +8674,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", + "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -8749,14 +8771,14 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/next": { "version": "16.1.6", "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", @@ -9459,7 +9481,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9469,7 +9490,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9481,8 +9501,7 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-markdown": { "version": "10.1.0", @@ -9516,7 +9535,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -9614,8 +9632,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -9724,6 +9741,7 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -9804,7 +9822,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -9934,6 +9951,7 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -9970,6 +9988,7 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -9981,7 +10000,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/semver": { "version": "6.3.1", @@ -10276,6 +10296,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -10294,6 +10315,7 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "license": "MIT", + "peer": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -10594,6 +10616,7 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -10612,6 +10635,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -10687,7 +10711,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10885,7 +10908,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11182,6 +11204,7 @@ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "license": "MIT", + "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -11201,6 +11224,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -11249,6 +11273,7 @@ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" } @@ -11258,6 +11283,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -11271,6 +11297,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -11460,7 +11487,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/supabase/migrations/20260423130000_recover_leaderboard_rls_no_recursion.sql b/supabase/migrations/20260423130000_recover_leaderboard_rls_no_recursion.sql new file mode 100644 index 0000000..faba67f --- /dev/null +++ b/supabase/migrations/20260423130000_recover_leaderboard_rls_no_recursion.sql @@ -0,0 +1,73 @@ +-- Recovery migration: remove recursive RLS interaction between +-- leaderboards <-> leaderboard_members policies. +-- This restores invite lookup stability and membership reads. + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'leaderboards' + AND policyname = 'Public or member leaderboards are viewable' + ) THEN + DROP POLICY "Public or member leaderboards are viewable" ON public.leaderboards; + END IF; +END +$$; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'leaderboard_members' + AND policyname = 'Users can see own memberships and visible leaderboard memberships' + ) THEN + DROP POLICY "Users can see own memberships and visible leaderboard memberships" ON public.leaderboard_members; + END IF; +END +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'leaderboards' + AND policyname = 'Public leaderboards are viewable' + ) THEN + CREATE POLICY "Public leaderboards are viewable" + ON public.leaderboards + FOR SELECT + USING (is_public = true OR owner_id = auth.uid()); + END IF; +END +$$; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'leaderboard_members' + AND policyname = 'Users can see memberships for visible leaderboards' + ) THEN + CREATE POLICY "Users can see memberships for visible leaderboards" + ON public.leaderboard_members + FOR SELECT + USING ( + user_id = auth.uid() + OR EXISTS ( + SELECT 1 + FROM public.leaderboards l + WHERE l.id = leaderboard_members.leaderboard_id + AND (l.is_public = true OR l.owner_id = auth.uid()) + ) + ); + END IF; +END +$$; diff --git a/supabase/migrations/20260423150000_optimize_rls_auth_uid_initplan.sql b/supabase/migrations/20260423150000_optimize_rls_auth_uid_initplan.sql new file mode 100644 index 0000000..2fc7ba3 --- /dev/null +++ b/supabase/migrations/20260423150000_optimize_rls_auth_uid_initplan.sql @@ -0,0 +1,82 @@ +-- Performance hardening for RLS policy evaluation: +-- Rewrites auth.uid() calls to (select auth.uid()) so PostgreSQL can init-plan once per statement. +-- This keeps policy behavior identical while reducing per-row function re-evaluation overhead. + +DO $$ +DECLARE + p record; + using_expr text; + check_expr text; + roles_clause text; + create_sql text; +BEGIN + FOR p IN + SELECT + schemaname, + tablename, + policyname, + permissive, + cmd, + roles, + qual, + with_check + FROM pg_policies + WHERE schemaname = 'public' + AND ( + coalesce(qual, '') LIKE '%auth.uid()%' + OR coalesce(with_check, '') LIKE '%auth.uid()%' + ) + LOOP + using_expr := CASE + WHEN p.qual IS NULL THEN NULL + ELSE replace(p.qual, 'auth.uid()', '(select auth.uid())') + END; + + check_expr := CASE + WHEN p.with_check IS NULL THEN NULL + ELSE replace(p.with_check, 'auth.uid()', '(select auth.uid())') + END; + + SELECT string_agg( + CASE + WHEN role_name = 'public' THEN 'public' + ELSE quote_ident(role_name) + END, + ', ' + ) + INTO roles_clause + FROM unnest(p.roles) AS role_name; + + IF roles_clause IS NULL OR roles_clause = '' THEN + roles_clause := 'public'; + END IF; + + EXECUTE format( + 'DROP POLICY %I ON %I.%I', + p.policyname, + p.schemaname, + p.tablename + ); + + create_sql := format( + 'CREATE POLICY %I ON %I.%I AS %s FOR %s TO %s', + p.policyname, + p.schemaname, + p.tablename, + p.permissive, + p.cmd, + roles_clause + ); + + IF using_expr IS NOT NULL THEN + create_sql := create_sql || format(' USING (%s)', using_expr); + END IF; + + IF check_expr IS NOT NULL THEN + create_sql := create_sql || format(' WITH CHECK (%s)', check_expr); + END IF; + + EXECUTE create_sql; + END LOOP; +END +$$; diff --git a/supabase/migrations/20260423162000_add_unsend_policy_for_messages.sql b/supabase/migrations/20260423162000_add_unsend_policy_for_messages.sql new file mode 100644 index 0000000..3ff2fa2 --- /dev/null +++ b/supabase/migrations/20260423162000_add_unsend_policy_for_messages.sql @@ -0,0 +1,5 @@ +-- Allow users to unsend (delete) only their own messages. +CREATE POLICY "senders can unsend their own messages" +ON public.messages +FOR DELETE +USING ((select auth.uid()) = sender_id);