Categories
Nodejs React

How to Create PDFs with Node.js

Everyone uses PDFs — it goes without saying that it’s one of the most common document formats. Therefore, a feature of many apps is to create PDFs from other kinds of documents. Creating PDFs is easy in Node.js by using third-party libraries, and we can easily add the feature to our own apps.

In this article, we will build an app that lets users enter their document in a rich text editor and generate a PDF from it. We will use Express for the backend and React for the frontend.

Backend

We will start with the backend. To start, we will create a project directory with a backend folder inside. Then in the backend folder, run npx express-generator to create the Express app. Then run npm i to install the packages.

Next, we install our own packages. We need Babel to run the app with the latest version of JavaScript, CORS for cross-domain requests from the frontend, HTML-PDF for converting HTML strings to PDF, Multer for file upload, Sequelize for the ORM, and SQLite3 for our database.

We install these by running npm i @babel/cli @babel/core @babel/node @babel/preset-env cors html-pdf sequelize sqlite3 multer.

After that, we change the scripts section of package.json to have:

"start": "nodemon --exec npm run babel-node --  ./bin/www",
"babel-node": "babel-node"

This lets us run our app with Babel instead of the regular Node runtime.

Then we need to create a .babelrc file in the backend folder and add:

{
    "presets": [
        "@babel/preset-env"
    ]
}

This specifies that we run our app with the latest version of JavaScript.

Next we add our database code. Run npx sequelize-cli init in the backend folder to generate the Sequelize code.

We should have a config.js in the project now. In there add:

{
  "development": {
    "dialect": "sqlite",
    "storage": "development.db"
  },
  "test": {
    "dialect": "sqlite",
    "storage": "test.db"
  },
  "production": {
    "dialect": "sqlite",
    "storage": "production.db"
  }
}

This declares SQLite for our database.

Next, create our model and migration by running:

npx sequelize-cli model:create --name Document --attributes name:string,document:text,pdfPath:string

This creates a Document model and Documents table.

To create our database, we then run:

npx sequelize-cli db:migrate

Now we need create our routes. Create a pdf.js file in the routes folder and add:

var express = require("express");
var pdf = require("html-pdf");
const models = require("../models");
var multer = require("multer");
const fs = require("fs");
var router = express.Router();
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, "./files");
  },
  filename: (req, file, cb) => {
    cb(null, `${file.fieldname}_${+new Date()}.jpg`);
  }
});

const upload = multer({
  storage
});

router.get("/", async (req, res, next) => {
  const documents = await models.Document.findAll();
  res.json(documents);
});

router.post("/", async (req, res, next) => {
  const document = await models.Document.create(req.body);
  res.json(document);
});

router.put("/:id", async (req, res, next) => {
  const id = req.params.id;
  const { name, document } = req.body;
  const doc = await models.Document.update(
    { name, document },
    { where: { id } }
  );
  res.json(doc);
});

router.delete("/:id", async (req, res, next) => {
  const id = req.params.id;
  await models.Document.destroy({ where: { id } });
  res.json({});
});

router.get("/generatePdf/:id", async (req, res, next) => {
  const id = req.params.id;
  const documents = await models.Document.findAll({ where: { id } });
  const document = documents[0];
  const stream = await new Promise((resolve, reject) => {
    pdf.create(document.document).toStream((err, stream) => {
      if (err) {
        reject(reject);
        return;
      }
      resolve(stream);
    });
  });

const fileName = `${+new Date()}.pdf`;
  const pdfPath = `${__dirname}/../files/${fileName}`;
  stream.pipe(fs.createWriteStream(pdfPath));
  const doc = await models.Document.update(
    { pdfPath: fileName },
    { where: { id } }
  );
  res.json(doc);
});

router.post("/uploadImage", upload.single('upload'), async (req, res, next) => {
  res.json({
    uploaded: true,
    url: `${process.env.BASE_URL}/${req.file.filename}`
  });
});

module.exports = router;

We perform the standard CRUD operations to the Documents table in the first 4 routes. We have GET for fetching all the Documents, POST for creating a Document from post parameters, PUT for updating a Document by ID, and DELETE for deleting a Document by looking it up by ID. We have the HTML in the document field for generating the PDF later.

The generatePdf is the function that allows us to build the PDF. We get the ID from the URL and then use the HTML-PDF package to generate the PDF. We generate the PDF by converting the HTML document to a file stream object with the HTML-PDF package. Then we write the stream to a file and save the path to the file in the Document model with the ID in the URL parameter.

We also have an uploadImage route to let user upload images with CKEditor. The plugin expects uploaded and url in the response which our function returns.

Then we need to add a files folder in the backend directory.

Next in app.js, we replace the existing code with:

var createError = require("http-errors");
var express = require("express");
var path = require("path");
var cookieParser = require("cookie-parser");
var logger = require("morgan");
var cors = require("cors");

var indexRouter = require("./routes/index");
var pdfRouter = require("./routes/pdf");

var app = express();

// view engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jade");

app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use(express.static(path.join(__dirname, "files")));
app.use(cors());

app.use("/", indexRouter);
app.use("/pdf", pdfRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get("env") === "development" ? err : {};

// render the error page
  res.status(err.status || 500);
  res.render("error");
});

module.exports = app;

We expose the file folder with:

app.use(express.static(path.join(__dirname, "files")));

And expose the pdf route with:

var pdfRouter = require("./routes/pdf");
app.use("/pdf", pdfRouter);

Frontend

Now that the backend is complte, we can move onto the front end. Create the React app by using Create React App. We run npx create-react-app frontend in the root folder of the project.

We then install our packages. We will use CKEditor for our rich text editor, Axios for making HTTP requests, Bootstrap for styling, MobX for simple state management, React Router for routing URLs to components, and Formik and Yup for form value handling and form validation respectively.

Install all the packages by running npm i @ckeditor/ckeditor5-build-classic @ckeditor/ckeditor5-react axios bootstrap formik mobx mobx-react react-bootstrap react-router-dom yup.

With the packages installed, we can get started. In App.js, we replace the existing code with:

import React from "react";
import HomePage from "./HomePage";
import { Router, Route } from "react-router-dom";
import { createBrowserHistory as createHistory } from "history";
import TopBar from "./TopBar";
import { DocumentStore } from "./store";
import "./App.css";
const history = createHistory();
const documentStore = new DocumentStore();

function App() {
  return (
    <div className="App">
      <Router history={history}>
        <TopBar />
        <Route
          path="/"
          exact
          component={props => (
            <HomePage {...props} documentStore={documentStore} />
          )}
        />
      </Router>
    </div>
  );
}

export default App;

This adds our top bar and route to the home page.

In App.css, we replace the existing code with:

.page {
  padding: 20px;
}

.content-invalid-feedback {
  width: 100%;
  margin-top: 0.25rem;
  font-size: 80%;
  color: #dc3545;
}

nav.navbar {
  background-color: green !important;
}

This adds some padding to our page and style the validation message for the Rich text editor and changes the color of the navbar.

Next we create the form for adding and editing documents. Create a DocumentForm.js in the src file and add:

import React from "react";
import * as yup from "yup";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import { observer } from "mobx-react";
import { Formik, Field } from "formik";
import { addDocument, editDocument, getDocuments, APIURL } from "./request";
import CKEditor from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";

const schema = yup.object({
  name: yup.string().required("Name is required")
});

function DocumentForm({ documentStore, edit, onSave, doc }) {
  const [content, setContent] = React.useState("");
  const [dirty, setDirty] = React.useState(false);

  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid || !content) {
      return;
    }
    const data = { ...evt, document: content };
    if (!edit) {
      await addDocument(data);
    } else {
      await editDocument(data);
    }
    getAllDocuments();
  };

  const getAllDocuments = async () => {
    const response = await getDocuments();
    documentStore.setDocuments(response.data);
    onSave();
  };

  return (
    <>
      <Formik
        validationSchema={schema}
        onSubmit={handleSubmit}
        initialValues={edit ? doc : {}}
      >
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors
        }) => (
          <Form noValidate onSubmit={handleSubmit}>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="name">
                <Form.Label>Name</Form.Label>
                <Form.Control
                  type="text"
                  name="name"
                  placeholder="Name"
                  value={values.name || ""}
                  onChange={handleChange}
                  isInvalid={touched.name && errors.name}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.name}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>

            <Form.Row>
              <Form.Group as={Col} md="12" controlId="content">
                <Form.Label>Content</Form.Label>
                <CKEditor
                  editor={ClassicEditor}
                  data={content || ""}
                  onInit={editor => {
                    if (edit) {
                      setContent(doc.document);
                    }
                  }}
                  onChange={(event, editor) => {
                    const data = editor.getData();
                    setContent(data);
                    setDirty(true);
                  }}
                  config={{
                    ckfinder: {
                      uploadUrl:
                        `${APIURL}/pdf/uploadImage`
                    }
                  }}
                />
                <div className="content-invalid-feedback">
                  {dirty && !content ? "Content is required" : null}
                </div>
              </Form.Group>
            </Form.Row>

            <Button type="submit" style={{ marginRight: 10 }}>
              Save
            </Button>
            <Button type="button">Cancel</Button>
          </Form>
        )}
      </Formik>
    </>
  );
}

export default observer(DocumentForm);

We wrap our React Bootstrap Form inside the Formik component to get the form handling function from Formik which we use directly in the React Bootstrap form fields.

We cannot do the same with CKEditor, so we write our own form handlers for the rich text editor. We set the data prop in the CKEditor to set the value of the input of the rich text editor. The onInit function is used when users try to edit an existing document since we have to set the data prop with the editor initializes by running setContent(doc.document);. The onChange prop is the handler function for setting content whenever it is updated so the data prop will have the latest value, which we will submit when the user clicks Save.

We use the CKFinder plugin to upload images. To make it work, we set the image upload URL to the URL of the upload route in our back end.

The form validation schema is provided by the Yup schema object which we create at the top of the code. We check if the name field exists.

The handleSubmit function is for submitting the data to back end. We check both the content and the evt object to see both fields exists since we cannot incorporate the Formik form handlers directly into the CKEditor component.

If everything is valid, then we add a new document or update the document depending on whether the edit prop is true or not.

When saving is successful, we call getAllDocuments to populate the latest documents into our MobX store by running documentStore.setDocuments(response.data); .

Next, we build our home page by creating HomePage.js in the src folder and add:

import React, { useState, useEffect } from "react";
import { withRouter } from "react-router-dom";
import DocumentForm from "./DocumentForm";
import Modal from "react-bootstrap/Modal";
import ButtonToolbar from "react-bootstrap/ButtonToolbar";
import Button from "react-bootstrap/Button";
import Table from "react-bootstrap/Table";
import { observer } from "mobx-react";
import { getDocuments, deleteDocument, generatePDF, APIURL } from "./request";

