diff --git a/app/Helpers/EncryptionHelper.php b/app/Helpers/EncryptionHelper.php new file mode 100644 index 0000000..7c7b704 --- /dev/null +++ b/app/Helpers/EncryptionHelper.php @@ -0,0 +1,182 @@ +getMessage()); + } + } + + /** + * Decrypt the given data (arrays/objects). + */ + public static function decryptData($encryptedData) + { + try { + $decrypted = Crypt::decryptString($encryptedData); + return json_decode($decrypted, true); + } catch (DecryptException $e) { + Log::error('Error al desencriptar los datos: ' . $e->getMessage()); + return null; + } catch (\Exception $e) { + Log::error('Error inesperado al desencriptar los datos: ' . $e->getMessage()); + return null; + } + } + + /** + * Encrypt a simple string (for tokens, passwords, etc.) + */ + public static function encryptString(string $string): string + { + try { + return Crypt::encryptString($string); + } catch (\Exception $e) { + throw new \RuntimeException("Error al encriptar el string: " . $e->getMessage()); + } + } + + /** + * Decrypt a simple string + */ + public static function decryptString(string $encryptedString): ?string + { + try { + return Crypt::decryptString($encryptedString); + } catch (DecryptException $e) { + Log::error('Error al desencriptar el string: ' . $e->getMessage()); + return null; + } catch (\Exception $e) { + Log::error('Error inesperado al desencriptar el string: ' . $e->getMessage()); + return null; + } + } + + /** + * Encrypt using a custom key (independent of APP_KEY) + * Useful for tokens that should survive APP_KEY rotation + */ + public static function encryptWithCustomKey(string $string, string $key): string + { + try { + $cipher = 'AES-256-CBC'; + $ivLength = openssl_cipher_iv_length($cipher); + $iv = openssl_random_pseudo_bytes($ivLength); + + $encrypted = openssl_encrypt($string, $cipher, $key, 0, $iv); + + if ($encrypted === false) { + throw new \RuntimeException('Encryption failed'); + } + + // Combinar IV + encrypted data y codificar en base64 + return base64_encode($iv . $encrypted); + } catch (\Exception $e) { + throw new \RuntimeException("Error al encriptar con clave personalizada: " . $e->getMessage()); + } + } + + /** + * Decrypt using a custom key (independent of APP_KEY) + */ + public static function decryptWithCustomKey(string $encryptedString, string $key): ?string + { + try { + $cipher = 'AES-256-CBC'; + $ivLength = openssl_cipher_iv_length($cipher); + + // Decodificar y separar IV + encrypted data + $data = base64_decode($encryptedString); + if ($data === false) { + return null; + } + + $iv = substr($data, 0, $ivLength); + $encrypted = substr($data, $ivLength); + + $decrypted = openssl_decrypt($encrypted, $cipher, $key, 0, $iv); + + if ($decrypted === false) { + Log::error('Error al desencriptar con clave personalizada'); + return null; + } + + return $decrypted; + } catch (\Exception $e) { + Log::error('Error inesperado al desencriptar con clave personalizada: ' . $e->getMessage()); + return null; + } + } + + /** + * Verify if a plain value matches a value encrypted with custom key + */ + public static function verifyWithCustomKey(string $plainValue, string $encryptedValue, string $key): bool + { + try { + $decrypted = self::decryptWithCustomKey($encryptedValue, $key); + return $decrypted === $plainValue; + } catch (\Exception $e) { + return false; + } + } + + /** + * Generate a hash for searchable encrypted data + */ + public static function hash(string $data, string $algorithm = 'sha256'): string + { + return hash($algorithm, $data); + } + + /** + * Encrypt multiple fields in an array + */ + public static function encryptFields(array $data, array $fields): array + { + foreach ($fields as $field) { + if (isset($data[$field])) { + $data[$field] = self::encryptData($data[$field]); + } + } + return $data; + } + + /** + * Decrypt multiple fields in an array + */ + public static function decryptFields(array $data, array $fields): array + { + foreach ($fields as $field) { + if (isset($data[$field])) { + $data[$field] = self::decryptData($data[$field]); + } + } + return $data; + } + + /** + * Verify if a plain value matches an encrypted value + */ + public static function verifyEncrypted(string $plainValue, string $encryptedValue): bool + { + try { + $decrypted = self::decryptString($encryptedValue); + return $decrypted === $plainValue; + } catch (\Exception $e) { + return false; + } + } +} diff --git a/app/Http/Controllers/Api/ArcoController.php b/app/Http/Controllers/Api/ArcoController.php index 5c766dc..7f534b7 100644 --- a/app/Http/Controllers/Api/ArcoController.php +++ b/app/Http/Controllers/Api/ArcoController.php @@ -32,8 +32,13 @@ public function index(Request $request) $arcos = $query->orderBy('created_at', 'desc')->get(); + // Agregar token desencriptado a cada arco + $arcos->each(function($arco) { + $arco->api_token_plain = $arco->obtenerTokenDesencriptado(); + }); + return ApiResponse::OK->response([ - 'arcos' => $arcos + 'arcos' => $arcos, ]); } @@ -48,15 +53,22 @@ public function store(Request $request) 'ip_address' => 'required|ip|unique:arcos,ip_address', 'ubicacion' => 'nullable|string|max:255', 'descripcion' => 'nullable|string', + 'antena_1' => 'nullable|string', + 'antena_2' => 'nullable|string', + 'antena_3' => 'nullable|string', + 'antena_4' => 'nullable|string', 'activo' => 'boolean' ]); $arco = Arco::create($validated); + // Obtener el token en texto plano para mostrárselo al usuario + $plainToken = $arco->obtenerTokenDesencriptado(); + return ApiResponse::CREATED->response([ 'message' => 'Arco creado exitosamente', 'arco' => $arco, - 'api_token' => $arco->api_token + 'api_token' => $plainToken // Token en texto plano para que el arco lo use ]); } @@ -100,7 +112,10 @@ public function update(Request $request, int $id) 'ip_address' => 'sometimes|required|ip|unique:arcos,ip_address,' . $id, 'ubicacion' => 'nullable|string|max:255', 'descripcion' => 'nullable|string', - 'activo' => 'boolean' + 'antena_1' => 'nullable|string', + 'antena_2' => 'nullable|string', + 'antena_3' => 'nullable|string', + 'antena_4' => 'nullable|string' ]); $arco->update($validated); diff --git a/app/Http/Middleware/ArcoTokenMiddleware.php b/app/Http/Middleware/ArcoTokenMiddleware.php index c128b69..23ad22a 100644 --- a/app/Http/Middleware/ArcoTokenMiddleware.php +++ b/app/Http/Middleware/ArcoTokenMiddleware.php @@ -15,15 +15,16 @@ class ArcoTokenMiddleware */ public function handle(Request $request, Closure $next): Response { - $token = $request->bearerToken(); + $tokenPlano = $request->bearerToken(); - if (!$token) { + if (!$tokenPlano) { return ApiResponse::UNAUTHORIZED->response([ 'message' => 'Token de arco no proporcionado' ]); } - $arco = Arco::buscarPorToken($token); + // Buscar arco por token plano (el método desencripta internamente) + $arco = Arco::buscarPorToken($tokenPlano); if (!$arco) { return ApiResponse::UNAUTHORIZED->response([ @@ -37,6 +38,20 @@ public function handle(Request $request, Closure $next): Response ]); } + // Validación de IP deshabilitada para desarrollo + // TODO: Habilitar en producción o configurar nginx para enviar X-Forwarded-For + // $requestIp = $request->ip(); + // if ($arco->ip_address !== $requestIp) { + // return ApiResponse::FORBIDDEN->response([ + // 'message' => 'Token no autorizado para esta IP', + // 'detail' => [ + // 'ip_registrada_arco' => $arco->ip_address, + // 'ip_del_request' => $requestIp, + // 'arco_nombre' => $arco->nombre + // ] + // ]); + // } + // Agregar el arco al request para uso posterior $request->merge(['arco_autenticado' => $arco]); diff --git a/app/Models/Arco.php b/app/Models/Arco.php index 0e7857a..7b4377e 100644 --- a/app/Models/Arco.php +++ b/app/Models/Arco.php @@ -3,7 +3,7 @@ * @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved */ - +use App\Helpers\EncryptionHelper; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Str; @@ -25,7 +25,11 @@ class Arco extends Model 'nombre', 'ip_address', 'ubicacion', - 'activo' + 'activo', + 'antena_1', + 'antena_2', + 'antena_3', + 'antena_4' ]; protected $casts = [ @@ -44,7 +48,8 @@ protected static function boot() static::creating(function ($arco) { if (!$arco->api_token) { - $arco->api_token = Str::random(64); + $plainToken = Str::random(64); + $arco->api_token = EncryptionHelper::encryptString($plainToken); } }); } @@ -74,20 +79,39 @@ public static function buscarPorIp(string $ip): ?self } /** - * Buscar arco por API token + * Buscar arco por API token (recibe token plano, busca desencriptando) */ - public static function buscarPorToken(string $token): ?self + public static function buscarPorToken(string $tokenPlano): ?self { - return self::where('api_token', $token)->first(); + $arcos = self::all(); + + foreach ($arcos as $arco) { + // Desencriptar el token de la BD y comparar con el token plano recibido + if (EncryptionHelper::verifyEncrypted($tokenPlano, $arco->api_token)) { + return $arco; + } + } + + return null; } /** - * Regenerar API token + * Regenerar token encriptado */ public function regenerarToken(): string { - $this->api_token = Str::random(64); + $plainToken = Str::random(64); + $this->api_token = EncryptionHelper::encryptString($plainToken); $this->save(); - return $this->api_token; + + return $plainToken; + } + + /** + * Obtener token desencriptado + */ + public function obtenerTokenDesencriptado(): ?string + { + return EncryptionHelper::decryptString($this->api_token); } } diff --git a/app/Models/Detection.php b/app/Models/Detection.php index 9a62d51..8250a76 100644 --- a/app/Models/Detection.php +++ b/app/Models/Detection.php @@ -17,6 +17,7 @@ class Detection extends Model public $timestamps = false; protected $fillable = [ + 'arco_id', 'epc', 'vin', 'placa', @@ -29,4 +30,12 @@ class Detection extends Model protected $casts = [ 'fecha_deteccion' => 'datetime:Y-m-d H:i:s' ]; + + /** + * Relación con el modelo Arco + */ + public function arco() + { + return $this->belongsTo(Arco::class, 'arco_id'); + } } diff --git a/app/Services/VehicleService.php b/app/Services/VehicleService.php index e16bdb6..8fff7d5 100644 --- a/app/Services/VehicleService.php +++ b/app/Services/VehicleService.php @@ -45,14 +45,14 @@ private function consultarNuevoVehiculo(string $epc, string $fastId): array $datosVehiculo = $this->consultaRepuveCons->consultarVehiculoPorTag($fastId); if (!$datosVehiculo || !$datosVehiculo['vin']) { - Log::warning('Vehículo NO encontrado en API externa', [ + Log::warning('Vehículo NO encontrado.', [ 'epc' => $epc, 'fast_id' => $fastId ]); return [ 'success' => false, - 'message' => 'No se encontró información del vehículo en la API externa' + 'message' => 'No se encontró información del vehículo.' ]; } diff --git a/bootstrap/app.php b/bootstrap/app.php index 17eece1..651d791 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -38,6 +38,15 @@ 'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class, 'arco.token' => \App\Http\Middleware\ArcoTokenMiddleware::class, ]); + + // Configurar proxies confiables para detectar IP real en Docker + $middleware->trustProxies( + at: '*', + headers: Request::HEADER_X_FORWARDED_FOR | + Request::HEADER_X_FORWARDED_HOST | + Request::HEADER_X_FORWARDED_PORT | + Request::HEADER_X_FORWARDED_PROTO + ); }) ->withExceptions(function (Exceptions $exceptions) { $exceptions->render(function (ServiceUnavailableHttpException $e, Request $request) { diff --git a/database/migrations/2026_01_07_155424_add_encrypted_token_fields_to_arcos_table.php b/database/migrations/2026_01_07_155424_add_encrypted_token_fields_to_arcos_table.php new file mode 100644 index 0000000..8dc369e --- /dev/null +++ b/database/migrations/2026_01_07_155424_add_encrypted_token_fields_to_arcos_table.php @@ -0,0 +1,40 @@ +Key_name; + try { + DB::statement("ALTER TABLE arcos DROP INDEX `{$indexName}`"); + } catch (\Exception $e) { + } + } + + DB::statement('ALTER TABLE arcos MODIFY COLUMN api_token TEXT NULL'); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::statement('ALTER TABLE arcos MODIFY COLUMN api_token VARCHAR(64) NULL'); + + Schema::table('arcos', function (Blueprint $table) { + $table->unique('api_token'); + $table->index('api_token'); + }); + } +}; diff --git a/database/migrations/2026_01_07_162646_add_antenas_to_arcos_table.php b/database/migrations/2026_01_07_162646_add_antenas_to_arcos_table.php new file mode 100644 index 0000000..8513574 --- /dev/null +++ b/database/migrations/2026_01_07_162646_add_antenas_to_arcos_table.php @@ -0,0 +1,32 @@ +string('antena_1')->nullable()->after('activo'); + $table->string('antena_2')->nullable()->after('antena_1'); + $table->string('antena_3')->nullable()->after('antena_2'); + $table->string('antena_4')->nullable()->after('antena_3'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('arcos', function (Blueprint $table) { + $table->dropColumn(['antena_1', 'antena_2', 'antena_3', 'antena_4']); + }); + } +};