Creación de chat con WS + NodeJS + VueJS

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

expressWs.getWss().on('connection', function(ws) {
  ws['id_conexion'] = uuid.v4()
});

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:

clientes.forEach(cliente => {
    cliente.send(JSON.stringify({
      accion: 'reporte_online',
      reporte: reporte_conectados
    }))
  })

El Frontend

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.

Por lo que el template nos quedaría así:

<template>
  <div class="row">
    <div class="col p-3">

      <div class="row">
        <div class="col mensaje-cont">

          <div class="row" v-for="(msg) in mensajes" :key="msg">
            <div class="col">
              <span class="badge" :class="{ 'bg-secondary':msg.autor.id != datos_usuario.id, 'bg-primary': msg.autor.id == datos_usuario.id }">{{msg.autor.nombre}}:</span> {{msg.texto}}
            </div>
          </div>

        </div>

        <div class="col-2">
          <div claSS="row conectados-tit">
            <div class="col">
              Conectados ({{online.length}})
            </div>
          </div>
          <div class="row" v-for="(user) in online" :key="user">
            <div class="col">
              {{user}}
            </div>
          </div>
        </div>
      </div>

      <div class="row mt-3">
        <div class="col">
          <b-form-input
                id="input-msg"
                v-model="mensaje.texto"
                type="text" placeholder="Mensaje"
                required></b-form-input>
        </div>

        <div class="col-auto">
          <b-button variant="success" @click="enviarMensaje()">Enviar</b-button>
        </div>
      </div>
     
    </div>
  </div>  

  <div class="app-modal h-100" v-if="modal.mostrar">
    <div class="row h-100">
      <div class="mt-auto mb-auto col-12 col-sm-8 offset-sm-2 col-md-6 offset-md-3 col-lg-4 offset-lg-4">

        <div class="card">
          <div class="card-header">
            <h5 class="card-title">Ingrese el nombre a mostrar</h5>
          </div>
          <div class="card-body">
            
            <div class="row mb-3">
              <div class="col">
                <b-form-input
                  id="input-nombre"
                  v-model="modelo_registro.nombre"
                  type="text" placeholder="Nombre"
                  required></b-form-input>
              </div>
            </div>

            <div class="row">
              <div class="col">
                <b-button variant="success" @click="registrarse()">Registrarse</b-button>
              </div>
            </div>
  
          </div>
        </div>

      </div>
    </div>
  </div>
</template>

El código JS encargado de establecer la conexión también es sencillo:

<script setup>
  import { ref, onMounted } from 'vue';

  const mensajes  = ref([])
  const conexion  = ref({})
  const online    = ref([])

  const mensaje = ref({
    texto: '',
    accion: 'mensaje',
    autor: {}
  })

  const datos_usuario = ref({
    id: -1,
    nombre: 'Juan Perez'
  })

  const modelo_registro = ref({
    nombre: ''
  })

  const modal = ref({
    mostrar: false
  })

  function registrarse(){
    let registro = {
      nombre: modelo_registro.value.nombre,
      accion: 'registro'
    }
    conexion.value.send( JSON.stringify( registro ));
  }

  function enviarMensaje(){
    if (mensaje.value.texto !== ''){
      mensaje.value.autor = datos_usuario.value
      conexion.value.send( JSON.stringify( mensaje.value ))
      mensaje.value.texto = ''
    }
  }

  onMounted(async ()=>{

    if (datos_usuario.value.id == -1){
      modal.value.mostrar = true
    }

    conexion.value = new WebSocket( process.env.VUE_APP_API_URL )
    
    conexion.value.onmessage = function(event) {
      let msgRec = null
      try {
        msgRec = JSON.parse(event.data)
      } catch (error) {
        console.log(error)
      }

      if (msgRec !== null){
        switch(msgRec.accion){
          case 'mensaje':
            mensajes.value.push( msgRec )
          break;

          case 'mensaje_sys':
            mensajes.value.push( {
              texto: msgRec.msg,
              accion: 'mensaje_sys',
              autor: {
                id: -1,
                nombre: 'Sistema'
              }
            } )
          break;

          case 'registro':
            datos_usuario.value = msgRec
            modal.value.mostrar = false
          break;

          case 'alerta':
            alert(msgRec.msg)
          break;

          case 'conectado':
            alert(msgRec.id)
          break;

          case 'reporte_online':
            online.value = msgRec.reporte
          break;
        }
        
      }

    }

    conexion.value.onopen = function(event) {
      console.log(event)
      console.log("Conectado a servidor de websocket...")
    }
  })
