Wilfredo Meneses
Profesor universitario
Edición - 2021
Unidad I: Introducción
Unidad II: Backend
Unidad III: Frontend
Unidad IV: Despliegue en producción
1. Instalador de node.js
2. Git
3. Postman
Extensiones de VSC
Objetivo
Conocer los conceptos básicos relacionados con el desarrollo de Aplicaciones Web
Las aplicaciones web son un tipo de software que se codifica en un lenguaje soportado por los navegadores web y cuya ejecución es llevada a cabo por el navegador en Internet o de una intranet (de ahí que reciban el nombre de App web).
Las aplicaciones web se ejecutan por medio de un navegador web y no necesitan ser instaladas en tu pc o smartphone, ya que los datos o archivos utilizados están almacenados en una red o en la nube (Hosting).
Las aplicaciones web se relacionan estrechamente con el almacenamiento de datos en la nube, ya que toda la información requerida esta en servidores web, que además de alojar la información, la envían a nuestros dispositivos cuando es requerida.
Lectura obligatoria: https://wiboomedia.com/que-son-las-aplicaciones-web-ventajas-y-tipos-de-desarrollo-web/
La arquitectura cliente-servidor es un modelo de diseño de software en el que las tareas se reparten entre los proveedores de recursos o servicios, llamados servidores, y los demandantes, llamados clientes. Un cliente realiza peticiones a otro programa, el servidor, quien le da respuesta.
const http = require('http')
const server = http.createServer(function (req, res) {
console.log(req)
})
const PORT = 5000
server.listen(PORT, function () {
console.log('El servidor está corriendo en el puerto ' + PORT)
})node.js podríamos decir que es un entorno de código abierto (Open Source), multiplataforma y que ejecuta el código Javascript fuera de un navegador.
Y es precisamente la necesidad de ejecutar este lenguaje del lado del servidor el que hace surgir Node.js.
Node soporta protocolos TCP, DNS y HTTP.
Lectura obligatoria: https://dev.to/denispixi/que-es-node-js-y-como-funciona-46ck
Cliente
Servidor
Front End es la parte de una aplicación que interactúa con los usuarios, es conocida como el lado del cliente. Básicamente es todo lo que vemos en la pantalla cuando accedemos a un sitio web o aplicación: tipos de letra, colores, adaptación para distintas pantallas(RWD), los efectos del ratón, teclado, movimientos, desplazamientos, efectos visuales… y otros elementos que permiten navegar dentro de una página web.
Este conjunto crea la experiencia del usuario.
El back end del sitio web consiste en un servidor, una aplicación y una base de datos. Se toman los datos, se procesa la información y se envía al usuario.
Un desarrollador Full Stack es el encargado de manejar cada uno de los aspectos relacionados con la creación y el mantenimiento de una aplicación web. Para ello es fundamental que tenga conocimientos en desarrollo Front-End y Back-End además de manejar diferentes sistemas operativos y lenguajes de programación.
Una API (Application Programming Interfaces) es un conjunto de procedimientos y funciones creados para permitir el acceso al backend de aplicaciones de terceros con el fin de reutilizar servicios ya creados.
Ejemplos: Facebook, Twitter, Google Maps, etc.
API (interfaz del programa de aplicación) es un conjunto de reglas que permite que diferentes programas se comuniquen entre sí.
Describe la manera apropiada para que un desarrollador de software componga un programa en un servidor que se comunica con varias aplicaciones cliente.
Lectura obligatoria: https://www.iebschool.com/blog/que-es-api-rest-integrar-negocio-business-tech/
Postman nace como una herramienta que principalmente nos permite crear peticiones sobre APIs de una forma muy sencilla y poder, de esta manera, probar las APIs.
El back end del sitio web consiste en un servidor, una aplicación y una base de datos. Se toman los datos, se procesa la información y se envía al usuario.
(Plantillas literales)
Las plantillas literales son cadenas literales que habilitan el uso de expresiones incrustadas.
// ...
const PORT = 5000
server.listen(PORT, function () {
console.log(`El servidor está corriendo en el puerto ${PORT}`)
})La desestructuración es una expresión de JavaScript que permite desempacar valores de arreglos o propiedades de objetos en distintas variables
const server = http.createServer(function (req, res) {
const { method, headers, url } = req
console.log(method, headers, url)
res.end()
})npm initLo primero es inicializar nuestro proyecto. Esto crea un archivo package.json
De cierta forma, podemos considerar este package.json como un manifiesto de nuestro proyecto.
Node usa la herramienta npm. Esta herramienta, que normalmente se instala junto con Node, tiene dos roles fundamentales:
Manejar la publicación de un proyecto al registro público de npm (para que otros puedan descargarlo y utilizarlo como dependencia en sus propios proyectos).
Administrar las dependencias de tu proyecto.
Para esto, guarda un registro en un archivo llamado, justamente, package.json.
Dentro de este archivo se definen y manejan características como:
Nombre de tu proyecto.
Versión.
Dependencias.
Repositorio.
Autores.
Licencia.
Y más.
Para crearlo normalmente se usa un asistente.
De sus siglas NPM (Node Package Manager) es un gestor de paquetes desarrollado en su totalidad bajo el lenguaje JavaScript por Isaac Schlueter.
A través de NPM podemos obtener cualquier librería con tan solo una sencilla línea de código, lo cual nos permitirá agregar dependencias de forma simple, distribuir paquetes y administrar eficazmente tanto los módulos como el proyecto a desarrollar en general.
npm install --save nodemonEn la terminal
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},En package.json
npm run devEn 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.
const http = require('http')
const todos = [
{
id: 1,
text: 'Ir al supermercado',
},
{
id: 2,
text: 'Terminar la tarea de sobre API de Aplicaciones Web',
},
{
id: 3,
text: 'Bañar al perro',
},
]
const server = http.createServer(function (req, res) {
// res.setHeader('Content-type', 'text/plain')
// res.setHeader('Content-type', 'text/html')
res.setHeader('Content-type', 'application/json')
res.setHeader('X-Powered-By', 'Node.js')
// res.write('<h1>Hola</h1>')
// res.write('<h2>Hola de nuevo</h2>')
res.end(JSON.stringify({ success: true, data: todos }))
})
const PORT = 5000
server.listen(PORT, function () {
console.log(`El servidor está corriendo en el puerto ${PORT}`)
})
JSON, cuyo nombre corresponde a las siglas JavaScript Object Notation o Notación de Objetos de JavaScript, es un formato ligero de intercambio de datos, que resulta sencillo de leer y escribir para los programadores y simple de interpretar y generar para las máquinas.
Lectura obligatoria: https://www.nextu.com/blog/que-es-json/
JSON es un formato de texto completamente independiente de lenguaje
Los más importantes
Los códigos de estado de respuesta HTTP indican si se ha completado satisfactoriamente una solicitud HTTP específica. Las respuestas se agrupan en cinco clases:
Lectura obligatoria: https://developer.mozilla.org/es/docs/Web/HTTP/Methods
GET
POST
PUT/PATCH
DELETE
Recibir un recurso
Enviar un recurso
Actualizar recurso
Borrar/destruir recurso
Los más importantes
1.xx Respuesta Informativa
2.xx Respuesta satisfactoria
200 (OK) La solicitud ha tenido éxito.
201 Created
204 Not content
3.xx Redirección
304 Not modified.
4.xx Errores de cliente
400 Bad request401
401 Unauthorized
404 Not found
5.xx Errores de servidor
500 Internal Server Error
Lectura obligatoria: https://developer.mozilla.org/es/docs/Web/HTTP/Status
const http = require('http')
const todos = [
{
id: 1,
text: 'Ir al supermercado',
},
{
id: 2,
text: 'Terminar la tarea de sobre API de Aplicaciones Web',
},
{
id: 3,
text: 'Bañar al perro',
},
]
const server = http.createServer(function (req, res) {
res.writeHead(404, {
'Content-type': 'application/json',
'X-Powered-By': 'Node.js',
})
res.end(
JSON.stringify({
success: false,
error: 'No se encontró el recurso solicitado',
data: null,
})
)
})
const PORT = 5000
server.listen(PORT, function () {
console.log(`El servidor está corriendo en el puerto ${PORT}`)
})
// ...
// console.log(req.headers.authorization)
let body = []
req
.on('data', (chunk) => {
body.push(chunk)
})
.on('end', () => {
body =
console.log(body)
})
// ...GET /todos
GET /todos/1
POST /todos
PUT/PATCH /todos/1
DELETE /todos/1
Obtener todos
Obtener todo con id 1
Agregar todo
Actualizar todo con id 1
Borrar todo con id 1
const { equal } = require('assert')
const http = require('http')
const todos = [
{
id: 1,
text: 'Ir al supermercado',
},
{
id: 2,
text: 'Terminar la tarea de sobre API de Aplicaciones Web',
},
{
id: 3,
text: 'Bañar al perro',
},
]
const server = http.createServer(function (req, res) {
const { method, url } = req
let body = []
req
.on('data', (chunk) => {
body.push(chunk)
})
.on('end', () => {
body = Buffer.concat(body).toString()
let status = 404
const response = {
success: false,
data: null,
error: null,
}
if (method === 'GET' && url === '/todos') {
status = 200
response.success = true
response.data = todos
} else if (method === 'POST' && url === '/todos') {
const { id, text } = JSON.parse(body)
if (!id || !text) {
status = 400
response.error = 'Por favor agregar id y texto del todo'
} else {
todos.push({ id, text })
status = 201
response.success = true
response.data = todos
}
}
res.writeHead(status, {
'Content-type': 'application/json',
'X-Powered-By': 'Node.js',
})
res.end(JSON.stringify(response))
})
})
const PORT = 5000
server.listen(PORT, function () {
console.log(`El servidor está corriendo en el puerto ${PORT}`)
})
(Función de flecha)
Una expresión de función flecha es una alternativa compacta a una expresión de función tradicional.
Una función de callback es una función que se pasa a otra función como un argumento, que luego se invoca dentro de la función externa para completar algún tipo de rutina o acción.
Objetivo
Desarrollar el Backend del proyecto de fin de curso
// Crear el folder backend
npm init
npm install express dotenv
npm install -D nodemonExpressjs es un framework rápido, minimalista y flexible de Node.js. Permite crear APIs y aplicaciones web fácilmente, provee un conjunto de características como manejo de rutas (direccionamiento), archivos estáticos, integración con bases de datos, manejo de errores, middlewares entre otras.
Las variables de entorno nos permiten administrar la configuración de nuestras aplicaciones por separado de nuestro código base.
Las configuraciones separadas facilitan la implementación de nuestra aplicación en diferentes entornos (development, test, production, etc).
const express = require('express')
const dotenv = require('dotenv')
// Load env vars
dotenv.config({ path: './config/config.env' })
const app = express()
const PORT = process.env.PORT || 5000
app.listen(PORT, () => {
console.log(`Server is running in ${process.env.NODE_ENV} on port ${PORT}`)
})
server.js
Git es un software de control de versiones diseñado por Linus Torvalds, pensando en la eficiencia, la confiabilidad y compatibilidad del mantenimiento de versiones de aplicaciones cuando éstas tienen un gran número de archivos de código fuente.
Estructura
/api/v1/products
/api/v1/users
/api/v1/orders
...
Básicamente MVC es un patrón de diseño que propone separar nuestro código por responsabilidad.
Model: Es el encargado de manejar la información de nuestra aplicación (por ejemplo, se encarga de guardar información en un archivo).
View: Es lo que el usuario ve y con lo que este interactúa.
Controller: Es el encargado de unir el Model y la View, y también posee la lógica para transformar los datos para que los entienda tanto el Model como la View.
Controllers
Los encargados de la lógica...
Middleware en Express JS. Un middleware es una función que se puede ejecutar antes o después del manejo de una ruta.
(records)
Lectura obligatoria: https://www.acens.com/wp-content/images/2014/02/bbdd-nosql-wp-acens.pdf
products.js
[
{
"name": "Auriculares inalámbricos Bluetooth Airpods",
"image": "/images/airpods.jpg",
"description": "La tecnología Bluetooth le permite conectarlo con dispositivos compatibles de forma inalámbrica El audio AAC de alta calidad ofrece una experiencia auditiva envolvente El micrófono incorporado le permite recibir llamadas mientras trabaja",
"brand": "Apple",
"category": "Electrónica",
"price": 89.99,
"countInStock": 3,
"rating": 0,
"numReviews": 0
},
{
"name": "iPhone 11 Pro 256GB Memoria",
"image": "/images/phone.jpg",
"description": "Presentamos el iPhone 11 Pro. Un sistema transformador de triple cámara que agrega toneladas de capacidad sin complejidad. Un salto sin precedentes en la duración de la batería",
"brand": "Apple",
"category": "Electrónica",
"price": 599.99,
"countInStock": 10,
"rating": 0,
"numReviews": 0
},
{
"name": "Cámara Cannon EOS 80D DSLR",
"image": "/images/camera.jpg",
"description": "Caracterizada por especificaciones de imagen versátiles, la Canon EOS 80D se aclara aún más mediante un par de sistemas de enfoque robustos y un diseño intuitivo",
"brand": "Cannon",
"category": "Electrónica",
"price": 929.99,
"countInStock": 0,
"rating": 0,
"numReviews": 0
},
{
"name": "Sony Playstation 4 Pro versión blanca",
"image": "/images/playstation.jpg",
"description": "El centro de entretenimiento doméstico definitivo comienza con PlayStation. Ya sea que le gusten los juegos, las películas HD, la televisión, la música",
"brand": "Sony",
"category": "Electrónica",
"price": 399.99,
"countInStock": 10,
"rating": 0,
"numReviews": 0
},
{
"name": "Mouse para juegos Logitech G-Series",
"image": "/images/mouse.jpg",
"description": "Controle mejor sus juegos con este mouse para juegos Logitech LIGHTSYNC. Los seis botones programables permiten la personalización para una experiencia de juego fluida",
"brand": "Logitech",
"category": "Electrónica",
"price": 49.99,
"countInStock": 7,
"rating": 0,
"numReviews": 0
},
{
"name": "Amazon Echo Dot 3ra Generación",
"image": "/images/alexa.jpg",
"description": "Conoce a Echo Dot: nuestro altavoz inteligente más popular con un diseño de tela. Es nuestro altavoz inteligente más compacto que se adapta perfectamente a espacios reducidos",
"brand": "Amazon",
"category": "Electrónica",
"price": 29.99,
"countInStock": 0,
"rating": 0,
"numReviews": 0
}
]
Video de análisis obligatorio: https://youtu.be/gFvWNjVy-wc
Video de análisis obligatorio: https://youtu.be/5XyzLfPBpZs
UTF-8
Video de análisis obligatorio: https://youtu.be/JG4FtfAiPE4
Video de análisis obligatorio: https://youtu.be/wfogZfIS03U
Las expresiones regulares son patrones que se utilizan para hacer coincidir combinaciones de caracteres en cadenas.
Video de análisis obligatorio: https://youtu.be/wfogZfIS03U
Las expresiones regulares son patrones que se utilizan para hacer coincidir combinaciones de caracteres en cadenas.
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/Lectura obligatoria: https://openwebinars.net/blog/que-es-json-web-token-y-como-funciona/
Se define un mecanismo para poder propagar entre dos partes, y de forma segura, la identidad de un determinado usuario, además con una serie de claims o privilegios.
[
{
"_id": "6041ab94157a1f54e0e75666",
"name": "Auriculares inalámbricos Bluetooth Airpods",
"image": "/images/airpods.jpg",
"description": "La tecnología Bluetooth le permite conectarlo con dispositivos compatibles de forma inalámbrica El audio AAC de alta calidad ofrece una experiencia auditiva envolvente El micrófono incorporado le permite recibir llamadas mientras trabaja",
"brand": "Apple",
"category": "Electrónica",
"price": 89.99,
"countInStock": 3,
"rating": 0,
"numReviews": 0,
"user": "6041a8da4e0425528d2454ed"
},
{
"_id": "6041ab75157a1f54e0e75664",
"name": "iPhone 11 Pro 256GB Memoria",
"image": "/images/phone.jpg",
"description": "Presentamos el iPhone 11 Pro. Un sistema transformador de triple cámara que agrega toneladas de capacidad sin complejidad. Un salto sin precedentes en la duración de la batería",
"brand": "Apple",
"category": "Electrónica",
"price": 599.99,
"countInStock": 10,
"rating": 0,
"numReviews": 0,
"user": "6041a8da4e0425528d2454ed"
},
{
"_id": "6041a8cf33f6a452cad6d885",
"name": "Cámara Cannon EOS 80D DSLR",
"image": "/images/camera.jpg",
"description": "Caracterizada por especificaciones de imagen versátiles, la Canon EOS 80D se aclara aún más mediante un par de sistemas de enfoque robustos y un diseño intuitivo",
"brand": "Cannon",
"category": "Electrónica",
"price": 929.99,
"countInStock": 0,
"rating": 0,
"numReviews": 0,
"user": "6041a8da4e0425528d2454ed"
},
{
"_id": "6041a8cf33f6a452cad6d886",
"name": "Sony Playstation 4 Pro versión blanca",
"image": "/images/playstation.jpg",
"description": "El centro de entretenimiento doméstico definitivo comienza con PlayStation. Ya sea que le gusten los juegos, las películas HD, la televisión, la música",
"brand": "Sony",
"category": "Electrónica",
"price": 399.99,
"countInStock": 10,
"rating": 0,
"numReviews": 0,
"user": "6041a8da4e0425528d2454ed"
},
{
"_id": "6041a8cf33f6a452cad6d883",
"name": "Mouse para juegos Logitech G-Series",
"image": "/images/mouse.jpg",
"description": "Controle mejor sus juegos con este mouse para juegos Logitech LIGHTSYNC. Los seis botones programables permiten la personalización para una experiencia de juego fluida",
"brand": "Logitech",
"category": "Electrónica",
"price": 49.99,
"countInStock": 7,
"rating": 0,
"numReviews": 0,
"user": "6041a8da4e0425528d24541a"
},
{
"_id": "6041a8cf33f6a452cad6d884",
"name": "Amazon Echo Dot 3ra Generación",
"image": "/images/alexa.jpg",
"description": "Conoce a Echo Dot: nuestro altavoz inteligente más popular con un diseño de tela. Es nuestro altavoz inteligente más compacto que se adapta perfectamente a espacios reducidos",
"brand": "Amazon",
"category": "Electrónica",
"price": 29.99,
"countInStock": 0,
"rating": 0,
"numReviews": 0,
"user": "6041a8da4e0425528d24541a"
}
]
[
{
"_id": "6041a8da4e0425528d2454ed",
"name": "Joshua Bonilla",
"email": "joshua@gmail.com",
"password": "123456"
},
{
"_id": "6041a8da4e0425528d24541a",
"name": "Ashley López",
"email": "ashley@gmail.com",
"password": "123456"
}
]
products.js
users.js
[
{
"title": "Muy buen producto",
"comment": "Tengo seis meses de usar el producto y me ha parecido excelente",
"product": "6041ab75157a1f54e0e75664",
"rating": 8,
"user": "6041a8da4e0425528d2454ed"
},
{
"title": "Excelente",
"comment": "Calidad inigualable",
"product": "6041ab75157a1f54e0e75664",
"rating": 10,
"user": "6041a8da4e0425528d24541a"
}
]
reviews.js
Lectura obligatoria: https://programacionymas.com/blog/jwt-vs-cookies-y-sesiones
Objetivo
Desarrollar el Frontend del proyecto de fin de curso
Es una librería Javascript focalizada en el desarrollo de interfaces de usuario (UI)
Otros
"emmet.includeLanguages": {
"javascript": "javascriptreact"
},
JSX es una extensión de sintaxis de JavaScript que nos permite mezclar JS y HTML (XML), de ahí su nombre JavaScript XML.
Lectura obligatoria: https://medium.com/@simonhoyos/qu%C3%A9-es-jsx-95006a2f94f9
react-router-dom
react-boostrap
Es un contenedor predecible de estados de nuestra aplicación
Es un contenedor predecible de estados de nuestra aplicación
Store
Fuente única de la verdad
Es una función pura que maneja los estados
Agregar producto
Reducer
State: [products]
{
"_id": "6041ab94157a1f54e0e75666",
"name": "Auriculares inalámbricos Bluetooth Airpods",
"image": "/images/airpods.jpg",
"description": "La tecnología Bluetooth le permite conectarlo con dispositivos compatibles de forma inalámbrica El audio AAC de alta calidad ofrece una experiencia auditiva envolvente El micrófono incorporado le permite recibir llamadas mientras trabaja",
"brand": "Apple",
"category": "Electrónica",
"price": 89.99,
"countInStock": 3,
"rating": 0,
"numReviews": 0,
"user": "6041a8da4e0425528d2454ed"
},state
Página o vista
Action
R
Reducer
R
R
R
Dipatcher
middlewares
API
Store
mkdir MBShop
cd MBShop
npx create-react-app frontend
cd frontend
npmstart
// Borrar archivo innecesarios del frontend
// Sacar el .gitignore a la raiz
// Agregar noe_modules/ y .env al .gitignore
// Borrar el .git
Ctrl + C
cd ..
git init
git add .
git commit -m 'React setup'
cd frontend
npm startnpm install react-bootstrap npm install react-router-dom react-router-bootstrapcd frontend
npm install axios// Desde MBShop
npm i -D nodemon concurrently// ...
"scripts": {
"start": "node backend/server",
"server": "nodemon backend/server",
"client": "npm start --prefix frontend",
"dev": "concurrently \"npm run server\" \"npm run client\""
},
// ...MBShop/package.json
// https://www.npmjs.com/package/dotenv
npm install dotenvNODE_ENV = development
PORT = 5000MBShop/.env
"type": "module"MBShop/package.json
npm i bcryptjscd frontend
npm i redux react-redux redux-thunk redux-devtools-extensionnpm i jsonwebtokenconst 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/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
"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
"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
node_modulesCuando 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
www.heroku.com
git init
git add .
git commit -m "Commit inicial"//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//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 logsOpen 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
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
Hacer que Google conozca nuestra aplicación
Hacer que Google conozca nuestra aplicación
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.jsconst 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);
app.get(
"/auth/google",
passport.authenticate("google", {
scope: ["profile", "email"]
})
);Nota: este cambio puede tomar en algunos casos hasta 5 minutos para que google agregue la redirección.
app.get('/auth/google/callback', passport.authenticate('google'));Ver código generado por el accessToken
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);
}
)
);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
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
// ... 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));
}
}
);
}
)
);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.
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 igual1. Desde la terminal
npm install --save cookie-session2. 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 igual3. 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
app.get('/api/current_user', (req, res) => {
res.send(req.user);
});Agregar esta ruta en authRoute
app.get('/api/logout', (req, res) => {
req.logout();
res.send(req.user);
});cookie-session
express-session
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.
En la versión en producción deben usarse credenciales seguras. En nuestro caso es básica por fines de simplicidad.
1
2
3
Desde la terminal ejecutamos el siguiente comando para obtener la ruta de la aplicación alojada en Heroku
heroku openmodule.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.jsEn .gitignore
if(process.env.NODE_ENV === 'production') {
module.exports = require('./prod');
} else {
module.exports = require('./dev');
}En keys.js
1
2
3
En la terminal
git status
git add .
git commit -m "Flujo Auth finalizado"
git push heroku master
heroku opennew GoogleStrategy(
{
clientID: keys.googleClientID,
clientSecret: keys.googleClientSecret,
callbackURL: "/auth/google/callback",
proxy: true
},
// el resto queda igualgit status
git add .
git commit -m "Ajuste al Google Strategy"
git push heroku master
heroku open(El lado del cliente)
npm install -g create-react-appSe recomienda que la consola tenga permisos de administrador
npx create-react-app my-app
// Client tiene su propio servidor
cd client
npm startSe puede trabajar juntos, pero de forma separada create-react-app ahorra mucho tiempo con la configuración de dependencias.
1. Se puede trabajar con dos consolas separadas
Consola 1 - en la carpeta de "client"
Consola 2 - desde la carpeta "server"
npm startnpm run devnpm install --save concurrently2. 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 devTerminal
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 --saveAhora 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'
})
);
};
cd client
npm run buildNota: Hay que agregar el callback
function fetchAlbums() {
fetch('http://rallycoding.herokuapp.com/api/music_albums')
.then(res => res.json())
.then(json => console.log(json));
}
fetchAlbums();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
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
Dejamos el directorio src de la siguiente forma (todo va desde cero):
cd client
npm install --save redux react-redux react-router-domEn index.js
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(); 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 ..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")
);
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());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
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
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
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';Para la apariencia vamos a utilizar un framework de propósito general que puede utilizar con cualquier librería o framework JavaScript
cd client
npm install --save materialize-cssDesde la terminal
En la primera línea de index.js dentro de src
import 'materialize-css/dist/css/materialize.min.css';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)
cd client
npm install --save axios redux-thunkEn 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
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
// 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
})
);
};
};
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;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;
}
}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 });
};
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
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
// ...
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
// ...
app.get(
'/auth/google/callback',
passport.authenticate('google'),
(req, res) => {
res.redirect('/surveys');
}
);
// ...Del lado del servidor authRoutes.js, modificar
// ...
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('/');
});
// ...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 errorEn App.js
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>
);
}
Somos malos en seguridad
La facturación es difícil
Somos malos en seguridad
La facturación es difícil
// Desde la terminal
cd client
npm install --save react-stripe-checkoutInstalar react-stripe-checkout
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
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
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';// ...
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
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
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 });
};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);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) => {
});
}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)
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
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
// ...
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
// ...
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.
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
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
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
// ...
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
// ...
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
By Wilfredo Meneses