Aplicaciones Web

http://slides.com/meneboni/aplicaciones-web

Unidad I: Programación para la Web

Unidad II: Backend

Unidad III: Frontend

Unidad IV: Deployment y Administración

Recursos

1.  Instalador de node.js

node -v

2.  Prettier - Formatea el código

Proceso

Flujo de trabajo

Mockup

Mockup

Mockup

Mockup

Arquitectura de la aplicación

Relación entre node y express

mkdir server
cd server
npm init
npm install --save express

Generar nuestra app con express

const express = require("express");
const app = express();

app.get("/", (req, res) => {
    res.send({ hola: 'Mundo' });
});

app.listen(5000);

// Desde la consola ejecutamos el comando: node index.js
// En el navegador escribimos http://localhost:5000/

Rutas en express (Route handler)

Deployment en heroku

Deployment en heroku

En muchos entornos (por ejemplo, Heroku), y como convención, configuran la variable de entorno PORT para decirle a su servidor web en qué puerto escuchar.

 

Entonces process.env.PORT || 5000 significa: lo que sea que esté en la variable de entorno PORT, o 5000 si no hay nada allí.

const PORT = process.env.PORT || 5000;
app.listen(PORT);

modificamos index.js

Deployment en heroku

  "main": "index.js",
  "engines": {
    "node": "12.14.1",
    "npm": "6.13.4"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },

modificamos package.json

Agregar "engines" entre "main" y "script" para especificar el entorno que tenemos en local y evitar errores de compatibilidad en la versión en producción

Deployment en heroku

  "scripts": {
    "start": "node index.js"
  },

modificamos package.json

Dentro de "scripts" borramos "test" y agregamos "start" con el comando que hemos especificado para ejecutar index.js en local

Deployment en heroku

node_modules

Cuando instalamos dependencias como express, de forma común no se controla versión de los mismos, tampoco deben desplegarse en heroku, porque haremos que heroku haga las instalaciones por si mismo. Para eso es .gitignore

  1. Creamos el archivo .gitignore, desde visual studio code
  2. Dentro de .gitignore especificar el siguiente código

Desplegar App en heroku

Crear cuenta heroku

www.heroku.com

Hacer un commit - código base

git init
git add .
git commit -m "Commit inicial"

Instalar Heroku CLI

//Desde la línea de comandos
heroku -v
//Para hacer login solicitará el email y contraseña de nuestra cuenta heroku
heroku login
// Creamos la App. Heroku le asigna un nombre 
// y provee la ruta git para el diployment
heroku create

Desplegar la App con git

//Desde la línea de comandos
heroku -v
//Para hacer login solicitará el email y contraseña de nuestra cuenta heroku
heroku login
// Creamos la App. Heroku le asigna un nombre 
// y provee la ruta git para el diployment
heroku create
// Ejemplo de proyecto creado en Heroku
// https://fathomless-waters-86018.herokuapp.com/ | https://git.heroku.com/fathomless-waters-86018.git

// Agregar el repositorio remoto
git remote add heroku https://git.heroku.com/fathomless-waters-86018.git

// Pasar la información del repositorio local al remoto
git push heroku master

// Probamos la App
heroku open

// En caso de error
heroku logs

Introducción a la autenticación con Google OAuth

Recordemos el flujo

OAuth

Open Authorization es un estándar abierto que permite flujos simples de autorización para sitios web o aplicaciones informáticas. Se trata de un protocolo propuesto por Blaine Cook y Chris Messina, que permite autorización segura de una API de modo estándar y simple para aplicaciones de escritorio, móviles y web.

 

Tomado de: https://es.wikipedia.org/wiki/OAuth

Flujo de OAuth

Passport JS

Configurar Passport JS

npm install --save passport passport-google-oauth20 
const express = require('express');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;

const app = express();

passport.use(new GoogleStrategy());

const PORT = process.env.PORT || 5000;
app.listen(PORT);

index.js

En la terminal

Habilitar la API Google OAuth

Hacer que Google conozca nuestra aplicación

  • Debe disponer de una cuenta en google (gmail)
  • Ir a: https://console.developers.google.com/
  • Crear el proyecto: mb-feedback-dev
  • Seleccionar el proyecto creado
  • Crear Pantalla de consentimiento de OAuth (Externa)

Habilitar la API Google OAuth

Hacer que Google conozca nuestra aplicación

  • Desde el menú credenciales seleccionamos Crear Credenciales / ID de Cliente de OAuth
  • Dejamos la configuración como se ve en la imagen
  • Luego de crear, copiamos nuestro ID de cliente y el código de cliente secreto, de  momento lo podemos pegar como comentario dentro del index.js para usarlos más adelante.

Asegurando las claves de la API

1. Crear el arhivo keys.js

module.exports = {
    googleClientID: '439988331208-j7m4lnq3m4sr14soce1ro8c7l41vvp6e.apps.googleusercontent.com',
    googleClientSecret: 'PUkKItVVoJ3nRQKqoTybgcWy'
}

