# 9.8.2 ¿Cómo obtener la ciudad del usuario por medio de la IP?

## Obtener los datos del navegador

En las peticiones a los servicios Rest en el header se suele incluir la versión del navegador en el Header **User-Agent**, aunque este método fácilmente lo pueden cambiar los hackers, lo podemos utilizar como estadística, o podemos agregar un nuevo Header que se llame por ejemplo **Secreto** y pedir a todos los clientes que se conecten a nuestros servicios que envíen un Header con el nombre **Secreto**

Para obtener datos acerca de la petición a nuestros servicios rest necesitamos un objeto de la clase  necesitamos un objeto de la interfaz&#x20;

```
IHttpContextAccessor
```

Agregamos a nuestra clase controller una variable y la pasamos en el constructor

{% code title="UsuariosController.cs" %}

```csharp
public class UsuariosController : BaseController
{
    private IHttpContextAccessor _accessor;
    
    public UsuariosController(CaducaContext context,
                              LocService localize,
                              IConfiguration config,
                              IHostingEnvironment hostingEnvironment,
                              IHttpContextAccessor accessor) 
                              : base(context, localize)
    {           
        _config = config;
        _accessor = accessor;
        _hostingEnvironment = hostingEnvironment;
        usuarioDAO = new UsuarioDAO(context, localize,
                              _hostingEnvironment.ContentRootPath);
    }
}
```

{% endcode %}

A nuestro método para el Login agregamos el método para obtener el navegador

{% code title="UsuarioController.cs" %}

```csharp
public async Task<IActionResult> PostAsync([FromBody] LoginDTO loginDTO)
{
    var navegador=_accessor.HttpContext?.Request.Headers["User-Agent"];
    //Para obtener cualquier otro dato en el Header
    //var otroDato = _accessor.HttpContext?.Request.Headers["Secreto"];
}
```

{% endcode %}

## Obtener los datos de la ciudad por medio de la IP

Otra recomendación es registrar las ciudades y dispositivos de donde se conecta el usuario normalmente y enviar un correo cuando se conecta de otra ciudad diferente.

Existen bases de datos gratuitas y de pago donde por medio de la ip te regresan la ciudad aproximada de donde se conectó el usuario.&#x20;

Si deseas bajar tu base de datos aqui tienes una gratuita. Esta base de datos se actualiza constantemente.

{% embed url="<https://lite.ip2location.com/?r=piwik>" %}

Existen servicios rest en donde tu envías la ip y te regresan los datos de la ip, algunos ofrecen servicios gratuitos limitados a por ejemplo 1,000 peticiones por día o varía el precio de acuerdo al número de peticiones.

Algunos de los servicios gratuitos son los siguientes, los cuales te permiten 1,000 peticiones por día.

{% embed url="<https://ipgeolocation.io/pricing.html>" %}

{% embed url="<https://www.iplocate.io/pricing>" %}

En mi ejemplo utilizare iplocate

{% hint style="info" %}
Si deseas puedes guardar cada día las ip de los usuarios para consultar de tu base de datos en lugar del servicio, si varios usuarios se conectan del mismo lugar.
{% endhint %}

Para mandar llamar el servicio puedes conectarte por postman o desde el navegador utilizando la siguiente petición

<https://www.iplocate.io/api/lookup/35.232.52.59>

Te regresa un json similar a este

```json
{
  "ip"          : "35.232.52.59",
  "country"     : "United States",
  "country_code": "US",
  "city"        : "Mountain View",
  "continent"   : "North America",
  "latitude"    : 37.4043,
  "longitude"   : -122.0748,
  "time_zone"   : "America/Los_Angeles",
  "postal_code" : "94043",
  "org"         : "Google LLC",
  "asn"         : "AS15169",
  "subdivision" : "California",
  "subdivision2": null
}
```

Puedes entrar a la siguiente página donde le copias el json regresado por algún servicio y te genera una clase en c# o puedes copiar el json a visual studio utilizando la opción Pegado Especial -> Pegar Json como Clase

{% embed url="<https://www.jsonutils.com>" %}

Agregamos la clase generada en nuestra carpeta **DTO**

{% code title="DatosIPDTO.cs" %}

```csharp
public class DatosIPDTO
{
    public string ip { get; set; }
    public string country { get; set; }
    public string country_code { get; set; }
    public string city { get; set; }
    public string continent { get; set; }
    public double latitude { get; set; }
    public double longitude { get; set; }
    public string time_zone { get; set; }
    public string postal_code { get; set; }
    public string org { get; set; }
    public string asn { get; set; }
    public string subdivision { get; set; }
    public object subdivision2 { get; set; }
}
```

{% endcode %}

Agregamos una clase **IPLocate** la cual llamará al servicio rest de IPLocate para a través de la IP nos regrese la ciudad.&#x20;

{% code title="IPLocate" %}