function HomePage({ documentStore, history }) {
  const [openAddModal, setOpenAddModal] = useState(false);
  const [openEditModal, setOpenEditModal] = useState(false);
  const [initialized, setInitialized] = useState(false);
  const [doc, setDoc] = useState([]);

  const openAddTemplateModal = () => {
    setOpenAddModal(true);
  };

  const closeAddModal = () => {
    setOpenAddModal(false);
    setOpenEditModal(false);
  };

  const cancelAddModal = () => {
    setOpenAddModal(false);
  };

  const cancelEditModal = () => {
    setOpenEditModal(false);
  };

  const getAllDocuments = async () => {
    const response = await getDocuments();
    documentStore.setDocuments(response.data);
    setInitialized(true);
  };

  const editTemplate = d => {
    setDoc(d);
    setOpenEditModal(true);
  };

  const onSave = () => {
    cancelAddModal();
    cancelEditModal();
  };

  const deleteSingleDocument = async id => {
    await deleteDocument(id);
    getAllDocuments();
  };

  const generateSinglePdf = async id => {
    await generatePDF(id);
    alert("PDF Generated");
    getAllDocuments();
  };

  useEffect(() => {
    if (!initialized) {
      getAllDocuments();
    }
  });

  return (
    <div className="page">
      <h1 className="text-center">Documents</h1>
      <ButtonToolbar onClick={openAddTemplateModal}>
        <Button variant="primary">Add Document</Button>
      </ButtonToolbar>

      <Modal show={openAddModal} onHide={closeAddModal}>
        <Modal.Header closeButton>
          <Modal.Title>Add Document</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <DocumentForm
            onSave={onSave.bind(this)}
            cancelModal={cancelAddModal.bind(this)}
            documentStore={documentStore}
          />
        </Modal.Body>
      </Modal>

      <Modal show={openEditModal} onHide={cancelEditModal}>
        <Modal.Header closeButton>
          <Modal.Title>Edit Document</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <DocumentForm
            edit={true}
            doc={doc}
            onSave={onSave.bind(this)}
            cancelModal={cancelEditModal.bind(this)}
            documentStore={documentStore}
          />
        </Modal.Body>
      </Modal>
      <br />
      <Table striped bordered hover>
        <thead>
          <tr>
            <th>Name</th>
            <th>PDF</th>
            <th>Generate PDF</th>
            <th>Edit</th>
            <th>Delete</th>
          </tr>
        </thead>
        <tbody>
          {documentStore.documents.map(d => {
            return (
              <tr key={d.id}>
                <td>{d.name}</td>
                <td>
                  <a href={`${APIURL}/${d.pdfPath}`} target="_blank">
                    Open
                  </a>
                </td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={generateSinglePdf.bind(this, d.id)}
                  >
                    Generate PDF
                  </Button>
                </td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={editTemplate.bind(this, d)}
                  >
                    Edit
                  </Button>
                </td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={deleteSingleDocument.bind(this, d.id)}
                  >
                    Delete
                  </Button>
                </td>
              </tr>
            );
          })}
        </tbody>
      </Table>
    </div>
  );
}

export default withRouter(observer(HomePage));

We have a React Bootstrap table for listing the documents with buttons to edit or delete documents, and a button to generate a PDF. Also, there is an ppen link for opening the PDF in each row. We have a button to create a PDF on top of the table.

When the page loads, we call getAllDocuments and populate them in the MobX store. We open and close the add and edit modals with the openAddTemplateModal, closeAddModal, cancelAddModal, cancelEditModal functions.

Next create request.js in the src folder and add:

export const APIURL = "http://localhost:3000";
const axios = require("axios");

export const getDocuments = () => axios.get(`${APIURL}/pdf`);

export const addDocument = data => axios.post(`${APIURL}/pdf`, data);

export const editDocument = data => axios.put(`${APIURL}/pdf/${data.id}`, data);

export const deleteDocument = id => axios.delete(`${APIURL}/pdf/${id}`);

export const generatePDF = id => axios.get(`${APIURL}/pdf/generatePdf/${id}`);

to add the functions to make requests to our routes in the back end.

Then we create our MobX store. Create store.js in the src folder and put:

import { observable, action, decorate } from "mobx";

class DocumentStore {
  documents = [];

setDocuments(documents) {
    this.documents = documents;
  }
}

DocumentStore = decorate(DocumentStore, {
  documents: observable,
  setDocuments: action
});

export { DocumentStore };

We have the function setDocuments to put the photo data in the store, which we used in HomePage and DocumentForm , and we instantiated it before exporting so that we only have to do it in one place.

This block:

DocumentStore = decorate(DocumentStore, {
  documents: observable,
  setDocuments: action
});

designates the documents array in DocumentStore as the entity that can be watched by components for changes. The setDocuments function is designated as the function that can be used to set the documents array in the store.

Next we create the top bar by creating a TopBar.js file in the src folder and add:

import React from "react";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import { withRouter } from "react-router-dom";

function TopBar({ location }) {
  return (
    <Navbar bg="primary" expand="lg" variant="dark">
      <Navbar.Brand href="#home">PDF App</Navbar.Brand>
      <Navbar.Toggle aria-controls="basic-navbar-nav" />
      <Navbar.Collapse id="basic-navbar-nav">
        <Nav className="mr-auto">
          <Nav.Link href="/" active={location.pathname == "/"}>
            Home
          </Nav.Link>
        </Nav>
      </Navbar.Collapse>
    </Navbar>
  );
}

export default withRouter(TopBar);

This contains the React Bootstrap Navbar to show a top bar with a link to the home page and the name of the app. We only display it with the token present in local storage. We check the pathname to highlight the right links by setting the active prop.

Next in index.html, we replace the existing code with:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>PDF App</title>
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

This adds the Bootstrap CSS and change the title.

After writing all that code, we can run our app. Before running anything, install nodemon by running npm i -g nodemon so that we don’t have to restart back end ourselves when files change.

Then start backend by running npm start in the backend folder and npm start in the frontend folder, then choose ‘yes’ if you’re asked to run it from a different port.

Then you get:

Categories
Nodejs React

How to Make Your Own Bitbucket App

Bitbucket is a great repository hosting service that lets you host Git repositories for free. You can upgrade to their paid plans to get more features for a low price. It also has a comprehensive API for automation and getting data.

Developers have made Node.js clients for Bitbucket’s API. We can easily use it to do what we want like manipulating commits, adding, removing, or editing repositories, tracking issues, manipulating build pipelines and a lot more.

The Bitbucket.js package is one of the easiest packages to use for writing Node.js Bitbucket apps. All we have to do is log in with the Bitbucket instance with our Bitbucket username and password and call the built in functions listed at https://bitbucketjs.netlify.com/#api-_ to do almost anything we want with the Bitbucket packages.

In this article, we will make a simple Bitbucket app with authentication. We will let users sign up for an account and set their Bitbucket username and password for their account. Once the user is logged in and set their Bitbucket credentials, they can view their repositories and the commits for each repository.

Our app will consist of an back end and a front end. The back end handles the user authentication and communicates with Bitbucket via the Bitbucket API. The front end has the sign up form, log in form, a settings form for setting password and Bitbucket username and password.

