# 🔒 SQL для копи-пасте в Supabase Dashboard

**Project:** Damka Royal / Шашки Рояль
**Project ref:** `jsykbnkbrwwsxcdurzcw`
**Запускать:** Supabase Dashboard → SQL Editor → New query → вставить блок → **Run**

⚠️ **Запускать в указанном порядке.** Каждый блок обратно-совместим (IF NOT EXISTS, CREATE OR REPLACE), но не запускайте Block 2 раньше Block 1.

---

## 📦 Block 1: Production Hardening (RLS, CAS, heartbeat, cleanup)

Файл: `supabase/migrations/20260522_production_hardening.sql`

**Что делает:**
- Включает RLS на `games` и `moves` со строгими политиками
- Добавляет колонки `white_last_seen_at`, `black_last_seen_at`
- Создаёт UNIQUE индекс на `room_code`
- Создаёт RPC `apply_move_safe(...)` — CAS защита от stale UPDATE
- Создаёт RPC `heartbeat_game(...)` — обновляет last_seen
- Создаёт RPC `cleanup_stale_rooms()` — закрывает waiting >10 мин
- Создаёт триггер автообновления timestamps

👉 **Открыть файл:** `/app/project/supabase/migrations/20260522_production_hardening.sql`

```bash
cat /app/project/supabase/migrations/20260522_production_hardening.sql
```

---

## 📦 Block 2: P0 — Server-Side Move Validation (apply_move_v2)

Файл: `supabase/migrations/20260522_p0_server_move_validation.sql`

**Что делает:**
- Создаёт хелперы `_apply_step_to_board(...)` и `_count_pieces(...)`
- Создаёт **главную RPC `apply_move_v2(...)`** — сервер сам пересчитывает board из присланных `steps[]` и сверяет с клиентским
- Логирует подозрительные попытки в `audit_logs`
- Закрывает ~95% поверхностей подделки `board_state`

**Что проверяет на сервере:**
1. Игрок — участник этой игры
2. Статус игры = `playing`
3. Сейчас ход вызывающего (по `current_turn`)
4. CAS: `prev_move_number == current_move_number`
5. Каждый step — диагональный, в границах, на тёмную клетку
6. Фигура на `from` принадлежит ходящему
7. Captured клетка содержит фигуру **соперника**
8. Количество фигур мовера не уменьшается (нет self-capture)
9. Количество захваченных фигур соперника === количество steps с capture
10. **Серверный пересчёт board === клиентский board** (если не совпадают — отказ)

👉 **Открыть файл:** `/app/project/supabase/migrations/20260522_p0_server_move_validation.sql`

```bash
cat /app/project/supabase/migrations/20260522_p0_server_move_validation.sql
```

---

## ✅ Проверка после применения

В SQL Editor запустите:

```sql
-- 1. RLS включён?
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname='public' AND tablename IN ('games', 'moves');
-- Должно быть rowsecurity=true для обеих

-- 2. RPC задеплоены?
SELECT proname FROM pg_proc
WHERE proname IN ('apply_move_safe', 'apply_move_v2', 'heartbeat_game', 'cleanup_stale_rooms', '_apply_step_to_board', '_count_pieces')
ORDER BY proname;
-- Должно быть 6 строк

-- 3. Колонки heartbeat?
SELECT column_name FROM information_schema.columns
WHERE table_schema='public' AND table_name='games'
  AND column_name IN ('white_last_seen_at', 'black_last_seen_at');
-- Должно быть 2 строки

-- 4. Триггер?
SELECT tgname FROM pg_trigger
WHERE tgname = 'games_touch_timestamps_trg';
-- Должно быть 1 строка

-- 5. Полный smoke-test apply_move_v2 на ваш страх и риск:
-- (создайте тестовую игру и попробуйте сделать ход через RPC)
SELECT cleanup_stale_rooms();
```

---

## 🔑 Переменные окружения (`.env` в корне проекта)

```env
VITE_SUPABASE_URL=https://jsykbnkbrwwsxcdurzcw.supabase.co
VITE_SUPABASE_ANON_KEY=<вставьте sb_publishable_... или eyJ... из Supabase Dashboard → Settings → API>
```

**Hercules secrets** (если деплоите через Hercules) — те же два ключа.

⚠️ **НИКОГДА** не вставляйте `sb_secret_...` или `service_role` в клиентский .env!

---

## 🧪 Проверка ходов в audit_logs

После применения миграции 2 запустите:

```sql
-- Все подозрительные события связанные с ходами
SELECT event_type, event_data, created_at
FROM audit_logs
WHERE event_type LIKE 'move.%'
ORDER BY created_at DESC
LIMIT 50;

-- Возможные event_type:
-- move.unauthorized    — не-участник попытался ходить
-- move.wrong_turn      — попытка походить не в свой ход
-- move.invalid_step    — невалидный step (за пределами, не диагональ, и т.п.)
-- move.board_mismatch  — клиент прислал board, не совпадающий с серверным
```

Если в `move.board_mismatch` появятся записи — **это потенциальный читер**, и в `event_data` будет полный snapshot:
- `server_board` — то, что вычислил сервер
- `client_board` — то, что прислал клиент
- `player_id` — кто пытался

---

## 📋 Что делать после применения

1. Запустить Block 1 → проверить (см. выше)
2. Запустить Block 2 → проверить (см. выше)
3. Обновить frontend (он уже подготовлен — слой 1 apply_move_v2, слой 2 apply_move_safe, слой 3 legacy)
4. Two-device smoke test:
   - Создать комнату на устройстве A
   - Войти на устройстве B по коду
   - Сделать ход → проверить, что в `moves` таблице появилась строка с `move_data.steps[]`
   - Сделать мультивзятие → проверить, что записан весь chain
5. Опционально настроить cron на `cleanup_stale_rooms()` — раз в 5 минут через Supabase Edge Function

---

## 🚨 Откат (rollback)

Если что-то пошло не так, можно откатить:

```sql
-- Удалить новые RPC
DROP FUNCTION IF EXISTS apply_move_v2(TEXT, UUID, INT, JSONB, JSONB, TEXT, INT);
DROP FUNCTION IF EXISTS apply_move_safe(TEXT, UUID, INT, JSONB, TEXT, INT, INT, INT, INT, INT);
DROP FUNCTION IF EXISTS heartbeat_game(TEXT, UUID);
DROP FUNCTION IF EXISTS cleanup_stale_rooms();
DROP FUNCTION IF EXISTS _apply_step_to_board(JSONB, INT, INT, INT, INT, INT, INT, TEXT);
DROP FUNCTION IF EXISTS _count_pieces(JSONB);

-- Снять строгие RLS (вернуть к старым "public can do everything")
DROP POLICY IF EXISTS "games_insert_self" ON games;
DROP POLICY IF EXISTS "games_update_participant" ON games;
DROP POLICY IF EXISTS "games_no_delete" ON games;
DROP POLICY IF EXISTS "moves_insert_participant" ON moves;

-- Heartbeat поля можно оставить — они не мешают
```

Клиент при отсутствии новых RPC автоматически fallback'ит на `apply_move_safe` → `legacy UPDATE`.