</script>

Creo que lo importante a destacar del mismo son:

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:

El código fuente está disponible en: https://github.com/Greenborn/chat_publico_back y https://github.com/Greenborn/chat_publico_front

También hay una demo funcional en: https://chat.greenborn.com.ar/

Dificultades

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.

Phaser3 Ruleta de preguntas

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.

Pueden verlo andando en: https://demos.greenborn.com.ar/ruleta_html5/

y el repo en: https://github.com/Greenborn/ruleta_html5

En que consiste el juego

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:

categorias:[
    { id:0, color: '#dfdd48', a_i:0, a_f:0, nombre: 'Mitología'   },
    { id:1, color: '#7d03ff', a_i:0, a_f:0, nombre: 'Deportes'    },
    { id:2, color: '#ff8203', a_i:0, a_f:0, nombre: 'Gastronomía' }, 
    { id:3, color: '#36dc22', a_i:0, a_f:0, nombre: 'Música'      },
    { id:4, color: '#FFFFFF', a_i:0, a_f:0, nombre: 'Ciencia'     },
    { id:5, color: '#0bace8', a_i:0, a_f:0, nombre: 'Política'    },
    { id:6, color: '#bf32b7', a_i:0, a_f:0, nombre: 'Cine'        },             
],

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.

tirar(){
        this.resultado_entregado = false;
        this.aceleracion = - ((Math.random() * 3)+3)/30;
        this.velocidad   = Math.floor(Math.random() * 30)+15;
    }

Efecto de giro y sonido

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.

Bueno eso es todo por ahora!

Actualización en Generador de Contraseñas

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.

Podes ver el generador de contraseñas en: https://demos.greenborn.com.ar/pass-generator/

Código fuente en: https://github.com/Greenborn/generador_contrase-as_web

Generador de contraseñas basado en palabras

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.

Puedes ver más información sobre la misma en: https://blog.greenborn.com.ar/2022/02/28/api-publica-de-palabras-al-azar/

Se agregaron las siguientes mejoras

  • Modificación del código hacia la programación orientada a objetos: Se mejora la programación del componente para poder hacerlo más fácil de mantener.
  • Se agrega posibilidad de generar contraseñas a partir del servicio de palabras al azar.
  • Se agrega imagen de fondo.

Podes usarlo en: https://demos.greenborn.com.ar/pass-generator/

Repositorio: https://github.com/Greenborn/generador_contrase-as_web

Mejora en Generador de Contraseñas

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.

Prueba de iluminación dinámica 2D

Hace un tiempo atrás, me interesé por el uso de iluminación dinámica 2D que sería utilizada como un recurso en la programación de juegos HTML5.

Claro, que hay varias opciones, pero la idea principal también es la de aprender mientras se hace, por lo que me propuse a realizar un motor de iluminación dinámica 2D.

Bueno la palabra “motor” a lo mejor le queda muy grande, más bien se trata de un experimento.

El ejemplo en cuestión

Puedes ver el código en: https://github.com/2012lucho/phaser-2d-dinamic-lights

Aquí mismo agrego un ej del mismo en funcionamiento:

En dicha demo, definí dos luces que permanecen inmóviles en su posición, una de ellas es de color verde la otra celeste; adicionalmente agregué tercera luz de color amarillo que se mueve siguiendo la posición del mouse.

¿Como generar el efecto de sombras?

Buscando la manera más sencilla y que a su vez consuma la menor cantidad de recursos posible, se me ocurrió que una buena forma de simular las luces y sombras sería el de utilizar una nueva imagen superpuesta, la cual funciona como si se tratara de una máscara (se le llamará así de ahora en adelante).

Inicialmente dicha imagen debería ocupar el tamaño total de la zona visible y estar siempre por encima de todos los objetos de la escena.

También todos sus pixeles serían negros y con la opacidad al 100%.

