diff --git a/STORAGE_RLS_FIX.sql b/STORAGE_RLS_FIX.sql new file mode 100644 index 0000000..99dd598 --- /dev/null +++ b/STORAGE_RLS_FIX.sql @@ -0,0 +1,120 @@ +-- ===================================================== +-- STORAGE RLS POLICIES FOR LOGO/FAVICON UPLOAD +-- ===================================================== +-- This fixes the "new row violates row-level security policy" error +-- when uploading logo/favicon to Supabase Storage +-- ===================================================== + +-- Step 1: Verify the 'content' bucket exists +SELECT * FROM storage.buckets WHERE name = 'content'; + +-- If no rows returned, create the bucket: +-- INSERT INTO storage.buckets (id, name, public) +-- VALUES ('content', 'content', true); + +-- Step 2: Drop existing policies (if any) on brand-assets +DROP POLICY IF EXISTS "Authenticated users can upload brand assets" ON storage.objects; +DROP POLICY IF EXISTS "Authenticated users can delete brand assets" ON storage.objects; +DROP POLICY IF EXISTS "Public can view brand assets" ON storage.objects; + +-- Step 3: Create policies for brand-assets upload + +-- Policy 1: Allow authenticated users to INSERT (upload) files to brand-assets folder +CREATE POLICY "Authenticated users can upload brand assets" +ON storage.objects FOR INSERT +TO authenticated +WITH CHECK ( + bucket_id = 'content' + AND (name LIKE 'brand-assets/logo/%' OR name LIKE 'brand-assets/favicon/%') +); + +-- Policy 2: Allow authenticated users to UPDATE (replace) files in brand-assets folder +CREATE POLICY "Authenticated users can update brand assets" +ON storage.objects FOR UPDATE +TO authenticated +USING ( + bucket_id = 'content' + AND (name LIKE 'brand-assets/logo/%' OR name LIKE 'brand-assets/favicon/%') +) +WITH CHECK ( + bucket_id = 'content' + AND (name LIKE 'brand-assets/logo/%' OR name LIKE 'brand-assets/favicon/%') +); + +-- Policy 3: Allow authenticated users to DELETE files in brand-assets folder +CREATE POLICY "Authenticated users can delete brand assets" +ON storage.objects FOR DELETE +TO authenticated +USING ( + bucket_id = 'content' + AND (name LIKE 'brand-assets/logo/%' OR name LIKE 'brand-assets/favicon/%') +); + +-- Policy 4: Allow public SELECT (view) on brand-assets (for displaying images) +CREATE POLICY "Public can view brand assets" +ON storage.objects FOR SELECT +TO public +USING ( + bucket_id = 'content' + AND (name LIKE 'brand-assets/logo/%' OR name LIKE 'brand-assets/favicon/%') +); + +-- Step 5: Allow LIST operation for authenticated users (needed for auto-delete) +CREATE POLICY "Authenticated users can list brand assets" +ON storage.objects FOR SELECT +TO authenticated +USING ( + bucket_id = 'content' + AND (name LIKE 'brand-assets/logo%' OR name LIKE 'brand-assets/favicon%') +); + +-- ===================================================== +-- VERIFICATION QUERIES +-- ===================================================== + +-- Check all policies on storage.objects +SELECT + schemaname, + tablename, + policyname, + permissive, + roles, + cmd, + qual, + with_check +FROM pg_policies +WHERE tablename = 'objects' +AND schemaname = 'storage'; + +-- Test if you can access the bucket +SELECT * FROM storage.objects WHERE bucket_id = 'content' LIMIT 5; + +-- ===================================================== +-- TROUBLESHOOTING +-- ===================================================== + +-- If still getting RLS errors, check: + +-- 1. Are you logged in? +SELECT auth.uid(); + +-- 2. Check RLS is enabled +SELECT tablename, rowsecurity +FROM pg_tables +WHERE schemaname = 'storage' +AND tablename = 'objects'; + +-- 3. Check bucket is public +SELECT * FROM storage.buckets WHERE name = 'content'; + +-- ===================================================== +-- ALTERNATIVE: Less restrictive policies (NOT RECOMMENDED for production) +-- ===================================================== +-- Only use these if you trust all authenticated users completely + +-- -- Allow full access to content bucket for authenticated users +-- CREATE POLICY "Authenticated users have full access to content bucket" +-- ON storage.objects FOR ALL +-- TO authenticated +-- USING (bucket_id = 'content') +-- WITH CHECK (bucket_id = 'content'); diff --git a/src/components/admin/settings/BrandingTab.tsx b/src/components/admin/settings/BrandingTab.tsx index ab3290e..cfb2b3c 100644 --- a/src/components/admin/settings/BrandingTab.tsx +++ b/src/components/admin/settings/BrandingTab.tsx @@ -54,6 +54,10 @@ export function BrandingTab() { const [uploadingLogo, setUploadingLogo] = useState(false); const [uploadingFavicon, setUploadingFavicon] = useState(false); + // Preview states for selected files + const [logoPreview, setLogoPreview] = useState(null); + const [faviconPreview, setFaviconPreview] = useState(null); + const logoInputRef = useRef(null); const faviconInputRef = useRef(null); @@ -241,20 +245,64 @@ export function BrandingTab() { const handleLogoSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; - if (file) handleLogoUpload(file); + if (!file) return; + + // Validate file size (2MB max) + if (file.size > 2 * 1024 * 1024) { + toast({ title: 'Error', description: 'Ukuran file maksimal 2MB', variant: 'destructive' }); + return; + } + + // Show preview first + const reader = new FileReader(); + reader.onloadend = () => { + setLogoPreview(reader.result as string); + }; + reader.readAsDataURL(file); }; const handleFaviconSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; - if (file) handleFaviconUpload(file); + if (!file) return; + + // Validate file size (1MB max) + if (file.size > 1 * 1024 * 1024) { + toast({ title: 'Error', description: 'Ukuran file maksimal 1MB', variant: 'destructive' }); + return; + } + + // Show preview first + const reader = new FileReader(); + reader.onloadend = () => { + setFaviconPreview(reader.result as string); + }; + reader.readAsDataURL(file); + }; + + const handleConfirmLogoUpload = async () => { + const file = logoInputRef.current?.files?.[0]; + if (file) { + await handleLogoUpload(file); + setLogoPreview(null); // Clear preview after upload + } + }; + + const handleConfirmFaviconUpload = async () => { + const file = faviconInputRef.current?.files?.[0]; + if (file) { + await handleFaviconUpload(file); + setFaviconPreview(null); // Clear preview after upload + } }; const handleRemoveLogo = () => { setSettings({ ...settings, brand_logo_url: '' }); + setLogoPreview(null); }; const handleRemoveFavicon = () => { setSettings({ ...settings, brand_favicon_url: '' }); + setFaviconPreview(null); }; if (loading) return
; @@ -314,7 +362,39 @@ export function BrandingTab() { />
- {settings.brand_logo_url ? ( + {/* Show preview if file selected, otherwise show current logo or upload button */} + {logoPreview ? ( +
+
+ Logo preview +
+
+ + +
+
+ ) : settings.brand_logo_url ? (
- {settings.brand_favicon_url ? ( + {/* Show preview if file selected, otherwise show current favicon or upload button */} + {faviconPreview ? ( +
+
+ Favicon preview +
+
+ + +
+
+ ) : settings.brand_favicon_url ? (