Similar Posts
Laboratorio: eliminar HTML en cajas de texto con jQuery
Trasteando un poco con jQuery me ha dado por hacer una función que elimina las etiquetas HTML de una caja de texto de un formulario. Esto puede ser necesario cuando no se quiere que se introduzca HTML en un campo. Lógicamente, esto es la parte cliente, en el servidor debería haber una función similar que lo hiciera.
La función es sencilla, en cada input:text añadimos el evento change para que cuando se modifique el contenido, se ejecute la función de strip_tag, la cual crea un elemento DIV auxiliar, hago que su contenido sea el valor de la caja de texto, me quedo son con el texto e inserto este valor en el input:
$('document').ready(function() {
$(':text').change(function() {
$(this).attr('value', $('document').add('<div></div>').html($(this).attr('value')).text());
});
});
¿Dudas?, alguna. Si entre el texto pongo un <script>alert(1);</script> me ejecuta el script, algo no muy elegante. ¿Qué pasa con el elemento DIV creado?, ¿se queda en el limbo o se destruye?.
Bueno, para mis primeras pruebas con jQuery no está mal del todo.
Laboratorio: input password estilo iPhone con jQuery
Una de las cosas que mas me gusta del iPhone/iPod Touch es que cuando estás metiendo una password ves el último carácter que has tecleado.
Por ello he hecho este pequeño plugin para jQuery (inacabado) que realiza la misma función. Muestra la última letra tecleada y oculta el resto. Para conseguirlo lo que he hecho es transformar el input en tipo text y guardar lo que se va tecleando.
$.fn.hidder = function() {
return this.filter(':password').each(function() {
this.config = {
delay: 1,
value: '',
char: '•'
}
this.type = "text";
this.config.value = this.value;
this.value = this.value.replace(/./g, this.config.char);
$(this).bind('keydown', function(evt) {
switch(evt.which) {
case 8:
this.config.value = this.config.value.substring(0, this.config.value.length-1);
this.value = this.value.substring(0, this.value.length-1);
break;
}
});
$(this).bind('keyup', function(evt) {
if (this.value.length > this.config.value.length) {
var last = this.value.substring(this.value.length-1);
this.config.value += last;
this.value = this.value.substring(0, this.value.length-1).replace(/./g, this.config.char)+last;
var elem = this;
setTimeout(function() {elem.value = elem.value.replace(/./g, elem.config.char);}, elem.config.delay*1000);
}
});
});
}
Le faltan muchas cosas por hacer, y fallan otras, por ejemplo tratar el pulsar los cursores, restaurar el valor antes del submit del formulario, ocultar el texto despues del formulario, …
Yo personalmente no lo usaría en mi página ni loco, pero para experimento no está mal.
Hapi.js + Vue.js llamadas autenticadas al servidor con JWT (login)
Hoy toca realizar conexiones con el servidor usando llamadas autenticadas. Para eso usaremos JWT (JSON Web Token), que viene a ser el envío de un token en cada llamada que necesita autenticación. El token está formado por tres cadenas codificadas en base64 y separadas por un punto (header.payload.signature). En el payload se envía toda la info que queramos, pero sin pasarse porque el token no debería ser muy largo y sobre todo sin enviar información sensible, porque el token no está encriptado, es por ello que la comunicación navegador/servidor debe ser mediante HTTPS. Si quieres una explicación más detallada, aquí te lo explican mejor.
En el payload vamos a guardar varios claims, entre ellos el username y un uuid que usaremos para validar que el usuario es correcto. JWT asegura que el token es válido, pero no viene mal añadir cierta seguridad. Cuando el usuario se loguea un nuevo uuid es generado, por lo que se comprobará si coinciden para el username enviado.
Ya tenemos la explicación teórica, ahora vamos con la parte de programación, primero la parte servidor y luego el frontend.
Lo primero que tenemos que hacer es añadir un nuevo plugin a hapi.js que se encargue de la autenticación, registrando un nuevo strategy a server.auth que use JWT. El plugin hapi-auth-jwt2 se encargará de toda la autenticación JWT y añadiremos una capa extra de validación comprobando que el username y el uuid coinciden.
/**
* Auth controller
*
* It uses JWT
*/
const jwt2 = require( 'hapi-auth-jwt2' );
const User = require( '../models/users' );
const validate = async function( decoded ) {
const user = await User.findByUUID( decoded.id );
return { isValid: !! user && user.username === decoded.username };
};
exports.plugin = {
name: 'auth',
register: async function( server, options ) {
await server.register( jwt2 );
server.auth.strategy( 'jwt', 'jwt',
{
key: options.jwt.secret,
validate: validate,
verifyOptions: { algorithms: [ 'HS256' ] }, // pick a strong algorithm
}
);
server.auth.default( 'jwt' );
},
};
En el modelo user añadiremos 2 nuevos métodos estáticos, que permitirán encontrar usuarios por username/email y por uuid:
userSchema.static( 'findByUserOrEmail', async function( username, email ) {
const result = await new Promise( ( resolve, reject ) => {
this.model( 'User' )
.findOne( {
$or: [
{ username: username },
{ email: email },
],
} )
.exec( ( error, data ) => {
if ( error ) {
reject( error );
}
resolve( data );
} );
} );
return result;
} );
userSchema.static( 'findByUUID', async function( uuid ) {
const result = await new Promise( ( resolve, reject ) => {
this.model( 'User' )
.findOne( {
uuid,
} )
.exec( ( error, data ) => {
if ( error ) {
reject( error );
}
resolve( data );
} );
} );
return result;
} );
En el manifest de Glue hay que añadir la configuración para CORS, ya que el token viajará usando la cabecera Authorization:
const manifest = {
server: {
port: Config.get( '/server/port' ),
routes: {
cors: {
origin: [ '*' ],
credentials: true,
exposedHeaders: [ 'Authorization' ],
},
},
},
};
Y en la configuración general del servidor hay que añadir una cadena que se usará para encriptar el signature del token JWT.
const config = {
auth: {
jwt: {
secret: process.env.JWT_SECRET,
},
},
};
Y por último añadimos una nueva ruta para autenticar (login) el usuario, comprobamos que el usuario y la contraseña coinciden, y si es así, creamos un uuid que guardamos en la bd y generamos el JWT y lo enviamos en la cabecera Authorization:
/**
* Authenticates an user
*/
server.route( {
method: 'POST',
path: '/user/auth',
options: {
tags: [ 'api', 'user', 'auth' ],
description: 'Server authenticate user',
notes: 'Server authenticate user',
auth: false,
validate: {
payload: {
username: Joi.string().alphanum().min( 3 ).max( 20 ).required(),
password: Joi.string().min( 8 ).required(),
},
},
},
/**
* Route handler
*
* @param {object} request
* @param {object} h Hapi object
* @returns {object}
*/
handler: async( request, h ) => { // eslint-disable-line
try {
const user = await User.findByUserOrEmail( request.payload.username, request.payload.email );
if ( ! user ) {
return Boom.badData( 'User or password incorrect' );
}
const isValidPassword = await bcrypt.compare( request.payload.password, user.password );
if ( ! isValidPassword ) {
return Boom.badData( 'User or password incorrect' );
}
const claims = {
id: uuid(),
exp: new Date().getTime() + ( 180 * 24 * 60 * 60 * 1000 ), // 3 months
username: user.username,
};
user.uuid = claims.id;
user.save();
const token = JWT.sign( claims, Config.get( '/auth' ).jwt.secret ); // synchronous
return h.response( {
response: true,
message: 'Check Auth Header for your Token',
} ).header( 'Authorization', token );
} catch ( error ) {
return Boom.badImplementation( 'Error', { error } );
}
},
} );
Vale, ya tenemos la parte del servidor, ahora la parte del frontend. Primero es añadir las rutas para /login, /account y /logout. He añadido un meta que indica si la ruta tiene que ser obligatoriamente autenticada, obligatoriamente no autenticada o como sea. Para ello, para cada ruta comprobará si el JWT token está almacenado o no y según el meta redirigirá a home o continuará:
import Vue from 'vue';
import Router from 'vue-router';
import Home from '@/js/components/Home';
import Login from '@/js/components/Login';
import Account from '@/js/components/Account';
import Logout from '@/js/components/Logout';
import Storage from '@/js/utils/storage';
Vue.use( Router );
const router = new Router( {
mode: 'history',
routes: [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/login',
name: 'Login',
component: Login,
meta: {
auth: false,
},
},
{
path: '/account',
name: 'Account',
component: Account,
meta: {
auth: true,
},
},
{
path: '/logout',
name: 'Logout',
component: Logout,
meta: {
auth: true,
},
},
],
} );
router.beforeEach( ( to, from, next ) => {
if ( !! to.meta ) {
const storage = new Storage();
const jwtToken = storage.getJWTToken();
// Can't be logged
if ( to.meta.auth === false ) {
if ( jwtToken ) {
next( '/' );
}
// Must be logged
} else if ( to.meta.auth === true ) {
if ( ! jwtToken ) {
next( '/' );
}
}
next();
} else {
next();
}
} );
export default router;
Para gestionar el almacenamiento en el navegador en vez de cookies vamos a usar sessionStorage y localStorage para guardar el token JWT. Como el formulario de login permite recordar la sesión, vamos a usar ambos storages. Si no se recuerda usaremos sessionStorage, que se borrará cuando se cierra el navegador, en caso contrario usaremos localStorage.
import Config from '@/js/config';
/**
* API methods for sesion/local storage.
* Depending on `this.session` it saves only on `sessionStorage` or also in `localStorage`
*
* @since v0.8.0
*/
class storage {
/**
* Constructor
*
* @param {boolean} session If stored only in session
*/
constructor( session = false ) {
this.session = session;
}
/**
* It saves the token in the session (and local storage is this.session === false),
*
* @param {strig} token JWT token
*/
setJWTToken( token ) {
sessionStorage.setItem( Config.jwt.storageKey, token );
if ( ! this.session ) {
localStorage.setItem( Config.jwt.storageKey, token );
}
}
/**
* It gets a value from session storage or in local if session = false
*
* @returns {string}
*/
getJWTToken() {
const sessionValue = sessionStorage.getItem( Config.jwt.storageKey );
if ( sessionValue ) {
return sessionValue;
}
if ( ! this.session ) {
const storedValue = localStorage.getItem( Config.jwt.storageKey );
return storedValue;
}
return null;
}
/**
* Removes JWT token from session and local storage
*/
removeJWTToken() {
sessionStorage.removeItem( Config.jwt.storageKey );
localStorage.removeItem( Config.jwt.storageKey );
}
}
export default storage;
También hemos creado una librería para tratar las llamadas a la API. Hay dos métodos, uno para autenticar el usuario (login) que mirará la cabecera Authorization, y otro método que obtiene los datos del usuario actual realizando una llamada autenticada enviando el JWT token en la cabecera Authorization:
import Config from '@/js/config';
import Storage from '@/js/utils/storage';
/**
* API backend methods
*
* @since 0.8.0
*/
class apiFetch {
/**
* Authenticate an user, if ok, JWT token is sent by the server in Authorization header
*
* @param {string} username User name
* @param {string} password Password
*
* @returns {Promise}
*/
auth( username, password ) {
return fetch( Config.api.user.auth, {
method: 'POST',
body: JSON.stringify(
{ username, password }
),
mode: 'cors',
} ).then( response => {
const auth = response.headers.get( 'Authorization' );
if ( auth ) {
return {
response: true,
token: auth,
};
}
return {
response: false,
message: 'Username or password not valid',
};
} );
}
getUser( username ) {
return fetch( `${ Config.api.user.get }${ username }`, {
method: 'GET',
headers: {
Authorization: new Storage().getJWTToken(),
},
} ).then( response => response.json() );
}
}
export default apiFetch;
Ahora solo faltan los controladores para login, account y logout. Login realizar la llamada al servidor y si se obtiene el JWT se guarda:
<template>
<section class="section">
<div class="container">
<h1 class="title">
Login
</h1>
<div
v-if="error"
class="columns is-centered has-margin-bottom-2"
>
<b-notification
class="column is-7-tablet is-6-desktop is-5-widescreen"
type="is-danger"
has-icon
aria-close-label="Close notification"
role="alert"
size="is-small "
>
{{ error }}
</b-notification>
</div>
</div>
<div class="container">
<div class="columns is-centered">
<div class="column is-5-tablet is-4-desktop is-3-widescreen has-background-light login-form">
<b-field label="Username">
<b-input
v-model="username"
value=""
maxlength="30"
icon="account-circle-outline"
/>
</b-field>
<b-field label="Password">
<b-input
v-model="password"
value=""
type="password"
icon="lock-outline"
/>
</b-field>
<div class="field">
<b-checkbox v-model="remember">
Remember me
</b-checkbox>
</div>
<div class="has-text-right">
<b-button
type="is-primary"
@click="submit"
>
Log in
</b-button>
</div>
</div>
</div>
</div>
</section>
</template>
<script>
import ApiFectch from '@/js/utils/api';
import Storage from '@/js/utils/storage';
export default {
name: 'Login',
// Form data and error messages if login fails
data() {
return {
username: '',
password: '',
remember: false,
error: '',
};
},
methods: {
// It logs in, using the backend API for authenticate the user data.
// If user logs in, it saves the JWT token in the browser. If not, shows error message.
submit: function() {
const api = new ApiFectch();
api.auth( this.username, this.password )
.then( response => {
const storage = new Storage( ! this.remember );
if ( !! response.response && !! response.token ) {
storage.setJWTToken( response.token );
this.error = false;
// `go` refreshes the page, so user data is updated
this.$router.go( '/' );
} else {
storage.removeJWTToken();
this.error = response.message;
}
} );
},
},
};
</script>
<style lang="scss" scoped>
.login-form {
border-radius: 4px;
}
</style>
Logout borra los datos JWT del navegador y redirige a home:
<template>
<section class="hero is-medium is-primary is-bold">
<div class="hero-body">
<div class="container has-text-centered">
<h1 class="title">
{{ message }}
</h1>
<h2 class="subtitle">
Miss you 💛
</h2>
</div>
</div>
</section>
</template>
<script>
import User from '@/js/utils/user';
export default {
name: 'Logout',
// Dummy data
data() {
return {
message: 'Bye',
};
},
// After being created it logs out and go to home
created() {
new User().logout();
// `go` instead of `push` because refreshing the page updates the user data
// Maybe using vuex is a better way to do it, or not...
this.$router.go( '/' );
},
};
</script>
Y Account recupera los datos del usuario una vez que ha creado el controlador:
<template>
<div id="account">
<section class="hero is-primary is-bold">
<div class="hero-body">
<div class="has-text-centered">
<h1 class="title">
{{ message }}
</h1>
<h2 class="subtitle">
Your data
</h2>
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="tile is-ancestor">
<div class="tile is-parent">
<p class="tile is-child notification">
Some content
</p>
</div>
<div class="tile is-8 is-parent">
<div class="tile is-child notification is-info">
<ul id="data">
<li
v-for="( value, key ) in user"
:key="key"
>
{{ key }} : {{ value }}
</li>
</ul>
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<script>
// Dummy component
import ApiFectch from '@/js/utils/api';
import User from '@/js/utils/user';
export default {
name: 'Account',
data() {
return {
message: 'Account',
user: {},
};
},
created() {
const user = new User().getCurrentUser();
new ApiFectch().getUser( user.username )
.then( response => this.user = response );
},
};
</script>
Recuerda que tienes todo el código aquí
Incluye sonido en tu web
Creo que no es necesario decir que cualquier exceso es malo, y que tampoco hay que demonizar ni al Flash ni al sonido en una web (esto me cuesta decirlo, pero es cierto). Pero en el caso de que queramos incluir sonido a nuestra página web y controlarlo mediante Javascript, si nos puede ser útil esta librerÃa.
Se trata de Javascript Sound Kit, una clase Javascript que envuelve a un objeto ActionScript, en el cual se carga el sonido, que puede ser controlado por Javascript. Su uso parece bastante sencillo:
var mysound = new Sound();
mysound.loadSound("sonido.mp3", true);
mysound.setVolume(30);
Para aquellos que quieran criticar el uso ya sea de Flash o de sonido en la web (tienen todo mi apoyo), quizás les interese un ejemplo en el que puede ser útil, cuando tengo abierto GMail todo el rato, me avisa si llega un mensaje nuevo mediante un bip. Claro, que no lo necesito porque en Firefox tengo instalada una extensión que ya hace eso, pero no todo el mundo usa Firefox y no todo el mundo que usa Firefox tiene instalada la extensión para GMail.
VÃa / menéame
Evita el uso de eval en Javascript
Buen truco, o mejor dicho, implementación, para crear un objeto en Javascript en el que las funciones son referenciadas mediante un string. Puede ser muy útil cuando tenemos en un string el nombre de la función que queremos ejecutar, pero no queremos usar eval.
Para ello lo que creamos es un array de funciones, y el array, implementado como una tabla hash (los indices son strings), hace referencia a una función.
var obj = {
funciones : new Array(),
creaMetodo : function(nombre, fn) {
this.funciones[nombre] = fn;
},
ini : function() {
this.creaMetodo("prueba", function(){alert(1);});
}
}
Ahora podremos referenciar a la función ejecutando:
obj.funciones['prueba']();
VÃa / Scriptia
Hapi.js+Vue.js reorganizar la configuración del servidor
Algo bastante importante en un proyecto es la configuración y cómo se gestiona. Para facilitar la gestión usaremos dos librerías dotenv y confidence, la primera permite usar ficheros .env en nuestro entorno de desarrollo para simular variables de entorno. La segunda nos ayudará a recuperar las variables de un objeto permitiendo usar filtros, por ejemplo según de las variables de entorno.
Instalaremos los paquetes:
npm i dotenv
npm i confidence
Confidence necesitará un criterio que nos permitirá obtener distintos resultados según su valor. Imaginemos que tenemos el siguiente criterio:
const criteria = {
env: 'development',
};
Y estos datos de configuración:
{
debugLevel: {
$filter: 'env',
development: INFO,
production: ERROR,
},
}
Si queremos acceder al nivel de debug, al ser env igual a development, obtendíamos INFO.
Vale, ¿y cómo lo usamos en el proyecto? Primero creamos una carpeta config, donde crearemos el fichero index.js que tendrá toda la configuración del servidor:
const Confidence = require( 'confidence' );
const Dotenv = require( 'dotenv' );
Dotenv.config( { silent: true } );
// NODE_ENV is used in package.json for running development or production environment
const criteria = {
env: process.env.NODE_ENV,
};
const config = {
port: 3001,
};
const store = new Confidence.Store( config );
exports.get = function( key ) {
return store.get( key, criteria );
};
exports.meta = function( key ) {
return store.meta( key, criteria );
};
Dotenv simplemente se usa para obtener de las variables de entorno de servidor el valor de NODE_ENV. Por ahora solo tendremos la variable port, pero ya estará preparado para poder añadir otras variables de configuración posteriormente.
Creamos un store de Confidence y exportaremos los métodos get y meta.
Haremos algo parecido para el manifest necesario para Glue, creando el fichero manifest.js dentro del directorio config:
const Confidence = require( 'confidence' );
const Config = require( './index' );
const criteria = {
env: process.env.NODE_ENV,
};
const manifest = {
server: {
port: Config.get( '/port' ),
},
};
const store = new Confidence.Store( manifest );
exports.get = function( key ) {
return store.get( key, criteria );
};
exports.meta = function( key ) {
return store.meta( key, criteria );
};
Como se puede apreciar fácilmente obtenemos el valor de port de forma bastante simple.
Y por último modificamos el fichero index.js para hacer eso de estos nuevos ficheros:
const Glue = require( '@hapi/glue' );
const Manifest = require( './config/manifest' );
const options = {
relativeTo: __dirname,
};
const startServer = async function() {
try {
const manifest = Manifest.get( '/' );
const server = await Glue.compose( manifest, options );
await server.start();
console.log( 'hapi days!' ); // eslint-disable-line
} catch ( err ) {
console.error( err ); // eslint-disable-line
process.exit( 1 );
}
};
startServer();
Puedes bajarte el código aquí