Luego se simula el trazado de los rayos de luz de cada una de las luces, de forma secuencial, de forma que cada una de ellas se dibuja en orden.

En cada posición en donde se determina que pasa el rayo de luz, se modifica el pixel correspondiente de la máscara redefiniendo su color y su valor de opacidad, de forma que dicho pixel ahora tenga cierto grado de transparencia, permitiendo ver así los objetos que se encuentran por debajo.

En las posiciones en donde los rayos de luz se superponen, se realiza la suma de sus colores en proporción a un valor correspondiente a la intensidad que tendría la luz en dicha parte del recorrido, para imitar su comportamiento natural.

También hay que tener en cuenta que no pretende ser una simulación 100% realista.

¿Por que se ve tan pixelado?

Seguro sea una de las primeras cosas que salta a la vista, bueno eso se debe a la forma en la cual funciona, por que para cada fotograma se deben hacer miles de cálculos para simular los trazos que realizarían los rayos de luz.

Esto si se hace en una GPU no supondría gran problema ya que están pensadas para ello, pero en este caso los cálculos los realiza un único núcleo de CPU.

Por lo que la forma más fácil que se me ocurrió, para que el sistema de iluminación no consuma grandes recursos, es el de bajar la resolución con respecto al cálculo de luces.

Por lo que la máscara en este caso es 5 veces menor que el área visible, y luego se re-dimensiona para que ocupe la totalidad de la superficie.

Así como también se redució la resolución de la simulación, también se puede definir que la misma no se realice en todos los fotogramas, de forma de minimizar aún más la carga de trabajo.

¿Y los obstáculos?

Sin obstáculos, la luz avanza en linea recta hasta el infinito, en este caso avanza hasta salirse de la pantalla o hasta chocar con un obstáculo.

Por lo que se hace necesario también definir una especie de “mapa de durezas”, por lo que se define una matriz del mismo tamaño que la cantidad de píxeles de la máscara.

Y la idea principal, es que todos aquellos objetos del juego que interactúen con la luz actualicen el mapa de durezas.

Indicando si se trata de un pixel opaco o no, aunque también se podría mejorar de forma de definir grados de transparencia, indice de refracción, etc.

Generador de contraseñas en Angular

Una de las tareas que siempre se presenta es la generación de contraseñas seguras para todos los sistemas tanto en producción como en desarrollo, y usaré esa necesidad propia como escusa para crear un pequeño proyecto angular de generación de contraseñas, y al mismo tiempo ofrecer un servicio más en la web Greenborn.

Manos a la obra

Repo en GitHub

Lo primero que necesitaremos es un repositorio de Github, claramente no es estrictamente necesario pero es una buena practica mantener el código versionado y en este caso está bueno para a quien le interese chusmear el proyecto, si recién comienza a trabajar con dicho Framework.

Enlace a repositorio: https://github.com/Greenborn/generador_contrase-as_web

Prerequisitos

Antes de comenzar es necesario tener instalado:

  • NodeJS en su última versión estable.
  • NPM para la gestión de paquetes.
  • Angular-cli para el desarrollo del proyecto Angular, al proporcionar el comando ng
  • Git (opcional) para la gestión del repositorio.

Creación de proyecto base

El proyecto Angular base se deberá crear utilizado el comando ng de la siguiente manera:

$ ng new GeneraPass

Dicho comando preguntará si se desea utilizar el Router del Framework, a lo cual responderemos que si, ya que si bien puede que no lo necesitemos en un principio, luego puede ser que se necesite si se agregan varias páginas a la navegación.

Luego nos preguntará si deseamos usar CSS, SCSS, SASS o LESS, aquí no me complicaría mucho y elegiría CSS, igualmente cualquiera de las opciones anteriores son muy buenas.

Creación de proyecto angular satisfactoria

Bien, ya tenemos el proyecto generado, ahora solo resta probarlo, para lo cual es esencial ir al directorio del proyecto y ejecutar ng serve:

$ cd GeneraPass
$ ng serve

Si todo va bien, ya debería abrirse una nueva pestaña en el navegador predeterminado con el proyecto funcionando, si no es así hay que mirar en la terminal en que ubicación se sirve el proyecto, en general por defecto es: http://localhost:4200

