Миграция club_access.sql (access_requests, users.club_tier/yandex_id/ avatar_url, password NULLable, works.club_only). OAuth-роуты Яндекса (/api/auth/yandex[/callback]) с CSRF-state и мягкой 503 без ключей. Клуб: /api/club/tiers|request|my-request|gallery + админ requests/ approve/decline. Rate-limit на /api/auth сохранён. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
68 lines
3.6 KiB
PL/PgSQL
68 lines
3.6 KiB
PL/PgSQL
-- nika-gallery: миграция «Закрытый клуб» + вход через Яндекс ID.
|
|
-- Аддитивно к db/schema.sql. Идемпотентна (IF NOT EXISTS / IF EXISTS).
|
|
-- Задача: feat/club-yandex-auth (Claude Opus 4.8, 2026-08-05).
|
|
--
|
|
-- Что добавляет:
|
|
-- 1. users: club_tier (уровень клуба), yandex_id (uniq), avatar_url;
|
|
-- password делаем NULLable (вход только через Яндекс — без локального пароля).
|
|
-- 2. access_requests: заявки в клуб с модерацией (pending/approved/declined).
|
|
-- 3. works: флаг club_only (работа видна только членам клуба).
|
|
|
|
BEGIN;
|
|
|
|
-- ── users: поля клуба и Яндекс ID ───────────────────────────────
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS club_tier TEXT;
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS yandex_id TEXT;
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_url TEXT;
|
|
|
|
-- Допустимые уровни клуба (nullable — большинство пользователей вне клуба).
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_constraint WHERE conname = 'users_club_tier_check'
|
|
) THEN
|
|
ALTER TABLE users ADD CONSTRAINT users_club_tier_check
|
|
CHECK (club_tier IS NULL OR club_tier IN
|
|
('vip','collector','partner','photographer','organizer','press'));
|
|
END IF;
|
|
END $$;
|
|
|
|
-- yandex_id уникален, но только среди заполненных (частичный уникальный индекс).
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uniq_users_yandex_id
|
|
ON users(yandex_id) WHERE yandex_id IS NOT NULL;
|
|
|
|
-- Пароль больше не обязателен: пользователь может входить только через Яндекс.
|
|
-- (Локальная регистрация всё равно требует пароль на уровне API.)
|
|
ALTER TABLE users ALTER COLUMN password DROP NOT NULL;
|
|
|
|
-- ── access_requests: заявки в клуб с модерацией ─────────────────
|
|
CREATE TABLE IF NOT EXISTS access_requests (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
tier TEXT NOT NULL CHECK (tier IN
|
|
('vip','collector','partner','photographer','organizer','press')),
|
|
status TEXT NOT NULL DEFAULT 'pending'
|
|
CHECK (status IN ('pending','approved','declined')),
|
|
note TEXT, -- сообщение заявителя «о себе»
|
|
admin_note TEXT, -- комментарий модератора (опц.)
|
|
reviewed_by UUID REFERENCES users(id),
|
|
reviewed_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_access_requests_status
|
|
ON access_requests(status, created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_access_requests_user
|
|
ON access_requests(user_id, created_at DESC);
|
|
|
|
-- Не более одной активной (pending) заявки на пользователя.
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uniq_access_requests_pending
|
|
ON access_requests(user_id) WHERE status = 'pending';
|
|
|
|
-- ── works: закрытые (клубные) работы ────────────────────────────
|
|
ALTER TABLE works ADD COLUMN IF NOT EXISTS club_only BOOLEAN NOT NULL DEFAULT false;
|
|
CREATE INDEX IF NOT EXISTS idx_works_club_only ON works(club_only) WHERE club_only = true;
|
|
|
|
COMMIT;
|