2. Exportar claves para que puedan ser accedidas desde otros archivos

3. Ignorar las claves para que no estén disponibles en los diployment. Esto se hace desde el .gitignore

node_modules
keys.js

Opciones de Google Strategy

const express = require("express");
const passport = require("passport");
const GoogleStrategy = require("passport-google-oauth20").Strategy;
const keys = require("./config/keys");

const app = express();

passport.use(
  new GoogleStrategy(
    {
      clientID: keys.googleClientID,
      clientSecret: keys.googleClientSecret,
      callbackURL: "/auth/google/callback"
    },
    accessToken => {
      console.log(accessToken);
    }
  )
);

const PORT = process.env.PORT || 5000;
app.listen(PORT);

Probar OAuth

app.get(
  "/auth/google",
  passport.authenticate("google", {
    scope: ["profile", "email"]
  })
);

Autorizar redirección de URI

Nota: este cambio puede tomar en algunos casos hasta 5 minutos para que google agregue la redirección.

OAuth Callback

app.get('/auth/google/callback', passport.authenticate('google'));

Ver código generado por el accessToken

access y refresh tokens

passport.use(
  new GoogleStrategy(
    {
      clientID: keys.googleClientID,
      clientSecret: keys.googleClientSecret,
      callbackURL: "/auth/google/callback"
    },
    (accessToken, refreshToken, profile, done) => {
      console.log('access token: ', accessToken);
      console.log('refresh token: ', refreshToken);
      console.log('profile: ', profile);
    }
  )
);

Configurar Nodemon

npm install --save nodemon

En la terminal

  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js"
  },

En package.json

npm run dev

En la terminal

Nodemon es una utilidad que monitorea los cambios en el código fuente que se esta desarrollando y automáticamente reinicia el servidor.

Estructura del servidor (I)

Estructura del servidor (II)

const express = require("express");
require('./services/passport');
// const authRoutes = require('./routes/authRoutes');

const app = express();

require('./routes/authRoutes')(app);

const PORT = process.env.PORT || 5000;
app.listen(PORT);
const passport = require('passport');

module.exports = app => {
    app.get(
        "/auth/google",
        passport.authenticate("google", {
        scope: ["profile", "email"]
        })
    );
    
    app.get('/auth/google/callback', passport.authenticate('google'));
}

index.js

authRoutes.js

Estructura del servidor (III)

const passport = require("passport");
const GoogleStrategy = require("passport-google-oauth20").Strategy;
const keys = require("../config/keys");

passport.use(
    new GoogleStrategy(
      {
        clientID: keys.googleClientID,
        clientSecret: keys.googleClientSecret,
        callbackURL: "/auth/google/callback"
      },
      (accessToken, refreshToken, profile, done) => {
        console.log('access token: ', accessToken);
        console.log('refresh token: ', refreshToken);
        console.log('profile: ', profile);
      }
    )
);

passport.js

Teoría sobre autenticación

Usando Cookies

Iniciar sesión con OAuth

Iniciar sesión con OAuth (II)

MongoDB

Introducción a MongoDB (I)

Introducción a MongoDB (II)

(records)

Introducción a MongoDB (III)

Instalación de MongoDB (I)

  1. Crear una cuenta en https://www.mongodb.com/
  2. Ir al menú cloud/atlas, buscar la opción log in here

Instalación de MongoDB (II)

1

2

Instalación de MongoDB (III)

3

4

Instalación de MongoDB (IV)

5

Instalación de MongoDB (V)

6

7

Instalación de MongoDB (VI)

8

Instalación de MongoDB (VII)

9

Instalación de MongoDB (VIII)

10

Conectar Mongoose MongoDB 

npm install --save mongoose

1. En la terminal

const express = require("express");
const mongoose = require('mongoose');
require('./services/passport');
const keys = require("./config/keys");

mongoose.connect(
    keys.mongoURI, 
    { useNewUrlParser: true,
        useUnifiedTopology: true }
).then(console.log('Conexión establecida'));

2. En la keys.js

module.exports = {
    googleClientID: '439988331208-j7m4lnq3m4sr14soce1ro8c7l41vvp6e.apps.googleusercontent.com',
    googleClientSecret: 'PUkKItVVoJ3nRQKqoTybgcWy',
    mongoURI: 'mongodb://wilfredo:Password1.@ds061711.mlab.com:61711/mb-feedback-dev'
}

3. En la index.js

npm run dev

4. En la terminal

Revisemos

Model Class de Mogoose

1. Crear una nueva carpeta

2. En User.js

const mongoose = require('mongoose');
// const Schema = mongoose.Schema;
const { Schema } = mongoose;

const userSchema = new Schema({
    googleId: String
});

mongoose.model('users', userSchema);

3. Aplicar el require en index.js

const express = require("express");
const mongoose = require('mongoose');
const keys = require("./config/keys");
require('./models/User');
require('./services/passport');

//...

