Files
acmcc/supabase/migrations/20260520153409_c92ad975-6112-4313-82a0-081da18d6547.sql
2026-06-01 20:19:26 -04:00

97 lines
3.7 KiB
PL/PgSQL

-- Per-bill, per-board-member secure approval tokens for email approve/deny
CREATE TABLE IF NOT EXISTS public.bill_approval_email_tokens (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
bill_id uuid NOT NULL REFERENCES public.bills(id) ON DELETE CASCADE,
board_member_id uuid NOT NULL REFERENCES public.board_members(id) ON DELETE CASCADE,
token uuid NOT NULL DEFAULT gen_random_uuid() UNIQUE,
email text NOT NULL,
member_name text,
sent_at timestamptz,
acted_at timestamptz,
action text CHECK (action IN ('approved','denied')),
notes text,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (bill_id, board_member_id)
);
CREATE INDEX IF NOT EXISTS idx_baet_token ON public.bill_approval_email_tokens(token);
CREATE INDEX IF NOT EXISTS idx_baet_bill ON public.bill_approval_email_tokens(bill_id);
ALTER TABLE public.bill_approval_email_tokens ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Staff manage bill approval tokens" ON public.bill_approval_email_tokens
TO authenticated
USING (has_role(auth.uid(),'admin') OR has_role(auth.uid(),'manager'))
WITH CHECK (has_role(auth.uid(),'admin') OR has_role(auth.uid(),'manager'));
-- Lookup RPC for the public page (anonymous-friendly, security definer)
CREATE OR REPLACE FUNCTION public.lookup_bill_approval_by_token(p_token uuid)
RETURNS TABLE (
bill_id uuid,
member_name text,
member_email text,
acted_at timestamptz,
action text,
vendor_name text,
invoice_number text,
bill_date date,
due_date date,
amount numeric,
description text,
attachment_url text,
bill_status text,
association_name text
)
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public
AS $$
SELECT t.bill_id, t.member_name, t.email AS member_email, t.acted_at, t.action,
v.name AS vendor_name, b.invoice_number, b.bill_date, b.due_date, b.amount,
b.description, b.attachment_url, b.status AS bill_status, a.name AS association_name
FROM bill_approval_email_tokens t
JOIN bills b ON b.id = t.bill_id
LEFT JOIN vendors v ON v.id = b.vendor_id
LEFT JOIN associations a ON a.id = b.association_id
WHERE t.token = p_token;
$$;
GRANT EXECUTE ON FUNCTION public.lookup_bill_approval_by_token(uuid) TO anon, authenticated;
-- Record action via token
CREATE OR REPLACE FUNCTION public.record_bill_approval_by_token(p_token uuid, p_action text, p_notes text DEFAULT NULL)
RETURNS jsonb
LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
AS $$
DECLARE
tok record;
bill record;
BEGIN
IF p_action NOT IN ('approved','denied') THEN
RETURN jsonb_build_object('ok', false, 'error', 'Invalid action');
END IF;
SELECT * INTO tok FROM bill_approval_email_tokens WHERE token = p_token;
IF NOT FOUND THEN
RETURN jsonb_build_object('ok', false, 'error', 'Invalid token');
END IF;
IF tok.acted_at IS NOT NULL THEN
RETURN jsonb_build_object('ok', false, 'already_voted', true, 'error', 'You have already responded', 'action', tok.action);
END IF;
SELECT * INTO bill FROM bills WHERE id = tok.bill_id;
IF NOT FOUND THEN
RETURN jsonb_build_object('ok', false, 'error', 'Bill not found');
END IF;
INSERT INTO bill_approvals (bill_id, association_id, vendor_name, amount, status, notes, approved_date)
VALUES (tok.bill_id, bill.association_id, COALESCE(tok.member_name,'Board Member'), bill.amount,
p_action, p_notes, CURRENT_DATE);
UPDATE bills SET status = p_action, updated_at = now() WHERE id = tok.bill_id;
UPDATE bill_approval_email_tokens
SET acted_at = now(), action = p_action, notes = p_notes
WHERE token = p_token;
RETURN jsonb_build_object('ok', true, 'action', p_action);
END;
$$;
GRANT EXECUTE ON FUNCTION public.record_bill_approval_by_token(uuid, text, text) TO anon, authenticated;