Files
AutoBooking/docs/superpowers/plans/2026-05-30-security-implementation.md

30 KiB

AutoBooking Security — Plan de Implementación

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Implementar seguridad anti-spam, anti-ghost-registration, rate limiting, IP geolocation backup, documentos de conductor, y eliminar la API key de Google Maps del código fuente.

Architecture: Plugin nuevo autobooking-security centraliza rate limiting e IP blocking. Plugins existentes autobooking-admin-dashboard y autobooking-geo-restrict se modifican quirúrgicamente. Snippets #14, #15, #18, #19 NO se tocan.

Tech Stack: PHP 8.x, WordPress REST API, MariaDB, hCaptcha (hcaptcha.com), ip-api.com, WordPress transients.

Servidor: pi@ssh.famromdon.online:2230 pw Geronimo6&8 | Container: autobookingonline-wordpress-autobooking-1 | Plugins: /var/www/html/wp-content/plugins/ | Local: D:\Proyectos Software\AutoBooking\

Deploy estándar (usar en cada tarea):

$psftp = "C:\Program Files\PuTTY\psftp.exe"
$plink = "C:\Program Files\PuTTY\plink.exe"
$cmds = "$env:TEMP\sftp_cmd.txt"
# 1. subir archivo local → /tmp/archivo.php
[System.IO.File]::WriteAllText($cmds, "put `"D:\Proyectos Software\AutoBooking\ARCHIVO.php`" /tmp/ARCHIVO.php`nbye", (New-Object System.Text.UTF8Encoding $false))
& $psftp -pw "Geronimo6&8" -P 2230 -b $cmds pi@ssh.famromdon.online
# 2. mover al container
echo "y" | & $plink -pw "Geronimo6&8" -P 2230 pi@ssh.famromdon.online 'docker cp /tmp/ARCHIVO.php autobookingonline-wordpress-autobooking-1:/var/www/html/wp-content/plugins/PLUGIN/ARCHIVO.php'

Prerequisito manual (ANTES de empezar)

  • Ir a hcaptcha.com → crear cuenta gratuita → crear sitio autobooking.online → copiar Site Key y Secret Key
  • Google Cloud Console → Credenciales → key AIzaSyD3VSlYZvDEbbSKUhEFRdUD5rU1JWXX03Q → agregar restricción HTTP referrer: *.autobooking.online/*

Tarea 1: Plugin autobooking-security — Tablas DB + Rate Limiting

Archivos: Crear autobooking-security.php → subir a /var/www/html/wp-content/plugins/autobooking-security/autobooking-security.php

  • 1.1 Crear el archivo del plugin en D:\Proyectos Software\AutoBooking\autobooking-security.php:
<?php
/**
 * Plugin Name: AutoBooking Security
 * Description: Rate limiting, IP blocking, hCaptcha, email confirmation, driver documents.
 * Version:     1.0.0
 * Author:      AutoBooking
 */
if ( ! defined( 'ABSPATH' ) ) exit;

/* ── ACTIVACIÓN: crea tablas ──────────────────────────────── */
register_activation_hook( __FILE__, 'abs_activate' );
function abs_activate() {
    global $wpdb; $charset = $wpdb->get_charset_collate();
    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta( "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}ab_rate_limits (
        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        ip VARCHAR(45) NOT NULL DEFAULT '',
        endpoint VARCHAR(80) NOT NULL DEFAULT '',
        hits INT UNSIGNED NOT NULL DEFAULT 1,
        window_start DATETIME NOT NULL,
        PRIMARY KEY (id), KEY ip_ep_win (ip, endpoint, window_start)
    ) $charset;" );
    dbDelta( "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}ab_blocked_ips (
        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        ip VARCHAR(45) NOT NULL DEFAULT '',
        reason VARCHAR(120) NOT NULL DEFAULT '',
        blocked_at DATETIME NOT NULL,
        expires_at DATETIME NOT NULL,
        PRIMARY KEY (id), UNIQUE KEY ip (ip), KEY ip_exp (ip, expires_at)
    ) $charset;" );
    dbDelta( "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}ab_driver_documents (
        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        driver_id BIGINT UNSIGNED NOT NULL,
        doc_type ENUM('license','insurance','vehicle_photo') NOT NULL,
        file_url VARCHAR(512) NOT NULL DEFAULT '',
        expiry_date DATE NULL,
        status ENUM('pending','approved','rejected') NOT NULL DEFAULT 'pending',
        reviewed_by BIGINT UNSIGNED NULL,
        reviewed_at DATETIME NULL,
        created_at DATETIME NOT NULL,
        PRIMARY KEY (id), KEY driver_doc (driver_id, doc_type), KEY status (status)
    ) $charset;" );
}

/* ── HELPERS ──────────────────────────────────────────────── */
function abs_get_ip() {
    $ip = '';
    if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
        $parts = explode( ',', $_SERVER['HTTP_X_FORWARDED_FOR'] );
        $ip = trim( $parts[0] );
    }
    if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
        $ip = $_SERVER['REMOTE_ADDR'] ?? '';
    }
    return sanitize_text_field( $ip );
}

