# Express Template Engine

En este capítulo realizaremos un proyecto para conocer como trabajar con motores de plantilla y autenticación de usuarios entre otras cosas.

# Códigos

notas del editor

git push --all origin

# Objetivos

  • Motores de plantilla (template engine)
  • MongoDB
    • ShortURL
  • Auth User
    • Rutas protegidas
    • Sessions
    • Passport
  • Email
    • Enviar confirmación de cuenta
  • Subir archivos
    • Cambiar foto de perfil

# Próximos capítulos:

  • API REST
  • JWT
  • Firebase
    • Auth
    • Firestore
  • MEVN / MERN

# Template Engines

En una galaxia muy lejana...

npm init -y
npm i -D nodemon
npm i express express-handlebars

package.json

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

nodemon.json

{
    "ext": "js,json,hbs"
}

index.js

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

app.get("/", (req, res) => {
    res.send("probando...");
});

const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log("server andando 🔥"));

Directorios

└── views
    ├── home.hbs
    └── layouts
        └── main.hbs
const express = require("express");
const { create } = require("express-handlebars");

const app = express();

const hbs = create({
    extname: ".hbs",
});

app.engine(".hbs", hbs.engine);
app.set("view engine", ".hbs");
app.set("views", "./views");

app.get("/", (req, res) => {
    res.render("home", { titulo: "Página de inicio" });
});

const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log("server andando 🔥"));

views/layouts/main.hbs

<!DOCTYPE html>
<html lang="es">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Main</title>
    </head>
    <body>
        {{{body}}}
    </body>
</html>

views/home.hbs

<h1>{{titulo}}</h1>

# Partials/Components

const hbs = create({
    extname: ".hbs",
    partialsDir: ["views/components"],
});

views/components/Navbar.hbs

<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
    <div class="container">
        <a class="navbar-brand" href="/">Navbar</a>
        <button
            class="navbar-toggler"
            type="button"
            data-bs-toggle="collapse"
            data-bs-target="#navbarNavAltMarkup"
            aria-controls="navbarNavAltMarkup"
            aria-expanded="false"
            aria-label="Toggle navigation"
        >
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarNavAltMarkup">
            <div class="navbar-nav ms-auto">
                <a class="nav-link active" aria-current="page" href="/">Home</a>
                <a class="nav-link" href="/logup">LogUp</a>
                <a class="nav-link" href="/login">LogIn</a>
                <a class="nav-link" href="/logup">LogOut</a>
            </div>
        </div>
    </div>
</nav>

views/layout/main.hbs

<!DOCTYPE html>
<html lang="es">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Main</title>
        <link
            href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
            rel="stylesheet"
            integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
            crossorigin="anonymous"
        />
    </head>
    <body>
        {{> Navbar}}

        <div class="container mt-2">
            {{{body}}}
        </div>

        <script
            src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
            integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
            crossorigin="anonymous"
        ></script>
    </body>
</html>

# Helpers

app.get("/", (req, res) => {
    const urls = [
        { origin: "https://www.google.com", shortURL: nanoid(7) },
        { origin: "https://www.bluuweb.org", shortURL: nanoid(7) },
    ];
    res.render("home", { titulo: "Página de inicio", urls });
});
<h1 class="text-center my-5">{{titulo}}</h1>