To start building the app, we create a project folder with the backend folder inside. We then go into the backend folder and run the Express Generator by running npx express-generator . Next we install some packages ourselves. We need Babel to use import , BCrypt for hashing passwords, Bitbucket.js for using the Bitbucket API. Crypto-JS for encrypting and decrypting our Bitbucket password, Dotenv for storing hash and encryption secrets, Sequelize for ORM, JSON Web Token packages for authentication, CORS for cross domain communication and SQLite for database.

Run npm i @babel/cli @babel/core @babel/node @babel/preset-env bcrypt bitbucket cors crypto-js dotenv jsonwebtoken sequelize sqlite3 to install the packages.

In the script section of package.json , put:

"start": "nodemon --exec npm run babel-node --  ./bin/www",
"babel-node": "babel-node"

to start our app with Babel Node instead of the regular Node.js runtime so that we get the latest JavaScript features.

Then we create a file called .babelrc in the backend folder and add:

{
    "presets": [
        "@babel/preset-env"
    ]
}

to enable the latest features.

Then we have to add Sequelize code by running npx sequelize-cli init . After that we should get a config.json file in the config folder.

In config.json , we put:

{
  "development": {
    "dialect": "sqlite",
    "storage": "development.db"
  },
  "test": {
    "dialect": "sqlite",
    "storage": "test.db"
  },
  "production": {
    "dialect": "sqlite",
    "storage": "production.db"
  }
}

To use SQLite as our database.

Next we add a middleware for verifying the authentication token, add a middllewares folder in the backend folder, and in there add authCheck.js . In the file, add:

const jwt = require("jsonwebtoken");
const secret = process.env.JWT_SECRET;

export const authCheck = (req, res, next) => {
  if (req.headers.authorization) {
    const token = req.headers.authorization;
    jwt.verify(token, secret, (err, decoded) => {
      if (err) {
        res.send(401);
      } else {
        next();
      }
    });
  } else {
    res.send(401);
  }
};

We return 401 response if the token is invalid.

Next we create some migrations and models. Run:

npx sequelize-cli model:create --name User --attributes username:string,password:string,bitBucketUsername:string,bitBucketPassword:string

to create the model. Note that the attributes option has no spaces.

Then we add unique constraint to the username column of the Users table. To do this, run:

npx sequelize-cli migration:create addUniqueConstraintToUser

Then in newly created migration file, add:

"use strict";

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.addConstraint("Users", ["username"], {
      type: "unique",
      name: "usernameUnique"
    });
  },

down: (queryInterface, Sequelize) => {
    return queryInterface.removeConstraint("Users", "usernameUnique");
  }
};

Run npx sequelize-cli db:migrate to run all the migrations.

Next we create the routes. Create a file called bitbucket.js and add:

var express = require("express");
const models = require("../models");
const CryptoJS = require("crypto-js");
const Bitbucket = require("bitbucket");
const jwt = require("jsonwebtoken");
import { authCheck } from "../middlewares/authCheck";

const bitbucket = new Bitbucket();
var router = express.Router();

router.post("/setBitbucketCredentials", authCheck, async (req, res, next) => {
  const { bitBucketUsername, bitBucketPassword } = req.body;
  const token = req.headers.authorization;
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  const id = decoded.userId;
  const cipherText = CryptoJS.AES.encrypt(
    bitBucketPassword,
    process.env.CRYPTO_SECRET
  );
  await models.User.update(
    {
      bitBucketUsername,
      bitBucketPassword: cipherText.toString()
    },
    {
      where: { id }
    }
  );
  res.json({});
});

router.get("/repos/:page", authCheck, async (req, res, next) => {
  const page = req.params.page || 1;
  const token = req.headers.authorization;
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  const id = decoded.userId;
  const users = await models.User.findAll({ where: { id } });
  const user = users[0];
  const bytes = CryptoJS.AES.decrypt(
    user.bitBucketPassword.toString(),
    process.env.CRYPTO_SECRET
  );
  const password = bytes.toString(CryptoJS.enc.Utf8);
  bitbucket.authenticate({
    type: "basic",
    username: user.bitBucketUsername,
    password
  });
  let { data } = await bitbucket.repositories.list({
    username: user.bitBucketUsername,
    page,
    sort: "-updated_on"
  });
  res.json(data);
});

router.get("/commits/:repoName", authCheck, async (req, res, next) => {
  const token = req.headers.authorization;
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  const id = decoded.userId;
  const users = await models.User.findAll({ where: { id } });
  const user = users[0];
  const repoName = req.params.repoName;
  const bytes = CryptoJS.AES.decrypt(
    user.bitBucketPassword.toString(),
    process.env.CRYPTO_SECRET
  );
  const password = bytes.toString(CryptoJS.enc.Utf8);
  bitbucket.authenticate({
    type: "basic",
    username: user.bitBucketUsername,
    password
  });
  let { data } = await bitbucket.commits.list({
    username: user.bitBucketUsername,
    repo_slug: repoName,
    sort: "-date"
  });
  res.json(data);
});

module.exports = router;

In each route, we get the user from the token, since we will add the user ID into the token, and from there we get the Bitbucket username and password, which we use to log into the Bitbucket API. Note that we have to decrypt the password since we encrypted it before saving it to the database.

We set the Bitbucket credentials in the setBitbucketCredentials route. We encrypt the password before saving to keep it secure.

Then in the repos route, we get the repos of the user and sort by reversed update_on order since we specified -updated_on in the sort parameter. The commits are listed in reverse date order since we specified -date in the sort parameter.

Next we add the user.js in the routes folder and add:

const express = require("express");
const models = require("../models");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
import { authCheck } from "../middlewares/authCheck";
const router = express.Router();

router.post("/signup", async (req, res, next) => {
  try {
    const { username, password } = req.body;
    const hashedPassword = await bcrypt.hash(password, 10);
    const user = await models.User.create({
      username,
      password: hashedPassword
    });
    res.json(user);
  } catch (error) {
    res.status(400).json(error);
  }
});

router.post("/login", async (req, res, next) => {
  const { username, password } = req.body;
  const users = await models.User.findAll({ where: { username } });
  const user = users[0];
  const response = await bcrypt.compare(password, user.password);
  if (response) {
    const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET);
    res.json({ token });
  } else {
    res.status(401).json({});
  }
});