function abs_is_whitelisted( $ip ) {
    $raw = get_option( 'ab_sec_whitelist', '' );
    if ( ! $raw ) return false;
    return in_array( $ip, array_map( 'trim', explode( "\n", $raw ) ), true );
}

function abs_is_blocked( $ip ) {
    global $wpdb;
    return (bool) $wpdb->get_var( $wpdb->prepare(
        "SELECT id FROM {$wpdb->prefix}ab_blocked_ips WHERE ip = %s AND expires_at > %s",
        $ip, current_time( 'mysql' )
    ) );
}

function abs_block_ip( $ip, $reason, $hours ) {
    global $wpdb;
    $wpdb->query( $wpdb->prepare(
        "INSERT INTO {$wpdb->prefix}ab_blocked_ips (ip, reason, blocked_at, expires_at) VALUES (%s,%s,%s,%s)
         ON DUPLICATE KEY UPDATE reason=VALUES(reason), blocked_at=VALUES(blocked_at), expires_at=VALUES(expires_at)",
        $ip, $reason, current_time('mysql'), date('Y-m-d H:i:s', time() + $hours * 3600)
    ) );
}

function abs_check_rate( $ip, $endpoint, $limit, $window_sec, $block_hours = 0 ) {
    if ( ! $ip || abs_is_whitelisted( $ip ) ) return true;
    if ( abs_is_blocked( $ip ) ) return false;
    global $wpdb; $table = $wpdb->prefix . 'ab_rate_limits';
    $window = date( 'Y-m-d H:i:s', time() - $window_sec );
    $hits = (int) $wpdb->get_var( $wpdb->prepare(
        "SELECT SUM(hits) FROM $table WHERE ip=%s AND endpoint=%s AND window_start>=%s",
        $ip, $endpoint, $window
    ) );
    if ( $hits >= $limit ) {
        if ( $block_hours > 0 ) abs_block_ip( $ip, "Rate limit: $endpoint", $block_hours );
        return false;
    }
    $minute = date( 'Y-m-d H:i:00' );
    $exists = $wpdb->get_var( $wpdb->prepare(
        "SELECT id FROM $table WHERE ip=%s AND endpoint=%s AND window_start=%s",
        $ip, $endpoint, $minute
    ) );
    if ( $exists ) {
        $wpdb->query( $wpdb->prepare(
            "UPDATE $table SET hits=hits+1 WHERE ip=%s AND endpoint=%s AND window_start=%s",
            $ip, $endpoint, $minute
        ) );
    } else {
        $wpdb->insert( $table, ['ip'=>$ip,'endpoint'=>$endpoint,'hits'=>1,'window_start'=>$minute] );
    }
    return true;
}

/* ── RATE LIMITING: login ─────────────────────────────────── */
add_action( 'wp_login_failed', function( $u ) {
    abs_check_rate( abs_get_ip(), 'login', 10, 900, 2 );
} );
add_action( 'login_init', function() {
    if ( abs_is_blocked( abs_get_ip() ) )
        wp_die( 'Tu IP fue bloqueada temporalmente por demasiados intentos fallidos.', 'Bloqueado', ['response'=>429] );
} );