Guardar instancias de Model

const passport = require("passport");
const GoogleStrategy = require("passport-google-oauth20").Strategy;
const mongoose = require('mongoose');
const keys = require("../config/keys");

const User = mongoose.model('users');

passport.use(
    new GoogleStrategy(
      {
        clientID: keys.googleClientID,
        clientSecret: keys.googleClientSecret,
        callbackURL: "/auth/google/callback"
      },
      (accessToken, refreshToken, profile, done) => {
        new User({ googleId: profile.id }).save();
      }
    )
);

Consultas con mogoose (I)

Consultas con mogoose (II)

// ... el código anterior queda igual

passport.use(
    new GoogleStrategy(
      {
        clientID: keys.googleClientID,
        clientSecret: keys.googleClientSecret,
        callbackURL: "/auth/google/callback"
      },
      (accessToken, refreshToken, profile, done) => {
        // Retorna una promesa
        User.findOne({ googleId: profile.id })
          .then( (existingUser) => {
            if(existingUser) {
              // Tenemos un registro con el Id de perfil
            } else {
              // No tenemos un registro del Id de Perfil, 
              // hacemos un nuevo registro
              new User({ googleId: profile.id }).save();
            }
          }
        );
      }
    )
);

passport callback

// ... el código anterior queda igual

passport.use(
    new GoogleStrategy(
      {
        clientID: keys.googleClientID,
        clientSecret: keys.googleClientSecret,
        callbackURL: "/auth/google/callback"
      },
      (accessToken, refreshToken, profile, done) => {
        // Retorna una promesa
        User.findOne({ googleId: profile.id })
          .then( (existingUser) => {
            if(existingUser) {
              // Tenemos un registro con el Id de perfil
              // El primer parámtro indica que no hay error
              // El segundo parámetro indica a passport el 
              // usuario que hemos encontrado
              done(null, existingUser);
            } else {
              // No tenemos un registro del Id de Perfil, hacemos un nuevo registro
              new User({ googleId: profile.id })
                .save()
                .then(user => DelayNode(null, user));
            }
          }
        );
      }
    )
);

Serializar usuario (I)

Serializar usuario (II)

En passport.js

// ... el código anterio queda igual

const User = mongoose.model('users');

passport.serializeUser((user, done)=> {
  done(null, user.id);
});

// ... el resto de código queda igual

¿Por qué el ID interno y no el del perfil?

passport.serializeUser () configura el id como cookie en el navegador del usuario y passport.deserializeUser () obtiene la identificación de la cookie.

Deserializar usuario

En passport.js

// ... el código anterio queda igual
passport.serializeUser((user, done)=> {
  done(null, user.id);
});

// El primer pqarámetro del arrow function es el token (id)
// que deseamos buscar en la cookie
passport.deserializeUser( (id, done) => {
  User.findById(id)
    .then(user => {
      done(null, user);
    });
});

// ... el resto de código queda igual

Habilitar coockies

1. Desde la terminal

npm install --save cookie-session

2. En index.js

// ... el código anterior queda igual
const mongoose = require('mongoose');
const cookieSession = require('cookie-session');
const passport = require('passport');
// ... el código posterior queda igual

3. En keys.js, agregar coockieKey

module.exports = {
    //... lo anterior queda igual
    cookieKey: 'qiwpsbjtarsporle'
}
// ... el resto de código queda igual
const app = express();

app.use(
    cookieSession({
        maxAge: 30 * 24 * 60 * 60 * 1000,
        keys: [keys.cookieKey]
    })
);
app.use(passport.initialize());
app.use(passport.session());

4. En el index.js

Probar autenticación

app.get('/api/current_user', (req, res) => {
  res.send(req.user);
});

Agregar esta ruta en authRoute

Cerrar sesión

app.get('/api/logout', (req, res) => {
  req.logout();
  res.send(req.user);
});

Veamos esto en mayor profundidad

Middleware en Express JS. Un middleware es una función que se puede ejecutar antes o después del manejo de una ruta.

Veamos esto en mayor profundidad

cookie-session

express-session

Keys de desarrollo (Dev) vs keys de producción (Prod)

Debe evitarse que si alguien accede a nuestra computadora pueda tomar las claves (keys) y afectar nuestro entorno de producción. Por eso en producción debemos tener otras keys por seguridad.

Keys de desarrollo (Dev) vs keys de producción (Prod)

Generar recursos de producción(I)

En la versión en producción deben usarse credenciales seguras. En nuestro caso es básica por fines de simplicidad.

Generar recursos de producción(II)

Generar recursos de producción(II)

1

2

3

Generar recursos de producción(III)

Generar recursos de producción(IV)

Desde la terminal ejecutamos el siguiente comando para obtener la ruta de la aplicación alojada en Heroku

heroku open

Generar recursos de producción(V)

Generar recursos de producción(VI)

Determinar el entorno

