feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
|
|
@ -0,0 +1,611 @@
|
|||
BEGIN;
|
||||
|
||||
-- Direct multi-row team mutations cannot preserve ownership invariants. All
|
||||
-- writes below are routed through locked SECURITY DEFINER RPCs.
|
||||
DROP POLICY IF EXISTS "teams_insert_own" ON public.teams;
|
||||
DROP POLICY IF EXISTS "team_members_insert_admin" ON public.team_members;
|
||||
DROP POLICY IF EXISTS "team_members_update_admin" ON public.team_members;
|
||||
DROP POLICY IF EXISTS "team_members_delete_admin_or_self" ON public.team_members;
|
||||
DROP POLICY IF EXISTS "team_invites_insert_admin" ON public.team_invites;
|
||||
DROP POLICY IF EXISTS "team_invites_read" ON public.team_invites;
|
||||
DROP POLICY IF EXISTS "team_invites_delete" ON public.team_invites;
|
||||
|
||||
CREATE TEMP TABLE duplicate_active_team_invites ON COMMIT DROP AS
|
||||
SELECT
|
||||
id,
|
||||
row_number() OVER (
|
||||
PARTITION BY team_id, lower(btrim(email))
|
||||
ORDER BY created_at DESC, id
|
||||
) AS duplicate_rank
|
||||
FROM public.team_invites
|
||||
WHERE accepted_at IS NULL;
|
||||
|
||||
DELETE FROM public.team_invites AS invite
|
||||
USING duplicate_active_team_invites AS duplicate
|
||||
WHERE invite.id = duplicate.id AND duplicate.duplicate_rank > 1;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_team_invites_active_email
|
||||
ON public.team_invites(team_id, lower(btrim(email)))
|
||||
WHERE accepted_at IS NULL;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.create_team(team_name text)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
current_user_id uuid := auth.uid();
|
||||
normalized_name text := btrim(team_name);
|
||||
created_team public.teams;
|
||||
BEGIN
|
||||
IF current_user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'authentication_required' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
IF normalized_name = '' OR char_length(normalized_name) > 80 THEN
|
||||
RAISE EXCEPTION 'invalid_team_name' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
|
||||
PERFORM pg_advisory_xact_lock(hashtextextended(current_user_id::text, 73041));
|
||||
IF (
|
||||
SELECT count(*)
|
||||
FROM public.teams
|
||||
WHERE owner_id = current_user_id
|
||||
) >= 25 THEN
|
||||
RAISE EXCEPTION 'team_limit_reached' USING ERRCODE = '54000';
|
||||
END IF;
|
||||
|
||||
INSERT INTO public.teams (name, owner_id)
|
||||
VALUES (normalized_name, current_user_id)
|
||||
RETURNING * INTO created_team;
|
||||
|
||||
INSERT INTO public.team_members (team_id, user_id, role)
|
||||
VALUES (created_team.id, current_user_id, 'owner');
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'id', created_team.id,
|
||||
'name', created_team.name,
|
||||
'owner_id', created_team.owner_id,
|
||||
'role', 'owner',
|
||||
'member_count', 1,
|
||||
'created_at', created_team.created_at,
|
||||
'updated_at', created_team.updated_at
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.create_team_invite(
|
||||
target_team_id uuid,
|
||||
invited_email text,
|
||||
invited_role text DEFAULT 'member'
|
||||
)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
current_user_id uuid := auth.uid();
|
||||
current_user_email text;
|
||||
caller_team_role text;
|
||||
normalized_email text := lower(btrim(invited_email));
|
||||
existing_invite public.team_invites;
|
||||
created_invite public.team_invites;
|
||||
invite_token text;
|
||||
BEGIN
|
||||
IF current_user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'authentication_required' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
IF target_team_id IS NULL
|
||||
OR normalized_email !~ '^[^[:space:]@]+@[^[:space:]@]+[.][^[:space:]@]+$'
|
||||
OR char_length(normalized_email) > 254
|
||||
OR invited_role NOT IN ('admin', 'member') THEN
|
||||
RAISE EXCEPTION 'invalid_invite' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
|
||||
SELECT lower(email) INTO current_user_email FROM auth.users WHERE id = current_user_id;
|
||||
IF current_user_email = normalized_email THEN
|
||||
RAISE EXCEPTION 'cannot_invite_self' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
SELECT role INTO caller_team_role
|
||||
FROM public.team_members
|
||||
WHERE team_id = target_team_id AND user_id = current_user_id;
|
||||
IF caller_team_role NOT IN ('owner', 'admin') THEN
|
||||
RAISE EXCEPTION 'team_admin_required' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
IF invited_role = 'admin' AND caller_team_role <> 'owner' THEN
|
||||
RAISE EXCEPTION 'owner_required_for_admin_invite' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
|
||||
PERFORM pg_advisory_xact_lock(hashtextextended(target_team_id::text || ':' || normalized_email, 73042));
|
||||
IF (
|
||||
SELECT count(*)
|
||||
FROM public.team_invites
|
||||
WHERE invited_by = current_user_id
|
||||
AND created_at >= now() - interval '24 hours'
|
||||
) >= 50 THEN
|
||||
RAISE EXCEPTION 'invite_rate_limited' USING ERRCODE = '54000';
|
||||
END IF;
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM public.team_members AS member
|
||||
JOIN auth.users AS account ON account.id = member.user_id
|
||||
WHERE member.team_id = target_team_id AND lower(account.email) = normalized_email
|
||||
) THEN
|
||||
RAISE EXCEPTION 'already_team_member' USING ERRCODE = '23505';
|
||||
END IF;
|
||||
|
||||
SELECT * INTO existing_invite
|
||||
FROM public.team_invites
|
||||
WHERE team_id = target_team_id
|
||||
AND lower(email) = normalized_email
|
||||
AND accepted_at IS NULL
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
FOR UPDATE;
|
||||
|
||||
IF existing_invite.id IS NOT NULL AND existing_invite.expires_at > now() THEN
|
||||
IF existing_invite.role <> invited_role THEN
|
||||
UPDATE public.team_invites
|
||||
SET role = invited_role
|
||||
WHERE id = existing_invite.id
|
||||
RETURNING * INTO existing_invite;
|
||||
END IF;
|
||||
RETURN jsonb_build_object(
|
||||
'id', existing_invite.id,
|
||||
'team_id', existing_invite.team_id,
|
||||
'email', existing_invite.email,
|
||||
'token', existing_invite.token,
|
||||
'role', existing_invite.role,
|
||||
'expires_at', existing_invite.expires_at,
|
||||
'duplicate', true
|
||||
);
|
||||
END IF;
|
||||
IF existing_invite.id IS NOT NULL THEN
|
||||
DELETE FROM public.team_invites WHERE id = existing_invite.id;
|
||||
END IF;
|
||||
|
||||
invite_token := public.generate_invite_token();
|
||||
INSERT INTO public.team_invites (
|
||||
team_id, invited_by, email, role, token
|
||||
)
|
||||
VALUES (
|
||||
target_team_id, current_user_id, normalized_email, invited_role, invite_token
|
||||
)
|
||||
RETURNING * INTO created_invite;
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'id', created_invite.id,
|
||||
'team_id', created_invite.team_id,
|
||||
'email', created_invite.email,
|
||||
'token', created_invite.token,
|
||||
'role', created_invite.role,
|
||||
'expires_at', created_invite.expires_at,
|
||||
'duplicate', false
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.accept_team_invite(invite_token text)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
current_user_id uuid := auth.uid();
|
||||
current_email text;
|
||||
invite public.team_invites;
|
||||
final_role text;
|
||||
BEGIN
|
||||
IF current_user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'authentication_required' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
IF invite_token IS NULL OR char_length(invite_token) NOT BETWEEN 20 AND 128 THEN
|
||||
RAISE EXCEPTION 'invalid_invite_token' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
|
||||
PERFORM pg_advisory_xact_lock(hashtextextended(invite_token, 73043));
|
||||
SELECT * INTO invite
|
||||
FROM public.team_invites
|
||||
WHERE token = invite_token
|
||||
FOR UPDATE;
|
||||
IF invite.id IS NULL THEN
|
||||
RAISE EXCEPTION 'invite_not_found' USING ERRCODE = 'P0002';
|
||||
END IF;
|
||||
IF invite.accepted_at IS NOT NULL THEN
|
||||
IF invite.accepted_by = current_user_id THEN
|
||||
SELECT role INTO final_role
|
||||
FROM public.team_members
|
||||
WHERE team_id = invite.team_id AND user_id = current_user_id;
|
||||
IF final_role IS NULL THEN
|
||||
RAISE EXCEPTION 'invite_already_accepted' USING ERRCODE = '23505';
|
||||
END IF;
|
||||
RETURN jsonb_build_object(
|
||||
'team_id', invite.team_id,
|
||||
'role', final_role,
|
||||
'duplicate', true,
|
||||
'already_member', true
|
||||
);
|
||||
END IF;
|
||||
RAISE EXCEPTION 'invite_already_accepted' USING ERRCODE = '23505';
|
||||
END IF;
|
||||
IF invite.expires_at <= now() THEN
|
||||
RAISE EXCEPTION 'invite_expired' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
|
||||
SELECT lower(email) INTO current_email FROM auth.users WHERE id = current_user_id;
|
||||
IF current_email IS NULL OR current_email <> lower(invite.email) THEN
|
||||
RAISE EXCEPTION 'invite_email_mismatch' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
|
||||
INSERT INTO public.team_members (team_id, user_id, role)
|
||||
VALUES (invite.team_id, current_user_id, invite.role)
|
||||
ON CONFLICT (team_id, user_id) DO NOTHING;
|
||||
|
||||
SELECT role INTO final_role
|
||||
FROM public.team_members
|
||||
WHERE team_id = invite.team_id AND user_id = current_user_id
|
||||
FOR UPDATE;
|
||||
IF final_role IS NULL THEN
|
||||
RAISE EXCEPTION 'team_membership_failed';
|
||||
END IF;
|
||||
|
||||
UPDATE public.team_invites
|
||||
SET accepted_at = now(), accepted_by = current_user_id
|
||||
WHERE id = invite.id AND accepted_at IS NULL;
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'invite_acceptance_conflict';
|
||||
END IF;
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'team_id', invite.team_id,
|
||||
'role', final_role,
|
||||
'duplicate', false,
|
||||
'already_member', false
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.update_team_member_role(
|
||||
target_team_id uuid,
|
||||
member_user_id uuid,
|
||||
new_role text
|
||||
)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
current_user_id uuid := auth.uid();
|
||||
caller_role text;
|
||||
prior_role text;
|
||||
BEGIN
|
||||
IF current_user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'authentication_required' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
IF new_role NOT IN ('admin', 'member') THEN
|
||||
RAISE EXCEPTION 'invalid_team_role' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
PERFORM pg_advisory_xact_lock(hashtextextended(target_team_id::text, 73044));
|
||||
SELECT role INTO caller_role FROM public.team_members
|
||||
WHERE team_id = target_team_id AND user_id = current_user_id;
|
||||
IF caller_role <> 'owner' THEN
|
||||
RAISE EXCEPTION 'team_owner_required' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
SELECT role INTO prior_role FROM public.team_members
|
||||
WHERE team_id = target_team_id AND user_id = member_user_id
|
||||
FOR UPDATE;
|
||||
IF prior_role IS NULL THEN
|
||||
RAISE EXCEPTION 'team_member_not_found' USING ERRCODE = 'P0002';
|
||||
END IF;
|
||||
IF prior_role = 'owner' OR member_user_id = current_user_id THEN
|
||||
RAISE EXCEPTION 'team_owner_immutable' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
UPDATE public.team_members SET role = new_role
|
||||
WHERE team_id = target_team_id AND user_id = member_user_id;
|
||||
RETURN jsonb_build_object(
|
||||
'team_id', target_team_id,
|
||||
'user_id', member_user_id,
|
||||
'role', new_role
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.remove_team_member(
|
||||
target_team_id uuid,
|
||||
member_user_id uuid
|
||||
)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
current_user_id uuid := auth.uid();
|
||||
caller_role text;
|
||||
target_role text;
|
||||
BEGIN
|
||||
IF current_user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'authentication_required' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
PERFORM pg_advisory_xact_lock(hashtextextended(target_team_id::text, 73045));
|
||||
SELECT role INTO caller_role FROM public.team_members
|
||||
WHERE team_id = target_team_id AND user_id = current_user_id;
|
||||
SELECT role INTO target_role FROM public.team_members
|
||||
WHERE team_id = target_team_id AND user_id = member_user_id
|
||||
FOR UPDATE;
|
||||
IF target_role IS NULL THEN
|
||||
RAISE EXCEPTION 'team_member_not_found' USING ERRCODE = 'P0002';
|
||||
END IF;
|
||||
IF target_role = 'owner' THEN
|
||||
RAISE EXCEPTION 'team_owner_cannot_leave' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
IF member_user_id <> current_user_id AND (
|
||||
caller_role NOT IN ('owner', 'admin')
|
||||
OR (target_role = 'admin' AND caller_role <> 'owner')
|
||||
) THEN
|
||||
RAISE EXCEPTION 'team_admin_required' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
|
||||
DELETE FROM public.team_members
|
||||
WHERE team_id = target_team_id AND user_id = member_user_id;
|
||||
RETURN jsonb_build_object(
|
||||
'team_id', target_team_id,
|
||||
'user_id', member_user_id,
|
||||
'removed', true
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.list_team_members(target_team_id uuid)
|
||||
RETURNS TABLE (
|
||||
user_id uuid,
|
||||
role text,
|
||||
joined_at timestamptz,
|
||||
display_name text,
|
||||
avatar_url text,
|
||||
is_current boolean
|
||||
)
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
SELECT
|
||||
member.user_id,
|
||||
member.role,
|
||||
member.joined_at,
|
||||
profile.name,
|
||||
profile.avatar_url,
|
||||
member.user_id = auth.uid()
|
||||
FROM public.team_members AS member
|
||||
LEFT JOIN public.profiles AS profile ON profile.id = member.user_id
|
||||
WHERE member.team_id = target_team_id
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM public.team_members AS caller
|
||||
WHERE caller.team_id = target_team_id AND caller.user_id = auth.uid()
|
||||
)
|
||||
ORDER BY
|
||||
CASE member.role WHEN 'owner' THEN 0 WHEN 'admin' THEN 1 ELSE 2 END,
|
||||
member.joined_at,
|
||||
member.user_id;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.list_team_invites(target_team_id uuid)
|
||||
RETURNS TABLE (
|
||||
id uuid,
|
||||
email text,
|
||||
role text,
|
||||
expires_at timestamptz,
|
||||
created_at timestamptz
|
||||
)
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
SELECT invite.id, invite.email, invite.role, invite.expires_at, invite.created_at
|
||||
FROM public.team_invites AS invite
|
||||
WHERE invite.team_id = target_team_id
|
||||
AND invite.accepted_at IS NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM public.team_members AS caller
|
||||
WHERE caller.team_id = target_team_id
|
||||
AND caller.user_id = auth.uid()
|
||||
AND caller.role IN ('owner', 'admin')
|
||||
)
|
||||
ORDER BY invite.created_at DESC, invite.id;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.cancel_team_invite(invite_id uuid)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
current_user_id uuid := auth.uid();
|
||||
invite public.team_invites;
|
||||
caller_role text;
|
||||
BEGIN
|
||||
IF current_user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'authentication_required' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
SELECT * INTO invite FROM public.team_invites WHERE id = invite_id FOR UPDATE;
|
||||
IF invite.id IS NULL OR invite.accepted_at IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'invite_not_found' USING ERRCODE = 'P0002';
|
||||
END IF;
|
||||
SELECT role INTO caller_role FROM public.team_members
|
||||
WHERE team_id = invite.team_id AND user_id = current_user_id;
|
||||
IF invite.invited_by <> current_user_id AND caller_role NOT IN ('owner', 'admin') THEN
|
||||
RAISE EXCEPTION 'team_admin_required' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
DELETE FROM public.team_invites WHERE id = invite.id;
|
||||
RETURN jsonb_build_object('id', invite.id, 'cancelled', true);
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.create_team(text) FROM PUBLIC, anon;
|
||||
REVOKE ALL ON FUNCTION public.create_team_invite(uuid, text, text) FROM PUBLIC, anon;
|
||||
REVOKE ALL ON FUNCTION public.accept_team_invite(text) FROM PUBLIC, anon;
|
||||
REVOKE ALL ON FUNCTION public.update_team_member_role(uuid, uuid, text) FROM PUBLIC, anon;
|
||||
REVOKE ALL ON FUNCTION public.remove_team_member(uuid, uuid) FROM PUBLIC, anon;
|
||||
REVOKE ALL ON FUNCTION public.list_team_members(uuid) FROM PUBLIC, anon;
|
||||
REVOKE ALL ON FUNCTION public.list_team_invites(uuid) FROM PUBLIC, anon;
|
||||
REVOKE ALL ON FUNCTION public.cancel_team_invite(uuid) FROM PUBLIC, anon;
|
||||
GRANT EXECUTE ON FUNCTION public.create_team(text) TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.create_team_invite(uuid, text, text) TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.accept_team_invite(text) TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.update_team_member_role(uuid, uuid, text) TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.remove_team_member(uuid, uuid) TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.list_team_members(uuid) TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.list_team_invites(uuid) TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.cancel_team_invite(uuid) TO authenticated;
|
||||
|
||||
-- Replace the legacy Expo-only registration shape with provider-aware,
|
||||
-- device-owned registrations. Old tokens cannot be proven to belong to a bare
|
||||
-- RN installation, so they are invalidated and clients must register again.
|
||||
DELETE FROM public.push_tokens;
|
||||
UPDATE public.devices SET push_token = NULL WHERE push_token IS NOT NULL;
|
||||
DROP INDEX IF EXISTS public.idx_devices_push_token;
|
||||
DROP POLICY IF EXISTS "push_tokens_own" ON public.push_tokens;
|
||||
|
||||
ALTER TABLE public.push_tokens
|
||||
ADD COLUMN IF NOT EXISTS provider text,
|
||||
ADD COLUMN IF NOT EXISTS last_registered_at timestamptz NOT NULL DEFAULT now();
|
||||
ALTER TABLE public.push_tokens
|
||||
ALTER COLUMN device_id SET NOT NULL,
|
||||
ALTER COLUMN provider SET NOT NULL,
|
||||
ADD CONSTRAINT push_tokens_provider_check
|
||||
CHECK (provider IN ('fcm', 'apns', 'webpush'));
|
||||
|
||||
CREATE POLICY "push_tokens_no_direct_access" ON public.push_tokens
|
||||
FOR SELECT USING (false);
|
||||
|
||||
DROP POLICY IF EXISTS "devices_insert_own" ON public.devices;
|
||||
CREATE POLICY "devices_insert_own" ON public.devices
|
||||
FOR INSERT WITH CHECK (
|
||||
user_id = auth.uid() AND revoked_at IS NULL AND push_token IS NULL
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.reject_direct_device_push_token()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
BEGIN
|
||||
IF NEW.push_token IS DISTINCT FROM OLD.push_token THEN
|
||||
RAISE EXCEPTION 'device_push_token_is_server_managed' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER reject_direct_device_push_token_update
|
||||
BEFORE UPDATE OF push_token ON public.devices
|
||||
FOR EACH ROW EXECUTE FUNCTION public.reject_direct_device_push_token();
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.register_push_registration(
|
||||
push_device_id uuid,
|
||||
push_provider text,
|
||||
registration_id text
|
||||
)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
current_user_id uuid := auth.uid();
|
||||
owned_device public.devices;
|
||||
existing_owner uuid;
|
||||
registered public.push_tokens;
|
||||
BEGIN
|
||||
IF current_user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'authentication_required' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
IF push_provider NOT IN ('fcm', 'apns', 'webpush')
|
||||
OR registration_id IS NULL
|
||||
OR char_length(registration_id) NOT BETWEEN 20 AND 4096
|
||||
OR registration_id ~ '[[:space:]]' THEN
|
||||
RAISE EXCEPTION 'invalid_push_registration' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
|
||||
PERFORM pg_advisory_xact_lock(hashtextextended(push_device_id::text, 73046));
|
||||
PERFORM pg_advisory_xact_lock(hashtextextended(registration_id, 73047));
|
||||
SELECT * INTO owned_device
|
||||
FROM public.devices
|
||||
WHERE id = push_device_id AND user_id = current_user_id
|
||||
FOR UPDATE;
|
||||
IF owned_device.id IS NULL OR owned_device.revoked_at IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'active_device_not_found' USING ERRCODE = 'P0002';
|
||||
END IF;
|
||||
IF (owned_device.platform = 'android' AND push_provider <> 'fcm')
|
||||
OR (owned_device.platform = 'ios' AND push_provider <> 'apns')
|
||||
OR (owned_device.platform = 'web' AND push_provider <> 'webpush') THEN
|
||||
RAISE EXCEPTION 'push_provider_platform_mismatch' USING ERRCODE = '22023';
|
||||
END IF;
|
||||
|
||||
SELECT user_id INTO existing_owner
|
||||
FROM public.push_tokens
|
||||
WHERE token = registration_id
|
||||
FOR UPDATE;
|
||||
IF existing_owner IS NOT NULL AND existing_owner <> current_user_id THEN
|
||||
RAISE EXCEPTION 'push_registration_owned_by_other_user' USING ERRCODE = '23505';
|
||||
END IF;
|
||||
|
||||
DELETE FROM public.push_tokens
|
||||
WHERE device_id = push_device_id OR token = registration_id;
|
||||
INSERT INTO public.push_tokens (
|
||||
user_id, token, platform, device_name, device_id, provider, last_registered_at
|
||||
)
|
||||
VALUES (
|
||||
current_user_id,
|
||||
registration_id,
|
||||
owned_device.platform,
|
||||
owned_device.device_name,
|
||||
owned_device.id,
|
||||
push_provider,
|
||||
now()
|
||||
)
|
||||
RETURNING * INTO registered;
|
||||
|
||||
RETURN jsonb_build_object(
|
||||
'device_id', registered.device_id,
|
||||
'provider', registered.provider,
|
||||
'registered_at', registered.last_registered_at
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.unregister_push_registration(push_device_id uuid)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = ''
|
||||
AS $$
|
||||
DECLARE
|
||||
current_user_id uuid := auth.uid();
|
||||
removed_count integer;
|
||||
BEGIN
|
||||
IF current_user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'authentication_required' USING ERRCODE = '42501';
|
||||
END IF;
|
||||
DELETE FROM public.push_tokens
|
||||
WHERE device_id = push_device_id AND user_id = current_user_id;
|
||||
GET DIAGNOSTICS removed_count = ROW_COUNT;
|
||||
RETURN jsonb_build_object(
|
||||
'device_id', push_device_id,
|
||||
'removed', removed_count > 0
|
||||
);
|
||||
END;
|
||||
$$;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.register_push_registration(uuid, text, text) FROM PUBLIC, anon;
|
||||
REVOKE ALL ON FUNCTION public.unregister_push_registration(uuid) FROM PUBLIC, anon;
|
||||
GRANT EXECUTE ON FUNCTION public.register_push_registration(uuid, text, text) TO authenticated;
|
||||
GRANT EXECUTE ON FUNCTION public.unregister_push_registration(uuid) TO authenticated;
|
||||
|
||||
COMMIT;
|
||||
Loading…
Add table
Add a link
Reference in a new issue