/* ── RATE LIMITING: registro ──────────────────────────────── */
add_filter( 'registration_errors', function( $errors, $login, $email ) {
    if ( ! abs_check_rate( abs_get_ip(), 'register', 5, 3600, 24 ) )
        $errors->add( 'rate_limit', '<strong>Error:</strong> Demasiados intentos desde tu IP. Intenta más tarde.' );
    return $errors;
}, 10, 3 );

/* ── RATE LIMITING: REST ──────────────────────────────────── */
add_filter( 'rest_dispatch_request', function( $result, $request, $route, $handler ) {
    if ( strpos( $route, '/autobooking/v1/' ) === false ) return $result;
    if ( $request->get_method() === 'GET' ) return $result;
    if ( ! abs_check_rate( abs_get_ip(), 'rest_post', 120, 60, 0 ) )
        return new WP_Error( 'rate_limited', 'Demasiadas solicitudes.', ['status'=>429] );
    return $result;
}, 10, 4 );

/* ── LIMPIEZA DIARIA ──────────────────────────────────────── */
add_action( 'abs_daily_cleanup', function() {
    global $wpdb;
    $wpdb->query("DELETE FROM {$wpdb->prefix}ab_rate_limits WHERE window_start < DATE_SUB(NOW(), INTERVAL 2 DAY)");
    $wpdb->query("DELETE FROM {$wpdb->prefix}ab_blocked_ips WHERE expires_at < NOW()");
} );
if ( ! wp_next_scheduled('abs_daily_cleanup') ) wp_schedule_event( time(), 'daily', 'abs_daily_cleanup' );

/* ── HCAPTCHA ─────────────────────────────────────────────── */
add_action( 'register_form', function() {
    $key = get_option('ab_hcaptcha_site_key','');
    if ( ! $key ) return;
    echo '<div class="h-captcha" data-sitekey="'.esc_attr($key).'" style="margin:12px 0;"></div>';
    echo '<script src="https://js.hcaptcha.com/1/api.js" async defer></script>';
} );
add_filter( 'registration_errors', function( $errors, $login, $email ) {
    $secret = get_option('ab_hcaptcha_secret','');
    if ( ! $secret ) return $errors;
    $token = sanitize_text_field( $_POST['h-captcha-response'] ?? '' );
    if ( ! $token ) { $errors->add('captcha_missing','<strong>Error:</strong> Completa el captcha.'); return $errors; }
    $res = wp_remote_post('https://api.hcaptcha.com/siteverify', ['body'=>['secret'=>$secret,'response'=>$token,'remoteip'=>abs_get_ip()],'timeout'=>10]);
    if ( ! is_wp_error($res) ) {
        $body = json_decode(wp_remote_retrieve_body($res), true);
        if ( empty($body['success']) ) $errors->add('captcha_invalid','<strong>Error:</strong> Captcha inválido.');
    }
    return $errors;
}, 20, 3 );

/* ── EMAIL CONFIRMATION ───────────────────────────────────── */
add_action( 'user_register', function( $uid ) {
    $user = get_userdata($uid);
    if ( ! $user || ! in_array('driver_pending',(array)$user->roles,true) ) return;
    $token = bin2hex(random_bytes(32));
    update_user_meta($uid,'ab_email_token',$token);
    update_user_meta($uid,'ab_email_token_expires',date('Y-m-d H:i:s',time()+86400));
    update_user_meta($uid,'ab_email_confirmed',0);
    $url = add_query_arg('token',$token,get_rest_url(null,'autobooking/v1/confirm-email'));
    wp_mail($user->user_email,'Confirma tu email — AutoBooking',
        "Hola {$user->display_name},\n\nConfirma tu email (válido 24h):\n\n$url\n\nAutoBooking");
} );
add_action( 'template_redirect', function() {
    if ( ! is_page('driver-dashboard') && ! is_page('pending-driver') ) return;
    if ( ! is_user_logged_in() ) return;
    $user = wp_get_current_user(); $roles = (array)$user->roles;
    if ( ! in_array('driver_pending',$roles,true) && ! in_array('driver',$roles,true) ) return;
    if ( (int)get_user_meta($user->ID,'ab_email_confirmed',true) ) return;
    $resend = esc_url(get_rest_url(null,'autobooking/v1/resend-confirmation'));
    wp_die('<div style="font-family:system-ui;max-width:500px;margin:60px auto;text-align:center;padding:32px;background:#111;color:#fff;border-radius:16px;">'
        .'<p style="font-size:20px;font-weight:700;color:#FF6F00;">Confirma tu email</p>'
        .'<p style="color:rgba(255,255,255,.7);">Revisa tu bandeja de entrada y haz clic en el enlace de confirmación.</p>'
        .'<form method="post" action="'.$resend.'">'.wp_nonce_field('wp_rest','_wpnonce',true,false)
        .'<button type="submit" style="margin-top:16px;background:#FF6F00;color:#fff;border:none;padding:12px 28px;border-radius:10px;font-weight:700;cursor:pointer;">Reenviar email</button>'
        .'</form></div>','Confirma tu email',['response'=>403]);
} );