Proyecto base funcionando correctamente

El comando ng serve se usa para montar un servidor de desarrollo el cual se encarga de actualizar el proyecto automáticamente, cada vez que se detectan cambios, de forma que siempre veremos la versión actualizada del proyecto.

Primeras modificaciones

Bien, ahora ya podremos modificar el proyecto a nuestra conveniencia

Lo primero que podremos hacer es cambiar el favicon, por el que utilizaríamos en el proyecto, para lo que hay que reemplazar el archivo:

src/favicon.ico

El siguiente paso es la eliminación del código del template base, osea la vista que vemos al iniciar el proyecto.

Para eso deberemos editar el archivo: src/app/app.component.html

En este caso borramos todo su contenido, por lo cual obtendremos una vista en blanco.

Elección de Framework de estilos

En este punto uno puede o no utilizar un Framework como Bootstrap, para el maquetado de las vistas, en este caso, la decisión es de utilizarlo, por lo cual lo agregaremos al proyecto con el siguiente comando:

$ ng add @ng-bootstrap/ng-bootstrap

Dicho comando ya nos instala todas las librerías necesarias, y nos deja todo listo para ya poder utilizar Bootstrap en las vistas!

Puedes ver el sitio oficial de ng-bootstrap en: https://ng-bootstrap.github.io

Otra librería que utilizaremos, será la de iconos de Bootstrap, la cual podremos instalar con el comando:

npm i ngx-bootstrap-icons

Recomiendo leer la documentación de la misma en: https://www.npmjs.com/package/ngx-bootstrap-icons

Una vez que la dependencia se haya instalado correctamente, procederemos a incluir la misma en el archivo: src/app/app.module.ts

Dicho archivo ahora debería verse así:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { GeneratePasswordComponent } from './components/generate-password/generate-password.component';
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

