# 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): ```powershell $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 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', 'Error: 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 '
'; echo ''; } ); 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','Error: 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','Error: 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('
' .'

Confirma tu email

' .'

Revisa tu bandeja de entrada y haz clic en el enlace de confirmación.

' .'
'.wp_nonce_field('wp_rest','_wpnonce',true,false) .'' .'
','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))'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\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\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\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** ```powershell $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 Admin** → `https://autobooking.online/wp-admin/plugins.php` → Activar "AutoBooking Security" - [ ] **1.4 Verificar tablas creadas** ```powershell 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** ```bash 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** ```powershell 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: ```php // 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: ```php // 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`: ```php // ANTES: 'gmaps_key' => self::GMAPS_KEY, // DESPUÉS: 'gmaps_key' => esc_attr( get_option( 'ab_gmaps_key', '' ) ), ``` **Cambio D** — en `rest_settings_get()`: ```php // 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** ```bash 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** ```powershell 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): ```php 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: ```php // 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** ```bash 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 `` que cierra el card de tarifas, ~línea 840): ```php
Seguridad
IPs bloqueadas

``` - [ ] **4.2 Agregar JS** al final de `assets/admin-dashboard.js`: ```javascript // ── 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 = '

Sin IPs bloqueadas.

'; return; } el.innerHTML = '' + r.blocked.map(b => ``).join('') + '
IPMotivoExpira
${b.ip}${b.reason}${b.expires_at}
'; } 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** ```bash 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)