/* ── REST: confirm-email, resend, docs, admin IPs ─────────── */
add_action( 'rest_api_init', function() {

    register_rest_route('autobooking/v1','/confirm-email',[
        'methods'=>'GET','permission_callback'=>'__return_true',
        'callback'=>function(WP_REST_Request $req){
            global $wpdb;
            $token = sanitize_text_field($req->get_param('token'));
            if(!$token){wp_redirect(home_url('/?ab_confirm=invalid'));exit;}
            $uid = $wpdb->get_var($wpdb->prepare(
                "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key='ab_email_token' AND meta_value=%s LIMIT 1",$token));
            if(!$uid){wp_redirect(home_url('/?ab_confirm=invalid'));exit;}
            if(strtotime(get_user_meta((int)$uid,'ab_email_token_expires',true))<time()){
                wp_redirect(home_url('/?ab_confirm=expired'));exit;}
            update_user_meta((int)$uid,'ab_email_confirmed',1);
            delete_user_meta((int)$uid,'ab_email_token');
            delete_user_meta((int)$uid,'ab_email_token_expires');
            wp_redirect(home_url('/pending-driver/?ab_confirm=ok'));exit;
        },
    ]);

    register_rest_route('autobooking/v1','/resend-confirmation',[
        'methods'=>'POST','permission_callback'=>'is_user_logged_in',
        'callback'=>function(){
            $user=wp_get_current_user(); $token=bin2hex(random_bytes(32));
            update_user_meta($user->ID,'ab_email_token',$token);
            update_user_meta($user->ID,'ab_email_token_expires',date('Y-m-d H:i:s',time()+86400));
            $url=add_query_arg('token',$token,get_rest_url(null,'autobooking/v1/confirm-email'));
            wp_mail($user->user_email,'Confirma tu email — AutoBooking',"Nuevo enlace (válido 24h):\n\n$url");
            wp_redirect(home_url('/pending-driver/?ab_confirm=resent'));exit;
        },
    ]);

    register_rest_route('autobooking/v1','/driver/upload-doc',[
        'methods'=>'POST','permission_callback'=>'is_user_logged_in',
        'callback'=>function(WP_REST_Request $req){
            global $wpdb; $user=wp_get_current_user();
            $doc_type=sanitize_text_field($req->get_param('doc_type'));
            if(!in_array($doc_type,['license','insurance','vehicle_photo'],true))
                return new WP_Error('invalid_type','Tipo inválido.',['status'=>400]);
            if(empty($_FILES['file'])) return new WP_Error('no_file','Sin archivo.',['status'=>400]);
            $file=$_FILES['file'];
            if(!in_array($file['type'],['image/jpeg','image/png','application/pdf'],true))
                return new WP_Error('invalid_mime','Solo JPG, PNG o PDF.',['status'=>400]);
            if($file['size']>5*1024*1024) return new WP_Error('file_too_large','Máx 5 MB.',['status'=>400]);
            require_once ABSPATH.'wp-admin/includes/file.php';
            $up=wp_upload_dir(); $dir=$up['basedir'].'/ab-driver-docs/'.$user->ID;
            wp_mkdir_p($dir);
            $ext=pathinfo($file['name'],PATHINFO_EXTENSION);
            $fname=$doc_type.'_'.time().'.'.$ext;
            if(!move_uploaded_file($file['tmp_name'],$dir.'/'.$fname))
                return new WP_Error('upload_failed','Error al guardar.',['status'=>500]);
            $url=$up['baseurl'].'/ab-driver-docs/'.$user->ID.'/'.$fname;
            $data=['driver_id'=>$user->ID,'doc_type'=>$doc_type,'file_url'=>$url,'status'=>'pending','created_at'=>current_time('mysql')];
            $exists=$wpdb->get_var($wpdb->prepare(
                "SELECT id FROM {$wpdb->prefix}ab_driver_documents WHERE driver_id=%d AND doc_type=%s",$user->ID,$doc_type));
            if($exists) $wpdb->update("{$wpdb->prefix}ab_driver_documents",$data,['driver_id'=>$user->ID,'doc_type'=>$doc_type]);
            else $wpdb->insert("{$wpdb->prefix}ab_driver_documents",$data);
            return rest_ensure_response(['ok'=>true,'file_url'=>$url]);
        },
    ]);

    register_rest_route('autobooking/v1','/driver/my-docs',[
        'methods'=>'GET','permission_callback'=>'is_user_logged_in',
        'callback'=>function(){
            global $wpdb; $uid=get_current_user_id();
            return rest_ensure_response(['docs'=>$wpdb->get_results($wpdb->prepare(
                "SELECT doc_type,file_url,status,expiry_date FROM {$wpdb->prefix}ab_driver_documents WHERE driver_id=%d",$uid),ARRAY_A)?:[]]);
        },
    ]);

    register_rest_route('autobooking/v1','/admin/driver/(?P<id>\d+)/docs',[
        'methods'=>'GET','permission_callback'=>function(){return current_user_can('manage_autobooking');},
        'callback'=>function(WP_REST_Request $req){
            global $wpdb; $id=absint($req['id']);
            return rest_ensure_response(['docs'=>$wpdb->get_results($wpdb->prepare(
                "SELECT * FROM {$wpdb->prefix}ab_driver_documents WHERE driver_id=%d",$id),ARRAY_A)?:[]]);
        },
    ]);

    register_rest_route('autobooking/v1','/admin/doc/(?P<id>\d+)/review',[
        'methods'=>'POST','permission_callback'=>function(){return current_user_can('manage_autobooking');},
        'callback'=>function(WP_REST_Request $req){
            global $wpdb; $id=absint($req['id']);
            $status=in_array($req->get_param('status'),['approved','rejected'],true)?$req->get_param('status'):'pending';
            $wpdb->update("{$wpdb->prefix}ab_driver_documents",
                ['status'=>$status,'reviewed_by'=>get_current_user_id(),'reviewed_at'=>current_time('mysql')],['id'=>$id]);
            return rest_ensure_response(['ok'=>true]);
        },
    ]);

    register_rest_route('autobooking/v1','/admin/blocked-ips',[
        'methods'=>'GET','permission_callback'=>function(){return current_user_can('manage_autobooking');},
        'callback'=>function(){
            global $wpdb;
            return rest_ensure_response([
                'blocked'=>$wpdb->get_results("SELECT id,ip,reason,blocked_at,expires_at FROM {$wpdb->prefix}ab_blocked_ips WHERE expires_at>NOW() ORDER BY blocked_at DESC LIMIT 100",ARRAY_A)?:[],
                'blocked_24h'=>(int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}ab_blocked_ips WHERE blocked_at>=DATE_SUB(NOW(),INTERVAL 24 HOUR)"),
            ]);
        },
    ]);

    register_rest_route('autobooking/v1','/admin/blocked-ips/(?P<id>\d+)/unblock',[
        'methods'=>'POST','permission_callback'=>function(){return current_user_can('manage_autobooking');},
        'callback'=>function(WP_REST_Request $req){
            global $wpdb; $wpdb->delete("{$wpdb->prefix}ab_blocked_ips",['id'=>absint($req['id'])]);
            return rest_ensure_response(['ok'=>true]);
        },
    ]);

    register_rest_route('autobooking/v1','/admin/security-settings/save',[
        'methods'=>'POST','permission_callback'=>function(){return current_user_can('manage_autobooking');},
        'callback'=>function(WP_REST_Request $req){
            $body=$req->get_json_params();
            if(isset($body['whitelist'])) update_option('ab_sec_whitelist',sanitize_textarea_field($body['whitelist']));
            if(isset($body['hcaptcha_site_key'])) update_option('ab_hcaptcha_site_key',sanitize_text_field($body['hcaptcha_site_key']));
            if(isset($body['hcaptcha_secret'])) update_option('ab_hcaptcha_secret',sanitize_text_field($body['hcaptcha_secret']));
            return rest_ensure_response(['ok'=>true]);
        },
    ]);
} );
  • 1.2 Subir y activar el plugin