```csharp
public class IPLocate
{
    /// <summary>
    /// LLamamos al servicio IPLocate para obtener los datos de una ip
    /// </summary>
    /// <param name="ip">Ip del cliente</param>
    /// <returns></returns>
    public async Task<DatosIPDTO> ObtenerDatosPorIpAsync(string ip)
    {
        HttpClient client = new HttpClient();
        DatosIPDTO datosIP;
            
        var respuesta = await 
                client.GetStringAsync
                    ("https://www.iplocate.io/api/lookup/" + ip);
        datosIP = JsonConvert.DeserializeObject<DatosIPDTO>(respuesta);           
        return datosIP;
    }
}
```

{% endcode %}

{% hint style="info" %}
Como una mejora, se puede crear una interfaz **IPServicio** con el método **ObtenesDatosPorIpAsync** para si a futuro se desea cambiar el método para obtener las IP lo deseamos cambiar a alguna de paga o de otra compañía.
{% endhint %}

Como en el modo de desarrollo desde visual studio el código para obtener la ip no funciona agregamos una ip por default, para obtener la ip podemos utilizar el objeto \_accesor, la opción RemoteIpAdrress. Si se tiene un servidor Linux se puede configurar Nginx para guardar la IP en el header X-Real-IP en la sección de configuración de servidor en linux se explica como realizar este paso.

{% code title="UsuariosController.cs" %}

```csharp
public class UsuariosController : BaseController
{
    public async Task<IActionResult> PostAsync([FromBody] LoginDTO loginDTO)
    {
       //Código del navegador
        string ip = "189.145.141.65"; //Set default ip
        if (_hostingEnvironment.IsProduction())        
             ip =_accessor.HttpContext?.Connection?
                                     .RemoteIpAddress?.ToString();
        //En servidores linux con nginx y el forward de X-Real-Ip
        //var ip = this.httpContextAccessor.HttpContext
        //          .Request.Headers["X-Real-IP"];        
    }
 }
```

{% endcode %}

En nuestra clase **UsuarioAccesoDAO** agregamos un método para obtener la ip llamado **ObtenerDatosIPAsync**

{% code title="UsuarioDAO.cs" %}

```csharp
public class UsuarioAccesoDAO
{
    private DatosIPDTO datosIP;
        
    public async Task<DatosIPDTO> ObtenerDatosIPAsync(string ip)
    {
        var ipLocate = new IPLocate();
        return  await ipLocate.ObtenerDatosPorIpAsync(ip);           
    }
```

{% endcode %}

En nuestro método **LoginAsync** agregamos el parámetro de la ip y el navegador y guardamos los valores

{% code title="UsuarioDAO.cs" %}

```csharp
public async Task<bool> GuardarAccesoAsync(TokenDTO tokenDTO, 
                           int usuarioId, string ip, string navegador)
{           
    if (datosIP == null)
        datosIP = await ObtenerDatosIPAsync(ip);
}
```

{% endcode %}

Agregamos otra función en nuestra clase **UsurioAccesoDAO** para revisar si el usuario tiene mas de un acceso registrado revisamos si el usuario se ha conectado de una nueva ip o de una nueva ciudad

{% code title="UsuarioAccesoDAO.cs" %}

```csharp
public async Task<bool> GuardarAccesoAsync(TokenDTO tokenDTO, int usuarioId, string ip, string navegador)
{           
    if (datosIP == null)
        datosIP = await ObtenerDatosIPAsync(ip);
    var usuarioAcceso = new UsuarioAcceso();
    usuarioAcceso.Ciudad = datosIP.city;
    usuarioAcceso.Estado = datosIP.subdivision;
    usuarioAcceso.Navegador = navegador;
    //Demás código
}
```

{% endcode %}

Agregamos funciones para validar si el usuario se registra de una nueva ciudad o navegador

{% code title="UsuarioAccesoDAO.cs" %}

```csharp
public async Task<bool> EsOtraCiudadAsync(string ip, int usuarioId)
{
    //Obtenemos los datos de la ip
    datosIP = await this.ObtenerDatosIPAsync(ip);
    //Revisamos si ya registro algún acceso, si no tiene
    //accesos no se debe enviar ningún correo 
    var acceso = contexto.UsuarioAcceso
             .FirstOrDefault(u => u.UsuarioId == usuarioId);
    if (acceso == null)
        return false;
    //Revisamos si tiene un registro en otra ciudad
    acceso = contexto.UsuarioAcceso
                 .FirstOrDefault(u => u.UsuarioId == usuarioId
                                 && u.Ciudad!=datosIP.city);
    //Si ya tiene un acceso para esa ciudad regresamos false
    return acceso!=null;
}

public bool EsOtroNavegadorAsync(string navegador, int usuarioId)
{
     //Revisamos si ya registro algún acceso, si no tiene
    //accesos no se debe enviar ningún correo 
    var acceso = contexto.UsuarioAcceso
                    .FirstOrDefault(u => u.UsuarioId == usuarioId);
    if (acceso == null)
        return false;
    //Revisamos si tiene un registro en otra ciudad
    acceso = contexto.UsuarioAcceso
                      .FirstOrDefault(u => u.UsuarioId == usuarioId
                                      && u.Navegador != navegador);
    //Si ya tiene un acceso para esa ciudad regresamos false
    return acceso != null;
}
```

{% endcode %}

