diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 77e48a7..98fc144 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -665,6 +665,65 @@ export type Database = { } Relationships: [] } + conversation_members: { + Row: { + conversation_id: string + created_at: string + id: string + last_read_at: string + user_id: string + } + Insert: { + conversation_id: string + created_at?: string + id?: string + last_read_at?: string + user_id: string + } + Update: { + conversation_id?: string + created_at?: string + id?: string + last_read_at?: string + user_id?: string + } + Relationships: [ + { + foreignKeyName: "conversation_members_conversation_id_fkey" + columns: ["conversation_id"] + isOneToOne: false + referencedRelation: "conversations" + referencedColumns: ["id"] + }, + ] + } + conversations: { + Row: { + created_at: string + created_by: string | null + id: string + name: string | null + type: string + updated_at: string + } + Insert: { + created_at?: string + created_by?: string | null + id?: string + name?: string | null + type: string + updated_at?: string + } + Update: { + created_at?: string + created_by?: string | null + id?: string + name?: string | null + type?: string + updated_at?: string + } + Relationships: [] + } document_folders: { Row: { case_id: string @@ -1123,6 +1182,118 @@ export type Database = { }, ] } + message_attachments: { + Row: { + created_at: string + id: string + message_id: string + mime_type: string | null + name: string + size_bytes: number | null + storage_path: string + } + Insert: { + created_at?: string + id?: string + message_id: string + mime_type?: string | null + name: string + size_bytes?: number | null + storage_path: string + } + Update: { + created_at?: string + id?: string + message_id?: string + mime_type?: string | null + name?: string + size_bytes?: number | null + storage_path?: string + } + Relationships: [ + { + foreignKeyName: "message_attachments_message_id_fkey" + columns: ["message_id"] + isOneToOne: false + referencedRelation: "messages" + referencedColumns: ["id"] + }, + ] + } + message_mentions: { + Row: { + conversation_id: string + created_at: string + id: string + mentioned_user_id: string + message_id: string + } + Insert: { + conversation_id: string + created_at?: string + id?: string + mentioned_user_id: string + message_id: string + } + Update: { + conversation_id?: string + created_at?: string + id?: string + mentioned_user_id?: string + message_id?: string + } + Relationships: [ + { + foreignKeyName: "message_mentions_conversation_id_fkey" + columns: ["conversation_id"] + isOneToOne: false + referencedRelation: "conversations" + referencedColumns: ["id"] + }, + { + foreignKeyName: "message_mentions_message_id_fkey" + columns: ["message_id"] + isOneToOne: false + referencedRelation: "messages" + referencedColumns: ["id"] + }, + ] + } + messages: { + Row: { + body: string + conversation_id: string + created_at: string + edited_at: string | null + id: string + sender_id: string + } + Insert: { + body?: string + conversation_id: string + created_at?: string + edited_at?: string | null + id?: string + sender_id: string + } + Update: { + body?: string + conversation_id?: string + created_at?: string + edited_at?: string | null + id?: string + sender_id?: string + } + Relationships: [ + { + foreignKeyName: "messages_conversation_id_fkey" + columns: ["conversation_id"] + isOneToOne: false + referencedRelation: "conversations" + referencedColumns: ["id"] + }, + ] + } profiles: { Row: { created_at: string @@ -1283,6 +1454,10 @@ export type Database = { Returns: boolean } is_admin: { Args: { _user_id: string }; Returns: boolean } + is_conversation_member: { + Args: { _conv_id: string; _user_id: string } + Returns: boolean + } } Enums: { app_role: "admin" | "attorney" | "staff" diff --git a/supabase/migrations/20260417022728_ba6920ef-8f66-491b-afe3-b0227823de19.sql b/supabase/migrations/20260417022728_ba6920ef-8f66-491b-afe3-b0227823de19.sql new file mode 100644 index 0000000..e41e62f --- /dev/null +++ b/supabase/migrations/20260417022728_ba6920ef-8f66-491b-afe3-b0227823de19.sql @@ -0,0 +1,145 @@ +-- Conversations +CREATE TABLE public.conversations ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + type text NOT NULL CHECK (type IN ('dm','channel')), + name text, + created_by uuid, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +ALTER TABLE public.conversations ENABLE ROW LEVEL SECURITY; + +-- Conversation members +CREATE TABLE public.conversation_members ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + conversation_id uuid NOT NULL REFERENCES public.conversations(id) ON DELETE CASCADE, + user_id uuid NOT NULL, + last_read_at timestamptz NOT NULL DEFAULT now(), + created_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (conversation_id, user_id) +); +ALTER TABLE public.conversation_members ENABLE ROW LEVEL SECURITY; +CREATE INDEX idx_cm_conv ON public.conversation_members(conversation_id); +CREATE INDEX idx_cm_user ON public.conversation_members(user_id); + +-- Security definer to avoid recursion +CREATE OR REPLACE FUNCTION public.is_conversation_member(_conv_id uuid, _user_id uuid) +RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$ + SELECT EXISTS (SELECT 1 FROM public.conversation_members WHERE conversation_id = _conv_id AND user_id = _user_id) +$$; + +-- Messages +CREATE TABLE public.messages ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + conversation_id uuid NOT NULL REFERENCES public.conversations(id) ON DELETE CASCADE, + sender_id uuid NOT NULL, + body text NOT NULL DEFAULT '', + edited_at timestamptz, + created_at timestamptz NOT NULL DEFAULT now() +); +ALTER TABLE public.messages ENABLE ROW LEVEL SECURITY; +CREATE INDEX idx_msg_conv ON public.messages(conversation_id, created_at DESC); + +-- Attachments +CREATE TABLE public.message_attachments ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + message_id uuid NOT NULL REFERENCES public.messages(id) ON DELETE CASCADE, + storage_path text NOT NULL, + name text NOT NULL, + mime_type text, + size_bytes bigint, + created_at timestamptz NOT NULL DEFAULT now() +); +ALTER TABLE public.message_attachments ENABLE ROW LEVEL SECURITY; +CREATE INDEX idx_att_msg ON public.message_attachments(message_id); + +-- Mentions +CREATE TABLE public.message_mentions ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + message_id uuid NOT NULL REFERENCES public.messages(id) ON DELETE CASCADE, + conversation_id uuid NOT NULL REFERENCES public.conversations(id) ON DELETE CASCADE, + mentioned_user_id uuid NOT NULL, + created_at timestamptz NOT NULL DEFAULT now() +); +ALTER TABLE public.message_mentions ENABLE ROW LEVEL SECURITY; +CREATE INDEX idx_mention_user ON public.message_mentions(mentioned_user_id); +CREATE INDEX idx_mention_msg ON public.message_mentions(message_id); + +-- Trigger to update conversations.updated_at on new message +CREATE OR REPLACE FUNCTION public.tg_touch_conversation() +RETURNS trigger LANGUAGE plpgsql SET search_path = public AS $$ +BEGIN + UPDATE public.conversations SET updated_at = now() WHERE id = NEW.conversation_id; + RETURN NEW; +END; +$$; +CREATE TRIGGER trg_touch_conv AFTER INSERT ON public.messages +FOR EACH ROW EXECUTE FUNCTION public.tg_touch_conversation(); + +CREATE TRIGGER trg_conv_updated BEFORE UPDATE ON public.conversations +FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at(); + +-- RLS POLICIES +-- Conversations +CREATE POLICY conv_select ON public.conversations FOR SELECT TO authenticated +USING (public.is_conversation_member(id, auth.uid())); +CREATE POLICY conv_insert ON public.conversations FOR INSERT TO authenticated +WITH CHECK (auth.uid() IS NOT NULL); +CREATE POLICY conv_update ON public.conversations FOR UPDATE TO authenticated +USING (public.is_conversation_member(id, auth.uid())); +CREATE POLICY conv_delete ON public.conversations FOR DELETE TO authenticated +USING (created_by = auth.uid() OR public.is_admin(auth.uid())); + +-- Conversation members +CREATE POLICY cm_select ON public.conversation_members FOR SELECT TO authenticated +USING (public.is_conversation_member(conversation_id, auth.uid())); +CREATE POLICY cm_insert ON public.conversation_members FOR INSERT TO authenticated +WITH CHECK (auth.uid() IS NOT NULL); +CREATE POLICY cm_update_self ON public.conversation_members FOR UPDATE TO authenticated +USING (user_id = auth.uid()); +CREATE POLICY cm_delete ON public.conversation_members FOR DELETE TO authenticated +USING (user_id = auth.uid() OR public.is_admin(auth.uid())); + +-- Messages +CREATE POLICY msg_select ON public.messages FOR SELECT TO authenticated +USING (public.is_conversation_member(conversation_id, auth.uid())); +CREATE POLICY msg_insert ON public.messages FOR INSERT TO authenticated +WITH CHECK (sender_id = auth.uid() AND public.is_conversation_member(conversation_id, auth.uid())); +CREATE POLICY msg_update_own ON public.messages FOR UPDATE TO authenticated +USING (sender_id = auth.uid()); +CREATE POLICY msg_delete_own ON public.messages FOR DELETE TO authenticated +USING (sender_id = auth.uid() OR public.is_admin(auth.uid())); + +-- Attachments +CREATE POLICY att_select ON public.message_attachments FOR SELECT TO authenticated +USING (EXISTS (SELECT 1 FROM public.messages m WHERE m.id = message_id AND public.is_conversation_member(m.conversation_id, auth.uid()))); +CREATE POLICY att_insert ON public.message_attachments FOR INSERT TO authenticated +WITH CHECK (EXISTS (SELECT 1 FROM public.messages m WHERE m.id = message_id AND m.sender_id = auth.uid())); +CREATE POLICY att_delete ON public.message_attachments FOR DELETE TO authenticated +USING (EXISTS (SELECT 1 FROM public.messages m WHERE m.id = message_id AND (m.sender_id = auth.uid() OR public.is_admin(auth.uid())))); + +-- Mentions +CREATE POLICY mention_select ON public.message_mentions FOR SELECT TO authenticated +USING (mentioned_user_id = auth.uid() OR public.is_conversation_member(conversation_id, auth.uid())); +CREATE POLICY mention_insert ON public.message_mentions FOR INSERT TO authenticated +WITH CHECK (public.is_conversation_member(conversation_id, auth.uid())); +CREATE POLICY mention_delete ON public.message_mentions FOR DELETE TO authenticated +USING (EXISTS (SELECT 1 FROM public.messages m WHERE m.id = message_id AND m.sender_id = auth.uid())); + +-- Realtime +ALTER PUBLICATION supabase_realtime ADD TABLE public.messages; +ALTER PUBLICATION supabase_realtime ADD TABLE public.conversation_members; +ALTER PUBLICATION supabase_realtime ADD TABLE public.message_mentions; +ALTER PUBLICATION supabase_realtime ADD TABLE public.conversations; + +-- Storage bucket for attachments +INSERT INTO storage.buckets (id, name, public) VALUES ('message-attachments', 'message-attachments', false) +ON CONFLICT (id) DO NOTHING; + +-- Storage policies: any authenticated user can upload/read (we gate via signed URLs and DB-level access) +CREATE POLICY "msg_attach_select" ON storage.objects FOR SELECT TO authenticated +USING (bucket_id = 'message-attachments'); +CREATE POLICY "msg_attach_insert" ON storage.objects FOR INSERT TO authenticated +WITH CHECK (bucket_id = 'message-attachments' AND auth.uid()::text = (storage.foldername(name))[1]); +CREATE POLICY "msg_attach_delete" ON storage.objects FOR DELETE TO authenticated +USING (bucket_id = 'message-attachments' AND auth.uid()::text = (storage.foldername(name))[1]); \ No newline at end of file