118 lines
4.5 KiB
PL/PgSQL
118 lines
4.5 KiB
PL/PgSQL
-- Fix respond_shift_swap:
|
|
-- 1. Guard against double-processing (idempotent — return early if already terminal)
|
|
-- 2. SECURITY DEFINER to bypass RLS for cross-user duty_schedule ownership checks
|
|
-- (the function still enforces caller identity via auth.uid())
|
|
|
|
CREATE OR REPLACE FUNCTION public.respond_shift_swap(p_swap_id uuid, p_action text)
|
|
RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$
|
|
DECLARE
|
|
v_swap RECORD;
|
|
BEGIN
|
|
SELECT * INTO v_swap FROM public.swap_requests WHERE id = p_swap_id;
|
|
IF NOT FOUND THEN
|
|
RAISE EXCEPTION 'swap request not found';
|
|
END IF;
|
|
|
|
IF p_action NOT IN ('accepted','rejected','admin_review') THEN
|
|
RAISE EXCEPTION 'invalid action';
|
|
END IF;
|
|
|
|
-- Idempotency guard: already in terminal state → nothing more to do.
|
|
-- This prevents spurious "ownership changed" errors when a stale UI retries
|
|
-- an acceptance that was already processed on another device or by an admin.
|
|
IF v_swap.status IN ('accepted', 'rejected') THEN
|
|
RETURN;
|
|
END IF;
|
|
|
|
IF p_action = 'accepted' THEN
|
|
-- only recipient or admin/dispatcher can accept
|
|
IF NOT (
|
|
v_swap.recipient_id = auth.uid()
|
|
OR EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','dispatcher'))
|
|
) THEN
|
|
RAISE EXCEPTION 'permission denied';
|
|
END IF;
|
|
|
|
-- ensure both shifts are still owned by the expected users before swapping
|
|
IF NOT EXISTS (SELECT 1 FROM public.duty_schedules WHERE id = v_swap.shift_id AND user_id = v_swap.requester_id) THEN
|
|
RAISE EXCEPTION 'requester shift ownership changed, cannot accept swap';
|
|
END IF;
|
|
IF v_swap.target_shift_id IS NULL THEN
|
|
RAISE EXCEPTION 'target shift missing';
|
|
END IF;
|
|
IF NOT EXISTS (SELECT 1 FROM public.duty_schedules WHERE id = v_swap.target_shift_id AND user_id = v_swap.recipient_id) THEN
|
|
RAISE EXCEPTION 'target shift ownership changed, cannot accept swap';
|
|
END IF;
|
|
|
|
-- perform the swap (atomic within function) and stamp swap_request_id
|
|
UPDATE public.duty_schedules
|
|
SET user_id = v_swap.recipient_id, swap_request_id = p_swap_id
|
|
WHERE id = v_swap.shift_id;
|
|
|
|
UPDATE public.duty_schedules
|
|
SET user_id = v_swap.requester_id, swap_request_id = p_swap_id
|
|
WHERE id = v_swap.target_shift_id;
|
|
|
|
UPDATE public.swap_requests
|
|
SET status = 'accepted', updated_at = now()
|
|
WHERE id = p_swap_id;
|
|
|
|
INSERT INTO public.swap_request_participants(swap_request_id, user_id, role)
|
|
VALUES (p_swap_id, auth.uid(), 'approver')
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
INSERT INTO public.notifications(user_id, actor_id, type, created_at)
|
|
VALUES (v_swap.requester_id, auth.uid(), 'swap_update', now());
|
|
|
|
ELSIF p_action = 'rejected' THEN
|
|
-- only recipient or admin/dispatcher can reject
|
|
IF NOT (
|
|
v_swap.recipient_id = auth.uid()
|
|
OR EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin','dispatcher'))
|
|
) THEN
|
|
RAISE EXCEPTION 'permission denied';
|
|
END IF;
|
|
|
|
UPDATE public.swap_requests
|
|
SET status = 'rejected', updated_at = now()
|
|
WHERE id = p_swap_id;
|
|
|
|
INSERT INTO public.swap_request_participants(swap_request_id, user_id, role)
|
|
VALUES (p_swap_id, auth.uid(), 'approver')
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
INSERT INTO public.notifications(user_id, actor_id, type, created_at)
|
|
VALUES (v_swap.requester_id, auth.uid(), 'swap_update', now());
|
|
|
|
ELSE -- admin_review
|
|
-- only requester may escalate for admin review
|
|
IF NOT (v_swap.requester_id = auth.uid()) THEN
|
|
RAISE EXCEPTION 'permission denied';
|
|
END IF;
|
|
|
|
UPDATE public.swap_requests
|
|
SET status = 'admin_review', updated_at = now()
|
|
WHERE id = p_swap_id;
|
|
|
|
INSERT INTO public.notifications(user_id, actor_id, type, created_at)
|
|
VALUES (v_swap.requester_id, auth.uid(), 'swap_update', now());
|
|
END IF;
|
|
END;
|
|
$$;
|
|
|
|
-- Re-grant EXECUTE to authenticated users.
|
|
-- SECURITY DEFINER functions drop existing grants on replace, so this must
|
|
-- be explicit.
|
|
GRANT EXECUTE ON FUNCTION public.respond_shift_swap(uuid, text) TO authenticated;
|
|
|
|
-- Enable full replica identity so UPDATE events include all columns,
|
|
-- and add to the realtime publication so .stream() receives changes.
|
|
-- Without this, the Flutter client never sees status transitions (e.g.
|
|
-- pending → accepted) via Supabase Realtime — only the initial REST fetch.
|
|
ALTER TABLE public.swap_requests REPLICA IDENTITY FULL;
|
|
DO $$ BEGIN
|
|
ALTER PUBLICATION supabase_realtime ADD TABLE public.swap_requests;
|
|
EXCEPTION WHEN duplicate_object THEN
|
|
NULL; -- already present
|
|
END $$;
|