Creamos otro método para enviar el correo en caso de encontrar un nuevo acceso en otra ciudad o Navegador.

Agregamos un nuevo template para enviar el correo de aviso de sesión. En nuestra carpeta **Templates** creamos un archivo **NuevoAcceso.html**

{% code title="NuevoAcceso.html" %}

```markup
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <style>
        body {
            font-size: 1.3rem;
            color: #05678e;
        }

        .acceso {
            border-radius: 10px;
            border: solid 1px #ccc;
            padding: 20px;
        }

        label {
            display: inline-block;
            width: 130px;
            font-weight: bold;
        }

        .titulo {
            font-size: 1.5rem;
            font-weight: bold;
        }
    </style>
</head>
<body>
    <p>
        ¡Hola {{usuario}}!
        <h3>Alguien accedió a tu cuenta en una nueva ciudad y/o 
        navegador</h3>
    </p>
    <div class="acceso">
        <span class="titulo">
            Nuevo acceso en:
        </span>
        <br />
        <label>Navegador:</label>{{navegador}}
        <br />
        <label>El:</label>{{fecha}}
        <br />
        <label>Cerca de:</label>{{ciudad}}, {{estado}}, {{pais}}
        <br />
        <label> Ip:</label>{{ip}}
        <hr />
        <p>
            Te enviamos este correo electrónico para asegurarnos de
            que hayas sido tú.
            Si no fuiste tú, por favor comunícate con nosotros.
        </p>
    </div>
</body>
</html>
```

{% endcode %}

{% code title="UsuarioDAO.cs" %}

```csharp
private void EnviaCorreoNuevoAcceso(string path, string usuario, 
                                    string email, 
                                    DatosIPDTO datosIP, 
                                    string ip, 
                                    string navegador)
{
    string body = System.IO.File.ReadAllText(Path.Combine(path, "Templates", "NuevoAcceso.html"));
    body = body.Replace("{{usuario}}", usuario);
    body = body.Replace("{{ciudad}}", datosIP.city);
    body = body.Replace("{{estado}}", datosIP.subdivision);
    body = body.Replace("{{pais}}", datosIP.country);
    body = body.Replace("{{ip}}", ip);
    body = body.Replace("{{navegador}}", navegador);
            
    Correo mail = new Correo()
    {
        Para = email,
        Mensaje = body,
        Asunto = "Tu cuenta ha sido bloqueada"
    };
    try
    {
        mail.Enviar();
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.InnerException);
    }
}
```

{% endcode %}

Por último agregamos las funciones para registrar si el usuario se conecta de una nueva ciudad o ip.

{% code title="UsuarioDAO.cs" %}

```csharp
public async Task<TokenDTO> LoginAsync(LoginDTO loginDTO, 
                    IConfiguration config, string ip, string navegador)
{
    //Demás código
    var esOtroNavegador = usuarioAccesoDAO
               .EsOtroNavegador(navegador, usuario.Id);
    var esOtraCiudad = await usuarioAccesoDAO
               .EsOtraCiudadAsync(ip, usuario.Id);
    if (esOtroNavegador || esOtraCiudad)
        EnviaCorreoNuevoAcceso(_path, usuario.Clave, usuario.Email, usuarioAccesoDAO.datosIP, ip, navegador);
    //Demás código para guardar el acceso
}
```

{% endcode %}

Para practicar puedes agregar otra validación para detectar el sistema operativo.

{% hint style="info" %}
Como mejora se puede crear una interfaz que tenga una variable string y un método para validar el acceso, para realizar la mismas validaciones como se hizo con la interfaz IRegla
{% endhint %}

{% hint style="info" %}
Otra mejora puede ser registrar las ips y su ciudad para no hacer tantas peticiones al servicio y ahorras costos.
{% endhint %}

Como practica puedes conectarte al servicio <https://api.ipgeolocation.io/ipgeo>.&#x20;

Para poder reemplazar el servicio rest&#x20;

1. Crea una **interfaz** **IPLocation** con una función para obtener los datos de la IP
2. Crea una clase **IPGeoLocation** que implemente la interfaz IPLocation para obtener los datos de la IP y regrese el mismo objeto **DatosIPDto**
3. En **UsuarioAccesoDAO** agrega la siguiente función para recibir un objeto de la interfaz IPLocation.

```csharp
public async Task<DatosIPDTO> ObtenerDatosIPAsync
                               (string ip, IPLocation ipLocation)
{
     return await ipLocation.ObtenerDatosPorIpAsync(ip);
}
```

4\. Cambia la función **EsOtraCiudadAsync** para pasar como parámetro un objeto de la nueva clase **IPGeoLocation**.

```csharp
public async Task<bool> EsOtraCiudadAsync(string ip, int usuarioId)
{
    IPGeoLocation ipGeoLocation = new IPGeoLocation();
    //Obtenemos los datos de la ip
    datosIP = await this.ObtenerDatosIPAsync(ip, ipGeoLocation);
```

De esta forma si en el futuro deseas agregar otro proveedor, solo debes crear la clase que implemente la interfaz IPLocation y agregarlo como parámetro
