82 lines
4.5 KiB
SQL
82 lines
4.5 KiB
SQL
-- ─────────────────────────────────────────────────────────────────────────
|
||
-- 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);
|