## New Tables Created (Sprint 1 - DDL Roadmap Q1-2026) ### users schema (4 tables): - profiles: Extended user profile information - user_settings: User preferences and configurations - kyc_verifications: KYC/AML verification records - risk_profiles: Trading risk assessment profiles ### admin schema (3 tables): - admin_roles: Platform administrative roles - platform_analytics: Aggregated platform metrics - api_keys: Programmatic API access keys ### notifications schema (1 table): - notifications: Multi-channel notification system ### market_data schema (4 tables): - tickers: Financial instruments catalog - ohlcv_5m: 5-minute OHLCV price data - technical_indicators: Pre-calculated TA indicators - ohlcv_5m_staging: Staging table for data ingestion ## Features: - Multi-tenancy with RLS policies - Comprehensive indexes for query optimization - Triggers for computed fields and timestamps - Helper functions for common operations - Views for dashboard and reporting - Full GRANTS configuration Roadmap: orchestration/planes/ROADMAP-IMPLEMENTACION-DDL-2026-Q1.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
359 lines
11 KiB
PL/PgSQL
359 lines
11 KiB
PL/PgSQL
-- ============================================================================
|
|
-- SCHEMA: admin
|
|
-- TABLE: api_keys
|
|
-- DESCRIPTION: API Keys para acceso programatico
|
|
-- VERSION: 1.0.0
|
|
-- CREATED: 2026-01-16
|
|
-- SPRINT: Sprint 1 - DDL Implementation Roadmap Q1-2026
|
|
-- ============================================================================
|
|
|
|
-- Enum para tipo de API key
|
|
DO $$ BEGIN
|
|
CREATE TYPE admin.api_key_type AS ENUM (
|
|
'public', -- Solo lectura publica
|
|
'private', -- Acceso completo a recursos propios
|
|
'admin', -- Acceso administrativo
|
|
'service', -- Servicio a servicio
|
|
'webhook', -- Solo para recibir webhooks
|
|
'readonly' -- Solo lectura
|
|
);
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
|
|
-- Enum para estado de API key
|
|
DO $$ BEGIN
|
|
CREATE TYPE admin.api_key_status AS ENUM (
|
|
'active', -- Activa y funcional
|
|
'suspended', -- Suspendida temporalmente
|
|
'revoked', -- Revocada permanentemente
|
|
'expired' -- Expirada
|
|
);
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
|
|
-- Tabla de API Keys
|
|
CREATE TABLE IF NOT EXISTS admin.api_keys (
|
|
-- Identificadores
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE,
|
|
|
|
-- Identificacion de la key
|
|
name VARCHAR(100) NOT NULL,
|
|
description TEXT,
|
|
|
|
-- Key values (solo se almacena hash del secret)
|
|
key_prefix VARCHAR(8) NOT NULL, -- Primeros 8 caracteres para identificacion
|
|
key_hash VARCHAR(255) NOT NULL, -- Hash bcrypt del secret completo
|
|
key_hint VARCHAR(4), -- Ultimos 4 caracteres para referencia
|
|
|
|
-- Tipo y estado
|
|
type admin.api_key_type NOT NULL DEFAULT 'private',
|
|
status admin.api_key_status NOT NULL DEFAULT 'active',
|
|
|
|
-- Permisos
|
|
scopes JSONB NOT NULL DEFAULT '["read"]'::JSONB, -- Array de scopes permitidos
|
|
permissions JSONB DEFAULT '{}'::JSONB, -- Permisos granulares
|
|
|
|
-- Restricciones
|
|
allowed_ips INET[], -- IPs permitidas (NULL = todas)
|
|
allowed_origins TEXT[], -- Origenes CORS permitidos
|
|
allowed_user_agents TEXT[], -- User agents permitidos
|
|
|
|
-- Rate limiting
|
|
rate_limit_per_minute INTEGER DEFAULT 60,
|
|
rate_limit_per_hour INTEGER DEFAULT 1000,
|
|
rate_limit_per_day INTEGER DEFAULT 10000,
|
|
|
|
-- Uso
|
|
last_used_at TIMESTAMPTZ,
|
|
last_used_ip INET,
|
|
last_used_user_agent TEXT,
|
|
total_requests BIGINT NOT NULL DEFAULT 0,
|
|
total_errors BIGINT NOT NULL DEFAULT 0,
|
|
|
|
-- Tracking de uso diario
|
|
requests_today INTEGER NOT NULL DEFAULT 0,
|
|
requests_today_date DATE DEFAULT CURRENT_DATE,
|
|
|
|
-- Expiracion
|
|
expires_at TIMESTAMPTZ,
|
|
|
|
-- Rotacion
|
|
previous_key_hash VARCHAR(255), -- Hash anterior durante rotacion
|
|
previous_key_valid_until TIMESTAMPTZ, -- Validez del key anterior
|
|
rotated_at TIMESTAMPTZ,
|
|
rotation_count INTEGER NOT NULL DEFAULT 0,
|
|
|
|
-- Auditoria
|
|
created_by UUID REFERENCES users.users(id),
|
|
revoked_by UUID REFERENCES users.users(id),
|
|
revoked_at TIMESTAMPTZ,
|
|
revocation_reason TEXT,
|
|
|
|
-- Metadata
|
|
environment VARCHAR(20) DEFAULT 'production', -- 'development', 'staging', 'production'
|
|
metadata JSONB DEFAULT '{}'::JSONB,
|
|
|
|
-- Timestamps
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
-- Constraints
|
|
CONSTRAINT api_keys_unique_name_per_user UNIQUE (user_id, name),
|
|
CONSTRAINT api_keys_unique_prefix UNIQUE (key_prefix)
|
|
);
|
|
|
|
COMMENT ON TABLE admin.api_keys IS
|
|
'API Keys para acceso programatico a la plataforma';
|
|
|
|
COMMENT ON COLUMN admin.api_keys.key_prefix IS
|
|
'Primeros 8 caracteres de la key para identificacion sin exponer el secret';
|
|
|
|
COMMENT ON COLUMN admin.api_keys.key_hash IS
|
|
'Hash bcrypt del secret completo (el secret solo se muestra una vez al crear)';
|
|
|
|
COMMENT ON COLUMN admin.api_keys.scopes IS
|
|
'Array de scopes: ["read", "write", "trade", "admin"]';
|
|
|
|
-- Indices
|
|
CREATE INDEX IF NOT EXISTS idx_api_keys_tenant
|
|
ON admin.api_keys(tenant_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_api_keys_user
|
|
ON admin.api_keys(user_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_api_keys_prefix
|
|
ON admin.api_keys(key_prefix);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_api_keys_status_active
|
|
ON admin.api_keys(status)
|
|
WHERE status = 'active';
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_api_keys_type
|
|
ON admin.api_keys(type);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_api_keys_expires
|
|
ON admin.api_keys(expires_at)
|
|
WHERE expires_at IS NOT NULL AND status = 'active';
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_api_keys_last_used
|
|
ON admin.api_keys(last_used_at DESC)
|
|
WHERE status = 'active';
|
|
|
|
-- Trigger para updated_at
|
|
DROP TRIGGER IF EXISTS api_key_updated_at ON admin.api_keys;
|
|
CREATE TRIGGER api_key_updated_at
|
|
BEFORE UPDATE ON admin.api_keys
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION admin.update_admin_timestamp();
|
|
|
|
-- Trigger para resetear contador diario
|
|
CREATE OR REPLACE FUNCTION admin.reset_daily_requests()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF NEW.requests_today_date < CURRENT_DATE THEN
|
|
NEW.requests_today := 0;
|
|
NEW.requests_today_date := CURRENT_DATE;
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS api_key_reset_daily ON admin.api_keys;
|
|
CREATE TRIGGER api_key_reset_daily
|
|
BEFORE UPDATE ON admin.api_keys
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION admin.reset_daily_requests();
|
|
|
|
-- Funcion para validar API key
|
|
CREATE OR REPLACE FUNCTION admin.validate_api_key(
|
|
p_key_prefix VARCHAR(8),
|
|
p_client_ip INET DEFAULT NULL
|
|
)
|
|
RETURNS TABLE (
|
|
is_valid BOOLEAN,
|
|
key_id UUID,
|
|
user_id UUID,
|
|
tenant_id UUID,
|
|
key_type admin.api_key_type,
|
|
scopes JSONB,
|
|
rate_limit_remaining INTEGER,
|
|
error_message TEXT
|
|
) AS $$
|
|
DECLARE
|
|
v_key RECORD;
|
|
v_rate_remaining INTEGER;
|
|
BEGIN
|
|
-- Buscar key por prefijo
|
|
SELECT * INTO v_key
|
|
FROM admin.api_keys ak
|
|
WHERE ak.key_prefix = p_key_prefix
|
|
AND ak.status = 'active'
|
|
LIMIT 1;
|
|
|
|
IF v_key IS NULL THEN
|
|
RETURN QUERY SELECT
|
|
FALSE, NULL::UUID, NULL::UUID, NULL::UUID,
|
|
NULL::admin.api_key_type, NULL::JSONB, NULL::INTEGER,
|
|
'API key not found or inactive'::TEXT;
|
|
RETURN;
|
|
END IF;
|
|
|
|
-- Verificar expiracion
|
|
IF v_key.expires_at IS NOT NULL AND v_key.expires_at < NOW() THEN
|
|
-- Marcar como expirada
|
|
UPDATE admin.api_keys SET status = 'expired' WHERE id = v_key.id;
|
|
RETURN QUERY SELECT
|
|
FALSE, NULL::UUID, NULL::UUID, NULL::UUID,
|
|
NULL::admin.api_key_type, NULL::JSONB, NULL::INTEGER,
|
|
'API key has expired'::TEXT;
|
|
RETURN;
|
|
END IF;
|
|
|
|
-- Verificar IP si hay restricciones
|
|
IF v_key.allowed_ips IS NOT NULL AND array_length(v_key.allowed_ips, 1) > 0 THEN
|
|
IF p_client_ip IS NULL OR NOT (p_client_ip = ANY(v_key.allowed_ips)) THEN
|
|
RETURN QUERY SELECT
|
|
FALSE, NULL::UUID, NULL::UUID, NULL::UUID,
|
|
NULL::admin.api_key_type, NULL::JSONB, NULL::INTEGER,
|
|
'IP address not allowed'::TEXT;
|
|
RETURN;
|
|
END IF;
|
|
END IF;
|
|
|
|
-- Verificar rate limit diario
|
|
v_rate_remaining := v_key.rate_limit_per_day - v_key.requests_today;
|
|
IF v_rate_remaining <= 0 THEN
|
|
RETURN QUERY SELECT
|
|
FALSE, v_key.id, v_key.user_id, v_key.tenant_id,
|
|
v_key.type, v_key.scopes, 0,
|
|
'Daily rate limit exceeded'::TEXT;
|
|
RETURN;
|
|
END IF;
|
|
|
|
-- Key valida - actualizar uso
|
|
UPDATE admin.api_keys
|
|
SET
|
|
last_used_at = NOW(),
|
|
last_used_ip = p_client_ip,
|
|
total_requests = total_requests + 1,
|
|
requests_today = requests_today + 1
|
|
WHERE id = v_key.id;
|
|
|
|
RETURN QUERY SELECT
|
|
TRUE, v_key.id, v_key.user_id, v_key.tenant_id,
|
|
v_key.type, v_key.scopes, v_rate_remaining - 1,
|
|
NULL::TEXT;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Funcion para revocar API key
|
|
CREATE OR REPLACE FUNCTION admin.revoke_api_key(
|
|
p_key_id UUID,
|
|
p_revoked_by UUID,
|
|
p_reason TEXT DEFAULT NULL
|
|
)
|
|
RETURNS BOOLEAN AS $$
|
|
BEGIN
|
|
UPDATE admin.api_keys
|
|
SET
|
|
status = 'revoked',
|
|
revoked_by = p_revoked_by,
|
|
revoked_at = NOW(),
|
|
revocation_reason = p_reason
|
|
WHERE id = p_key_id
|
|
AND status = 'active';
|
|
|
|
RETURN FOUND;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Funcion para rotar API key (genera nuevo secret, mantiene el anterior temporalmente)
|
|
CREATE OR REPLACE FUNCTION admin.rotate_api_key(
|
|
p_key_id UUID,
|
|
p_new_key_hash VARCHAR(255),
|
|
p_grace_period_hours INTEGER DEFAULT 24
|
|
)
|
|
RETURNS BOOLEAN AS $$
|
|
BEGIN
|
|
UPDATE admin.api_keys
|
|
SET
|
|
previous_key_hash = key_hash,
|
|
previous_key_valid_until = NOW() + (p_grace_period_hours || ' hours')::INTERVAL,
|
|
key_hash = p_new_key_hash,
|
|
rotated_at = NOW(),
|
|
rotation_count = rotation_count + 1
|
|
WHERE id = p_key_id
|
|
AND status = 'active';
|
|
|
|
RETURN FOUND;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Vista de API keys activas
|
|
CREATE OR REPLACE VIEW admin.v_active_api_keys AS
|
|
SELECT
|
|
ak.id,
|
|
ak.tenant_id,
|
|
ak.user_id,
|
|
u.email AS user_email,
|
|
ak.name,
|
|
ak.key_prefix,
|
|
ak.key_hint,
|
|
ak.type,
|
|
ak.scopes,
|
|
ak.rate_limit_per_day,
|
|
ak.requests_today,
|
|
ak.total_requests,
|
|
ak.last_used_at,
|
|
ak.expires_at,
|
|
ak.created_at
|
|
FROM admin.api_keys ak
|
|
JOIN users.users u ON ak.user_id = u.id
|
|
WHERE ak.status = 'active'
|
|
ORDER BY ak.last_used_at DESC NULLS LAST;
|
|
|
|
-- Vista de uso de API keys por dia
|
|
CREATE OR REPLACE VIEW admin.v_api_key_usage AS
|
|
SELECT
|
|
ak.id,
|
|
ak.name,
|
|
ak.key_prefix,
|
|
ak.type,
|
|
ak.total_requests,
|
|
ak.total_errors,
|
|
ak.requests_today,
|
|
CASE WHEN ak.rate_limit_per_day > 0
|
|
THEN (ak.requests_today::DECIMAL / ak.rate_limit_per_day * 100)::DECIMAL(5,2)
|
|
ELSE 0
|
|
END AS usage_percent,
|
|
ak.last_used_at,
|
|
ak.last_used_ip
|
|
FROM admin.api_keys ak
|
|
WHERE ak.status = 'active'
|
|
ORDER BY ak.total_requests DESC;
|
|
|
|
-- RLS Policy para multi-tenancy
|
|
ALTER TABLE admin.api_keys ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY api_keys_tenant_isolation ON admin.api_keys
|
|
FOR ALL
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
|
|
|
-- Los usuarios solo pueden ver sus propias API keys
|
|
CREATE POLICY api_keys_user_isolation ON admin.api_keys
|
|
FOR SELECT
|
|
USING (user_id = current_setting('app.current_user_id', true)::UUID);
|
|
|
|
-- Grants
|
|
GRANT SELECT, INSERT, UPDATE ON admin.api_keys TO trading_app;
|
|
GRANT SELECT ON admin.api_keys TO trading_readonly;
|
|
GRANT SELECT ON admin.v_active_api_keys TO trading_app;
|
|
GRANT SELECT ON admin.v_api_key_usage TO trading_app;
|
|
GRANT EXECUTE ON FUNCTION admin.validate_api_key TO trading_app;
|
|
GRANT EXECUTE ON FUNCTION admin.revoke_api_key TO trading_app;
|
|
GRANT EXECUTE ON FUNCTION admin.rotate_api_key TO trading_app;
|