Добавление всех наработок за период практики.
This commit is contained in:
@@ -0,0 +1,493 @@
|
||||
-- Подключение расширения pgcrypto для генерации UUID.
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
CREATE SCHEMA IF NOT EXISTS binding;
|
||||
CREATE SCHEMA IF NOT EXISTS oauth;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS binding.portals (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
-- member_id - уникальный идентификатор портала из Битрикса.
|
||||
member_id text NOT NULL UNIQUE,
|
||||
-- domain - домен портала, например: example.bitrix24.ru.
|
||||
domain text NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS binding.tokens (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
portal_id bigint NOT NULL REFERENCES binding.portals(id) ON DELETE CASCADE,
|
||||
bitrix_user_id bigint NOT NULL,
|
||||
-- Хеш токена, генерируется сервером.
|
||||
token_hash bytea NOT NULL UNIQUE,
|
||||
expires_at timestamptz NOT NULL,
|
||||
consumed_at timestamptz,
|
||||
revoked_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS binding_tokens_owner_idx
|
||||
ON binding.tokens (portal_id, bitrix_user_id, created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS binding.user_bindings (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
portal_id bigint NOT NULL REFERENCES binding.portals(id) ON DELETE CASCADE,
|
||||
bitrix_user_id bigint NOT NULL,
|
||||
telegram_user_id bigint NOT NULL,
|
||||
telegram_chat_id bigint NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE (portal_id, bitrix_user_id),
|
||||
UNIQUE (portal_id, telegram_user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth.user_credentials (
|
||||
portal_id bigint NOT NULL REFERENCES binding.portals(id) ON DELETE CASCADE,
|
||||
bitrix_user_id bigint NOT NULL,
|
||||
access_token bytea NOT NULL,
|
||||
refresh_token bytea NOT NULL,
|
||||
expires_at timestamptz NOT NULL,
|
||||
-- Двойной механизм защиты от гонок данных при обновлении токенов.
|
||||
version bigint NOT NULL DEFAULT 1,
|
||||
refresh_locked_until timestamptz,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (portal_id, bitrix_user_id)
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION binding.issue_v1(
|
||||
p_member_id text,
|
||||
p_domain text,
|
||||
p_bitrix_user_id bigint,
|
||||
p_token_hash bytea,
|
||||
p_token_expires_at timestamptz,
|
||||
p_access_token bytea,
|
||||
p_refresh_token bytea,
|
||||
p_oauth_expires_at timestamptz
|
||||
)
|
||||
RETURNS TABLE(token_id uuid, expires_at timestamptz)
|
||||
LANGUAGE plpgsql
|
||||
-- Задаем SECURITY DEFINER, чтобы функция выполнялась с правами владельца
|
||||
-- схемы binding.
|
||||
SECURITY DEFINER
|
||||
-- Устанавливаем search_path в pg_catalog, чтобы нельзя было подменить
|
||||
-- функции в схеме binding или oauth.
|
||||
SET search_path = pg_catalog
|
||||
AS $function$
|
||||
#variable_conflict error
|
||||
DECLARE
|
||||
v_portal_id bigint;
|
||||
v_token_id uuid;
|
||||
BEGIN
|
||||
-- Добавляем данные о портале, если его ещё нет,
|
||||
-- или обновляем домен, если портал уже существует.
|
||||
INSERT INTO binding.portals(member_id, domain)
|
||||
VALUES (p_member_id, lower(p_domain))
|
||||
ON CONFLICT (member_id) DO UPDATE
|
||||
SET domain = EXCLUDED.domain,
|
||||
updated_at = now()
|
||||
RETURNING id INTO v_portal_id;
|
||||
|
||||
-- Сохраняем OAuth-данные пользователя, если их ещё нет,
|
||||
-- или обновляем их, если они уже существуют.
|
||||
INSERT INTO oauth.user_credentials(
|
||||
portal_id,
|
||||
bitrix_user_id,
|
||||
access_token,
|
||||
refresh_token,
|
||||
expires_at
|
||||
)
|
||||
VALUES (
|
||||
v_portal_id,
|
||||
p_bitrix_user_id,
|
||||
p_access_token,
|
||||
p_refresh_token,
|
||||
p_oauth_expires_at
|
||||
)
|
||||
ON CONFLICT (portal_id, bitrix_user_id) DO UPDATE
|
||||
SET access_token = EXCLUDED.access_token,
|
||||
refresh_token = EXCLUDED.refresh_token,
|
||||
expires_at = EXCLUDED.expires_at,
|
||||
version = oauth.user_credentials.version + 1,
|
||||
refresh_locked_until = NULL,
|
||||
updated_at = now();
|
||||
|
||||
-- Отзываем все предыдущие неиспользованные токены пользователя.
|
||||
UPDATE binding.tokens
|
||||
SET revoked_at = now()
|
||||
WHERE portal_id = v_portal_id
|
||||
AND bitrix_user_id = p_bitrix_user_id
|
||||
AND consumed_at IS NULL
|
||||
AND revoked_at IS NULL;
|
||||
|
||||
-- Выпускаем новый токен привязки.
|
||||
INSERT INTO binding.tokens(
|
||||
portal_id,
|
||||
bitrix_user_id,
|
||||
token_hash,
|
||||
expires_at
|
||||
)
|
||||
VALUES (
|
||||
v_portal_id,
|
||||
p_bitrix_user_id,
|
||||
p_token_hash,
|
||||
p_token_expires_at
|
||||
)
|
||||
RETURNING id INTO v_token_id;
|
||||
|
||||
-- Возвращаем идентификатор токена и срок его действия.
|
||||
RETURN QUERY SELECT v_token_id, p_token_expires_at;
|
||||
END
|
||||
$function$;
|
||||
|
||||
COMMENT ON FUNCTION binding.issue_v1(
|
||||
text,
|
||||
text,
|
||||
bigint,
|
||||
bytea,
|
||||
timestamptz,
|
||||
bytea,
|
||||
bytea,
|
||||
timestamptz
|
||||
)
|
||||
IS $doc$
|
||||
Сохраняет OAuth-данные пользователя и выпускает токен привязки.
|
||||
|
||||
Гарантии:
|
||||
- предыдущие неиспользованные токены отзываются;
|
||||
- при обновлении OAuth-данных увеличивается их версия.
|
||||
|
||||
Возвращает:
|
||||
- token_id - идентификатор токена;
|
||||
- expires_at - срок действия токена.
|
||||
$doc$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION binding.consume_v1(
|
||||
p_token_hash bytea,
|
||||
p_telegram_user_id bigint,
|
||||
p_telegram_chat_id bigint
|
||||
)
|
||||
RETURNS TABLE(
|
||||
member_id text,
|
||||
domain text,
|
||||
bitrix_user_id bigint,
|
||||
telegram_user_id bigint
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog
|
||||
AS $function$
|
||||
#variable_conflict error
|
||||
DECLARE
|
||||
v_portal_id bigint;
|
||||
v_bitrix_user_id bigint;
|
||||
BEGIN
|
||||
-- UPDATE не позволит двум запросам погасить один токен.
|
||||
UPDATE binding.tokens AS token
|
||||
SET consumed_at = now()
|
||||
WHERE token.token_hash = p_token_hash
|
||||
AND token.consumed_at IS NULL
|
||||
AND token.revoked_at IS NULL
|
||||
AND token.expires_at > now()
|
||||
RETURNING token.portal_id, token.bitrix_user_id
|
||||
INTO v_portal_id, v_bitrix_user_id;
|
||||
|
||||
-- Если UPDATE не вернул ни одной строки, значит токен недействителен.
|
||||
IF NOT FOUND THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Удаляем все привязки к Telegram для данного портала и пользователя Битрикса,
|
||||
-- кроме той, которая соответствует текущему пользователю Битрикса.
|
||||
DELETE FROM binding.user_bindings AS user_binding
|
||||
WHERE user_binding.portal_id = v_portal_id
|
||||
AND user_binding.telegram_user_id = p_telegram_user_id
|
||||
AND user_binding.bitrix_user_id <> v_bitrix_user_id;
|
||||
|
||||
-- Добавляем или обновляем привязку к Telegram для данного портала
|
||||
-- и пользователя Битрикса.
|
||||
INSERT INTO binding.user_bindings(
|
||||
portal_id,
|
||||
bitrix_user_id,
|
||||
telegram_user_id,
|
||||
telegram_chat_id
|
||||
)
|
||||
VALUES (
|
||||
v_portal_id,
|
||||
v_bitrix_user_id,
|
||||
p_telegram_user_id,
|
||||
p_telegram_chat_id
|
||||
)
|
||||
ON CONFLICT ON CONSTRAINT user_bindings_pkey DO UPDATE
|
||||
SET telegram_user_id = EXCLUDED.telegram_user_id,
|
||||
telegram_chat_id = EXCLUDED.telegram_chat_id,
|
||||
updated_at = now();
|
||||
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
portal.member_id,
|
||||
portal.domain,
|
||||
v_bitrix_user_id,
|
||||
p_telegram_user_id
|
||||
FROM binding.portals AS portal
|
||||
WHERE portal.id = v_portal_id;
|
||||
END
|
||||
$function$;
|
||||
|
||||
COMMENT ON FUNCTION binding.consume_v1(
|
||||
bytea,
|
||||
bigint,
|
||||
bigint
|
||||
)
|
||||
IS $doc$
|
||||
Погашает токен привязки и сохраняет привязку к Telegram.
|
||||
|
||||
Гарантии:
|
||||
- токен погашается только один раз;
|
||||
- если токен недействителен, функция возвращает пустой результат;
|
||||
- если токен действителен, функция возвращает данные портала и пользователя
|
||||
Битрикса, а также сохраняет привязку к Telegram;
|
||||
- если пользователь Битрикса уже был привязан к другому пользователю Telegram,
|
||||
старая привязка удаляется.
|
||||
|
||||
Возвращает:
|
||||
- member_id - идентификатор портала;
|
||||
- domain - домен портала;
|
||||
- bitrix_user_id - идентификатор пользователя Битрикса;
|
||||
- telegram_user_id - идентификатор пользователя Telegram.
|
||||
$doc$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION binding.find_by_telegram_v1(
|
||||
p_telegram_user_id bigint,
|
||||
p_member_id text DEFAULT NULL
|
||||
)
|
||||
RETURNS TABLE(
|
||||
member_id text,
|
||||
domain text,
|
||||
bitrix_user_id bigint,
|
||||
telegram_user_id bigint
|
||||
)
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog
|
||||
AS $function$
|
||||
SELECT
|
||||
portal.member_id,
|
||||
portal.domain,
|
||||
user_binding.bitrix_user_id,
|
||||
user_binding.telegram_user_id
|
||||
FROM binding.user_bindings AS user_binding
|
||||
JOIN binding.portals AS portal ON portal.id = user_binding.portal_id
|
||||
WHERE user_binding.telegram_user_id = p_telegram_user_id
|
||||
AND (p_member_id IS NULL OR portal.member_id = p_member_id)
|
||||
ORDER BY user_binding.updated_at DESC
|
||||
LIMIT 1
|
||||
$function$;
|
||||
|
||||
COMMENT ON FUNCTION binding.find_by_telegram_v1(
|
||||
bigint,
|
||||
text
|
||||
)
|
||||
IS $doc$
|
||||
Находит привязку к Telegram по идентификатору пользователя Telegram.
|
||||
|
||||
Возвращает:
|
||||
- member_id - идентификатор портала;
|
||||
- domain - домен портала;
|
||||
- bitrix_user_id - идентификатор пользователя Битрикса;
|
||||
- telegram_user_id - идентификатор пользователя Telegram.
|
||||
Если p_member_id не NULL, то поиск ограничивается указанным порталом.
|
||||
$doc$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION oauth.get_credentials_v1(
|
||||
p_member_id text,
|
||||
p_bitrix_user_id bigint
|
||||
)
|
||||
RETURNS TABLE(
|
||||
member_id text,
|
||||
domain text,
|
||||
bitrix_user_id bigint,
|
||||
access_token bytea,
|
||||
refresh_token bytea,
|
||||
expires_at timestamptz,
|
||||
version bigint
|
||||
)
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog
|
||||
AS $function$
|
||||
SELECT
|
||||
portal.member_id,
|
||||
portal.domain,
|
||||
credentials.bitrix_user_id,
|
||||
credentials.access_token,
|
||||
credentials.refresh_token,
|
||||
credentials.expires_at,
|
||||
credentials.version
|
||||
FROM oauth.user_credentials AS credentials
|
||||
JOIN binding.portals AS portal ON portal.id = credentials.portal_id
|
||||
WHERE portal.member_id = p_member_id
|
||||
AND credentials.bitrix_user_id = p_bitrix_user_id
|
||||
$function$;
|
||||
|
||||
COMMENT ON FUNCTION oauth.get_credentials_v1(
|
||||
text,
|
||||
bigint
|
||||
)
|
||||
IS $doc$
|
||||
Находит OAuth-данные пользователя по идентификатору портала и идентификатору
|
||||
пользователя Битрикса.
|
||||
|
||||
Возвращает:
|
||||
- member_id - идентификатор портала;
|
||||
- domain - домен портала;
|
||||
- bitrix_user_id - идентификатор пользователя Битрикса;
|
||||
- access_token - токен доступа;
|
||||
- refresh_token - токен обновления;
|
||||
- expires_at - срок действия токена доступа;
|
||||
- version - версия данных.
|
||||
$doc$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION oauth.claim_refresh_v1(
|
||||
p_member_id text,
|
||||
p_bitrix_user_id bigint,
|
||||
p_version bigint
|
||||
)
|
||||
RETURNS boolean
|
||||
LANGUAGE sql
|
||||
VOLATILE
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog
|
||||
AS $function$
|
||||
-- Создаем временную таблицу claimed,
|
||||
-- которая будет содержать результат обновления (CTE).
|
||||
WITH claimed AS (
|
||||
UPDATE oauth.user_credentials AS credentials
|
||||
SET refresh_locked_until = now() + interval '30 seconds'
|
||||
FROM binding.portals AS portal
|
||||
WHERE credentials.portal_id = portal.id
|
||||
AND portal.member_id = p_member_id
|
||||
AND credentials.bitrix_user_id = p_bitrix_user_id
|
||||
AND credentials.version = p_version
|
||||
AND (
|
||||
credentials.refresh_locked_until IS NULL
|
||||
OR credentials.refresh_locked_until < now()
|
||||
)
|
||||
RETURNING 1
|
||||
)
|
||||
SELECT EXISTS(SELECT 1 FROM claimed)
|
||||
$function$;
|
||||
|
||||
COMMENT ON FUNCTION oauth.claim_refresh_v1(
|
||||
text,
|
||||
bigint,
|
||||
bigint
|
||||
)
|
||||
IS $doc$
|
||||
Пытается захватить токен обновления для пользователя.
|
||||
|
||||
Возвращает:
|
||||
- true, если захват успешен;
|
||||
- false, если захват не удался.
|
||||
$doc$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION oauth.finish_refresh_v1(
|
||||
p_member_id text,
|
||||
p_bitrix_user_id bigint,
|
||||
p_version bigint,
|
||||
p_access_token bytea,
|
||||
p_refresh_token bytea,
|
||||
p_expires_at timestamptz
|
||||
)
|
||||
RETURNS boolean
|
||||
LANGUAGE sql
|
||||
VOLATILE
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog
|
||||
AS $function$
|
||||
WITH updated AS (
|
||||
UPDATE oauth.user_credentials AS credentials
|
||||
SET access_token = p_access_token,
|
||||
refresh_token = p_refresh_token,
|
||||
expires_at = p_expires_at,
|
||||
version = credentials.version + 1,
|
||||
refresh_locked_until = NULL,
|
||||
updated_at = now()
|
||||
FROM binding.portals AS portal
|
||||
WHERE credentials.portal_id = portal.id
|
||||
AND portal.member_id = p_member_id
|
||||
AND credentials.bitrix_user_id = p_bitrix_user_id
|
||||
AND credentials.version = p_version
|
||||
RETURNING 1
|
||||
)
|
||||
SELECT EXISTS(SELECT 1 FROM updated)
|
||||
$function$;
|
||||
|
||||
COMMENT ON FUNCTION oauth.finish_refresh_v1(
|
||||
text,
|
||||
bigint,
|
||||
bigint,
|
||||
bytea,
|
||||
bytea,
|
||||
timestamptz
|
||||
) IS $doc$
|
||||
Завершает процесс обновления токена для пользователя.
|
||||
|
||||
Возвращает:
|
||||
- true, если обновление успешно завершено;
|
||||
- false, если обновление не удалось.
|
||||
$doc$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION oauth.release_refresh_v1(
|
||||
p_member_id text,
|
||||
p_bitrix_user_id bigint,
|
||||
p_version bigint
|
||||
)
|
||||
RETURNS void
|
||||
LANGUAGE sql
|
||||
VOLATILE
|
||||
SECURITY DEFINER
|
||||
SET search_path = pg_catalog
|
||||
AS $function$
|
||||
UPDATE oauth.user_credentials AS credentials
|
||||
SET refresh_locked_until = NULL
|
||||
FROM binding.portals AS portal
|
||||
WHERE credentials.portal_id = portal.id
|
||||
AND portal.member_id = p_member_id
|
||||
AND credentials.bitrix_user_id = p_bitrix_user_id
|
||||
AND credentials.version = p_version
|
||||
$function$;
|
||||
|
||||
COMMENT ON FUNCTION oauth.release_refresh_v1(
|
||||
text,
|
||||
bigint,
|
||||
bigint
|
||||
) IS $doc$
|
||||
Освобождает токен обновления для пользователя.
|
||||
|
||||
Возвращает:
|
||||
- void.
|
||||
$doc$;
|
||||
|
||||
-- Отзываем все права у PUBLIC.
|
||||
REVOKE ALL ON ALL TABLES IN SCHEMA binding, oauth FROM PUBLIC;
|
||||
REVOKE EXECUTE ON ALL FUNCTIONS IN SCHEMA binding, oauth FROM PUBLIC;
|
||||
|
||||
-- Даем права на использование схемы и выполнение функций ролям site_role
|
||||
-- и bot_role.
|
||||
GRANT USAGE ON SCHEMA binding TO site_role, bot_role;
|
||||
GRANT USAGE ON SCHEMA oauth TO bot_role;
|
||||
|
||||
-- Даем права на выполнение функций ролям site_role и bot_role.
|
||||
GRANT EXECUTE ON FUNCTION binding.issue_v1(
|
||||
text, text, bigint, bytea, timestamptz, bytea, bytea, timestamptz
|
||||
) TO site_role;
|
||||
GRANT EXECUTE ON FUNCTION binding.consume_v1(bytea, bigint, bigint) TO bot_role;
|
||||
GRANT EXECUTE ON FUNCTION binding.find_by_telegram_v1(bigint, text) TO bot_role;
|
||||
GRANT EXECUTE ON FUNCTION oauth.get_credentials_v1(text, bigint) TO bot_role;
|
||||
GRANT EXECUTE ON FUNCTION oauth.claim_refresh_v1(text, bigint, bigint) TO bot_role;
|
||||
GRANT EXECUTE ON FUNCTION oauth.finish_refresh_v1(
|
||||
text, bigint, bigint, bytea, bytea, timestamptz
|
||||
) TO bot_role;
|
||||
GRANT EXECUTE ON FUNCTION oauth.release_refresh_v1(text, bigint, bigint)
|
||||
TO bot_role;
|
||||
Reference in New Issue
Block a user