En la actualidad me encuentro con la necesidad de actualizar el porfolio, tener un buen formato de CV adaptable para postularme a ofertas de trabajo o simplemente para resumir mis aptitudes y experiencia.
Por lo que me plantee algunos requerimientos mínimos que el mismo deba cumplir para estar satisfecho y que a su vez sirva a mis objetivos:
Debe:
Tener una versión Web Responsive.
Poder descargarse en formato .pdf.
Ser fácil de adaptar dependiendo al tipo de postulación a la que aspiro.
Tener un formato legible por los sistemas de procesamiento de CV’s de las consultoras.
Ser un diseño simple que pueda ir mejorando a futuro.
Elección de la opción
En si hay muchas opciones viables, la mas sencilla es tener actualizado el perfil de Linkedin para poder descargarlo como .pdf, hay sitios web que asisten a la generación de un formato profesional a partir de plantillas, lo podría diseñar en algún procesador de textos.
Si bien cualquiera de esas opciones son buenas y cómodas, buscaba realizar un pequeño proyecto en el cual en si mismo pueda demostrar mis capacidades y que cumpla con los puntos anteriormente mencionados.
Por lo que decidí hacer un nuevo desarrollo usando VueJS que me permitirá tener total libertad creativa.
Nuevo proyecto VueJS
Cree un nuevo proyecto usando Vue + Vite (disponible en Github) a partir de su configuración inicial, al cual solo agregué el CDN de Bootstrap y la librería jsPDF para la posterior descarga del mismo.
La información propia del CV está definida en un JSON al estilo:
De esta forma puedo tener toda la información cargada y de acuerdo al caso comentar o des-comentar secciones.
Luego se define la estructura de componentes básica agregando un componente por cada sección:
Diseño general
Opté por un diseño minimalista haciendo énfasis en el contenido, priorizando el orden de acuerdo a la importancia de la información, manteniendo la formalidad sin agregados que causen distracción.
Con la idea de interiorizarme y aprender sobre los Web Sockets (WS) me decidí a realizar un nuevo proyecto usando NodeJS con framework Express y VueJS para el front.
¿Por que se eligieron dichas tecnologías?
Se optó por usar NodeJS por que tiene un muy buen manejo de WebSockets, ya que con el paquete express-ws podremos realizarlo de forma sencilla, dicho paquete se apoya en el framework Express; el cual me parece atractivo al no obligar al programador a usar un patrón de diseño en específico y es muy liviano.
NodeJS también tiene la ventaja, por sobre PHP, de poder almacenar información en memoria lo que nos permite prescindir de base de datos, ya que PHP al no guardar estados no lo permite y deberíamos encontrar alguna forma de mantener la persistencia de los datos.
El front podría haberlo desarrollado en Angular, framework con el cual vengo trabajando desde ya hace un tiempo, pero en cambio esta vez opté por VueJS, por que el mismo es más liviano, consume menos recursos en proceso de desarrollo (RAM principalmente), las APPs generadas son más livianas.
Y VueJS en si es más sencillo de usar y da menos problemas con respecto al manejo de dependencias, es más debugeable que Angular (aunque Angular mejoró bastante en dicho aspecto en el último tiempo)
Realmente no encuentro motivo para volver a Angular, por lo que Vue ya es mi framework favorito.
Otro punto que para mi suma es el no tener que usar TypeScript lenguaje que no termino de tomar muy en serio por que luego se transpila a JavaScript y para mi el tipado artificial que aplica solo suma una capa más sin aportar mucho, se pueden evitar los problemas de los lenguajes no tipados prestando un poco de atención.
¿Qué módulos conformarán el proyecto?
Como en muchos sistemas webs, el proyecto se dividirá en un Backend, el cual se encargará de proporcionar un endpoint al cual los clientes se podrán conectar por medio de WebSockets, llevará el listado de clientes conectados y retransmitirá los mensajes a todos los clientes conectados.
Y un front que se encargará solo de establecer la conexión e intercambiar mensajes.
El Backend
El código del backend es muy sencillo y de hecho hasta lo tendríamos en un solo archivo server.js:
let express = require('express');
let app = express();
let expressWs = require('express-ws')(app);
let uuid = require("uuid")
require("dotenv").config()
let registro_clientes = []
let reporte_conectados = []
expressWs.getWss().on('connection', function(ws) {
ws['id_conexion'] = uuid.v4()
});
app.ws('/', function(ws, req) {
ws.on('message', function(msg) {
let msgJson = null
try {
msgJson = JSON.parse( msg )
console.log(req.id)
if (msgJson.hasOwnProperty('accion')){
switch(msgJson.accion){
case 'registro':
msgJson.nombre = msgJson.nombre.replace(/]+(>|$)/g, "")
if (msgJson.nombre.length < 4){
ws.send(JSON.stringify({
accion: 'alerta',
msg: 'El nombre de usuario debe tener al menos 4 caracteres'
}))
return;
}
let registro = {
id: uuid.v4(),
nombre: msgJson.nombre,
accion: 'registro',
id_conexion: this.id_conexion
}
//comprobamos que no haya alguien registrado con el mismo nombre
for(let c=0; c < registro_clientes.length; c++){
if (registro.nombre == registro_clientes[c].nombre){
ws.send(JSON.stringify({
accion: 'alerta',
msg: 'El usuario especificado ya existe'
}))
return;
}
}
registro_clientes.push( registro )
reporte_conectados.push( registro.nombre )
console.log('se registro nuevo usuario', registro)
ws.send(JSON.stringify(registro))
let clientes = expressWs.getWss().clients
clientes.forEach(cliente => {
cliente.send(JSON.stringify({
accion: 'mensaje_sys',
msg: registro.nombre + ' Se ha unido a la sala'
}))
})
break;
case 'mensaje':
//comprobamos que el mensaje provenga de un cliente registrado
let encontrado = false
for(let c=0; c < registro_clientes.length; c++){
if (msgJson.autor.id == registro_clientes[c].id && msgJson.autor.nombre == registro_clientes[c].nombre ){
encontrado = true;
break;
}
}
//si es asi lo reenviamos al resto de los clientes
if (encontrado === true){
//se hace sanitizacion
msgJson.texto = msgJson.texto.replace(/]+(>|$)/g, "")
//se hace validacion
if (msgJson.texto.length > 500){
break;
}
let clientes = expressWs.getWss().clients
clientes.forEach(cliente => {
cliente.send(JSON.stringify(msgJson))
})
}
break;
}
}
} catch( error ){
console.log('error', error)
}
});
ws.on('close', function(code) {
console.log('desconectado', this.id_conexion)
let nombre = ''
for(let c=0; c < registro_clientes.length; c++){
if (registro_clientes[c].id_conexion == this.id_conexion){
nombre = registro_clientes[c].nombre
registro_clientes.splice(c,1)
reporte_conectados.splice(c,1)
break;
}
}
if (nombre != ''){
let clientes = expressWs.getWss().clients
clientes.forEach(cliente => {
cliente.send(JSON.stringify({
accion: 'mensaje_sys',
msg: nombre+' ha abandonado la sala'
}))
})
}
})
});
setInterval(()=>{
let clientes = expressWs.getWss().clients
clientes.forEach(cliente => {
cliente.send(JSON.stringify({
accion: 'reporte_online',
reporte: reporte_conectados
}))
})
}, 500)
app.listen(process.env.PUERTO);
console.log('puerto', process.env.PUERTO)
Creo que algo interesante a resaltar es que el manejo de la comunicación se realiza por medio de eventos.
Por ej nos podremos subscribir al evento “connection” que se dispara por cada nueva conexión establecida por el cliente
Este evento lo aprovechamos para poder generar identificadores únicos para cada cliente conectado.
Luego usamos Express como intermediario para atender los WS
app.ws('/', function(ws, req) {
....
Luego podremos atender el evento “message” que se disparará por cada mensaje recibido de algún cliente en particular.
Dichos mensajes usarán el formato JSON, por que creo que es la forma más facil de manejar datos estructurados, que puedan ser codificados y decodificados facilmente.
Y es necesario definir una estructura básica a la información ya que existen varios tipos de mensajes a ser enviados y recibidos.
Tipos de mensajes
reporte_online: Este mensaje se envía de forma períodica a todos los clientes conectados, y se usa para informar sobre los usuarios que actualmente están online, un ej del mismo sería: { “accion”:”reporte_online”, “reporte”: [ “Flor”, “Pepe”, “María”, “Juan” ] }
mensaje_sys: Se trata de un mensaje del sistema, que será mostrado a todos los clientes, se usa para informar por ej cuando alguien ingresa o egresa de la sala, ej del mismo sería: { “accion”:”mensaje_sys”, “msg”: “Flor ha abandonado la sala” }
alerta: Se usa para mostrar alertas o mensajes de error, por ej: { “accion”:”alerta”, “msg”: “El usuario especificado ya existe” }
registro: Se usa para registrar un nuevo usuario, ya que ni bien se ingresa al chat se solicita un nombre de usuario con el cual interectuar, para lo cual en el submit del formulario de nombre de usuario, se envia un mensaje de tipo “registro”, por ej: { “accion”:”registro”, “nombre”: “Pepe” }
mensaje: Se trata de un mensaje en sis mismo enviado por un usuario registrado, un ej: { “accion”:”mensaje”, “autor”: { “id”: “dsafdsfawe5433245-43534w”, “nombre”: “Marta” }, “texto” :”Hola Pepe!” }.
Algunas comprobaciones básicas
Se procede a sanitizar la entrada de caracteres de las entradas tanto al intentar elegir un nombre de usuario como en los mensajes en si mismos.
Excluyendo los caracteres que pudieran corresponder a tags html, ya que la idea es que se utilice solo texto, y quitar algunos caracteres problemáticos.
Si bien luego VueJS en el front por si solo salinitiza los datos para que no se incruste por ej código JS en los mensajes / CSS / HTML, creo que está bueno hacerlo de todos modos.
Para eso basta solo con usar: replace(/]+(>|$)/g, “”)
En el nombre de usuario se establece una longitud mínima del mismo y en el texto de los mensajes una longitud máxima.
Broadcast
Para el envio masivo de mensajes, primero se debe obtener el listado de clientes conectados, el cual se realiza usando:
let clientes = expressWs.getWss().clients
Luego se pueden recorrer los clientes usando un for, por ej:
El frontend en si mismo no tiene mucho, toda la lógica la implementé en un nuevo componente llamado VentanaChat.
El cual se encarga de establecer la conexión e intercambiar mensajes.
Para realizar la vista usé Bootstrap ya que creo que el framework de estilos más facil de usar, además existe el packete Bootstrap-vue3 que lo implementa y provee los componentes bàsicos para ser facilmente utilizable en Vue.
El establecimiento de la conexción con el WebSocket, que se realiza con:
conexion.value = new WebSocket( process.env.VUE_APP_API_URL )
Muy facil, no?
El “process.env.VUE_APP_API_URL” hace referencia a la variable VUE_APP_API_URL definida en un .env del proyecto, que se usa para poder configurar facilmente la url del endpoint
Y luego se atienden los eventos con:
conexion.value.onmessage = function(event) {
El proyecto en ejecución
Una vez que esten levantados tanto el back como el front, en el navegador lo podremos ver de la siguiente forma:
Se presentaron algunos problemas al intentar montar la demo, ya que fue necesario configurar Nginx para redireccionar un puerto y poder usar certificados SSL con la comunicación con el WebSocket, algo que trataré en un post aparte.
Hace unos días estuve hablando con un posible cliente sobre la posibilidad de realizar un pequeño juego de preguntas y respuestas que sería usado en el marco de una exposición.
Al final dicho proyecto no prosperó, por que luego no volví a ser contactado, pero de todas formas me parecía una buena escusa para realizar un pequeño proyecto en Phaser Framework.
El juego se puede dividir en 3 vistas o escenas, en la primera contamos con una ruleta que se acciona con un botón, la cual al detenerse se selecciona una categoría luego de lo cual se pasa a la siguiente escena.
En donde se presenta una pregunta al azar correspondiente a la categoría seleccionada con 4 posibles respuestas, una vez seleccionada la respuesta, se pasa a la siguiente escena.
La cual muestra si la pregunta se contestó de forma correcta y el detalle de la respuesta, luego de lo cual se vuelve a la pantalla inicial.
La estructura del proyecto
La estructura básica de archivos y directorios que definí para el juego fue la siguiente:
Assets
audio
bien1.mp3: Respuesta correcta
click1.mp3: Sonido de ruleta.
mal1.mp3: Respuesta incorrecta
wclick1.mp3: Botón presionado
imagen
areapreg.svg: Fondo para texto de pregunta
b1.svg – b4.svg: Botones para elegir opción.
btn_sig.svg: Botón siguiente.
btn_tirar.svg: Botón para girar ruleta.
ruleta.svg: Imagen de la ruleta.
select_ruleta.svg: Imagen del selector de categoría.
js
boton.js: Usado para definir el comportamiento de todos los botones.
display.pregunta.js: Define el comportamiento del display de preguntas.
elemento.juego.js: Código común a todos los elementos del juego.
general.js: Usado para definición de funciones auxiliares, como la de conversión de ángulos.
listado.preguntas.js: La lista de preguntas en si misma.
pantalla.preguntas.js: Se define el comportamiento de la vista de pregunta.
pantalla.respuestas.js: Define el comportamiento de la vista de respuesta.
pantalla.ruleta.js: Define el comportamiento de la vista de la ruleta.
principal.js: Crea la instancia general del juego, define las vistas y gestiona la precarga.
puntuador.js: Por ahora no se usa, a futuro para manejar puntajes.
ruleta.js: Define el comportamiento de la ruleta.
index.html: Importa todos los scripts.
La ruleta
La ruleta consiste en una imagen circular, y al igual que el resto, se trata de una imagen vectorial.
Las categorías están definidas en un arreglo de configuración, de la siguiente forma:
Estando ordenadas de forma antihoraria con respecto a como se definen en la imagen, el parámetro color e id los agregue pensando a futuro, ya que por el momento están hardcodeadas, pero si a futuro hago un backend en donde puedan definirse las mismas, son parámetros necesarios.
La aleatoriedad
Para que la misma se detenga en cada tirada en una posición diferente, definí dos propiedades: aceleración y velocidad que toman valores aleatorios con cada tiro.
El efecto de giro por el cual comienza girando rápido y luego reduce su velocidad hasta detenerse, no es algo complicado, por ejemplo por cada frame se ejecuta el siguiente código:
update(){
this.phaserSprite.angle += this.velocidad;
this.velocidad += this.aceleracion;
if (this.velocidad < 0){
this.velocidad = 0;
if (!this.resultado_entregado){
this.resultado_entregado = true;
this.ultimo_resultado = this.getResultado();
this.ultima_pregunta = this.listado_preguntas.getPregunta( this.ultimo_resultado );
this.callback_resultado();
}
} else {
//Se hace el sonido de la ruleta
this.click_cnt = Math.round(this.phaserSprite.angle/this.intervalo_subdivision);
if (this.click_cnt != this.click_cnt_ant){
this.click_cnt_ant = this.click_cnt;
this.juego.sound.play('click_ruleta');
}
}
}
Obtención del resultado
El resultado de la categoría en la cual se detiene la ruleta se obtiene por medio del ángulo en la cual queda al detenerse, el único problema al que me enfrenté es que Phaser define el valor del ángulo de los sprites entre 0 y 180 y luego entre -180 y 0, cuando lo lógico ubiera sido que el mismo se defina entre 0 y 360.
Para suplirlo agregué un par de funciones que se encargan de convertir el valor de los ángulos:
function anguloComunAPhaser( angulo ){
if (angulo > 180){
return 360 - angulo;
}
return angulo;
}
function anguloPhaserAComun( angulo ){
if (angulo < 0){
return angulo + 360;
}
return angulo;
}
Para saber en que valor de ángulo comienza y finaliza la categoría, luego de crear la ruleta agregué un for que se encarga de definir los valores de los mismos en el arreglo de categorías:
this.intervalo_subdivision = 360/this.config.categorias.length;
//Se asignan los valores de angulos a las categorias
//Se usan angulos de 0 a 360 como seria lògico
for (let c=0; c < this.config.categorias.length; c++){
this.config.categorias[c].a_i = c*this.intervalo_subdivision;
this.config.categorias[c].a_f = (c+1)*this.intervalo_subdivision;
}
Por lo que una vez que ya están definidos dichos valores, la categoría se obtiene facilmente:
getResultado(){
//Se le suma 90º por que el selector esta arriba, no a la derecha de la ruleta
let pos = anguloPhaserAComun( this.phaserSprite.angle + 90 );
this.ultimo_resultado = null;
for (let c=0; c < this.config.categorias.length; c++){
if ( numeroEntre(pos, this.config.categorias[c].a_i, this.config.categorias[c].a_f) ){
return this.config.categorias[c];
}
}
return null;
}
Allí también llamo a una función que se encarga de verificar si el angulo obtenido se encuentra entre dos números sin importar si num1 es mayor a num2 o viceversa (inicialmente la definí así por la forma en la que Phaser define los ángulos):
function numeroEntre(n, num1, num2){
if (num2 < num1){
let aux = num2;
num2 = num1;
num1 = aux;
}
if (n >= num1 && n <= num2){
return true;
}
return false;
}
Obtención de la preguntas:
Las preguntas por el momento están definidas en un arreglo, también hardcodeadas como las categorías, aunque a futuro podría guardarlas en una base de datos y consultarlas vía API.
Creo que lo único interesante de las mismas es que para hacer que no se repitan lo más simple fue borrarlas del arreglo cada vez que saliera una nueva elegida.
Mostrar mensajes de error con alerts, no es lo mejor, por que por ej si el usuario los tiene deshabilitados nunca se podrán ver y por que no son personalizables y el navegador decide que mostrar.
Por lo cual hoy actualicé el código para reemplazar dichos mensajes por un modal que indica el error en si.
Anteriormente también se agregaron los metatags de OpenGraph para que pudiera mostrarse una mejor previsualización del enlace al compartirlo.
En el proyecto del generador online de contraseñas, ya se agrega la posibilidad de generar nuevas a partir de la API pública de Greenborn de palabras al azar.
Bien, anteriormente he desarrollado un pequeño proyecto de generador de contraseñas, que surgió con la necesidad de utilizar una herramienta propia para dicho propósito y brindar un servicio más en el servidor de Greenborn.
Y la siguiente mejora de dicho proyecto consiste en la generación de contraseñas basadas en palabras.
Para hacer posible dicha mejora se hace necesario contar con una base de datos de palabras (en principio en español) y la posibilidad de consultar una al azar.
Claro que también se podría utilizar para otros propósitos.
Y ya que la API necesitaría contar con un end-point público, creo que también estaría bueno brindar la documentación necesaria para hacer posible su uso por parte de cualquier persona, para el propósito que considere necesario.
¿Cómo se usa?
Su uso es muy sencillo, sólo hace falta hacer una petición GET de la siguiente URL:
A lo cual la petición respondería con una palabra al azar.
También se puede especificar si el resultado debe devolverse en JSON o XML de acuerdo al header content-type pudiendo definirse en application/json o application/xml.
Además si en la URL se agrega el parámetro ?c=x, siendo x un número entre 1 y 100, se puede especificar la cantidad de palabras a retornar.
Condiciones de uso y Limitaciones
No se ofrece ninguna garantía con respecto a cualquier tipo de aspecto del servicio ofrecido.
El usuario será responsable de cualquier tipo de uso que le de a la API, desligándose Greenborn de cualquier tipo de responsabilidad sobre el mismo.
El uso GRATUITO del servicio está limitado a un máximo de 500 consultas por hora, por dirección IP.
Cualquier aspecto de las condiciones de uso, podrá ser modificado sin previo aviso, al igual que no se garantiza la disponibilidad del servicio (salvo que acordemos el pago por cuota de uso).
Actualmente el dataset es reducido y se ampliará con el tiempo.
Ampliación del dataset
La base de datos de palabras no se cargará a mano, de hecho se realiza de forma semiautomática.
A partir de la carga de archivos de texto plano de libros en español, a lo cual se llegó alrededor de 40k palabras (existiendo variaciones que agregan mayúsculas y minúsculas).
En este caso los libros en español de donde se obtuvieron las palabras, provienen de: http://www.libroteca.net/
Anteriormente ya había programado un simple generador de contraseñas y desde el principio la idea es no abandonarlo e ir agregando mejoras a medida que pasa el tiempo, de acuerdo a las posibilidades.
Por lo que hoy se agregaron las siguientes actualizaciones:
Mejora en presentación de botones y texto informativo: Ahora se usan los botones propios de Bootstrap y se le asignan colores de forma que sean más fáciles de identificar.
Se agrega posibilidad de modificar los símbolos, números y letras utilizados en la generación de contraseñas.
Se agrega enlace de sistema de reporte de errores: El enlace redirige a un sistema propio de Greenborn en el cual se sistematizan los diferentes bugs detectados por los usuarios.
Posibles mejoras a futuro
Algo que estoy pensando agregar en un futuro cercano, es la posibilidad de generar contraseñas a partir de palabras concatenadas.
De forma que se podría generar una contraseña que ademas de ser larga, pueda ser fácil de recordar.
Por ej. es claro que una contraseña al estilo: “RUYI6Bw41}e#X6W8p2&s33l6w@jG64Bs/26~” es mucho más difícil de recordar que otra al estilo: “CEIBO.GUAYMALLEN.PILI.BARCO.TAMIKÉN.PARLA”.
¿Que tan segura son las contraseñas basadas en palabras?
En este caso, la combinatoria estaría de nuestro lado si las palabras se eligen desde una gran base de datos.
Por ej. si se tuvieran registradas 10.000 palabras, y se quisieran saber todas las posibles contraseñas a generar con:
1: 10.000 posibilidades.
2: 100.000.000 posibilidades.
3: 1.000.000.000.000 posibilidades.
5: 100.000.000.000.000.000.000 posibilidades.
Básicamente la cantidad de combinaciones se calcula de la forma:
#pass = #palabras_elegibles ^ cantidad_palabras
Siendo #pass la cantidad máxima de combinaciones, #palabras_elegibles la cantidad de palabras disponible en la base de datos elevado a la longitud de la contraseña expresada en cantidad_palabras.
Adicionalmente se podría definir la posibilidad de crear una contraseña mixta, es decir una en la cual se puedan intercalar palabras con las opciones anteriores ya definidas.
No se trata de ningún invento
Las contraseñas basadas en palabras, se usan mucho por ej. en las billeteras de cripto monedas (Wallets) como una de las opciones de recuperación de la cuenta, ya que si de diera el caso de que se borrara la configuración del wallet se pudiera recuperar su acceso a partir de dicha clave.
Y también es claro que se pueden usar en otros sistemas que requieran el uso de contraseñas, aunque tal vez su mayor limitante sea la longitud del texto generado, ya que existen páginas que limitan la longitud de las claves, y en otras ocasiones el límite no siempre es explicito.