router.post("/changePassword", authCheck, async (req, res, next) => {
  const { password } = req.body;
  const token = req.headers.authorization;
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  const id = decoded.userId;
  const hashedPassword = await bcrypt.hash(password, 10);
  await models.User.update({ password: hashedPassword }, { where: { id } });
  res.json({});
});

router.get("/currentUser", authCheck, async (req, res, next) => {
  const token = req.headers.authorization;
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  const id = decoded.userId;
  const users = await models.User.findAll({ where: { id } });
  const { username, bitBucketUsername } = users[0];
  res.json({ username, bitBucketUsername });
});

module.exports = router;

We have routes for sign up, log in, and change password. We hash the password before saving when we sign up or changing password.

The currentUser route will be used for a settings form in the front end.

In app.js we replace the existing code with:

require("dotenv").config();
var createError = require("http-errors");
var express = require("express");
var path = require("path");
var cookieParser = require("cookie-parser");
var logger = require("morgan");
const cors = require("cors");

var indexRouter = require("./routes/index");
var usersRouter = require("./routes/users");
var bitbucketRouter = require("./routes/bitbucket");

var app = express();

// view engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jade");

app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use(cors());

app.use("/", indexRouter);
app.use("/users", usersRouter);
app.use("/bitbucket", bitbucketRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get("env") === "development" ? err : {};

// render the error page
  res.status(err.status || 500);
  res.render("error");
});

module.exports = app;

to add the CORS add-on to enable cross domain communication, and add the users and bitbucket routes in our app by adding:

app.use("/users", usersRouter);
app.use("/bitbucket", bitbucketRouter);

This finishes the back end portion of the app. Now we can build the front end. We will build it with React, so we start by running npx create-react-app frontend from the project’s root folder.

Next we have to install some packages. We will install Bootstrap for styling, React Router for routing, Formik and Yup for form value handling and form validation respectively and Axios for making HTTP requests.

To do this run npm i axios bootstrap formik react-bootstrap react-router-dom yup to install all the packages.

Next we modify the App.js folder by replacing the existing code with the following:

import React from "react";
import HomePage from "./HomePage";
import "./App.css";
import ReposPage from "./ReposPage";
import CommitsPage from "./CommitsPage";
import SettingsPage from "./SettingsPage";
import { createBrowserHistory as createHistory } from "history";
import { Router, Route } from "react-router-dom";
import SignUpPage from "./SignUpPage";
import RequireAuth from "./RequireAuth";
const history = createHistory();

function App() {
  return (
    <div className="App">
      <Router history={history}>
        <Route path="/" exact component={HomePage} />
        <Route path="/signup" exact component={SignUpPage} />
        <Route
          path="/settings"
          component={props => (
            <RequireAuth {...props} Component={SettingsPage} />
          )}
        />
        <Route
          path="/repos"
          exact
          component={props => <RequireAuth {...props} Component={ReposPage} />}
        />
        <Route
          path="/commits/:repoName"
          exact
          component={props => (
            <RequireAuth {...props} Component={CommitsPage} />
          )}
        />
      </Router>
    </div>
  );
}

export default App;

We add the routes for the pages in our app here. RequiredAuth is a higher order component which takes a component that requires authentication to access and return redirect if not authorized or the page if the user is authorized.

In App.css , we replace the existing code with:

.page {
    padding: 20px;
}

to add some padding.

Next we add CommitsPage.js in the src folder and add:

import React, { useState, useEffect } from "react";
import { withRouter } from "react-router-dom";
import { commits } from "./requests";
import Card from "react-bootstrap/Card";
import LoggedInTopBar from "./LoggedInTopBar";
const moment = require("moment");

function CommitsPage({ match: { params } }) {
  const [initialized, setInitialized] = useState(false);
  const [repoCommits, setRepoCommits] = useState([]);

  const getCommits = async page => {
    const repoName = params.repoName;
    const response = await commits(repoName, page);
    setRepoCommits(response.data.values);
  };

  useEffect(() => {
    if (!initialized) {
      getCommits(1);
      setInitialized(true);
    }
  });

  return (
    <>
      <LoggedInTopBar />
      <div className="page">
        <h1 className="text-center">Commits</h1>
        {repoCommits.map(rc => {
          return (
            <Card style={{ width: "90vw", margin: "0 auto" }}>
              <Card.Body>
                <Card.Title>{rc.message}</Card.Title>
                <p>Message: {rc.author.raw}</p>
                <p>
                  Date:{" "}
                  {moment(rc.date).format("dddd, MMMM Do YYYY, h:mm:ss a")}
                </p>
                <p>Hash: {rc.hash}</p>
              </Card.Body>
            </Card>
          );
        })}
      </div>
    </>
  );
}

export default withRouter(CommitsPage);

We get the commits given the repository name in the URL parameter and display them in a list of Cards provided by React Bootstrap.

Next we create the Home Page, which lets us sign in. Create a HomePage.js file in the src folder and add:

import React, { useState, useEffect } from "react";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import { Link } from "react-router-dom";
import * as yup from "yup";
import { logIn } from "./requests";
import { Redirect } from "react-router-dom";
import Navbar from "react-bootstrap/Navbar";

const schema = yup.object({
  username: yup.string().required("Username is required"),
  password: yup.string().required("Password is required")
});