$psftp = "C:\Program Files\PuTTY\psftp.exe"; $plink = "C:\Program Files\PuTTY\plink.exe"
$cmds = "$env:TEMP\sftp1.txt"
[System.IO.File]::WriteAllText($cmds, "put `"D:\Proyectos Software\AutoBooking\autobooking-security.php`" /tmp/abs.php`nbye", (New-Object System.Text.UTF8Encoding $false))
& $psftp -pw "Geronimo6&8" -P 2230 -b $cmds pi@ssh.famromdon.online
echo "y" | & $plink -pw "Geronimo6&8" -P 2230 pi@ssh.famromdon.online 'mkdir -p /tmp/abs_plugin && cp /tmp/abs.php /tmp/abs_plugin/autobooking-security.php && docker exec autobookingonline-wordpress-autobooking-1 mkdir -p /var/www/html/wp-content/plugins/autobooking-security && docker cp /tmp/abs_plugin/autobooking-security.php autobookingonline-wordpress-autobooking-1:/var/www/html/wp-content/plugins/autobooking-security/autobooking-security.php'
  • 1.3 Activar desde WP Adminhttps://autobooking.online/wp-admin/plugins.php → Activar "AutoBooking Security"

  • 1.4 Verificar tablas creadas

echo "y" | & $plink -pw "Geronimo6&8" -P 2230 pi@ssh.famromdon.online 'docker exec autobookingonline-wordpress-autobooking-1 php -r "require \"/var/www/html/wp-load.php\"; global \$wpdb; foreach([\"ab_rate_limits\",\"ab_blocked_ips\",\"ab_driver_documents\"] as \$t){ echo \$wpdb->get_var(\"SHOW TABLES LIKE \\\"{$wpdb->prefix}\$t\\\"\") ? \"OK \$t\n\" : \"MISSING \$t\n\"; }"'

Esperado: OK ab_rate_limits, OK ab_blocked_ips, OK ab_driver_documents

  • 1.5 Commit
git add autobooking-security.php && git commit -m "feat: autobooking-security plugin — rate limiting, hCaptcha, email confirmation, driver docs"

Tarea 2: Eliminar GMAPS_KEY del código fuente

Archivos: Modificar autobooking-admin-dashboard.php (local)

  • 2.1 Backup en servidor
echo "y" | & $plink -pw "Geronimo6&8" -P 2230 pi@ssh.famromdon.online 'docker exec autobookingonline-wordpress-autobooking-1 cp /var/www/html/wp-content/plugins/autobooking-admin-dashboard/autobooking-admin-dashboard.php /var/www/html/wp-content/plugins/autobooking-admin-dashboard/autobooking-admin-dashboard.php.bak'
  • 2.2 En el archivo local, hacer 3 cambios quirúrgicos:

Cambio A — línea 17, reemplazar la constante:

// ANTES:
const GMAPS_KEY = 'AIzaSyD3VSlYZvDEbbSKUhEFRdUD5rU1JWXX03Q';
// DESPUÉS (eliminar la línea, la key se lee de wp_options)

Cambio B — en enqueue(), reemplazar el uso de la constante en la URL de Google Maps:

// ANTES:
'https://maps.googleapis.com/maps/api/js?key=' . self::GMAPS_KEY . '&libraries=places,drawing',
// DESPUÉS:
'https://maps.googleapis.com/maps/api/js?key=' . esc_attr( get_option( 'ab_gmaps_key', '' ) ) . '&libraries=places,drawing',

Cambio C — en wp_localize_script:

// ANTES:
'gmaps_key' => self::GMAPS_KEY,
// DESPUÉS:
'gmaps_key' => esc_attr( get_option( 'ab_gmaps_key', '' ) ),

Cambio D — en rest_settings_get():

// ANTES:
'gmaps_key' => get_option( 'ab_gmaps_key', self::GMAPS_KEY ),
// DESPUÉS:
'gmaps_key' => get_option( 'ab_gmaps_key', '' ),
  • 2.3 Desplegar (mismo comando deploy con autobooking-admin-dashboard.php)

  • 2.4 Verificar — en WP Admin → AutoBooking Admin → tab CONFIG, asegurarse que el campo "Google Maps Key" tiene el valor AIzaSyD3VSlYZvDEbbSKUhEFRdUD5rU1JWXX03Q. Si está vacío, pegarlo y guardar.

  • 2.5 Verificar en HTML — Ver fuente de /admin-dashboard/ (Ctrl+U) → buscar AIzaSyD3 → NO debe aparecer como texto plano en el source (aparece como valor de option, no hardcoded).

  • 2.6 Commit

git add autobooking-admin-dashboard.php && git commit -m "fix: Google Maps API key moved from hardcode to wp_options"

Tarea 3: IP Geolocation en geo-restrict

Archivos: Modificar autobooking-geo-restrict.php (local)

  • 3.1 Backup en servidor
echo "y" | & $plink -pw "Geronimo6&8" -P 2230 pi@ssh.famromdon.online 'docker exec autobookingonline-wordpress-autobooking-1 cp /var/www/html/wp-content/plugins/autobooking-geo-restrict/autobooking-geo-restrict.php /var/www/html/wp-content/plugins/autobooking-geo-restrict/autobooking-geo-restrict.php.bak'
  • 3.2 Agregar función helper después de ab_is_country_allowed() (~línea 67):
function ab_get_country_from_ip( $ip ) {
    if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) return null;
    $cache_key = 'ab_geo_ip_' . md5( $ip );
    $cached    = get_transient( $cache_key );
    if ( $cached !== false ) return $cached ?: null;
    $response  = wp_remote_get( 'http://ip-api.com/json/' . rawurlencode( $ip ) . '?fields=countryCode,status', ['timeout'=>5] );
    if ( is_wp_error( $response ) ) { set_transient( $cache_key, '', 3600 ); return null; }
    $body = json_decode( wp_remote_retrieve_body( $response ), true );
    $code = ( ! empty( $body['status'] ) && $body['status'] === 'success' ) ? strtoupper( $body['countryCode'] ?? '' ) : '';
    set_transient( $cache_key, $code, 86400 );
    return $code ?: null;
}

