176 lines
5.7 KiB
PL/PgSQL
176 lines
5.7 KiB
PL/PgSQL
BEGIN;
|
|
|
|
-- These helpers are used by RLS policies with auth.uid(). Historically an
|
|
-- authenticated caller could invoke them with somebody else's UUID and learn
|
|
-- that account's team ids. Preserve the policy signature while binding every
|
|
-- authenticated invocation to the caller.
|
|
CREATE OR REPLACE FUNCTION public.user_team_ids(uid uuid)
|
|
RETURNS SETOF uuid
|
|
LANGUAGE sql
|
|
STABLE
|
|
SECURITY DEFINER
|
|
SET search_path = ''
|
|
AS $$
|
|
SELECT member.team_id
|
|
FROM public.team_members AS member
|
|
WHERE uid = auth.uid()
|
|
AND member.user_id = auth.uid();
|
|
$$;
|
|
|
|
CREATE OR REPLACE FUNCTION public.user_admin_team_ids(uid uuid)
|
|
RETURNS SETOF uuid
|
|
LANGUAGE sql
|
|
STABLE
|
|
SECURITY DEFINER
|
|
SET search_path = ''
|
|
AS $$
|
|
SELECT member.team_id
|
|
FROM public.team_members AS member
|
|
WHERE uid = auth.uid()
|
|
AND member.user_id = auth.uid()
|
|
AND member.role IN ('owner', 'admin');
|
|
$$;
|
|
|
|
REVOKE ALL ON FUNCTION public.user_team_ids(uuid) FROM PUBLIC, anon;
|
|
REVOKE ALL ON FUNCTION public.user_admin_team_ids(uuid) FROM PUBLIC, anon;
|
|
GRANT EXECUTE ON FUNCTION public.user_team_ids(uuid) TO authenticated;
|
|
GRANT EXECUTE ON FUNCTION public.user_admin_team_ids(uuid) TO authenticated;
|
|
|
|
-- Push delivery is initiated by an authenticated user but performed with the
|
|
-- service role. This service-only ledger makes deduplication and limits atomic
|
|
-- across Edge Function instances.
|
|
CREATE TABLE public.push_dispatch_attempts (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
caller_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
event_type text NOT NULL CHECK (event_type IN (
|
|
'transcription.completed',
|
|
'team.invite.created',
|
|
'billing.status.changed'
|
|
)),
|
|
resource_id uuid NOT NULL,
|
|
created_at timestamptz NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_push_dispatch_attempts_caller_created
|
|
ON public.push_dispatch_attempts(caller_id, created_at DESC);
|
|
|
|
CREATE INDEX idx_push_dispatch_attempts_event_resource_created
|
|
ON public.push_dispatch_attempts(caller_id, event_type, resource_id, created_at DESC);
|
|
|
|
ALTER TABLE public.push_dispatch_attempts ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE OR REPLACE FUNCTION public.reserve_push_dispatch(
|
|
push_event_type text,
|
|
push_resource_id uuid
|
|
)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = ''
|
|
AS $$
|
|
DECLARE
|
|
current_user_id uuid := auth.uid();
|
|
normalized_event_type text := lower(btrim(push_event_type));
|
|
existing_attempt_id uuid;
|
|
created_attempt_id uuid;
|
|
BEGIN
|
|
IF current_user_id IS NULL THEN
|
|
RAISE EXCEPTION 'authentication_required' USING ERRCODE = '42501';
|
|
END IF;
|
|
IF push_resource_id IS NULL OR normalized_event_type NOT IN (
|
|
'transcription.completed',
|
|
'team.invite.created',
|
|
'billing.status.changed'
|
|
) THEN
|
|
RAISE EXCEPTION 'invalid_push_event' USING ERRCODE = '22023';
|
|
END IF;
|
|
|
|
-- A caller-scoped lock serializes both aggregate limits and per-event
|
|
-- deduplication, including concurrent requests handled by different workers.
|
|
PERFORM pg_advisory_xact_lock(hashtextextended(current_user_id::text, 73048));
|
|
|
|
SELECT attempt.id INTO existing_attempt_id
|
|
FROM public.push_dispatch_attempts AS attempt
|
|
WHERE attempt.caller_id = current_user_id
|
|
AND attempt.event_type = normalized_event_type
|
|
AND attempt.resource_id = push_resource_id
|
|
AND attempt.created_at > now() - interval '30 seconds'
|
|
ORDER BY attempt.created_at DESC
|
|
LIMIT 1;
|
|
|
|
IF existing_attempt_id IS NOT NULL THEN
|
|
RETURN jsonb_build_object(
|
|
'reserved', false,
|
|
'duplicate', true,
|
|
'attempt_id', existing_attempt_id
|
|
);
|
|
END IF;
|
|
|
|
IF (
|
|
SELECT count(*)
|
|
FROM public.push_dispatch_attempts AS attempt
|
|
WHERE attempt.caller_id = current_user_id
|
|
AND attempt.created_at > now() - interval '1 minute'
|
|
) >= 10 OR (
|
|
SELECT count(*)
|
|
FROM public.push_dispatch_attempts AS attempt
|
|
WHERE attempt.caller_id = current_user_id
|
|
AND attempt.created_at > now() - interval '24 hours'
|
|
) >= 100 THEN
|
|
RAISE EXCEPTION 'push_rate_limited' USING ERRCODE = '54000';
|
|
END IF;
|
|
|
|
INSERT INTO public.push_dispatch_attempts (caller_id, event_type, resource_id)
|
|
VALUES (current_user_id, normalized_event_type, push_resource_id)
|
|
RETURNING id INTO created_attempt_id;
|
|
|
|
RETURN jsonb_build_object(
|
|
'reserved', true,
|
|
'duplicate', false,
|
|
'attempt_id', created_attempt_id
|
|
);
|
|
END;
|
|
$$;
|
|
|
|
-- Invite recipient resolution must cross the auth schema, which PostgREST
|
|
-- clients cannot join directly. Only the inviter of an active invite may ask.
|
|
CREATE OR REPLACE FUNCTION public.resolve_team_invite_recipient(invite_id uuid)
|
|
RETURNS uuid
|
|
LANGUAGE plpgsql
|
|
STABLE
|
|
SECURITY DEFINER
|
|
SET search_path = ''
|
|
AS $$
|
|
DECLARE
|
|
current_user_id uuid := auth.uid();
|
|
recipient_id uuid;
|
|
BEGIN
|
|
IF current_user_id IS NULL THEN
|
|
RAISE EXCEPTION 'authentication_required' USING ERRCODE = '42501';
|
|
END IF;
|
|
IF invite_id IS NULL THEN
|
|
RAISE EXCEPTION 'invalid_invite' USING ERRCODE = '22023';
|
|
END IF;
|
|
|
|
SELECT account.id INTO recipient_id
|
|
FROM public.team_invites AS invite
|
|
JOIN auth.users AS account ON lower(account.email) = lower(invite.email)
|
|
WHERE invite.id = invite_id
|
|
AND invite.invited_by = current_user_id
|
|
AND invite.accepted_at IS NULL
|
|
AND invite.expires_at > now()
|
|
ORDER BY account.created_at, account.id
|
|
LIMIT 1;
|
|
|
|
RETURN recipient_id;
|
|
END;
|
|
$$;
|
|
|
|
REVOKE ALL ON TABLE public.push_dispatch_attempts FROM PUBLIC, anon, authenticated;
|
|
GRANT ALL ON TABLE public.push_dispatch_attempts TO service_role;
|
|
REVOKE ALL ON FUNCTION public.reserve_push_dispatch(text, uuid) FROM PUBLIC, anon;
|
|
REVOKE ALL ON FUNCTION public.resolve_team_invite_recipient(uuid) FROM PUBLIC, anon;
|
|
GRANT EXECUTE ON FUNCTION public.reserve_push_dispatch(text, uuid) TO authenticated;
|
|
GRANT EXECUTE ON FUNCTION public.resolve_team_invite_recipient(uuid) TO authenticated;
|
|
|
|
COMMIT;
|