@NgModule({
declarations: [
AppComponent,
GeneratePasswordComponent
],
imports: [
BrowserModule,
AppRoutingModule,
NgbModule,
FormsModule,ReactiveFormsModule,
NgxBootstrapIconsModule.pick(allIcons)
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

En dicho Módulo, también se puede ver que se importan los módulos FormsModule, ReactiveFormsModule que son necesarios para poder bindear los inputs con el modelo.

Creación del componente PasswordGenerator

Los componentes son la base de cualquier proyecto Angular, y lo ideal sería separar lo mejor posible cada una de las funcionalidades del proyecto en componente que luego se puedan reutilizar.

En este caso por motivos de simplicidad y por que el proyecto en si no es muy complejo, se creará un solo componente.

Para crear nuevos componentes, lo mejor es usar el comando especifico para dicha función:

$ ng g c components/GeneratePassword

En dicho comando el argumento “g” indica que se debe generar un nuevo elemento, “c” indica que dicho elemento es un componente y components/GeneraPassword es la ruta en la cual se debe generar el mismo.

Componente generado correctamente, puede verse que también se especifica la modificación del archivo src/app/app.module.ts

Ahora ya podremos incluir dicho componente en cualquier vista, en el nuevo archivo src/app/components/generate-password/generate-password.component.ts se especifica el selector “app-generate-password“.

Lo que significa que podremos incluir dicho componente en cualquier vista referenciandolo de la siguiente manera:

<app-generate-password></app-generate-password>

Por ej lo incluiremos en src/app/app.component.html

Que es el archivo en el cual anteriormente borramos todo su contenido, por lo que ahora solo debería contener:

<div class="container-fluid">
     <app-generate-password></app-generate-password>
</div>

Vista de la página, luego de agregar dicho componente.

Maquetado de la vista

Ahora ya podremos modificar la vista, la cual se encuentra en el archivo: src/app/components/generate-password/generate-password.component.html

La cual actualmente solo contiene el siguiente código:

<p>generate-password works!</p>

Y lo reemplazaremos por el nuevo código HTML:

<div class="row"> 
<div class="col col-sm-8 offset-sm-4 col-lg-6 offset-lg-3 col-xxl-4 offset-xxl-4"> <div class="row mt-5">
<div class="col text-center">
<h2 class="title p-2">Generador de Contraseñas</h2>
</div>
</div>

<div class="row">
<div class="col mb-5"> <p>Puede crear una nueva contraseña segura, con nuestra herramienta On-line, solo complete el formulario y se generarán diferentes contraseñas aleatorias.</p>
</div>
</div>
<div class="row">
<div class="col m-3">
<div class="row mb-4"> <div class="col password-cont p-0">
<input class="p-2" id="passCont" value="{{password}}" placeholder="*************" />
</div>
<div class="col-auto p-2">
<i-bs name="clipboard" class="app-btn" (click)="copyToClipboard()"></i-bs>
<i-bs name="arrow-clockwise" class="app-btn" (click)="generatePassword()"></i-bs>
</div>
</div>
<div class="row options-content">
<div class="col">
<div class="row">
<div class="col text-center">
<h4 class="p-2">Opciones de generación Contraseña</h4>
</div>
</div>
<div class="row">
<div class="col-12 col-sm-6 mb-2">
<label for="longInput" class="form-label">Longitud</label>
<input [(ngModel)]="passwordOptions.long" type="number" min="0" class="form-control" id="longInput" placeholder="25">
</div>
</div>
<div class="row">
<div class="col-12 col-sm-6 mb-2">
<div class="form-check">
<input [(ngModel)]="passwordOptions.mayus" class="form-check-input" type="checkbox" value="" id="mayusInput" checked>
<label class="form-check-label" for="mayusInput"> Mayúsculas </label>
</div>
</div>
<div class="col-12 col-sm-6 mb-2">
<div class="form-check">
<input [(ngModel)]="passwordOptions.minus" class="form-check-input" type="checkbox" value="" id="minusInput" checked>
<label class="form-check-label" for="minusInput"> Minusculas </label>
</div>
</div>
<div class="col-12 col-sm-6 mb-2">
<div class="form-check">
<input [(ngModel)]="passwordOptions.symbol" class="form-check-input" type="checkbox" value="" id="simbolInput" checked>
<label class="form-check-label" for="simbolInput"> Símbolos </label>
</div>
</div>
<div class="col-12 col-sm-6 mb-2">
<div class="form-check">
<input [(ngModel)]="passwordOptions.numbers" class="form-check-input" type="checkbox" value="" id="numberInput" checked>
<label class="form-check-label" for="numberInput"> Números </label>
</div>
</div>

</div>
</div>
</div>
</div>
</div> </div>
</div>

Sobre el HTML, resaltaría los [(ngModel)] que definen el binding entre los valores de los inputs y el modelo (a definir) en donde se almacenarían los valores del formulario.

Y los (click) que definen la llamada a la función definida en el mismo, que será ejecutada al producirse el evento onClick.

Definición de modelo

La configuración usada para la generación de la contraseña se almacenará en un nuevo modelo que crearemos a tal propósito.

Para lo cual, dentro del directorio del componente, se creará un directorio models y dentro del mismo un archivo: password.options.ts

Dentro del cual se agregaría el siguiente contenido:


export class PasswordOptions {
    public long:number = 8;
    public mayus:boolean = true;
    public minus:boolean = true;
    public symbol:boolean = true;
    public numbers:boolean = true;
}

Programación del componente

Luego deberemos editar el archivo correspondiente al código del componente: src/app/components/generate-password/generate-password.component.ts

Dicho archivo nos quedaría de la siguiente manera:


import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { PasswordOptions } from './models/password.options';

@Component({
  selector: 'app-generate-password',
  templateUrl: './generate-password.component.html',
  styleUrls: ['./generate-password.component.css']
})
export class GeneratePasswordComponent implements OnInit {

  constructor() { }

  public password:string = '';
  public passwordOptions:PasswordOptions = new PasswordOptions();

  private passCont!: HTMLInputElement;

  ngOnInit(): void {
  }

  copyToClipboard(){
    if (this.password == '') {
      alert('Para poder copiar una contraseña primero es necesario generar una nueva');
      return false;
    }

    this.passCont = document.getElementById("passCont") as HTMLInputElement;
    this.passCont.select();
    document.execCommand('copy');
    alert("¡Contraseña copiada al portapapeles!");

    return true;
  }

  generatePassword(){
    if (this.passwordOptions.long < 0){
      alert('La longitud de la contraseña debe ser mayor a 0');
      return false;
    }

    if (!(this.passwordOptions.minus || this.passwordOptions.mayus || this.passwordOptions.numbers || this.passwordOptions.symbol)){
      alert('Es necesario elegir al menos una opcion de tipo de contraseña');
      return false;
    }

    if (this.passwordOptions.long >= 10000){
      alert('La cantidad de caracteres ingresada es demasiado grande!');
      return false;
    }

    this.password = '';
    for (let c=0; c < this.passwordOptions.long; c++){
      this.password += this.getPasswordChar();
    }

    return true;
  }

  getPasswordChar():string{
    //se genera un numero al azar entre 1 y 4, dicho numero representa el tipo de caracter a insertar
    let option:number = Math.round( Math.random() * (4 - 1) + 1 );
    let ch:string     = '';
    
    switch (option){
      case 1:
        if (!this.passwordOptions.mayus) { //si no se seleccionaron las mayusculas
          return this.getPasswordChar();
        }
        return this.getCaracter(this.letras).toUpperCase();
      break;

      case 2:
        if (!this.passwordOptions.minus) { //si no se seleccionaron las minusculas
          return this.getPasswordChar();
        }
        return this.getCaracter(this.letras);
      break;

      case 3:
        if (!this.passwordOptions.numbers) { //si no se seleccionaron numeros
          return this.getPasswordChar();
        }
        return this.getNumero();
      break;

      case 4:
        if (!this.passwordOptions.symbol) { //si no se seleccionaron simbolos
          return this.getPasswordChar();
        }
        return this.getCaracter(this.simbolos);
      break;
    }
    return ch;
  }

  private letras:string = 'abcdefghijklmnñopqrstuvwxyz';
  private simbolos:string= '!$%&/()=?^_-:;@*º|#~{[]}';
  getCaracter(chs:string):string{
    let chsCount  = chs.length -1;
    let chsSelect = Math.round( Math.random() * (chsCount - 0) + 0 );
    return chs[chsSelect];
  }

  private getNumero():string{
    return String(Math.round( Math.random() * (9 - 0) + 0 ));
  }

}

Sobre el código, podría resaltar algunas cuestiones, como por ej el prestar atención al ámbito de las variables, es decir si son de tipo private, solo podran ser accedidas desde dentro del componente y no se podrá hacer binding con la vista, puede funcionar mientras se realizan pruebas con ng serve, pero al hacer el ng build va a saltar un error indicando la situación.

Definicón de estilos

Y el archivo CSS de nuestro componente (src/app/components/generate-password/generate-password.component.css):


.title{
    background-color: rgb(176, 255, 131);
    font-weight: bolder;
}

.password-cont{
    border: 1px solid #555;
}

.password-cont input{
    width:100%;
    border:none;
    color: rgb(0, 43, 22);
    font-weight: bolder;
}

.options-content{
    box-shadow: 0px 1px 6px #888;
    border-radius: 0.2rem;
}

Hay que tener en cuenta que los estilos definidos en este archivo solo se aplicarán al componente en cuestión, lo ideal es que en el mismo no haya nada que aplique a varios componentes, en esa situación los estilos que pudieran repetirse deberìan definirse el style.css.

En el archivo CSS de estilos global del template (src/styles.css) deberemos agregar:


.app-btn{
    cursor: pointer;
    padding: .5rem;
    border: 1px solid #555;
}

.app-btn:hover{
    background-color: rgb(161, 192, 255);
}

La clase app-btn se define en el archivo de estilos globales, por que se prevee que pueda ser utilizada en caualquier sección del código de la APP, y se utiliza para definir el comportamiento mínimo del botón en cuanto a lo visual, definiendo su borde, el cursor y el mouse hover.

Probando

Si no hay ningún error, deberíamos ver la siguiente pantalla en el navegador:

Y bien, ya estaría terminado nuestro generador de contraseñas con Angular!

Generando el build

El último paso sería hacer el build del proyecto, para tener lista la versión optimizada para producción, por lo cual bastará con ejecutar el comando:

$ ng build

El cual generá dicha versión dentro del directorio: dist

https://demos.greenborn.com.ar/pass-generator/