function HomePage() {
  const [redirect, setRedirect] = useState(false);

  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    try {
      const response = await logIn(evt);
      localStorage.setItem("token", response.data.token);
      setRedirect(true);
    } catch (ex) {
      alert("Invalid username or password");
    }
  };

  if (redirect) {
    return <Redirect to="/repos" />;
  }

  return (
    <>
      <Navbar bg="primary" expand="lg" variant="dark">
        <Navbar.Brand href="#home">Bitbucket App</Navbar.Brand>
      </Navbar>
      <div className="page">
        <h1 className="text-center">Log In</h1>
        <Formik validationSchema={schema} onSubmit={handleSubmit}>
          {({
            handleSubmit,
            handleChange,
            handleBlur,
            values,
            touched,
            isInvalid,
            errors
          }) => (
            <Form noValidate onSubmit={handleSubmit}>
              <Form.Row>
                <Form.Group as={Col} md="12" controlId="username">
                  <Form.Label>Username</Form.Label>
                  <Form.Control
                    type="text"
                    name="username"
                    placeholder="Username"
                    value={values.username || ""}
                    onChange={handleChange}
                    isInvalid={touched.username && errors.username}
                  />
                  <Form.Control.Feedback type="invalid">
                    {errors.username}
                  </Form.Control.Feedback>
                </Form.Group>
                <Form.Group as={Col} md="12" controlId="password">
                  <Form.Label>Password</Form.Label>
                  <Form.Control
                    type="password"
                    name="password"
                    placeholder="Password"
                    value={values.password || ""}
                    onChange={handleChange}
                    isInvalid={touched.password && errors.password}
                  />

                  <Form.Control.Feedback type="invalid">
                    {errors.password}
                  </Form.Control.Feedback>
                </Form.Group>
              </Form.Row>
              <Button type="submit" style={{ marginRight: "10px" }}>
                Log In
              </Button>
              <Link
                className="btn btn-primary"
                to="/signup"
                style={{ marginRight: "10px", color: "white" }}
              >
                Sign Up
              </Link>
            </Form>
          )}
        </Formik>
      </div>
    </>
  );
}

export default HomePage;

We use the Formik component provided by Formik to get automatic form value handling. It will pipe the values directly to the parameter of the onSubmit handler. In the handleSubmit function, we run the schema.validate function to check for validity of the form values according to the schema object generated from the Yup library and if that’s successful, then we call login from the requests.js file which we will create.

Next we create the top bar. Create LoggedInTopBar.js in the src folder and add:

import React, { useState } from "react";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import { withRouter, Redirect } from "react-router-dom";

function LoggedInTopBar({ location }) {
  const [redirect, setRedirect] = useState(false);
  const { pathname } = location;

  const isLoggedIn = () => !!localStorage.getItem("token");

  if (redirect) {
    return <Redirect to="/" />;
  }

  return (
    <div>
      {isLoggedIn() ? (
        <Navbar bg="primary" expand="lg" variant="dark">
          <Navbar.Brand href="#home">Bitbucket App</Navbar.Brand>
          <Navbar.Toggle aria-controls="basic-navbar-nav" />
          <Navbar.Collapse id="basic-navbar-nav">
            <Nav className="mr-auto">
              <Nav.Link href="/settings" active={pathname == "/settings"}>
                Settings
              </Nav.Link>
              <Nav.Link href="/repos" active={pathname == "/repos"}>
                Repos
              </Nav.Link>
              <Nav.Link>
                <span
                  onClick={() => {
                    localStorage.clear();
                    setRedirect(true);
                  }}
                >
                  Log Out
                </span>
              </Nav.Link>
            </Nav>
          </Navbar.Collapse>
        </Navbar>
      ) : null}
    </div>
  );
}

export default withRouter(LoggedInTopBar);

This contains the React Bootstrap Navbar to show a top bar with a link to the home page and the name of the app. We only display it with the token present in local storage. We check the pathname to highlight the right links by setting the active prop.

Next create ReposPage.js in the src folder to display the list of repositories the user owns. We add:

import React, { useState, useEffect } from "react";
import { repos } from "./requests";
import Card from "react-bootstrap/Card";
import { Link } from "react-router-dom";
import Pagination from "react-bootstrap/Pagination";
import LoggedInTopBar from "./LoggedInTopBar";

function ReposPage() {
  const [initialized, setInitialized] = useState(false);
  const [repositories, setRepositories] = useState([]);
  const [page, setPage] = useState(1);
  const [totalPages, setTotalPages] = useState(1);

  const getRepos = async page => {
    const response = await repos(page);
    setRepositories(response.data.values);
    setTotalPages(Math.ceil(response.data.size / response.data.pagelen));
  };

  useEffect(() => {
    if (!initialized) {
      getRepos(1);
      setInitialized(true);
    }
  });

  return (
    <div>
      <LoggedInTopBar />
      <h1 className="text-center">Your Repositories</h1>
      {repositories.map((r, i) => {
        return (
          <Card style={{ width: "90vw", margin: "0 auto" }} key={i}>
            <Card.Body>
              <Card.Title>{r.slug}</Card.Title>
              <Link className="btn btn-primary" to={`/commits/${r.slug}`}>
                Go
              </Link>
            </Card.Body>
          </Card>
        );
      })}
      <br />
      <Pagination style={{ width: "90vw", margin: "0 auto" }}>
        <Pagination.First onClick={() => getRepos(1)} />
        <Pagination.Prev
          onClick={() => {
            let p = page - 1;
            getRepos(p);
            setPage(p);
          }}
        />
        <Pagination.Next
          onClick={() => {
            let p = page + 1;
            getRepos(p);
            setPage(p);
          }}
        />
        <Pagination.Last onClick={() => getRepos(totalPages)} />
      </Pagination>
      <br />
    </div>
  );
}

export default ReposPage;

We get the list repositories and we add pagination to get more repositories.

In requests.js , we add:

const axios = require("axios");
const APIURL = "http://localhost:3000";

axios.interceptors.request.use(
  config => {
    config.headers.authorization = localStorage.getItem("token");
    return config;
  },
  error => Promise.reject(error)
);

axios.interceptors.response.use(
  response => {
    return response;
  },
  error => {
    if (error.response && error.response.status == 401) {
      localStorage.clear();
    }
    return Promise.reject(error);
  }
);

export const signUp = data => axios.post(`${APIURL}/users/signup`, data);

export const logIn = data => axios.post(`${APIURL}/users/login`, data);

export const changePassword = data =>
  axios.post(`${APIURL}/users/changePassword`, data);

export const currentUser = () => axios.get(`${APIURL}/users/currentUser`);

export const setBitbucketCredentials = data =>
  axios.post(`${APIURL}/bitbucket/setBitbucketCredentials`, data);

export const repos = page =>
  axios.get(`${APIURL}/bitbucket/repos/${page || 1}`);

export const commits = (repoName) =>
  axios.get(`${APIURL}/bitbucket/commits/${repoName}`);

