diff --git a/README.md b/README.md index 7f84356..fadebf0 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ vault kv put dev-secrets/crypto/master key=$(openssl rand -hex 32) # 2. CSRF secret в Vault vault kv put dev-secrets/csrf secret_key=$(openssl rand -hex 32) salt=csrf-salt digest=sha256 -# 3. DB schema (миграция идемпотентна — безопасно прогонять на existing БД) +# 3. DB schema — APPEND-ONLY / NON-DESTRUCTIVE +# Безопасно прогонять на existing БД. См. ниже "Schema is non-destructive". psql -h -U postgres_user -d postgres -f cryptowallet-schema.sql # 4. bitok public key в Vault (для kid из JWT header) @@ -93,9 +94,33 @@ ssh server@ -p 2222 'cd cryptowallet && docker compose up -d --build' - **Relay proxy** whitelist method+path — `/quote` (POST), `/intents/status/v3` (GET), `/execute/{swap|bridge}` (POST). Никаких freeform action'ов - **Bridge UI dropdown** ETH/BSC/SOL only (TRX через Relay убран — плохая ликвидность) +## Schema is non-destructive + +`cryptowallet-schema.sql` **append-only**. Re-run на боксе с уже настроенной БД = **zero DDL changes**. Если оператор добавил кастомные таблицы / индексы / constraints вручную — они **никогда** не будут перезаписаны или удалены. + +Что делает script: +- `CREATE TABLE IF NOT EXISTS users` / `wallets` +- `ALTER TABLE users ADD COLUMN ` (только если колонки нет — `encrypted_mnemonic`, `erc20`, `passport_data`) +- `CREATE UNIQUE INDEX users_email_lower_unique` (если индекса нет) +- `CREATE INDEX idx_users_active` / `idx_wallets_*` (если индексов нет) +- `ADD CONSTRAINT` × 4 (только если данного constraint name нет) + +Что script **НЕ делает**: +- ❌ Никогда не `DROP TABLE` +- ❌ Никогда не `DROP CONSTRAINT` +- ❌ Никогда не `DROP COLUMN` +- ❌ Никогда не перезаписывает существующие constraints / indexes + +Legacy cleanup (audit_log, idempotency_keys, sessions от старых версий) — **manual one-time** операторская задача, не часть этого script'а: +```bash +psql ... -c "DROP TABLE IF EXISTS audit_log CASCADE;" +psql ... -c "DROP TABLE IF EXISTS idempotency_keys CASCADE;" +psql ... -c "DROP TABLE IF EXISTS sessions CASCADE;" +``` + ## Logs -Файловых логов **нет**. Всё в stdout, подбирается Docker log driver: +Файловых логов **нет**. Весь код пишет в `process.stdout` (см. `apps/api/src/lib/logger.ts` и `lib/audit-log.ts`). Docker подбирает stdout через json-file driver и показывает через `docker compose logs`: ```bash docker compose logs -f api # все логи (structured JSON) diff --git a/cryptowallet-schema.sql b/cryptowallet-schema.sql index 0d32f65..a1561f6 100644 --- a/cryptowallet-schema.sql +++ b/cryptowallet-schema.sql @@ -1,14 +1,23 @@ -- ╔══════════════════════════════════════════════════════════════════╗ --- ║ CryptoWallet API — Production DB schema (idempotent, custodial) ║ --- ║ Применять: psql -h -U postgres_user -d postgres -f ... ║ --- ║ Безопасно прогонять повторно на existing БД. ║ +-- ║ CryptoWallet API — Production DB schema ║ +-- ║ ║ +-- ║ APPEND-ONLY / NON-DESTRUCTIVE: ║ +-- ║ Безопасно прогонять повторно. Ничего не DROP'ает, не overwrite. ║ +-- ║ Если оператор добавил кастомные таблицы / индексы / constraints ║ +-- ║ вручную — они НЕ будут затронуты. ║ +-- ║ ║ +-- ║ Применять: psql -h -U -d -f cryptowallet-schema.sql ║ -- ╚══════════════════════════════════════════════════════════════════╝ --- NOTE: idempotency_keys + audit_log таблицы УДАЛЕНЫ из БД. --- - idempotency_keys → KeyDB (Redis cache), см. apps/api/src/config/redis.ts --- - audit_log → stdout-only (Docker logs / log-aggregator подбирает JSON lines) --- Migration ниже drop'ает их если они существуют от прошлой версии. +-- NOTE: idempotency_keys и audit_log таблицы НЕ используются. +-- - idempotency_keys → KeyDB (Redis cache) — apps/api/src/config/redis.ts +-- - audit_log → stdout JSON-lines — apps/api/src/lib/audit-log.ts +-- Скрипт их НЕ дропает (чтобы re-run был non-destructive). +-- Если оператор хочет cleanup — manual one-time: +-- DROP TABLE IF EXISTS audit_log CASCADE; +-- DROP TABLE IF EXISTS idempotency_keys CASCADE; +-- ── USERS ─────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS users ( id VARCHAR(26) NOT NULL PRIMARY KEY, email VARCHAR(255) NOT NULL UNIQUE, @@ -33,7 +42,7 @@ CREATE TABLE IF NOT EXISTS users ( encrypted_mnemonic TEXT ); --- Idempotent ALTERs для existing БД без extension-columns +-- Idempotent ALTERs для existing БД у которой нет extension-columns (только ADD если нет колонки) DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'encrypted_mnemonic') THEN @@ -47,15 +56,16 @@ BEGIN END IF; END $$; --- Sanity check на blob size. Floor 100 (worst-case 12-word всё-3char mnemonic): +-- Constraint: blob size check (only ADDs if missing, никогда не DROP). +-- Floor 100 (worst-case 12-word 3-char mnemonic = 100 base64 chars). +-- Если оператор изменил этот constraint вручную — наш script его НЕ перезатрёт. DO $$ BEGIN - IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'users_encrypted_mnemonic_size') THEN - ALTER TABLE users DROP CONSTRAINT users_encrypted_mnemonic_size; + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'users_encrypted_mnemonic_size') THEN + ALTER TABLE users + ADD CONSTRAINT users_encrypted_mnemonic_size + CHECK (encrypted_mnemonic IS NULL OR (char_length(encrypted_mnemonic) BETWEEN 100 AND 512)); END IF; - ALTER TABLE users - ADD CONSTRAINT users_encrypted_mnemonic_size - CHECK (encrypted_mnemonic IS NULL OR (char_length(encrypted_mnemonic) BETWEEN 100 AND 512)); END $$; -- Case-insensitive email uniqueness (Alice@x.com ≠ alice@x.com → ACCOUNT HIJACKING fix) @@ -91,6 +101,7 @@ END $$; -- ── WALLETS ───────────────────────────────────────────────────────── -- ON DELETE RESTRICT: hard-delete user → запрос отвергнут пока есть wallets. +-- Это защита от unrecoverable fund loss при GDPR-wipe или admin удалении. CREATE TABLE IF NOT EXISTS wallets ( id VARCHAR(26) NOT NULL PRIMARY KEY, user_id VARCHAR(26) NOT NULL REFERENCES users(id) ON DELETE RESTRICT, @@ -105,21 +116,11 @@ CREATE TABLE IF NOT EXISTS wallets ( CREATE INDEX IF NOT EXISTS idx_wallets_user_id ON wallets(user_id); CREATE INDEX IF NOT EXISTS idx_wallets_address ON wallets(address); --- Idempotent FK migration: если raised на старой DB с CASCADE — поменять -DO $$ -BEGIN - IF EXISTS ( - SELECT 1 FROM information_schema.referential_constraints - WHERE constraint_name LIKE 'wallets_user_id_fkey%' AND delete_rule = 'CASCADE' - ) THEN - ALTER TABLE wallets DROP CONSTRAINT IF EXISTS wallets_user_id_fkey; - ALTER TABLE wallets ADD CONSTRAINT wallets_user_id_fkey - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT; - END IF; -END $$; - --- ── DROP legacy tables (если existing БД от прошлой версии) ──────── --- idempotency_keys → KeyDB cache (apps/api/src/lib/idempotency.ts → Redis) --- audit_log → stdout-only (apps/api/src/lib/audit-log.ts) -DROP TABLE IF EXISTS audit_log CASCADE; -DROP TABLE IF EXISTS idempotency_keys CASCADE; +-- NOTE: если БД старая и wallets.user_id_fkey ON DELETE CASCADE (а нужен RESTRICT +-- для защиты от fund loss при delete user), оператор делает manual ОДИН раз: +-- +-- ALTER TABLE wallets DROP CONSTRAINT wallets_user_id_fkey; +-- ALTER TABLE wallets ADD CONSTRAINT wallets_user_id_fkey +-- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT; +-- +-- Этот script ничего не дропает — re-run полностью non-destructive.