function ab_get_request_ip() {
    if ( function_exists( 'abs_get_ip' ) ) return abs_get_ip();
    $ip = '';
    if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { $parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); $ip = trim($parts[0]); }
    if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) $ip = $_SERVER['REMOTE_ADDR'] ?? '';
    return sanitize_text_field( $ip );
}
  • 3.3 Modificar el callback de /geo/check — reemplazar el return final del callback:
// ANTES (las últimas líneas del callback):
            return rest_ensure_response( [
                'allowed' => $allowed,
                'country' => $country,
                'message' => $allowed ? '' : get_option( 'ab_geo_blocked_msg', 'AutoBooking is not available in your current location.' ),
            ] );

// DESPUÉS:
            if ( $allowed && $country ) {
                $ip_country = ab_get_country_from_ip( ab_get_request_ip() );
                if ( $ip_country && $ip_country !== $country ) {
                    return rest_ensure_response(['allowed'=>false,'country'=>$country,'message'=>'Ubicación inconsistente. Verifica tu conexión.']);
                }
            }
            return rest_ensure_response( [
                'allowed' => $allowed,
                'country' => $country,
                'message' => $allowed ? '' : get_option( 'ab_geo_blocked_msg', 'AutoBooking is not available in your current location.' ),
            ] );
  • 3.4 Desplegar

  • 3.5 Verificar