This file has all the HTTP requests that we make for sign up, log in, set credentials, get repositories and commits, etc.

And for handling responses, if we get 401 responses then we clear local storage so we won’t be using an invalid token. The code is below:

axios.interceptors.response.use(
  response => {
    return response;
  },
  error => {
    if (error.response && error.response.status == 401) {
      localStorage.clear();
    }
    return Promise.reject(error);
  }
);

We attach the token to the request headers with:

axios.interceptors.request.use(
  config => {
    config.headers.authorization = localStorage.getItem("token");
    return config;
  },
  error => Promise.reject(error)
);

so we don’t have to set it for each authenticated requests.

Next create a file called RequiredAuth.js in the src folder and add:

import React from "react";
import { Redirect } from "react-router-dom";

function RequireAuth({ Component }) {
  if (!localStorage.getItem("token")) {
    return <Redirect to="/" />;
  }
  return <Component />;
}

export default RequireAuth;

We check for presence of the token in the authenticated routes and render the Component prop passed in if the token is present.

Then we create a file called SettingsPage.js in the src folder and add:

import React, { useState, useEffect } from "react";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import * as yup from "yup";
import {
  currentUser,
  setBitbucketCredentials,
  changePassword
} from "./requests";
import LoggedInTopBar from "./LoggedInTopBar";

const userFormSchema = yup.object({
  username: yup.string().required("Username is required"),
  password: yup.string().required("Password is required")
});

const bitBucketFormSchema = yup.object({
  bitBucketUsername: yup.string().required("Username is required"),
  bitBucketPassword: yup.string().required("Password is required")
});

function SettingsPage() {
  const [initialized, setInitialized] = useState(false);
  const [user, setUser] = useState({});
  const [bitbucketUser, setBitbucketUser] = useState({});

  const handleUserSubmit = async evt => {
    const isValid = await userFormSchema.validate(evt);
    if (!isValid) {
      return;
    }
    try {
      await changePassword(evt);
      alert("Password changed");
    } catch (error) {
      alert("Password change failed");
    }
  };

  const handleBitbucketSubmit = async evt => {
    const isValid = await bitBucketFormSchema.validate(evt);
    if (!isValid) {
      return;
    }
    try {
      await setBitbucketCredentials(evt);
      alert("Bitbucket credentials changed");
    } catch (error) {
      alert("Bitbucket credentials change failed");
    }
  };

  const getCurrentUser = async () => {
    const response = await currentUser();
    const { username, bitBucketUsername } = response.data;
    setUser({ username });
    setBitbucketUser({ bitBucketUsername });
  };

  useEffect(() => {
    if (!initialized) {
      getCurrentUser();
      setInitialized(true);
    }
  });

  return (
    <>
      <LoggedInTopBar />
      <div className="page">
        <h1 className="text-center">Settings</h1>
        <h2>User Settings</h2>
        <Formik
          validationSchema={userFormSchema}
          onSubmit={handleUserSubmit}
          initialValues={user}
          enableReinitialize={true}
        >
          {({
            handleSubmit,
            handleChange,
            handleBlur,
            values,
            touched,
            isInvalid,
            errors
          }) => (
            <Form noValidate onSubmit={handleSubmit}>
              <Form.Row>
                <Form.Group as={Col} md="12" controlId="username">
                  <Form.Label>Username</Form.Label>
                  <Form.Control
                    type="text"
                    name="username"
                    placeholder="Username"
                    value={values.username || ""}
                    onChange={handleChange}
                    isInvalid={touched.username && errors.username}
                    disabled
                  />
                  <Form.Control.Feedback type="invalid">
                    {errors.username}
                  </Form.Control.Feedback>
                </Form.Group>
                <Form.Group as={Col} md="12" controlId="password">
                  <Form.Label>Password</Form.Label>
                  <Form.Control
                    type="password"
                    name="password"
                    placeholder="Password"
                    value={values.password || ""}
                    onChange={handleChange}
                    isInvalid={touched.password && errors.password}
                  />

                  <Form.Control.Feedback type="invalid">
                    {errors.password}
                  </Form.Control.Feedback>
                </Form.Group>
              </Form.Row>
              <Button type="submit" style={{ marginRight: "10px" }}>
                Save
              </Button>
            </Form>
          )}
        </Formik>

        <br />
        <h2>BitBucket Settings</h2>
        <Formik
          validationSchema={bitBucketFormSchema}
          onSubmit={handleBitbucketSubmit}
          initialValues={bitbucketUser}
          enableReinitialize={true}
        >
          {({
            handleSubmit,
            handleChange,
            handleBlur,
            values,
            touched,
            isInvalid,
            errors
          }) => (
            <Form noValidate onSubmit={handleSubmit}>
              <Form.Row>
                <Form.Group as={Col} md="12" controlId="bitBucketUsername">
                  <Form.Label>BitBucket Username</Form.Label>
                  <Form.Control
                    type="text"
                    name="bitBucketUsername"
                    placeholder="BitBucket Username"
                    value={values.bitBucketUsername || ""}
                    onChange={handleChange}
                    isInvalid={
                      touched.bitBucketUsername && errors.bitBucketUsername
                    }
                  />
                  <Form.Control.Feedback type="invalid">
                    {errors.bitBucketUsername}
                  </Form.Control.Feedback>
                </Form.Group>
                <Form.Group as={Col} md="12" controlId="bitBucketPassword">
                  <Form.Label>Password</Form.Label>
                  <Form.Control
                    type="password"
                    name="bitBucketPassword"
                    placeholder="Bitbucket Password"
                    value={values.bitBucketPassword || ""}
                    onChange={handleChange}
                    isInvalid={
                      touched.bitBucketPassword && errors.bitBucketPassword
                    }
                  />

                  <Form.Control.Feedback type="invalid">
                    {errors.bitbucketPassword}
                  </Form.Control.Feedback>
                </Form.Group>
              </Form.Row>
              <Button type="submit" style={{ marginRight: "10px" }}>
                Save
              </Button>
            </Form>
          )}
        </Formik>
      </div>
    </>
  );
}

export default SettingsPage;

