Mapas de MapKit JS en Ruby on Rails y Stimulus
Ya que estoy pagando por la cuenta de desarrollador de Apple he querido hacer uso de su servicio de mapas para desarrollar Pizza Pals.
Apple proporciona toda la documentación necesaria para usar sus mapas. Aquí voy a explicar cómo los he integrado con una aplicación Rails.
Necesitamos un controlador con su ruta y su vista:
$ bin/rails g controller maps index
La documentación nos ofrece el script que debemos insertar para cargar MapKit. Lo puedes meter en la vista que acabas de generar o en el header de la aplicación.
<script src="https://cdn.apple-mapkit.com/mk/5.x.x/mapkit.core.js"
crossorigin async
data-callback="initMapKit"
data-libraries="services,full-map,geojson"></script>
Lo siguiente que vamos a generar es un controlador de Stimulus. En realidad este paso es totalmente innecesario 🤷♂️ se podría omitir y todo funcionaría igual.
$ bin/rails g stimulus maps
Actualiza la vista añadiendo el div que contendrá el mapa. Has de especificar ancho y alto o de lo contrario el mapa no se verá. También has de darle un id y, como hemos decidido usar Stimulus de manera innecesaria, llamar al controlador.
# app/views/maps/index.html.erb
<div style="width: 600px; height: 300px" id="map-container" data-controller="maps"></div>
Apple proporciona ejemplos de cómo usar la librería. Me voy a basar en el Embedded Map. Es posible que necesites una cuenta de desarrollador para acceder a este enlace.
Trasladamos el código a Stimulus y queda algo así:
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="maps"
export default class extends Controller {
async connect() {
this.initMap()
}
async setupMapKit() {
if (!window.mapkit || window.mapkit.loadedLibraries.length === 0) {
await new Promise(resolve => { window.initMapKit = resolve });
delete window.initMapKit;
}
const jwt = "Insert Your JWT Here";
mapkit.init({
authorizationCallback: done => { done(jwt); }
});
}
async initMap() {
await this.setupMapKit()
const cupertino = new mapkit.CoordinateRegion(
new mapkit.Coordinate(37.3316850890998, -122.030067374026),
new mapkit.CoordinateSpan(0.167647972, 0.354985255)
);
const map = new mapkit.Map("map-container");
map.region = cupertino;
}
}
Como todavia no hemos generado un jwt, veremos el siguiente error en la consola al visitar la página: [MapKit] Authorization token is invalid.
Vamos a solucionarlo. Necesitamos un identificador de mapa y una clave privada. Una vez creados puedes ir al dashboard y acceder a create token. Necesitaremos los datos generados en el paso anterior y subir el archivo .p8. En domain restriction puedes poner localhost o dejarlo vacío por el momento.
Una vez generado el token lo puedes pegar en el codigo: const jwt = "Insert Your JWT Here";
Si arrancas el servidor y vas a la ruta que has creado deberías ver el mapa. Si no ves nada, asegurate de haberle dado ancho y alto al div.
Gestionar tokens de esta manera es engorroso y peligroso. Por ello vamos a crear un endpoint en nuestro backend que genere tokens cada vez que se cargue un mapa. Puedes especificar la caducidad de un token y reutilizarlo tantas veces como quieras en un año.
Cambia dentro del controlador de Stimulus:
const jwt = "Insert Your JWT Here";
mapkit.init({
authorizationCallback: done => { done(jwt); }
});
por:
mapkit.init({
authorizationCallback: done => {
fetch("/tokens/mapkit")
.then(res => res.text())
.then(done)
}
})
Usando fetch, el inicializador del mapa hará una petición http a una ruta de nuestro servidor que devolverá el token. De este modo no lo tenemos incrustado en el código.
Crea otro controlador. En este caso lo he llamado tokens
y al método mapkit
. Añade también una ruta. No necesitamos la vista.
$ bin/rails g controller tokens mapkit
Dentro del controlador, en el metodo mapkit, puedes devolver el token:
def mapkit
render plain: "[reemplaza por el token]"
end
Todavia no es donde queremos llegar, pero es un paso mas cerca. Hemos pasado de tener el token incrustado en el frontend a tenerlo en el backend. Ahora, en vez de devolver el mismo token tenemos que generar uno cada vez. Cómo?
Vamos a usar una clase Mapkit
y un metodo estático token
para generar el token. Mientras la implementamos podemos actualizar el codigo del controlador y asi ya nos olvidamos:
def mapkit
render plain: Mapkit.token
end
Ahora crea la clase donde más rabia te dé dentro de tu aplicación Rails.
Como vamos pasito a pasito, puedes llevarte ahí el token que ya tenemos y comprobar que todo sigue funcionando:
class Mapkit
class << self
def token
"[reemplaza por el token]"
end
end
end
Vamos a necesitar algo de ayuda para generar el jwt. En este caso he decidido usar la gema jwt pero imagino que puedes usar la que te de la gana o hacértelo a mano.
$ bundle add jwt
La documentación dice que para construir el token necesitamos dos cosas: un header y un payload. Cada uno se compone de varias cosas:
En el header tenemos que especificar el algoritmo, que ha de ser ES256. También necesitamos especificar la key id que hemos obtenido antes y por último simplemente indicar que el typ es “JTW”.
En el payload tenemos que especificar el team id que también hemos obtenido antes, la fecha de generación del token (es decir, ahora), la fecha de expiración del token (hasta un año) y finalmente el origen. Como estamos desarrollando en local, he puesto localhost. En producción y otros entornos tendrás que especificar el dominio desde el que se usará, sino el token será inválido y los mapas no se mostrarán.
class Mapkit
class << self
ALGORITHM = "ES256"
def token
header = {
alg: ALGORITHM,
kid: "[replace with your key id]",
typ: "JWT"
}
payload = {
iss: "[replace with your team id]",
iat: Time.now.to_i,
exp: 1.day.from_now.to_i,
origin: "localhost"
}
JWT.encode(payload, private_key, ALGORITHM, header)
end
private
def private_key
OpenSSL::PKey::EC.new("-----BEGIN PRIVATE KEY-----
RANDOM STRING INSIDE THE .P8 FILE
-----END PRIVATE KEY-----")
end
end
end
Si miras el método private_key
verás que el contenido no está tabulado. Esto me trajo un poco de cabeza. Hay que poner el contenido tal cual esta en el fichero .p8 que te descargas, si no te va a dar errores como OpenSSL::PKey::ECError (invalid curve name):
.
Si reinicias el servidor todo debería seguir funcionando correctamente. Ya casi hemos terminado.
Obviamente tratándose de información sensible no la vamos a dejar al descubierto.
Puedes guardar las claves en variables de entorno. Yo he decidido hacerlo en los credenciales de Rails:
$ EDITOR="code --wait" bin/rails credentials:edit
Si no usas VS Code
no necesitas la primera parte.
mapkit:
key_id: your-key-id
team_id: your-team-id
key_file: |
-----BEGIN PRIVATE KEY-----
random text
-----END PRIVATE KEY-----
Fíjate bien en el símbolo | después de key_files
. Es parte de YAML y es muy importante ya que hace que se respeten los saltos de línea. Si no lo pones o usas el símbolo > como se usa a veces, volverás a tener el mismo error OpenSSL::PKey::ECError (invalid curve name):
de antes. Si crees que lo estás haciendo bien y aún así obtienes el error, prueba a reiniciar el servidor. Puede que no se hayan recargado los credenciales correctamente.