mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
Adds a profile tab to /settings with the five things every B2B SaaS
user expects:
- Display name — edits profiles.full_name.
- Avatar — upload / replace / remove, stored in a new `avatars`
Supabase Storage bucket with RLS scoped to the owner's folder.
Migration 008_profile_avatars_storage.sql creates the bucket (public
read, 2 MB cap, image mimes only) and the four storage.objects
policies.
- Email — goes through supabase.auth.updateUser({ email }), which
confirms to both the old and new addresses. UI surfaces a "check
both inboxes" banner after save.
- Password — three-field form with a current-password gate implemented
via a throwaway signInWithPassword() before calling updateUser.
- Active sessions — one-click "sign out of all devices" using
supabase.auth.signOut({ scope: 'global' }), with a confirm dialog.
Plus read-only role, joined date, and user id for operator debugging.
The header avatar and sidebar user row are now both dropdown menus
linking to /settings?tab=profile and /settings?tab=whatsapp; the
settings page reads ?tab= to land on the right tab. useAuth gets a
refreshProfile() callback so the header/sidebar update immediately
after a save without a full reload.
Deferred (separate work): account deletion, 2FA, phone/timezone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
60 lines
2.0 KiB
SQL
60 lines
2.0 KiB
SQL
-- ============================================================
|
|
-- 008_profile_avatars_storage.sql
|
|
--
|
|
-- Creates the `avatars` Supabase Storage bucket and the RLS policies
|
|
-- that let each user manage only their own avatar file while letting
|
|
-- everyone read (so rendering <img> tags without signed URLs works).
|
|
--
|
|
-- File path convention used by the app:
|
|
-- avatars/{auth.uid()}/avatar-<timestamp>.<ext>
|
|
-- The policies rely on the first path segment matching auth.uid()::text.
|
|
--
|
|
-- Idempotent — safe to re-run.
|
|
-- ============================================================
|
|
|
|
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
|
VALUES (
|
|
'avatars',
|
|
'avatars',
|
|
TRUE,
|
|
2097152, -- 2 MB
|
|
ARRAY['image/png', 'image/jpeg', 'image/webp', 'image/gif']
|
|
)
|
|
ON CONFLICT (id) DO UPDATE
|
|
SET
|
|
public = EXCLUDED.public,
|
|
file_size_limit = EXCLUDED.file_size_limit,
|
|
allowed_mime_types = EXCLUDED.allowed_mime_types;
|
|
|
|
-- Policies live on storage.objects. Drop-if-exists because Postgres
|
|
-- has no CREATE POLICY IF NOT EXISTS, and we want this migration to
|
|
-- re-run cleanly.
|
|
DROP POLICY IF EXISTS "Avatars are publicly readable" ON storage.objects;
|
|
CREATE POLICY "Avatars are publicly readable"
|
|
ON storage.objects FOR SELECT
|
|
USING (bucket_id = 'avatars');
|
|
|
|
DROP POLICY IF EXISTS "Users can upload their own avatar" ON storage.objects;
|
|
CREATE POLICY "Users can upload their own avatar"
|
|
ON storage.objects FOR INSERT
|
|
WITH CHECK (
|
|
bucket_id = 'avatars'
|
|
AND auth.uid()::text = (storage.foldername(name))[1]
|
|
);
|
|
|
|
DROP POLICY IF EXISTS "Users can update their own avatar" ON storage.objects;
|
|
CREATE POLICY "Users can update their own avatar"
|
|
ON storage.objects FOR UPDATE
|
|
USING (
|
|
bucket_id = 'avatars'
|
|
AND auth.uid()::text = (storage.foldername(name))[1]
|
|
);
|
|
|
|
DROP POLICY IF EXISTS "Users can delete their own avatar" ON storage.objects;
|
|
CREATE POLICY "Users can delete their own avatar"
|
|
ON storage.objects FOR DELETE
|
|
USING (
|
|
bucket_id = 'avatars'
|
|
AND auth.uid()::text = (storage.foldername(name))[1]
|
|
);
|