<ul>
    {{#each urls }}
    <li>{{this.origin}} - {{this.shortURL}}</li>
    {{/each}}
</ul>

components/Card.hbs

<article class="card mb-2">
    <div class="card-body">
        <p>{{url.origin}}</p>
        <p>{{url.shortURL}}</p>
        <button class="btn btn-primary">Copiar</button>
        <a href="#" class="btn btn-danger">Eliminar</a>
    </div>
</article>
<h1 class="text-center my-5">{{titulo}}</h1>

{{#each urls }} {{> Card url=this}} {{/each}}

# express.Router

  • Router (opens new window): Utilice la clase express.Router para crear manejadores de rutas montables y modulares. Una instancia Router es un sistema de middleware y direccionamiento completo.

routes/home.js

const express = require("express");
const { nanoid } = require("nanoid");
const router = express.Router();

router.get("/", (req, res) => {
    const urls = [
        { origin: "https://www.google.com", shortURL: nanoid(7) },
        { origin: "https://www.bluuweb.org", shortURL: nanoid(7) },
    ];
    res.render("home", { titulo: "Página de inicio", urls });
});

module.exports = router;
app.use("/", require("./routes/home"));

# CREATE

components/FormAcortar.hbs

<form action="/" method="post">
    <input
        type="text"
        placeholder="Ingresa URL"
        class="form-control my-2"
        name="originURL"
    />
    <button class="btn btn-dark mb-2 w-100">Acortar URL</button>
</form>
<h1 class="text-center my-5">{{titulo}}</h1>

{{> FormAcortar}} {{#each urls }} {{> Card url=this}} {{/each}}

index.js

app.use(express.urlencoded({ extended: true }));

home.js

router.post("/", (req, res) => {
    console.log(req.body);
    res.redirect("/");
});

# MongoDB

npm i dotenv
npm i mongoose

.env

URI=mongodb+srv://<username>:<password>@cluster0.xxx.mongodb.net/myFirstDatabase?retryWrites=true&w=majority

database/conexion.js

const mongoose = require("mongoose");

mongoose
    .connect(process.env.URI, {})
    .then(() => console.log("db conectada! 😍"))
    .catch((e) => console.log("error de conexión: " + e));

index.js

const express = require("express");
const { create } = require("express-handlebars");
require("dotenv").config();
require("./database/conexion");

# Schema

  • Schema (opens new window): Con Mongoose, todo se deriva de un esquema.
  • Cada esquema se asigna a una colección MongoDB y define la forma de los documentos dentro de esa colección.

models/Url.js

const mongoose = require("mongoose");
const { Schema } = mongoose;

const urlSchema = new Schema({
    origin: {
        type: String,
        unique: true,
        required: true,
    },
    shortURL: {
        type: String,
        unique: true,
        required: true,
        default: nanoid(7),
    },
});

# Model

  • Para usar nuestra definición de esquema, necesitamos convertirla a un modelo con el que podamos trabajar.
const Url = mongoose.model("Url", urlSchema);
module.exports = Url;

# routes/home.js

const Url = require("../models/Url");
router.post("/", async (req, res) => {
    // console.log(req.body);
    const { originURL } = req.body;
    const url = new Url({ origin: originURL });
    console.log(url);
    try {
    } catch (error) {
        console.log(error);
    }
    res.redirect("/");
});
router.post("/", async (req, res) => {
    const { originURL } = req.body;
    const url = new Url({ origin: originURL });
    try {
        await url.save();
        res.redirect("/");
    } catch (error) {
        console.log(error);
    }
});

# READ

  • lean (opens new window): La opción Lean le dice a Mongoose que omita la hidratación de los documentos de resultados. Esto hace que las consultas sean más rápidas y requieran menos memoria, pero los documentos de resultados son simples objetos JavaScript, no documentos Mongoose.
router.get("/", async (req, res) => {
    try {
        const urls = await Url.find().lean();
        console.log(urls);
        res.render("home", { titulo: "Página de inicio", urls });
    } catch (error) {
        console.log(error);
    }
});

# Middlewares

middlewares/validarURL.js

const { URL } = require("url");

const validarURL = (req, res, next) => {
    try {
        const { originURL } = req.body;
        const urlFrontend = new URL(originURL);
        if (urlFrontend.origin !== "null") {
            if (
                urlFrontend.protocol === "http:" ||
                urlFrontend.protocol === "https:"
            ) {
                return next();
            }
        }
        throw new Error("no válida 😲");
    } catch (error) {
        // console.log(error);
        return res.redirect("/");
    }
};

module.exports = validarURL;

routes/home.js

const validarURL = require("../middlewares/validarURL");
router.post("/", validarURL, async (req, res) => {
    // console.log(req.body);
    const { originURL } = req.body;

    const url = new Url({ origin: originURL });
    try {
        await url.save();
        res.redirect("/");
    } catch (error) {
        console.log(error);
    }
});

# Controllers

controllers/urlController.js

const Url = require("../models/Url");

const homeUrl = async (req, res) => {
    try {
        const urls = await Url.find().lean();
        res.render("home", { titulo: "Página de inicio", urls });
    } catch (error) {
        console.log(error);
    }
};

const agregarUrl = async (req, res) => {
    const { originURL } = req.body;

    const url = new Url({ origin: originURL });
    try {
        await url.save();
        res.redirect("/");
    } catch (error) {
        console.log(error);
    }
};

module.exports = {
    homeUrl,
    agregarUrl,
};

routes/home.js

const express = require("express");
const { homeUrl, agregarUrl } = require("../controllers/urlController");
const validarURL = require("../middlewares/validarURL");

const router = express.Router();

router.get("/", homeUrl);

router.post("/", validarURL, agregarUrl);

module.exports = router;

# DELETE

Card.hbs

<a href="/eliminar/{{url._id}}" class="btn btn-danger">Eliminar</a>

controllers/urlController.js

const eliminarUrl = async (req, res) => {
    const { id } = req.params;
    try {
        await Url.findByIdAndDelete(id);
        res.redirect("/");
    } catch (error) {
        console.log(error);
    }
};

routes/home.js

router.get("/eliminar/:id", eliminarUrl);

# UPDATE

Card.hbs

<a href="/editar/{{url._id}}" class="btn btn-warning">Editar</a>

controllers

const editarUrlForm = async (req, res) => {
    const { id } = req.params;
    try {
        const urlDB = await Url.findById(id).lean();
        console.log(urlDB);
        res.render("home", { titulo: "Página de inicio", urlDB });
    } catch (error) {
        console.log(error);
    }
};

routes

router.get("/editar/:id", editarUrlForm);

FormAcortar.hbs

{{#if urlDB}}

<form action="/editar/{{urlDB._id}}" method="post">
    <input
        type="text"
        placeholder="Ingresa URL"
        class="form-control my-2"
        name="originURL"
        value="{{urlDB.origin}}"
    />
    <button class="btn btn-warning mb-2 w-100">Editar URL</button>
</form>

{{else}}

<form action="/" method="post">
    <input
        type="text"
        placeholder="Ingresa URL"
        class="form-control my-2"
        name="originURL"
    />
    <button class="btn btn-dark mb-2 w-100">Acortar URL</button>
</form>

{{/if}}

controllers

const editarUrl = async (req, res) => {
    const { id } = req.params;
    const { originURL } = req.body;
    try {
        const url = await Url.findById(id);
        if (!url) {
            console.log("no exite");
            return res.send("error no existe el documento a editar");
        }

        await Url.findByIdAndUpdate(id, { origin: originURL });

        res.redirect("/");
    } catch (error) {
        console.log(error);
    }
};

routes

router.post("/editar/:id", validarURL, editarUrl);

# Redireccionamiento

controllers

const redireccionar = async (req, res) => {
    const { shortURL } = req.params;
    try {
        const url = await Url.findOne({ shortURL });
        // console.log(url);
        if (!url?.origin) {
            console.log("no exite");
            return res.send("error no existe el redireccionamiento");
        }

        res.redirect(url.origin);
    } catch (error) {
        console.log(error);
    }
};

routes

router.get("/:shortURL", redireccionar);

# clipboard

index.js

app.use(express.static(__dirname + "/public"));

main.hbs

<script src="/js/app.js"></script>

Card.hbs





 







<article class="card mb-2">
    <div class="card-body">
        <p>{{url.origin}}</p>
        <p>{{url.shortURL}}</p>
        <button class="btn btn-primary" data-short="{{url.shortURL}}">
            Copiar
        </button>
        <a href="/editar/{{url._id}}" class="btn btn-warning">Editar</a>
        <a href="/eliminar/{{url._id}}" class="btn btn-danger">Eliminar</a>
    </div>
</article>

public/js/app.js

document.addEventListener("click", (e) => {
    if (e.target.dataset.short) {
        const url = `http://localhost:5000/${e.target.dataset.short}`;

        navigator.clipboard
            .writeText(url)
            .then(() => {
                console.log("Text copied to clipboard...");
            })
            .catch((err) => {
                console.log("Something went wrong", err);
            });
    }
});

# Register User

En esta sección trabajaremos con el registro de usuarios:

index.js

app.use("/", require("./routes/auth"));

routes/auth.js

const express = require("express");
const { formRegister } = require("../controllers/authController");
const router = express.Router();

router.get("/register", formRegister);

module.exports = router;

controllers/authController.js

const formRegister = (req, res) => {
    res.render("register");
};

module.exports = {
    formRegister,
};

views/register.hbs

<h1 class="text-center my-5">Registro de usuarios</h1>
<div class="row justify-content-center">
    <div class="col-md-6">
        <form action="/register" method="post">
            <input
                class="form-control mb-2"
                name="nombre"
                placeholder="Ingrese nombre"
                type="text"
                value="bluuweb"
            />
            <input
                class="form-control mb-2"
                name="email"
                placeholder="Ingrese email"
                type="email"
                value="bluuweb@prueba.com"
            />
            <input
                class="form-control mb-2"
                name="password"
                placeholder="Ingrese contraseña"
                type="password"
                value="123123"
            />
            <input
                class="form-control mb-2"
                name="passwordRepit"
                placeholder="Repita contraseña"
                type="password"
                value="123123"
            />
            <button type="submit" class="btn btn-primary">Crear Cuenta</button>
        </form>
    </div>
</div>

# Model User

TIP

Bcrypt supports both sync and async methods. The asynchronous approach is recommended because hashing is CPU intensive, and the synchronous version will block the event loop and prevent your app from handling other requests until it finishes.

models/User.js

const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");
const { Schema } = mongoose;

const userSchema = new Schema({
    username: {
        type: String,
        lowercase: true,
        required: true,
        match: [/^[a-zA-Z0-9]+$/, "Solo letras y números"],
    },
    email: {
        type: String,
        lowercase: true,
        required: true,
        index: { unique: true },
    },
    image: {
        type: String,
    },
    password: {
        type: String,
        required: true,
    },
    tokenConfirm: {
        type: String,
        default: null,
    },
    confirm: {
        type: Boolean,
        default: false,
    },
});

userSchema.pre("save", async function(next) {
    const user = this;
    // only hash the password if it has been modified (or is new)
    if (!user.isModified("password")) return next();

    try {
        // generate a salt
        const salt = await bcrypt.genSalt(10);
        const hash = await bcrypt.hash(user.password, salt);
        user.password = hash;
        next();
    } catch (error) {
        console.log(error);
        throw new Error("Error al codificar la contraseña");
    }
});

userSchema.methods.comparePassword = async function(candidatePassword) {
    return await bcrypt.compare(candidatePassword, this.password);
};

module.exports = mongoose.model("User", userSchema);

routes/auth.js

const express = require("express");
const {
    formRegister,
    registrarUsuario,
    confirmarCuenta,
    formLogin,
    loginUsuario,
} = require("../controllers/authController");
const router = express.Router();

router.get("/register", formRegister);
router.post("/register", registrarUsuario);
router.get("/register/:tokenConfirm", confirmarCuenta);
router.get("/login", formLogin);
router.post("/login", loginUsuario);

module.exports = router;

controllers/authController.js

const { nanoid } = require("nanoid");
const User = require("../models/User");

const formRegister = (req, res) => {
    res.render("register");
};

const registrarUsuario = async (req, res) => {
    const { nombre, email, password, passwordRepit } = req.body;

    try {
        if (await User.findOne({ email })) {
            throw new Error("Ya existe este usuario");
        }

        const user = new User({ username: nombre, email, password });
        user.tokenConfirm = nanoid();
        console.log(user);
        await user.save();

        // res.json(user);
        res.render("login");
    } catch (error) {
        console.log(error);
        res.send(error.message);
    }
};

const confirmarCuenta = async (req, res) => {
    const { tokenConfirm } = req.params;
    try {
        const user = await User.findOne({ tokenConfirm });
        if (!user) throw new Error("no se pudo confirmar cuenta");

        user.tokenConfirm = null;
        user.confirm = true;

        await user.save();

        res.render("login");
    } catch (error) {
        console.log(error);
        res.send(error.message);
    }
};

const formLogin = (req, res) => {
    res.render("login");
};

const loginUsuario = async (req, res) => {
    const { email, password } = req.body;

    try {
        const user = await User.findOne({ email });

        if (!user) throw new Error("No existe el usuario");

        if (!user.confirm) throw new Error("Usuario no confirmado");

        if (!(await user.comparePassword(password))) {
            throw new Error("Password incorrecta");
        }
        res.render("home");
    } catch (error) {
        console.log(error);
        res.send(error.message);
    }
};

module.exports = {
    formRegister,
    registrarUsuario,
    confirmarCuenta,
    formLogin,
    loginUsuario,
};

index.js

app.use("/", require("./routes/auth"));
app.use("/", require("./routes/home"));

# Session & flash

npm i express-session
npm i connect-mongo
npm i connect-flash
const session = require("express-session");

app.use(
    session({
        secret: process.env.SESSIONSECRET,
        resave: false,
        saveUninitialized: false,
    })
);

app.get("/ruta-protegida", (req, res) => {
    res.json(req.session.usuario || "sin sessión de usuario");
});

app.get("/crear-session", (req, res) => {
    req.session.usuario = "bluuweb";
    res.redirect("/ruta-protegida");
});

app.get("/destruir-session", (req, res) => {
    req.session.destroy();
    res.redirect("/ruta-protegida");
});

flash

const express = require("express");
const session = require("express-session");
const flash = require("connect-flash");

const app = express();

app.use(
    session({
        secret: process.env.SESSIONSECRET,
        resave: false,
        saveUninitialized: false,
    })
);
app.use(flash());

app.get("/mensaje-flash", (req, res) => {
    res.json(req.flash("mensaje"));
});

app.get("/campos-validados", (req, res) => {
    req.flash("mensaje", "todos los campos fueron validados");
    res.redirect("/mensaje-flash");
});

# Express Validator

routes/auth.js

const express = require("express");
const { body } = require("express-validator");
const {
    loginForm,
    registerForm,
    registerUser,
    confirmarCuenta,
    loginUser,
} = require("../controllers/authController");

const router = express.Router();

router.get("/register", registerForm);
router.post(
    "/register",
    [
        body("userName", "Ingrese un nombre")
            .trim()
            .notEmpty()
            .escape(),
        body("email", "Ingrese un email válido")
            .trim()
            .isEmail()
            .normalizeEmail(),
        body("password", "Contraseña con 6 o más carácteres")
            .trim()
            .isLength({ min: 6 })
            .escape()
            .custom((value, { req }) => {
                if (value !== req.body.repassword) {
                    throw new Error("Password no coinciden");
                } else {
                    return value;
                }
            }),
    ],
    registerUser
);
router.get("/confirmar/:token", confirmarCuenta);
router.get("/login", loginForm);
router.post(
    "/login",
    [
        body("email", "Ingrese un email válido")
            .trim()
            .isEmail()
            .normalizeEmail(),
        body("password", "Contraseña no cumple el formato")
            .trim()
            .isLength({ min: 6 })
            .escape(),
    ],
    loginUser
);

module.exports = router;

controllers/authControllers.js

const User = require("../models/User");
const { validationResult } = require("express-validator");
const { nanoid } = require("nanoid");

const registerForm = (req, res) => {
    res.render("register", { mensajes: req.flash().mensajes });
};

const registerUser = async (req, res) => {
    // console.log(req.body);

    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        req.flash("mensajes", errors.array());
        return res.redirect("/auth/register");
    }

    const { userName, email, password } = req.body;
    try {
        let user = await User.findOne({ email: email });
        if (user) throw new Error("ya existe usuario");

        user = new User({ userName, email, password, tokenConfirm: nanoid() });
        await user.save();

        // enviar correo electrónico con la confirmación de la cuenta

        return res.redirect("/auth/login");
    } catch (error) {
        req.flash("mensajes", [{ msg: error.message }]);
        res.redirect("/auth/register");
    }
};

const confirmarCuenta = async (req, res) => {
    const { token } = req.params;

    try {
        const user = await User.findOne({ tokenConfirm: token });

        if (!user) throw new Error("No existe este usuario");

        user.cuentaConfirmada = true;
        user.tokenConfirm = null;

        await user.save();

        return res.redirect("/auth/login");
        // res.render("login");
    } catch (error) {
        req.flash("mensajes", [{ msg: error.message }]);
        res.redirect("/auth/login");
    }
};

const loginForm = (req, res) => {
    res.render("login", { mensajes: req.flash().mensajes });
};

const loginUser = async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        req.flash("mensajes", errors.array());
        return res.redirect("/auth/login");
    }

    const { email, password } = req.body;
    try {
        const user = await User.findOne({ email });
        if (!user) throw new Error("No existe este email");

        if (!user.cuentaConfirmada) throw new Error("Falta confirmar cuenta");

        if (!(await user.comparePassword(password)))
            throw new Error("Contraseña no correcta");

        return res.redirect("/");
    } catch (error) {
        req.flash("mensajes", [{ msg: error.message }]);
        res.redirect("/auth/login");
    }
};

module.exports = {
    loginForm,
    registerForm,
    registerUser,
    confirmarCuenta,
    loginUser,
};

# Rutas protegidas

npm install passport passport-local
const express = require("express");
const session = require("express-session");
const flash = require("connect-flash");
const passport = require("passport");
const app = express();

app.use(
    session({
        secret: "sessionSecreta",
        resave: false,
        saveUninitialized: false,
        name: "secreto-nombre-session",
    })
);
app.use(flash());

app.use(passport.initialize());
app.use(passport.session());

// Este va si o si
passport.serializeUser(
    (user, done) => done(null, { id: user._id, userName: user.userName }) //se guardará en req.user
);

// no preguntar en DB???
passport.deserializeUser(async (user, done) => {
    return done(null, user); //se guardará en req.user
});

// preguntar en DB por el usuario???
passport.deserializeUser(async (user, done) => {
    const userDB = await User.findById(user.id).exec();
    return done(null, { id: userDB._id, userName: userDB.userName }); //se guardará en req.user
});

middlewares/verificarUsuario.js

module.exports = (req, res, next) => {
    if (req.isAuthenticated()) {
        return next();
    }
    res.redirect("/auth/login");
};

controllers/authController.js


















 
 
 
 
 
 








 
 
 
 

const loginUser = async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        req.flash("mensajes", errors.array());
        return res.redirect("/auth/login");
    }

    const { email, password } = req.body;
    try {
        const user = await User.findOne({ email });
        if (!user) throw new Error("No existe este email");

        if (!user.cuentaConfirmada) throw new Error("Falta confirmar cuenta");

        if (!(await user.comparePassword(password)))
            throw new Error("Contraseña no correcta");

        req.login(user, function(err) {
            if (err) {
                throw new Error("Error de passport");
            }
            return res.redirect("/");
        });

        // return res.redirect("/");
    } catch (error) {
        req.flash("mensajes", [{ msg: error.message }]);
        res.redirect("/auth/login");
    }
};

const cerrarSesion = (req, res) => {
    req.logout();
    return res.redirect("/auth/login");
};

router/auth.js

router.post(
    "/login",
    [
        body("email", "Ingrese un email válido")
            .trim()
            .isEmail()
            .normalizeEmail(),
        body("password", "Contraseña no cumple el formato")
            .trim()
            .isLength({ min: 6 })
            .escape(),
    ],
    loginUser
);

router.get("/logout", cerrarSesion);

module.exports = router;

router/home.js

const verificarUsuario = require("../middlewares/verificarUsuario");

router.get("/", verificarUsuario, leerUrls);

controllers/homeController.js


 









const leerUrls = async (req, res) => {
    console.log(req.user);
    try {
        const urls = await Url.find().lean();
        return res.render("home", { urls: urls });
    } catch (error) {
        console.log(error);
        return res.send("falló algo...");
    }
};

# CSRF protection middleware.

npm install csurf

index.js

app.use(express.urlencoded({ extended: true }));

const csrf = require("csurf");

app.use(csrf());
app.use("/", require("./routes/home"));
app.use("/auth", require("./routes/auth"));

TIP

Se recomienda reiniciar el servidor (bajar nodemon y volver a levantarlo)

Todo lo que tenga formulario:

const registerForm = (req, res) => {
    res.render("register", {
        mensajes: req.flash().mensajes,
        csrfToken: req.csrfToken(),
    });
};

const loginForm = (req, res) => {
    res.render("login", {
        mensajes: req.flash().mensajes,
        csrfToken: req.csrfToken(),
    });
};
<input type="hidden" name="_csrf" value="{{csrfToken}}" />

Si no quieres enviarlo en cada render:

app.use(csrf());

// variables globales para las vistas
app.use((req, res, next) => {
    res.locals.csrfToken = req.csrfToken();
    next();
});

# Rutas protegidas (home)

routes/home.js

router.get("/", verficarUser, leerUrls);
router.post("/", verficarUser, urlValidar, agregarUrl);
router.get("/eliminar/:id", verficarUser, eliminarUrl);
router.get("/editar/:id", verficarUser, editarUrlForm);
router.post("/editar/:id", verficarUser, urlValidar, editarUrl);
router.get("/:shortURL", redireccionamiento);

middlewares/urlValida.js

const { URL } = require("url");
const urlValidar = (req, res, next) => {
    try {
        const { origin } = req.body;
        const urlFrontend = new URL(origin);
        if (urlFrontend.origin !== "null") {
            if (
                urlFrontend.protocol === "http:" ||
                urlFrontend.protocol === "https:"
            ) {
                return next();
            }
            throw new Error("tiene que contener https://");
        }
        throw new Error("url no válida 😲");
    } catch (error) {
        if (error.message === "Invalid URL") {
            req.flash("mensajes", [{ msg: "URL no válida" }]);
        } else {
            req.flash("mensajes", [{ msg: error.message }]);
        }
        return res.redirect("/");
    }
};

module.exports = urlValidar;

# Mensajes flash (home)

index.js

app.use((req, res, next) => {
    res.locals.csrfToken = req.csrfToken();
    res.locals.mensajes = req.flash("mensajes");
    next();
});

controllers/authController.js

const registerForm = (req, res) => {
    res.render("register");
};

const loginForm = (req, res) => {
    res.render("login");
};

controllers/homeController.js







 
 



const leerUrls = async (req, res) => {
    console.log(req.user);
    try {
        const urls = await Url.find().lean();
        return res.render("home", { urls: urls });
    } catch (error) {
        req.flash("mensajes", [{ msg: error.message }]);
        return res.redirect("/");
    }
};

# ref mongoDB

models/Url.js












 
 
 
 
 


const urlSchema = new Schema({
    origin: {
        type: String,
        unique: true,
        required: true,
    },
    shortURL: {
        type: String,
        unique: true,
        required: true,
    },
    user: {
        type: Schema.Types.ObjectId,
        ref: "User",
        required: true,
    },
});

homeController

Leer




 







const leerUrls = async (req, res) => {
    console.log(req.user);
    try {
        const urls = await Url.find({ user: req.user.id }).lean();
        return res.render("home", { urls: urls });
    } catch (error) {
        req.flash("mensajes", [{ msg: error.message }]);
        return res.redirect("/");
    }
};

Agregar








 


 







const agregarUrl = async (req, res) => {
    const { origin } = req.body;

    try {
        const url = new Url({
            origin: origin,
            shortURL: nanoid(8),
            user: req.user.id,
        });
        await url.save();
        req.flash("mensajes", [{ msg: "se agregó url correctamente" }]);
        return res.redirect("/");
    } catch (error) {
        req.flash("mensajes", [{ msg: error.message }]);
        return res.redirect("/");
    }
};

Eliminar (opcion 1)

const eliminarUrl = async (req, res) => {
    const { id } = req.params;
    try {
        const url = await Url.findById(id);
        if (!url.user.equals(req.user.id)) {
            throw new Error("no se puede eliminar url");
        }
        await url.remove();

        req.flash("mensajes", [{ msg: "se eliminó url correctamente" }]);
        return res.redirect("/");
    } catch (error) {
        req.flash("mensajes", [{ msg: error.message }]);
        return res.redirect("/");
    }
};

Form editar

const editarUrlForm = async (req, res) => {
    const { id } = req.params;
    try {
        const url = await Url.findById(id).lean();
        if (!url.user.equals(req.user.id)) {
            throw new Error("no se puede editar url");
        }
        return res.render("home", { url: url });
    } catch (error) {
        req.flash("mensajes", [{ msg: error.message }]);
        return res.redirect("/");
    }
};

POST editar

const editarUrl = async (req, res) => {
    const { id } = req.params;
    const { origin } = req.body;
    try {
        const url = await Url.findById(id);
        if (!url.user.equals(req.user.id)) {
            throw new Error("no se puede editar url");
        }
        await url.updateOne({ origin });
        req.flash("mensajes", [{ msg: "se editó url correctamente" }]);
        res.redirect("/");
    } catch (error) {
        req.flash("mensajes", [{ msg: error.message }]);
        return res.redirect("/");
    }
};

Redireccionamiento

const redireccionamiento = async (req, res) => {
    const { shortURL } = req.params;
    try {
        const urlDB = await Url.findOne({ shortURL: shortURL });
        if (!urlDB) throw new Error("404 no se encuentra la url");
        return res.redirect(urlDB.origin);
    } catch (error) {
        req.flash("mensajes", [{ msg: error.message }]);
        return res.redirect("/");
    }
};

# Nodemailer

npm install nodemailer
const nodemailer = require("nodemailer");
require("dotenv").config();

const registerUser = async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        req.flash("mensajes", errors.array());
        return res.redirect("/auth/register");
    }

    const { userName, email, password } = req.body;
    try {
        let user = await User.findOne({ email: email });
        if (user) throw new Error("ya existe usuario");

        user = new User({ userName, email, password, tokenConfirm: nanoid() });
        await user.save();

        // enviar correo electrónico con la confirmación de la cuenta
        const transport = nodemailer.createTransport({
            host: "smtp.mailtrap.io",
            port: 2525,
            auth: {
                user: process.env.userEmail,
                pass: process.env.passEmail,
            },
        });

        await transport.sendMail({
            from: '"Fred Foo 👻" <foo@example.com>',
            to: user.email,
            subject: "verifique cuenta de correo",
            html: `<a href="http://localhost:5000/auth/confirmar/${user.tokenConfirm}">verificar cuenta aquí</a>`,
        });

        req.flash("mensajes", [
            { msg: "Revisa tu correo electrónico y valida cuenta" },
        ]);
        return res.redirect("/auth/login");
    } catch (error) {
        req.flash("mensajes", [{ msg: error.message }]);
        return res.redirect("/auth/register");
        // return res.json({ error: error.message });
    }
};

# Subir archivos

models/User.js

imagen: {
    type: String,
    default: null,
},

login.hbs

<h1 class="text-center">{{user.userName}}</h1>
<div class="text-center mb-3">
    {{#if imagen}}
    <img src="/uploads/{{imagen}}" class="rounded-circle" />
    {{else}}
    <img
        src="/uploads/fotoPerfil.jpg"
        class="rounded-circle"
        width="200px"
        height="200px"
    />
    {{/if}}
</div>
<form
    action="/perfil?_csrf={{csrfToken}}"
    enctype="multipart/form-data"
    method="post"
>
    {{!-- <input type="hidden" name="_csrf" value="{{csrfToken}}" /> --}}
    <input class="form-control mb-2" type="file" id="formFile" name="myFile" />
    <button class="btn btn-dark">Cambir foto</button>
</form>

routes/home.js

router.get("/perfil", verficarUser, perfilForm);
router.post("/perfil", verficarUser, cambiarFotoPerfil);

controllers/perfilController.js

const formidable = require("formidable");
const fs = require("fs");
const Jimp = require("jimp");
const path = require("path");
const User = require("../models/User");

module.exports.perfilForm = async (req, res) => {
    try {
        const user = await User.findById(req.user.id);
        return res.render("perfil", { user: req.user, imagen: user.imagen });
    } catch (error) {
        req.flash("mensajes", [{ msg: "no se puede leer perfil" }]);
        return res.redirect("/perfil");
    }
};

module.exports.cambiarFotoPerfil = (req, res) => {
    const form = new formidable.IncomingForm();

    form.maxFileSize = 50 * 1024 * 1024; // 5MB

    form.parse(req, async (err, fields, files) => {
        // console.log(fields);
        // console.log(files);

        if (err) {
            req.flash("mensajes", [{ msg: "falló formidable" }]);
            return res.redirect("/perfil");
        }

        const file = files.myFile;

        try {
            if (file.originalFilename === "") {
                throw new Error("No se subió ninguna imagen");
            }

            const imageTypes = [
                "image/jpeg",
                "image/png",
                "image/webp",
                "image/gif",
            ];

            if (!imageTypes.includes(file.mimetype)) {
                throw new Error("Por favor agrega una imagen .jpg o png");
            }

            if (file.size > 50 * 1024 * 1024) {
                throw new Error("Máximo 5MB");
            }

            const extension = file.mimetype.split("/")[1];
            const dirFile = path.join(
                __dirname,
                `../public/uploads/${req.user.id}.${extension}`
            );

            fs.renameSync(file.filepath, dirFile);

            const image = await Jimp.read(dirFile);
            image
                .resize(200, 200)
                .quality(90)
                .writeAsync(dirFile);

            const user = await User.findById(req.user.id);
            user.imagen = `${req.user.id}.${extension}`;

            await user.save();

            req.flash("mensajes", [{ msg: "se guardó la imagen" }]);
            return res.redirect("/perfil");
        } catch (error) {
            console.log(error);
            req.flash("mensajes", [{ msg: error.message }]);
            return res.redirect("/perfil");
        }
    });
};

# connet-mongo

npm install connect-mongo

database/db.js

require("dotenv").config();
const mongoose = require("mongoose");

const clientDB = mongoose
    .connect(process.env.URI)
    .then((m) => {
        console.log("db conectada 🔥");
        return m.connection.getClient();
    })
    .catch((e) => console.log("falló la conexión " + e));

module.exports = clientDB;

index.js

const MongoStore = require("connect-mongo");
const clientDB = require("./database/db");

app.use(
    session({
        secret: "keyboard cat",
        resave: false,
        saveUninitialized: false,
        name: "secret-name-blablabal",
        store: MongoStore.create({
            clientPromise: clientDB,
            dbName: "dbUrlTwitch",
        }),
    })
);

# Saniteze mongo

express-mongo-sanitize

const mongoSanitize = require("express-mongo-sanitize");
app.use(mongoSanitize());

mongo-sanitize

const sanitize = require("mongo-sanitize");

const registerUser = async (req, res) => {
    req.body = sanitize(req.body);
};

const confirmarCuenta = async (req, res) => {
    req.params = sanitize(req.params);
};

const loginUser = async (req, res) => {
    req.body = sanitize(req.body);
};

# CORS

const cors = require("cors");
const corsOptions = {
    credentials: true,
    origin: "https://uur.herokuapp.com/",
};
app.use(cors(corsOptions));

# .env

URI=mongodbURI
USEREMAIL=
PASSEMAIL=
SESSION_SECRET=
PATHURL=http://localhost:5000

# Heroku

package.json

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

index.js

 









 



app.set("trust proxy", 1);
app.use(
    session({
        secret: process.env.SESSION_SECRET,
        resave: false,
        saveUninitialized: false,
        name: "name-session",
        store: MongoStore.create({
            clientPromise: clientDB,
        }),
        cookie: { secure: true, maxAge: 30 * 24 * 60 * 60 * 1000 },
    })
);

En registerUser cambiar variables de entorno

await transport.sendMail({
    from: '"Fred Foo 👻" <foo@example.com>',
    to: user.email,
    subject: "verifique cuenta de correo",
    html: `<a href="${process.env.PATHURL}/auth/confirmar/${user.tokenConfirm}">verificar cuenta aquí</a>`,
});

app.js (frontend) cambiar url

const url = `https://uur.herokuapp.com/${e.target.dataset.short}`;
Last Updated: 4/27/2022, 8:04:34 AM