GET https://autobooking.online/wp-json/autobooking/v1/geo/check?lat=40.7128&lng=-74.0060

Esperado: {"allowed":true,"country":"US","message":""}

  • 3.6 Commit
git add autobooking-geo-restrict.php && git commit -m "feat: IP geolocation backup in geo/check endpoint"

Tarea 4: Sección Seguridad en Admin Dashboard CONFIG

Archivos: Modificar autobooking-admin-dashboard.php (HTML del panel CONFIG)

  • 4.1 Agregar card de Seguridad en el HTML del panel panel-settings, después del card de tarifas (buscar </div> que cierra el card de tarifas, ~línea 840):
      <div class="abad-card">
        <div class="abad-card__title">Seguridad</div>
        <div class="abad-form-group">
          <label class="abad-label">IPs en whitelist (una por línea)</label>
          <textarea class="abad-textarea" id="sec-whitelist" rows="3" placeholder="127.0.0.1"></textarea>
        </div>
        <div class="abad-form-group">
          <label class="abad-label">hCaptcha Site Key</label>
          <input type="text" class="abad-input" id="sec-hcaptcha-site">
        </div>
        <div class="abad-form-group">
          <label class="abad-label">hCaptcha Secret Key</label>
          <input type="password" class="abad-input" id="sec-hcaptcha-secret">
        </div>
        <div class="abad-alert abad-alert--success" id="sec-saved" style="display:none">Guardado.</div>
        <button class="abad-btn abad-btn--brand" id="btn-save-sec">GUARDAR SEGURIDAD</button>
        <div class="abad-card__title" style="margin-top:20px">IPs bloqueadas</div>
        <p class="abad-note" id="sec-blocked-count"></p>
        <div id="sec-blocked-table"></div>
      </div>
  • 4.2 Agregar JS al final de assets/admin-dashboard.js:
// ── SECURITY ──────────────────────────────────────────────────
async function loadSecuritySettings() {
  try {
    const r = await apiFetch('/admin/blocked-ips');
    document.getElementById('sec-blocked-count').textContent = r.blocked_24h + ' IPs bloqueadas en las últimas 24h';
    const el = document.getElementById('sec-blocked-table');
    if (!r.blocked.length) { el.innerHTML = '<p style="color:rgba(255,255,255,.4);font-size:13px">Sin IPs bloqueadas.</p>'; return; }
    el.innerHTML = '<table class="abad-table"><thead><tr><th>IP</th><th>Motivo</th><th>Expira</th><th></th></tr></thead><tbody>'
      + r.blocked.map(b => `<tr><td>${b.ip}</td><td style="font-size:12px">${b.reason}</td><td style="font-size:12px">${b.expires_at}</td><td><button class="abad-btn abad-btn--outline abad-btn--sm" onclick="absUnblock(${b.id})">Desbloquear</button></td></tr>`).join('')
      + '</tbody></table>';
  } catch(e) {}
}
window.absUnblock = async id => { await apiFetch('/admin/blocked-ips/'+id+'/unblock',{method:'POST'}); loadSecuritySettings(); };
document.getElementById('btn-save-sec')?.addEventListener('click', async () => {
  await apiFetch('/admin/security-settings/save', {method:'POST', body: JSON.stringify({
    whitelist: document.getElementById('sec-whitelist').value,
    hcaptcha_site_key: document.getElementById('sec-hcaptcha-site').value,
    hcaptcha_secret: document.getElementById('sec-hcaptcha-secret').value,
  })});
  const el = document.getElementById('sec-saved'); el.style.display='block'; setTimeout(()=>el.style.display='none',3000);
});
document.querySelectorAll('.abad-tab').forEach(btn => {
  if (btn.dataset.tab === 'settings') btn.addEventListener('click', loadSecuritySettings);
});
  • 4.3 Desplegar autobooking-admin-dashboard.php + assets/admin-dashboard.js

  • 4.4 Verificar — Admin dashboard → tab CONFIG → ver sección Seguridad → ingresar hCaptcha keys → Guardar → confirmar mensaje "Guardado."

  • 4.5 Commit

git add autobooking-admin-dashboard.php && git commit -m "feat: security section in admin CONFIG tab"

Verificación final

  • Bot no puede crear más de 5 cuentas/hora → al intento 6 aparece error de rate limit
  • Conductor sin email confirmado → al entrar a /pending-driver/ ve pantalla "Confirma tu email"
  • Clic en enlace del email → redirige a /pending-driver/?ab_confirm=ok
  • GET /wp-json/autobooking/v1/geo/check?lat=40.7&lng=-74.0{"allowed":true,...}
  • Ver fuente de página admin → AIzaSyD3 no aparece hardcoded
  • Login con usuario que tenga rol driver + customer → redirige a /choose-role/ ✓ (no tocado)