We have 2 forms. One for setting the password of the user and another for saving the Bitbucket credentials. We valid both in separate Yup schemas and make the request to back end if the data entered is valid.

Next we create the sign up page by creating SignUpPage.js in the src folder. We add:

import React, { useState, useEffect } from "react";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import { Redirect } from "react-router-dom";
import * as yup from "yup";
import { signUp } from "./requests";
import Navbar from "react-bootstrap/Navbar";

const schema = yup.object({
  username: yup.string().required("Username is required"),
  password: yup.string().required("Password is required")
});

function SignUpPage() {
  const [redirect, setRedirect] = useState(false);

  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    try {
      await signUp(evt);
      setRedirect(true);
    } catch (ex) {
      alert("Username already taken");
    }
  };

  if (redirect) {
    return <Redirect to="/" />;
  }

  return (
    <>
      <Navbar bg="primary" expand="lg" variant="dark">
        <Navbar.Brand href="#home">Bitbucket App</Navbar.Brand>
      </Navbar>
      <div className="page">
        <h1 className="text-center">Sign Up</h1>
        <Formik validationSchema={schema} onSubmit={handleSubmit}>
          {({
            handleSubmit,
            handleChange,
            handleBlur,
            values,
            touched,
            isInvalid,
            errors
          }) => (
            <Form noValidate onSubmit={handleSubmit}>
              <Form.Row>
                <Form.Group as={Col} md="12" controlId="username">
                  <Form.Label>Username</Form.Label>
                  <Form.Control
                    type="text"
                    name="username"
                    placeholder="Username"
                    value={values.username || ""}
                    onChange={handleChange}
                    isInvalid={touched.username && errors.username}
                  />
                  <Form.Control.Feedback type="invalid">
                    {errors.username}
                  </Form.Control.Feedback>
                </Form.Group>
                <Form.Group as={Col} md="12" controlId="password">
                  <Form.Label>Password</Form.Label>
                  <Form.Control
                    type="password"
                    name="password"
                    placeholder="Password"
                    value={values.password || ""}
                    onChange={handleChange}
                    isInvalid={touched.password && errors.password}
                  />

                  <Form.Control.Feedback type="invalid">
                    {errors.password}
                  </Form.Control.Feedback>
                </Form.Group>
              </Form.Row>
              <Button type="submit" style={{ marginRight: "10px" }}>
                Sign Up
              </Button>
            </Form>
          )}
        </Formik>
      </div>
    </>
  );
}

export default SignUpPage;

It’s similar to the other log in request, except we call the sign up request instead of log in.

Finally, in index.html , we replace the existing code with:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>Bitbucket App</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

to add the Bootstrap CSS and change the title.

After writing all that code, we can run our app. First run back end by running npm start in the backend folder and npm start in the frontend folder, then choose ‘yes’ if you’re asked to run it from a different port.

Categories
Express JavaScript Answers

How to Serve a Single Static File with Express.js?

Sometimes, we want to serve a single static file with Express.js.

In this article, we’ll look at how to serve a single static file with Express.js.

Serve a Single Static File with Express.js

To serve a single static file with Express.js, we can use the express.static middleware.

For instance, we can write:

const express = require('express')
const app = express()
const port = 3000

app.use("/foo.txt", express.static(__dirname + '/foo.txt'));

app.get('/', (req, res) => {
  res.send('hello world')
});

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

to add:

app.use("/foo.txt", express.static(__dirname + '/foo.txt'));

to use the express.static middleware to expose the foo.txt file to the public.

We make it accessible by making a request to “/foo.txt” since that’s the first argument we passed into app.use .

And we make it serve the foo.txt file from the current working directory with:

express.static(__dirname + '/foo.txt')

So when we make a request to /foo.txt , we would see its contents in the response.

Conclusion

To serve a single static file with Express.js, we can use the express.static middleware.

Categories
Express JavaScript Answers

How to Generate robots.txt in Express?

Sometimes, we want to generate a robots.txt file from our Express app.

In this article, we’ll look at how to generate a robots.txt file from our Express app.

Generate robots.txt in Express

To generate a robots.txt file from our Express app, we can create a robots.txt file that returns the text content of the robots.txt file.

For instance, we can write:

const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
  res.send('hello world')
});

app.use('/robots.txt', function(req, res, next) {
  res.type('text/plain')
  res.send("User-agent: *\nDisallow: /");
});

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

to create the /robots.txt route by writing:

app.use('/robots.txt', function(req, res, next) {
  res.type('text/plain')
  res.send("User-agent: *\nDisallow: /");
});

We call res.type to set the MIME type of the response to text/plain .

And we call res.send with the robots.txt content we want to return.

So when we make a GET request to the /robots.txt route, we get:

User-agent: *
Disallow: /

Conclusion

To generate a robots.txt file from our Express app, we can create a robots.txt file that returns the text content of the robots.txt file.

Categories
Express JavaScript Answers

How to Add Response Timeout Feature to an Express.js App?

Sometimes, we want to add a response timeout feature to an Express.js app to end requests that take too long to process.

In this article, we’ll look at how to add a response timeout feature to an Express.js app to end requests that take too long to process.

Add Response Timeout Feature to an Express.js App

To add a response timeout feature to an Express.js app to end requests that take too long to process, we can use the connect-timeout package.

To install it, we run:

npm i `connect-timeout`

Then we can use it by writing:

const express = require('express')
const timeout = require('connect-timeout');
const app = express()
const port = 3000
const haltOnTimedout = (req, res, next) => {
  if (!req.timedout) {
    next();
  }
}

app.use(timeout(120000));
app.use(haltOnTimedout);

app.get('/', (req, res) => {
  res.send('hello world')
});

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

We include the module with require .

Then we create the haltOnTimeout function that checks the req.timedout property to see if the request has timed out.

If it hasn’t timed out, then req.timedout is false and we call next to move forward with the request.

This property is available which we added:

app.use(timeout(120000));

to add the connect-timeout middleware to detect the timeout.

120000 is in milliseconds and it’s the number of milliseconds before the request times out.

Conclusion

To add a response timeout feature to an Express.js app to end requests that take too long to process, we can use the connect-timeout package.