From 6f9bbb812f9f68b3f47891f6c2e836e5868586b1 Mon Sep 17 00:00:00 2001 From: Lee Robinson Date: Sun, 24 May 2026 20:37:28 -0500 Subject: [PATCH 1/2] Fix silent logo upload failure for signed-out users Uploads to the avatars bucket require an authenticated session (the bucket's RLS policy only allows the `authenticated` role), but the Add Company flow was reachable while signed out, so the upload was sent as `anon` and rejected with "new row violates row-level security policy". The error was swallowed and the local preview still rendered, making it look like the upload succeeded. - UploadLogo: verify the session before uploading, surface upload and validation errors via toast, revert the optimistic preview on failure, and show an uploading state. - AddCompanyButton: redirect signed-out visitors to /login instead of opening a form whose upload and save can never succeed. Co-authored-by: Cursor --- .../components/company/add-company-button.tsx | 37 ++++++++++-- apps/cursor/src/components/upload-logo.tsx | 58 ++++++++++++++----- 2 files changed, 74 insertions(+), 21 deletions(-) diff --git a/apps/cursor/src/components/company/add-company-button.tsx b/apps/cursor/src/components/company/add-company-button.tsx index a3979460..d760503b 100644 --- a/apps/cursor/src/components/company/add-company-button.tsx +++ b/apps/cursor/src/components/company/add-company-button.tsx @@ -1,21 +1,46 @@ "use client"; +import { usePathname, useRouter } from "next/navigation"; import { parseAsBoolean, useQueryStates } from "nuqs"; +import { useEffect, useState } from "react"; +import { createClient } from "@/utils/supabase/client"; import { Button } from "../ui/button"; export function AddCompanyButton({ redirect }: { redirect?: boolean }) { + const router = useRouter(); + const pathname = usePathname(); + const supabase = createClient(); + const [isAuthenticated, setIsAuthenticated] = useState(null); + const [_, setAddCompany] = useQueryStates({ addCompany: parseAsBoolean.withDefault(false), redirect: parseAsBoolean.withDefault(redirect ?? false), }); + useEffect(() => { + async function getUser() { + const session = await supabase.auth.getSession(); + + setIsAuthenticated(Boolean(session.data.session)); + } + + getUser(); + }, []); + + const handleClick = () => { + // Adding a company requires an authenticated session: the save action is + // auth-guarded and the logo upload hits an auth-only storage policy. Send + // signed-out visitors to sign in instead of into a form that can't succeed. + if (!isAuthenticated) { + router.push(`/login?next=${pathname}`); + return; + } + + setAddCompany({ addCompany: true, redirect }); + }; + return ( - ); diff --git a/apps/cursor/src/components/upload-logo.tsx b/apps/cursor/src/components/upload-logo.tsx index dead3681..61d88852 100644 --- a/apps/cursor/src/components/upload-logo.tsx +++ b/apps/cursor/src/components/upload-logo.tsx @@ -3,6 +3,7 @@ import { PlusIcon } from "lucide-react"; import Image from "next/image"; import { type ChangeEvent, type DragEvent, useRef, useState } from "react"; +import { toast } from "sonner"; import { createClient } from "@/utils/supabase/client"; interface UploadLogoProps { @@ -23,33 +24,52 @@ export default function UploadLogo({ const handleFile = async (file: File) => { if (!file.type.startsWith("image/")) { + toast.error("Please select an image file."); return; } const MAX_FILE_SIZE = 1024 * 1024; // 1MB in bytes if (file.size > MAX_FILE_SIZE) { + toast.error("Image must be smaller than 1MB."); return; } + const previousPreview = preview; setIsUploading(true); + // Show an optimistic preview while the upload is in flight. It is reverted + // below if the upload fails so the UI never shows an image that wasn't + // actually saved. + const reader = new FileReader(); + reader.onload = (e) => { + setPreview(e.target?.result as string); + }; + reader.readAsDataURL(file); + try { - // Create preview - const reader = new FileReader(); - reader.onload = (e) => { - const dataUrl = e.target?.result as string; - setPreview(dataUrl); - }; - reader.readAsDataURL(file); - - // Upload to Supabase Storage const supabase = createClient(); + + // Uploading to the avatars bucket requires an authenticated session. The + // bucket's row-level security policy only allows the `authenticated` + // role, so without a session the request is sent as `anon` and Storage + // rejects it with "new row violates row-level security policy". + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user) { + toast.error("Please sign in to upload an image."); + setPreview(previousPreview); + return; + } + const fileExt = file.name.split(".").pop(); const fileName = `${Math.random().toString(36).substring(2)}.${fileExt}`; + const path = `${prefix}/${fileName}`; - const { data, error } = await supabase.storage + const { error } = await supabase.storage .from("avatars") - .upload(`${prefix}/${fileName}`, file, { + .upload(path, file, { cacheControl: "3600", upsert: false, }); @@ -58,16 +78,18 @@ export default function UploadLogo({ throw error; } - // Get public URL const { data: { publicUrl }, - } = supabase.storage - .from("avatars") - .getPublicUrl(`${prefix}/${fileName}`); + } = supabase.storage.from("avatars").getPublicUrl(path); + setPreview(publicUrl); onUpload?.(publicUrl); } catch (error) { console.error("Error uploading file:", error); + toast.error( + error instanceof Error ? error.message : "Failed to upload image.", + ); + setPreview(previousPreview); } finally { setIsUploading(false); } @@ -149,6 +171,12 @@ export default function UploadLogo({ )} + + {isUploading && ( +
+ Uploading... +
+ )} ); } From 85468b714a696200ff8d89e35b1a6fe3c2d58a28 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 27 May 2026 02:33:10 +0000 Subject: [PATCH 2/2] Fix auth loading redirect and stale FileReader preview - Only redirect to login when session is known to be absent - Ignore FileReader onload after upload auth/storage failure or success Applied via @cursor push command --- apps/cursor/src/components/company/add-company-button.tsx | 6 +++++- apps/cursor/src/components/upload-logo.tsx | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/cursor/src/components/company/add-company-button.tsx b/apps/cursor/src/components/company/add-company-button.tsx index d760503b..467650e1 100644 --- a/apps/cursor/src/components/company/add-company-button.tsx +++ b/apps/cursor/src/components/company/add-company-button.tsx @@ -31,11 +31,15 @@ export function AddCompanyButton({ redirect }: { redirect?: boolean }) { // Adding a company requires an authenticated session: the save action is // auth-guarded and the logo upload hits an auth-only storage policy. Send // signed-out visitors to sign in instead of into a form that can't succeed. - if (!isAuthenticated) { + if (isAuthenticated === false) { router.push(`/login?next=${pathname}`); return; } + if (isAuthenticated === null) { + return; + } + setAddCompany({ addCompany: true, redirect }); }; diff --git a/apps/cursor/src/components/upload-logo.tsx b/apps/cursor/src/components/upload-logo.tsx index 61d88852..c1c6ec7d 100644 --- a/apps/cursor/src/components/upload-logo.tsx +++ b/apps/cursor/src/components/upload-logo.tsx @@ -40,8 +40,10 @@ export default function UploadLogo({ // Show an optimistic preview while the upload is in flight. It is reverted // below if the upload fails so the UI never shows an image that wasn't // actually saved. + let allowReaderPreview = true; const reader = new FileReader(); reader.onload = (e) => { + if (!allowReaderPreview) return; setPreview(e.target?.result as string); }; reader.readAsDataURL(file); @@ -59,6 +61,7 @@ export default function UploadLogo({ if (!user) { toast.error("Please sign in to upload an image."); + allowReaderPreview = false; setPreview(previousPreview); return; } @@ -82,6 +85,7 @@ export default function UploadLogo({ data: { publicUrl }, } = supabase.storage.from("avatars").getPublicUrl(path); + allowReaderPreview = false; setPreview(publicUrl); onUpload?.(publicUrl); } catch (error) { @@ -89,6 +93,7 @@ export default function UploadLogo({ toast.error( error instanceof Error ? error.message : "Failed to upload image.", ); + allowReaderPreview = false; setPreview(previousPreview); } finally { setIsUploading(false);