module.exports = {
    googleClientID: '439988331208-j7m4lnq3m4sr14soce1ro8c7l41vvp6e.apps.googleusercontent.com',
    googleClientSecret: 'PUkKItVVoJ3nRQKqoTybgcWy',
    mongoURI: 'mongodb://wilfredo:Password1.@ds061711.mlab.com:61711/mb-feedback-dev',
    cookieKey: 'qiwpsbjtarsporle'
}

En dev.js

module.exports = {
    googleClientID: process.env.GOOGLE_CLIENT_ID,
    googleClientSecret: process.env.GOOGLE_CLIENT_SECRET,
    mongoURI: process.env.MONGO_URI,
    cookieKey: process.env.COOKIE_KEY
}

En prod.js

node_modules
dev.js

En .gitignore

if(process.env.NODE_ENV === 'production') {
    module.exports = require('./prod');
} else {
    module.exports = require('./dev');
}

En keys.js

Variables en Heroku env (I)

1

2

3

Variables en Heroku env (II)

En la terminal

git status
git add .
git commit -m "Flujo Auth finalizado"
git push heroku master
heroku open

Reparar errores

new GoogleStrategy(
  {
    clientID: keys.googleClientID,
    clientSecret: keys.googleClientSecret,
    callbackURL: "/auth/google/callback",
    proxy: true
  },
  
  // el resto queda igual
git status
git add .
git commit -m "Ajuste al Google Strategy"
git push heroku master
heroku open

React

(El lado del cliente)

Generar la aplicación React

npm install -g create-react-app

Se recomienda que la consola tenga permisos de administrador

npx create-react-app my-app

// Client tiene su propio servidor
cd client
npm start

Front end separado

Se puede trabajar juntos, pero de forma separada create-react-app ahorra mucho tiempo con la configuración de dependencias.

Ejecutar cliente y sevidor

1. Se puede trabajar con dos consolas separadas

Consola 1 - en la carpeta de "client"

Consola 2 - desde la carpeta "server"

npm start
npm run dev
npm install --save concurrently

2. Usando concurrently

  "scripts": {
    "start": "node index.js",
    "server": "nodemon index.js",
    "client": "npm run start --prefix client",
    "dev": "concurrently \"npm run server\" \"npm run client\""
  },

En package.json

Desde server

npm run dev

Terminal

Enrutamiento - problemas

Si intentamos crear un enlace en el front-end que nos lleve a /auth/google tendremos problemas porque la ruta es manejada desde el backend, esta ruta puede cambiar según se trabaje en dev o prod

Solución: desde el client

cd client
npm install http-proxy-middleware --save

Ahora cree un archivo de configuración para su proxy. Debe nombrarlo setupProxy.js en su carpeta src en el lado del cliente y escriba el siguiente código:

const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function(app) {
  app.use(
    '/auth/google',
    createProxyMiddleware({
      target: 'http://localhost:5000'
    })
  );
};

Bondades de create-react-app

cd client
npm run build

Nota: Hay que agregar el callback

Solución error en redirección

Desarrollar el lado del cliente

Sintaxis AsyncAwait (I)

function fetchAlbums() {
    fetch('http://rallycoding.herokuapp.com/api/music_albums')
    .then(res => res.json())
    .then(json => console.log(json));
}

fetchAlbums();

Sintaxis AsyncAwait (II)

async function fetchAlbums() {
    const res = await fetch('http://rallycoding.herokuapp.com/api/music_albums')
    const json = await res.json();

    console.log(json);
}

fetchAlbums();

Esta forma es más sencilla y es con la que trabajaremos de ahora en adelante:

const fetchAlbums = async () => {
    const res = await fetch('http://rallycoding.herokuapp.com/api/music_albums')
    const json = await res.json();

    console.log(json);
}

fetchAlbums();

Con arrow function

Refactoring con AsyncAwait

async (accessToken, refreshToken, profile, done) => {
  const existingUser = await User.findOne({ googleId: profile.id });

  if (existingUser) {
    // Tenemos un registro con el Id de perfil
    return done(null, existingUser);
  } 
  // No tenemos un registro del Id de Perfil, hacemos un nuevo registro
  const user = await new User({ googleId: profile.id }).save();
  done(null, user);
}

En passport.js

Nota: se requiere una versión de node mínima 8.1.1

Tecnologías Front-end (I)

Tecnologías Front-end (II)

Tecnologías Front-end (III)

Tecnologías Front-end (IV)

Cliente - configurar react (I)

Dejamos el directorio src de la siguiente forma (todo va desde cero):

Cliente - configurar react (II)

cd client
npm install --save redux react-redux react-router-dom

En index.js

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(); 

Instalar módulo raíz

2. En index.js

import React from "react";
import ReactDOM from "react-dom";

import App from "./components/App";

ReactDOM.render(<App />, document.querySelector("#root"));

Para exportar componentes los nombres de los archivos inician con mayúsculas con convensión

import React from 'react';

const App = () => {
    return (
        <div>
            Hola
        </div>
    )
}

export default App;

1. En App.js

3. En la Terminal

cd ..

Configuración de Redux (I)

Configuración de Redux (II)

Configuración de Redux (III)

Configuración de Redux (IV)

Configuración de Redux (V)

En index.js

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';

import App from "./components/App";

const store = createStore(() => [], {}, applyMiddleware());

ReactDOM.render(
    <Provider store={store}><App /></Provider>, 
    document.querySelector("#root")
);

El Auth Reducer

2. En authReducer.js

export default function(state = {}, action) {
    switch(action.type) {
        default:
            return state;
    }
}

1. Crear la siguiente estructura

3. En index.js del directorio reducers

import { combineReducers } from 'redux';
import authReducer from './authReducer';

export default combineReducers({
    auth: authReducer
});

4. Modificar en index.js de src

import App from "./components/App";
import reducers from './reducers';

const store = createStore(reducers, {}, applyMiddleware());

Consideraciones acerca de Auth

Configurar react router

import React from 'react';
import { BrowserRouter, Route } from 'react-router-dom';

const Header = () => <h2>Header</h2>;
const Dashboard = () => <h2>Dashboard</h2>;
const SurveyNew = () => <h2>SurveyNew</h2>;
const Landing = () => <h2>Landing</h2>;

const App = () => {
    return (
        <div>
            <BrowserRouter>
                <div>
                    <Route path="/" component={Landing} />
                </div>
            </BrowserRouter>
        </div>
    )
}

export default App;

En components/App.js

Rutas con exact

import React from 'react';
import { BrowserRouter, Route } from 'react-router-dom';

const Header = () => <h2>Header</h2>;
const Dashboard = () => <h2>Dashboard</h2>;
const SurveyNew = () => <h2>SurveyNew</h2>;
const Landing = () => <h2>Landing</h2>;

const App = () => {
    return (
        <div>
            <BrowserRouter>
                <div>
                    <Route exact path="/" component={Landing} />
                    <Route path="/surveys" component={Dashboard} />
                </div>
            </BrowserRouter>
        </div>
    )
}

export default App;

En components/App.js

Componentes siempre visibles

import React from 'react';
import { BrowserRouter, Route } from 'react-router-dom';

const Header = () => <h2>Header</h2>;
const Dashboard = () => <h2>Dashboard</h2>;
const SurveyNew = () => <h2>SurveyNew</h2>;
const Landing = () => <h2>Landing</h2>;

const App = () => {
    return (
        <div>
            <BrowserRouter>
                <div>
                    <Header />
                    <Route exact path="/" component={Landing} />
                    <Route exact path="/surveys" component={Dashboard} />
                    <Route path="/surveys/new" component={SurveyNew} />
                </div>
            </BrowserRouter>
        </div>
    )
}

export default App;

En components/App.js

Materialize (I)

import React, { Component } from 'react';

class Header extends Component {
    render(){
        return (
            <div>
                Header
            </div>
        );
    }
}

export default Header;

2. Header.js

1. En components/App.js

3. En App.js

// Reemplazar 
const Header = () => <h2>Header</h2>;
// por 
import Header from './Header';

Materialize (II)

Para la apariencia vamos a utilizar un framework de propósito general que puede utilizar con cualquier librería o framework JavaScript

https://materializecss.com/

  cd client
  npm install --save materialize-css

Desde la terminal

webpack

  • Para CSS debemos especificar la extensión, para js Webpack los asume por defecto y no es necesario reflejarlo.
  • Cuando se trata de un componente (ejemplo el de materialize-css) no es necesario especificar la ruta relativa "./ "
  • No hay asignación a variables por lo que puede omitirse.

 

En la primera línea de index.js dentro de src

import 'materialize-css/dist/css/materialize.min.css';

Diseñar el Header

import React, { Component } from 'react';

class Header extends Component {
    render(){
        return (
            <nav>
                <div className="nav-wrapper">
                <a href="#" className="left brand-logo">mb-feedback</a>
                <ul id="nav-mobile" className="right hide-on-med-and-down">
                    <li><a href="#">Login con Google</a></li>
                </ul>
                </div>
            </nav>
        );
    }
}

export default Header;
return (
  <div className="container">
  <BrowserRouter>

En App.js (Poner la clase container)

Reglas Proxy (I)

  cd client
  npm install --save axios redux-thunk

En la terminal

// ....
import { createStore, applyMiddleware } from 'redux';
import reduxThunk from 'redux-thunk';

import App from "./components/App";
import reducers from './reducers';

const store = createStore(reducers, {}, applyMiddleware(reduxThunk));
// ....

En index.js de src

Reglas Proxy (II)

  app.use(
    '/api/*',
    createProxyMiddleware({ 
      target: 'http://localhost:5000'
    })
  );  

1. En setupProxy.js agregar

2. Crear la siguiente estructura

3. En types.js

export const FETCH_USER = 'fetch_user';
// Para hacer peticiones ajax
import axios from 'axios';
import { FETCH_USER } from './types';

export const fetchUser = () => {
    axios.get('/api/current_user');
}

4. En index.js

Entender redux thunk (I)

Entender redux thunk (II)

// Para hacer peticiones ajax
import axios from "axios";
import { FETCH_USER } from "./types";

export const fetchUser = () => {
  return function(dispatch) {
    axios.get("/api/current_user").then(res =>
      dispatch({
        type: FETCH_USER,
        payload: res
      })
    );
  };
};

Refactoring

import React, { Component } from 'react';
import { BrowserRouter, Route } from 'react-router-dom';

import Header from './Header';
const Dashboard = () => <h2>Dashboard</h2>;
const SurveyNew = () => <h2>SurveyNew</h2>;
const Landing = () => <h2>Landing</h2>;

class App extends Component {
    componentDidMount() {
        
    }
  
    render() {
        return (
            <div className="container">
                <BrowserRouter>
                    <div>
                        <Header />
                        <Route exact path="/" component={Landing} />
                        <Route exact path="/surveys" component={Dashboard} />
                        <Route path="/surveys/new" component={SurveyNew} />
                    </div>
                </BrowserRouter>
            </div>
        )
    };
    
}

export default App;

Probar fetchUser()

En App.js

import React, { Component } from 'react';
import { BrowserRouter, Route } from 'react-router-dom';
// Agregarmos estas importaciones para conectar la App con redux
import { connect } from 'react-redux';
import * as actions from '../actions';

// ...
    componentDidMount() {
	// Llamada al action
        this.props.fetchUser();
    }

// ...

// Pasamos los argumentos a App
export default connect(null, actions) (App);

En authReducer.js

export default function(state = {}, action) {
    console.log(action);
    switch(action.type) {
        default:
            return state;
    }
}

Refactoring

En actions/index.js

// Para hacer peticiones ajax
import axios from "axios";
import { FETCH_USER } from "./types";

export const fetchUser = () => async dispatch => {
  const res = await axios.get("/api/current_user");

  dispatch({ type: FETCH_USER, payload: res.data });
};

authReducer valores que retorna (I)

authReducer valores que retorna (II)

import { FETCH_USER } from '../actions/types';
export default function(state = null, action) {
    switch(action.type) {
        case FETCH_USER:
            return action.payload || false;
        default:
            return state;
    }
}

En authReducer.js

Acceder al estado desde el Header

import React, { Component } from 'react';
import { connect } from 'react-redux';

class Header extends Component {
    renderContent() {
        switch(this.props.auth) {
            case null:
                return 'Todavía decidiendo';
            case false:
                return 'Sessión cerrada';
            default: 
                return 'Sesión iniciada';
        }
    }
    render(){
        // console.log(this.props);
        return (
            <nav>
                <div className="nav-wrapper">
                <a href="#" className="brand-logo">mb-feedback</a>
                <ul id="nav-mobile" className="right hide-on-med-and-down">
                    { this.renderContent() }
                </ul>
                </div>
            </nav>
        );
    }
}

// function mapStateToProps(state) {
//     return { auth: state.auth };
// }
function mapStateToProps({ auth }) {
    return { auth };
}
export default connect(mapStateToProps)(Header);

En Header.js

Contenido del Header

// ...
renderContent() {
    switch (this.props.auth) {
      case null:
        return;
      case false:
        return <li><a href="/auth/google">Acceso con Google</a></li>;
      default:
        return <li><a href="">Salir</a></li>;
    }
  }

// ...

En Header.js

Redireccionar un usuario al autenticarse

// ...
app.get(
  '/auth/google/callback', 
  passport.authenticate('google'),
  (req, res) => {
    res.redirect('/surveys');
  }
);

// ...

Del lado del servidor authRoutes.js, modificar

Redireccionar al salir (I)

Redireccionar al salir (II)

// ...
renderContent() {
    switch (this.props.auth) {
      case null:
        return;
      case false:
        return <li><a href="/auth/google">Acceso con Google</a></li>;
      default:
        return <li><a href="/api/logout">Salir</a></li>;
    }
}

// ...

Del lado del cliente, en Header.js

Del lado del servidor, en authRoutes.js

// ...
app.get('/api/logout', (req, res) => {
  req.logout();
  res.redirect('/');
});
// ...

Componente Landing

import React from 'react';

const Landing = () => {
    return(
        <div style={{textAlign: 'center'}}>
            <h1>mb-feedback</h1>
            Obtener feedback de sus usuarios
        </div>
    );
};

export default Landing;

Del lado del cliente, en Landing.js

En App.js

// Reemplazar
const Landing = () => <h2>Landing</h2>;

// Por
import Landing from './Landing';
// Poner esto anter de las otras declaraciones para evitar error

Enlazar etiquetas (I)

En App.js

Enlazar etiquetas (II)

En Header.js

// ...
import { Link } from "react-router-dom";

// ...

render() {
    return (
      <nav>
        <div className="nav-wrapper">
          <Link 
            to={this.props.auth ? '/surveys' : '/'} 
            className="brand-logo"
          >
            mb-feedback
          </Link>
          <ul id="nav-mobile" className="right hide-on-med-and-down">
            {this.renderContent()}
          </ul>
        </div>
      </nav>
    );
  }

Pagos

Consideraciones para facturar

Reglas de facturación

Somos malos en seguridad

  • Nunca acepte números de tarjeta de crédito de forma directa
  • Nunca almacene números de tarjetas de crédito
  • Siempre use un procesador de pagos externo

La facturación es difícil

  • Evitar en lo posible pagos mensuales / planes múltiples
  • El fraude y las devoluciones de cargo son un dolor de cabeza

Consideraciones para facturar

Consideraciones para facturar

Reglas de facturación

Somos malos en seguridad

  • Nunca acepte números de tarjeta de crédito de forma directa
  • Nunca almacene números de tarjetas de crédito
  • Siempre use un procesador de pagos externo

La facturación es difícil

  • Evitar en lo posible pagos mensuales / planes múltiples
  • El fraude y las devoluciones de cargo son un dolor de cabeza

Explorar la API de Stripe

// Desde la terminal
cd client
npm install --save react-stripe-checkout

Instalar react-stripe-checkout 

Stripe API Keys

module.exports = {
    // ...
    stripePublishableKey: 'pk_test_maDckee4adUSBdCCQSefD3v100lI4N0eXi',
    stripeSecretKey: 'sk_test_NFUt0kfcFB0f8IMD39dG79FB00mqd8iN89'
}

En config/dev.js

module.exports = {
    // ...
    stripePublishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
    stripeSecretKey: process.env.STRIPE_SECRET_KEY
}

En config/prod.js

En heroku.com, agregar las variables al proyecto

Variables de entorno con React

REACT_APP_STRIPE_KEY=pk_test_maDckee4adUSBdCCQSefD3v100lI4N0eXi

.env.development

REACT_APP_STRIPE_KEY=pk_test_maDckee4adUSBdCCQSefD3v100lI4N0eXi

.env.production

// ...
console.log('STRIPE KEY ES', process.env.REACT_APP_STRIPE_KEY);
console.log('Eviroment es', process.env.NODE_ENV);

Para probar en index.js del client

El componente de pago

Payments.js

import React, { Component } from 'react';
import StripeCheckout from 'react-stripe-checkout';

class Payments extends Component {
    render() {
        debugger;
        return (
            <StripeCheckout 
                amount={500}
                token={token => console.log(token)}
                stripeKey={process.env.REACT_APP_STRIPE_KEY}
            />
        );
    }
} 

export default Payments;

En Header.js

// ...
import Payments from './Payments';

Token de Stripe

// ...

class Header extends Component {
  renderContent() {
    switch (this.props.auth) {
      case null:
        return;
      case false:
        return <li><a href="/auth/google">Acceso con Google</a></li>;
      default:
        return [
          <li key="1"><Payments /></li>,
          <li key="2"><a href="/api/logout">Salir</a></li>
        ];
    }
  }

// ...  

En Header.js

Pruebe a hacer un pago y vea en la consola el token y los resultados devueltos por Stripe

Ajustes varios a Pagos

import React, { Component } from 'react';
import StripeCheckout from 'react-stripe-checkout';

class Payments extends Component {
    render() {
        return (
            <StripeCheckout 
                name="mb-feeback"
                description="$5 para 5 créditos emails"
                amount={500}
                token={token => console.log(token)}
                stripeKey={process.env.REACT_APP_STRIPE_KEY}
            >
                <button className="btn">
                    Agregar Créditos
                </button>
            </StripeCheckout>
        );
    }
} 

export default Payments;

En Payments.js

Reusar acciones (actions) - I

Reusar acciones (actions) - II

En actions/index.js

// ...

export const handleToken = (token) => async dispatch => {
  const res = await axios.post("/api/stripe", token);

  dispatch({ type: FETCH_USER, payload: res.data });
};

Enviar el token de Stripe

En Payments.js

// ...
import { connect } from 'react-redux';
import * as actions from '../actions';

class Payments extends Component {
    render() {
        return (
            <StripeCheckout 
                name="mb-feeback"
                description="$5 para 5 créditos emails"
                amount={500}
                token={token => this.props.handleToken(token)}
                stripeKey={process.env.REACT_APP_STRIPE_KEY}
            >
                <button className="btn">
                    Agregar Créditos
                </button>
            </StripeCheckout>
        );
    }
} 

export default connect(null, actions)(Payments);

Manejador de la solicitud de Post del Token

En el server index.js

// ...
require('./routes/authRoutes')(app);
require('./routes/billingRoutes')(app);
// ...

En billingRoutes.js

module.exports = app => {
    app.post('/api/stripe', (req, res) => {
        
    });
}

Middleware bodyParser

Vamos a instalar desde el servidor una librería para Stripe que nos permite manejar los tokens que recibimos desde el front end y cargar ese monto en los créditos para emails: https://www.npmjs.com/package/stripe

// Desde el servidor
npm install --save stripe
npm install --save body-parser
// ...
const passport = require('passport');
const bodyParser = require('body-parser');
// ...

app.use(bodyParser.json());
app.use(
// ...

En index.js (server)

const keys = require('../config/keys');
const stripe = require('stripe')(keys.stripeSecretKey);

module.exports = app => {
    app.post('/api/stripe', (req, res) => {
      console.log(req.body);
    });
}

En billingRoutes.js (server)

Crear el objeto "charges"

const keys = require('../config/keys');
const stripe = require('stripe')(keys.stripeSecretKey);

module.exports = app => {
    app.post('/api/stripe', (req, res) => {
        stripe.charges.create({
            amount: 500,
            currency: 'usd',
            description: '$5 para 5 créditos emails',
            source: req.body.id
        });
    });
}

En billingRoutes.js

Finalizar "charges"

const keys = require('../config/keys');
const stripe = require('stripe')(keys.stripeSecretKey);

module.exports = app => {
    app.post('/api/stripe', async (req, res) => {
        const charge = await stripe.charges.create({
            amount: 500,
            currency: 'usd',
            description: '$5 para 5 créditos emails',
            source: req.body.id
        });

        console.log(charge);
    });
}

En billingRoutes.js

Agregar créditos a un usuario

// ...
const userSchema = new Schema({
    googleId: String,
    credits: { type: Number, default: 0 }
});

// ...

En models/User.js

// ...
module.exports = app => {
    app.post('/api/stripe', async (req, res) => {
        const charge = await stripe.charges.create({
            amount: 500,
            currency: 'usd',
            description: '$5 para 5 créditos emails',
            source: req.body.id
        });

        req.user.credits += 5;
        const user = await req.user.save();

        res.send(user);
    });
}

En billingRoutes.js

Requerir autenticación (I)

// ...
module.exports = app => {
    app.post('/api/stripe', async (req, res) => {
        if(!req.user){
            return res.status(401).send({ error: 'Debes hacer login' });
        }
    // ....

En billingRoutes.js

Lo anterior funciona, pero si queremos restringir otras rutas para que requieran autenticación tendríamos que copiar el código anterior en cada ruta, por lo que la solución viene en la siguiente presentación.

Requerir autenticación (II)

Vamos a crear un nuevo middleware para asegurarnos de que el usuario ha iniciado sesión

module.exports = (req, res, next) => {
    if(!req.user){
        return res.status(401).send({ error: 'Debes hacer login' });
    }
    // Si no hay problemas puede continuar
    next();
}

En requireLogin.js

Requerir autenticación (III)

Vamos a crear un nuevo middleware para asegurarnos de que el usuario ha iniciado sesión

module.exports = (req, res, next) => {
    if(!req.user){
        return res.status(401).send({ error: 'Debes hacer login' });
    }
    // Si no hay problemas puede continuar
    next();
}

En requireLogin.js

Requerir autenticación (IV)

const keys = require('../config/keys');
const stripe = require('stripe')(keys.stripeSecretKey);
const requireLogin = require('../middlewares/requireLogin');

module.exports = app => {
  // Observe que no hay llamada, express lo hará cuando haya una 
  // solicitud/request, puede haber tantos middleware como sea
  // necesario según la ruta
    app.post('/api/stripe', requireLogin, async (req, res) => {
        const charge = await stripe.charges.create({
            amount: 500,
            currency: 'usd',
            description: '$5 para 5 créditos emails',
            source: req.body.id
        });

        req.user.credits += 5;
        const user = await req.user.save();

        res.send(user);
    });
}

En billingRoutes.js

Mostrar cantidad de créditos

// ...
class Header extends Component {
  renderContent() {
    switch (this.props.auth) {
      case null:
        return;
      case false:
        return <li><a href="/auth/google">Acceso con Google</a></li>;
      default:
        return [
          <li key="1"><Payments /></li>,
          <li key='3' style={{ margin: '0 10px'}}>
            Credits: { this.props.auth.credits }
          </li>,
          <li key="2"><a href="/api/logout">Salir</a></li>
        ];
    }
  }
  
  // ...

En Header.js

Front end: rutas and producción

Mostrar cantidad de créditos

// ...
class Header extends Component {
  renderContent() {
    switch (this.props.auth) {
      case null:
        return;
      case false:
        return <li><a href="/auth/google">Acceso con Google</a></li>;
      default:
        return [
          <li key="1"><Payments /></li>,
          <li key='3' style={{ margin: '0 10px'}}>
            Credits: { this.props.auth.credits }
          </li>,
          <li key="2"><a href="/api/logout">Salir</a></li>
        ];
    }
  }
  
  // ...

En Header.js

Aplicaciones Web

By Wilfredo Meneses

Aplicaciones Web

  • 1,139