-- ───────────────────────────────────────────────────────────────────────── -- CryptoWallet API — DB schema (idempotent) -- Применить один раз на внешнюю БД при первом деплое. -- Версия 3.0 (custodial: AES-GCM encrypted_mnemonic на сервере). -- ───────────────────────────────────────────────────────────────────────── -- ── USERS ──────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS users ( id VARCHAR(26) PRIMARY KEY, -- ULID из JWT.sub email VARCHAR(255) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, -- "EXTERNAL_AUTH" (auth у BITOK) last_name VARCHAR(255), first_name VARCHAR(255), middle_name VARCHAR(255), birth_date DATE, crypto_wallet VARCHAR(255), phone VARCHAR(64), bik VARCHAR(64), account_number VARCHAR(64), card_number VARCHAR(64), inn VARCHAR(64), kyc_verified BOOLEAN NOT NULL DEFAULT FALSE, kyc_verified_at TIMESTAMPTZ, is_deleted BOOLEAN NOT NULL DEFAULT FALSE, encrypted_vault TEXT, -- legacy client-side AES blob (opt) vault_salt VARCHAR(128), -- legacy client KDF salt (opt) encrypted_mnemonic TEXT, -- AES-256-GCM blob (custodial) created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Добавляем encrypted_mnemonic если таблица существует с прошлой версии DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'encrypted_mnemonic' ) THEN ALTER TABLE users ADD COLUMN encrypted_mnemonic TEXT; END IF; END $$; -- CHECK: encrypted_mnemonic при значении должен иметь разумный размер. -- AES-GCM blob: 12 IV + plaintext + 16 tag. -- 12-word mnemonic ≈ 11*7 + 11 = 88 байт → 12+88+16 = 116 байт → ~156 base64 chars. -- 24-word mnemonic ≈ 23*7 + 23 = 184 байт → 12+184+16 = 212 байт → ~284 base64 chars. -- Минимум 140 (12 слов с маленькими словами), максимум 512 запас на 24 слова + padding. DO $$ BEGIN 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 140 AND 512)); END IF; END $$; -- ── WALLETS ────────────────────────────────────────────────────────────── CREATE TABLE IF NOT EXISTS wallets ( id VARCHAR(26) PRIMARY KEY, -- ULID user_id VARCHAR(26) NOT NULL REFERENCES users(id) ON DELETE CASCADE, chain VARCHAR(16) NOT NULL, -- ETH / BTC / SOL / TRX / BSC address VARCHAR(256) NOT NULL, derivation_path VARCHAR(64) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (user_id, chain) ); CREATE INDEX IF NOT EXISTS idx_wallets_user_id ON wallets(user_id); -- ── SESSIONS (placeholder, не используется в текущей версии) ───────────── CREATE TABLE IF NOT EXISTS sessions ( id VARCHAR(26) PRIMARY KEY, user_id VARCHAR(26) NOT NULL REFERENCES users(id) ON DELETE CASCADE, expires_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id); CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);