yazrkisvs3vvvsv33svrhetrjsvyvsvvyjsvsvs
tavsuysvssvvjvvyjyjkvsvqd
qfogsvvsvsegjdgdfgdskhgdksvqsvshdqsd
<?php
// auth.php — manejo de sesión y autenticación
if (session_status() === PHP_SESSION_NONE) {
    // Aumentar duración de sesión para evitar expiración durante uploads
    ini_set('session.gc_maxlifetime', 7200);
    ini_set('session.cookie_lifetime', 7200);
    ini_set('session.save_path', sys_get_temp_dir());
    // FASE 6 FIX: cookie_secure si HTTPS detectado, SameSite=Strict (anti-CSRF)
    $is_https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
             || ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'
             || ($_SERVER['SERVER_PORT'] ?? '') == 443;
    session_start([
        'cookie_httponly' => true,
        'cookie_secure'   => $is_https,
        'cookie_samesite' => 'Strict',
        'use_strict_mode' => true,
    ]);
}
require_once __DIR__ . '/db.php';
// ——— helpers ———
function dm_logged_in(): bool {
    return !empty($_SESSION['dm_user_id']);
}
function dm_is_admin(): bool {
    return ($_SESSION['dm_role'] ?? '') === 'admin';
}
function dm_user(): array {
    $color = $_SESSION['dm_color'] ?? '#E8272A';
    if (!preg_match('/^#[0-9a-fA-F]{6}$/', $color)) $color = '#E8272A';
    return [
        'id'    => $_SESSION['dm_user_id']   ?? 0,
        'name'  => $_SESSION['dm_name']      ?? '',
        'email' => $_SESSION['dm_email']     ?? '',
        'role'  => $_SESSION['dm_role']      ?? '',
        'color' => $color,
    ];
}
function dm_is_api_request(): bool {
    // Si ya se envió Content-Type: application/json, es un endpoint de API
    foreach (headers_list() as $h) {
        if (stripos($h, 'Content-Type: application/json') === 0) return true;
    }
    return false;
}
function dm_require_login(string $redirect = 'login.php'): void {
    if (!dm_logged_in()) {
        if (dm_is_api_request()) {
            echo json_encode(['ok' => false, 'error' => 'No autenticado']);
            exit;
        }
        header('Location: ' . $redirect);
        exit;
    }
    // Bloquear cuentas canceladas/suspendidas (skip para admins)
    if (!dm_is_admin()) {
        dm_check_subscription();
    }
    // Liberar lock de sesión en contextos API para no bloquear peticiones paralelas
    if (dm_is_api_request()) {
        session_write_close();
    }
}
function dm_require_admin(string $redirect = 'index.php'): void {
    dm_require_login();
    if (!dm_is_admin()) {
        if (dm_is_api_request()) {
            echo json_encode(['ok' => false, 'error' => 'Sin permisos']);
            exit;
        }
        header('Location: ' . $redirect);
        exit;
    }
}
function dm_login(string $email, string $password): array {
    global $pdo;
    if (!$pdo) return ['ok' => false, 'error' => 'Sin conexión a base de datos. Configura primero en setup.php.'];
    // Rate limiting: máx 5 intentos por IP en 15 minutos
    $ip = $_SERVER['REMOTE_ADDR'] ?? '';
    try {
        $attempts = $pdo->prepare("SELECT COUNT(*) FROM dm_activity WHERE action='login_fail' AND ip=? AND created_at > DATE_SUB(NOW(), INTERVAL 15 MINUTE)");
        $attempts->execute([$ip]);
        if ((int)$attempts->fetchColumn() >= 5) {
            return ['ok' => false, 'error' => 'Demasiados intentos fallidos. Espera 15 minutos.'];
        }
    } catch (Exception $e) {}
    $stmt = $pdo->prepare('SELECT * FROM dm_users WHERE email = ? AND active = 1 LIMIT 1');
    $stmt->execute([strtolower(trim($email))]);
    $user = $stmt->fetch();
    if (!$user || !password_verify($password, $user['password'])) {
        dm_log(0, null, 'login_fail', 'Intento fallido: ' . strtolower(trim($email)));
        return ['ok' => false, 'error' => 'Email o contraseña incorrectos.'];
    }
    session_regenerate_id(true);
    $_SESSION['dm_user_id'] = $user['id'];
    $_SESSION['dm_name']    = $user['name'];
    $_SESSION['dm_email']   = $user['email'];
    $_SESSION['dm_role']    = $user['role'];
    $_SESSION['dm_color']   = $user['avatar_color'];
    $pdo->prepare('UPDATE dm_users SET last_login = NOW() WHERE id = ?')->execute([$user['id']]);
    dm_log($user['id'], null, 'login', 'Inicio de sesión');
    return ['ok' => true, 'role' => $user['role']];
}
function dm_logout(): void {
    if (dm_logged_in()) dm_log($_SESSION['dm_user_id'], null, 'logout', 'Cierre de sesión');
    session_destroy();
    header('Location: login.php');
    exit;
}
function dm_csrf_token(): string {
    if (session_status() === PHP_SESSION_NONE) session_start();
    if (empty($_SESSION['dm_csrf'])) {
        $_SESSION['dm_csrf'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['dm_csrf'];
}
function dm_csrf_verify(): void {
    $token = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ($_POST['csrf_token'] ?? '');
    $stored = $_SESSION['dm_csrf'] ?? '';
    if (!$stored || !hash_equals($stored, $token)) {
        http_response_code(403);
        echo json_encode(['ok' => false, 'error' => 'Token de seguridad inválido']);
        exit;
    }
}
function dm_log(int $user_id, ?int $client_id, string $action, string $detail = ''): void {
    global $pdo;
    if (!$pdo) return;
    try {
        $ip = $_SERVER['REMOTE_ADDR'] ?? '';
        $pdo->prepare('INSERT INTO dm_activity (user_id,client_id,action,detail,ip) VALUES (?,?,?,?,?)')
            ->execute([$user_id, $client_id, $action, $detail, $ip]);
    } catch (Exception $e) { /* silencioso */ }
}
function dm_activity(int $client_id, string $action, string $detail = ''): void {
    $user = dm_user();
    dm_log($user['id'] ?? 0, $client_id, $action, $detail);
}
// Verifica que el usuario autenticado tenga acceso al cliente. Termina con 403 si no.
function dm_require_client_access(int $client_id): void {
    global $pdo;
    if (!$client_id) {
        http_response_code(400);
        echo json_encode(['ok' => false, 'error' => 'client_id inválido']);
        exit;
    }
    $user = dm_user();
    if ($user['role'] === 'admin') return;
    if (!$pdo) {
        http_response_code(500);
        echo json_encode(['ok' => false, 'error' => 'Sin conexión a BD']);
        exit;
    }
    $stmt = $pdo->prepare('SELECT 1 FROM dm_user_clients WHERE user_id = ? AND client_id = ?');
    $stmt->execute([$user['id'], $client_id]);
    if (!$stmt->fetch()) {
        http_response_code(403);
        echo json_encode(['ok' => false, 'error' => 'Sin acceso a este cliente']);
        exit;
    }
}
function dm_get_clients(): array {
    global $pdo;
    if (!$pdo) return [];
    $user = dm_user();
    if ($user['role'] === 'admin') {
        return $pdo->query('SELECT * FROM dm_clients WHERE active=1 ORDER BY name')->fetchAll();
    }
    $stmt = $pdo->prepare(
        'SELECT c.* FROM dm_clients c
         JOIN dm_user_clients uc ON uc.client_id = c.id
         WHERE uc.user_id = ? AND c.active = 1
         ORDER BY c.name'
    );
    $stmt->execute([$user['id']]);
    return $stmt->fetchAll();
}
// ══════════════════════════════════════════════════════════════
// SaaS — Subscription middleware
// ══════════════════════════════════════════════════════════════

/**
 * Retorna la suscripción del usuario actual o null si no tiene.
 */
function dm_get_subscription(?int $uid = null): ?array {
    global $pdo;
    if (!$pdo) return null;
    $uid = $uid ?? (int)(dm_user()['id'] ?? 0);
    if (!$uid) return null;
    $s = $pdo->prepare("SELECT * FROM dm_subscriptions WHERE user_id=?");
    $s->execute([$uid]);
    $sub = $s->fetch();
    if (!$sub) return null;
    $sub['addons']         = json_decode($sub['addons'] ?? '{}', true) ?: [];
    $sub['modules_chosen'] = json_decode($sub['modules_chosen'] ?? '[]', true) ?: [];
    $limits = match($sub['plan'] ?? 'starter') {
        'pro'        => ['clients'=>5,  'proposals'=>12, 'users'=>3,  'modules'=>99],
        'agency'     => ['clients'=>7,  'proposals'=>25, 'users'=>8,  'modules'=>99],
        'enterprise' => ['clients'=>15, 'proposals'=>50, 'users'=>20, 'modules'=>99],
        default      => ['clients'=>1,  'proposals'=>3,  'users'=>1,  'modules'=>3],
    };
    $sub['max_clients']          = $limits['clients'];
    $sub['proposals_limit_plan'] = $limits['proposals'];
    return $sub;
}

/**
 * Verifica que la suscripción esté activa (trial o active).
 * En past_due → modo lectura (read_only=true en response).
 * En blocked/canceled → bloqueo total.
 * Si no hay dm_subscriptions → se asume cuenta legacy/interna, pasa libre.
 */
function dm_check_subscription(bool $write_action = false): void {
    global $pdo;
    if (!$pdo) return;
    // Si la tabla aún no existe, dejar pasar (compatibilidad pre-SaaS)
    try { $pdo->query("SELECT 1 FROM dm_subscriptions LIMIT 1"); }
    catch (Throwable $e) { return; }

    $uid = (int)(dm_user()['id'] ?? 0);
    if (!$uid) return;

    $sub = dm_get_subscription($uid);
    if (!$sub) return; // cuenta interna sin registro → libre

    $status = $sub['status'] ?? 'active';

    if ($status === 'blocked' || $status === 'canceled') {
        if (dm_is_api_request()) {
            http_response_code(402);
            echo json_encode(['ok' => false, 'error' => 'Cuenta suspendida. Elige un plan en supercerebro/home/', 'suspended' => true]);
            exit;
        }
        header('Location: ../home/#pricing');
        exit;
    }

    if ($status === 'past_due' && $write_action) {
        http_response_code(402);
        echo json_encode(['ok' => false, 'error' => 'Pago pendiente. Actualiza tu método de pago.', 'past_due' => true]);
        exit;
    }
}

/**
 * Verifica que el usuario no haya excedido su límite de propuestas.
 * Llamar antes de procesar una generación en proposals.php.
 */
function dm_check_proposal_limit(): void {
    global $pdo;
    if (!$pdo) return;
    $uid = (int)(dm_user()['id'] ?? 0);
    if (!$uid) return;
    $sub = dm_get_subscription($uid);
    if (!$sub) return;
    $limit = (int)$sub['proposals_limit'];
    if ($limit === 0) {
        // Starter sin add-on — propuestas bloqueadas
        http_response_code(402);
        echo json_encode(['ok' => false, 'error' => 'Tu plan no incluye propuestas. Activa el add-on Sitios Web.', 'upgrade_required' => true]);
        exit;
    }
    if ((int)$sub['proposals_used'] >= $limit) {
        http_response_code(402);
        echo json_encode(['ok' => false, 'error' => "Alcanzaste tu límite de {$limit} propuestas este mes. Puedes comprar propuestas adicionales por \$3 c/u.", 'limit_reached' => true, 'used' => $sub['proposals_used'], 'limit' => $limit]);
        exit;
    }
}

/**
 * Incrementa el contador de propuestas usadas del usuario.
 */
function dm_increment_proposals(int $uid): void {
    global $pdo;
    if (!$pdo) return;
    $pdo->prepare("UPDATE dm_subscriptions SET proposals_used = proposals_used + 1 WHERE user_id=?")->execute([$uid]);
}

/**
 * Verifica si el módulo solicitado está disponible para el usuario.
 * Solo aplica a plan Starter (modules_chosen).
 */
function dm_check_module_access(string $module): void {
    global $pdo;
    if (!$pdo) return;
    $uid = (int)(dm_user()['id'] ?? 0);
    if (!$uid) return;
    $sub = dm_get_subscription($uid);
    if (!$sub) return;
    if ($sub['plan'] !== 'starter') return; // Pro/Agency/Enterprise tienen todo
    $addons = is_array($sub['addons']) ? $sub['addons'] : json_decode($sub['addons'] ?? '{}', true);
    $max_modules = !empty($addons['modules_extra']) ? 8 : 3;
    $chosen = $sub['modules_chosen'];
    if (!empty($chosen) && !in_array($module, $chosen)) {
        if (count($chosen) < $max_modules) return; // tiene slots disponibles
        http_response_code(403);
        $msg = !empty($addons['modules_extra'])
            ? "El módulo '{$module}' no está en tus 8 módulos activos. Cámbialo al inicio del próximo ciclo."
            : "El módulo '{$module}' no está en tus 3 módulos Starter. Agrega 5 módulos extra por $25/mes o cambia al inicio del próximo ciclo.";
        echo json_encode(['ok' => false, 'error' => $msg, 'module_locked' => true, 'can_buy_modules' => empty($addons['modules_extra'])]);
        exit;
    }
}

/**
 * Verifica que el usuario no haya superado su límite de clientes activos.
 * Llamar antes de crear un cliente nuevo.
 */
function dm_check_client_limit(): void {
    global $pdo;
    if (!$pdo) return;
    $uid = (int)(dm_user()['id'] ?? 0);
    if (!$uid) return;
    $sub = dm_get_subscription($uid);
    if (!$sub) return;
    $max = $sub['max_clients'] ?? 1;  // from dm_get_subscription() plan limits
    $cs = $pdo->prepare("SELECT COUNT(*) FROM dm_clients c JOIN dm_user_clients uc ON uc.client_id=c.id WHERE uc.user_id=? AND c.active=1");
    $cs->execute([$uid]);
    $count = (int)$cs->fetchColumn();
    if ($count >= $max) {
        http_response_code(402);
        echo json_encode(['ok' => false, 'error' => "Alcanzaste el límite de {$max} clientes de tu plan. Actualiza tu plan para agregar más.", 'client_limit' => true]);
        exit;
    }
}

/**
 * Verifica que una URL no apunte a rangos privados/reservados (SSRF guard).
 * Bloquea loopback, RFC-1918, link-local (169.254.x.x/AWS metadata), y esquemas no-HTTP.
 */
function dm_is_safe_url(string $url): bool {
    if (!filter_var($url, FILTER_VALIDATE_URL)) return false;
    $scheme = strtolower(parse_url($url, PHP_URL_SCHEME) ?? '');
    if (!in_array($scheme, ['http', 'https'], true)) return false;
    $host = parse_url($url, PHP_URL_HOST);
    if (!$host) return false;
    // Reject raw IPs in private/reserved ranges immediately
    if (filter_var($host, FILTER_VALIDATE_IP)) {
        if (!filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
            return false;
        }
    }
    // Resolve hostname and check resolved IP
    $ip = gethostbyname($host);
    if ($ip === $host && !filter_var($host, FILTER_VALIDATE_IP)) return false; // DNS failure
    $blocked = '/^(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|169\.254\.|0\.|::1$|localhost)/i';
    return !preg_match($blocked, $ip);
}
