One place for hosting & domains

      Frontend

      How To Set Up a PageKite Front-End Server on Debian 9


      The author selected the Open Internet/Free Speech Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      Private networks generally provide internet access to the hosts using NAT (network address translation), sharing a single public IP address with all hosts inside the private network. In NAT systems, the hosts inside the private network are not visible from outside the network. To expose services running on these hosts to the public internet, you would usually create NAT rules in the gateway, commonly called port forwarding rules. In several situations, though, you wouldn’t have access to the gateway to configure these rules. For situations such as this, tunneling solutions like PageKite come in handy.

      PageKite is a fast and secure tunneling solution that can expose a service inside a private network to the public internet without the need for port forwarding. To do this, it relies on an external server, called the front-end server, to which the server behind NAT and the clients connect to allow communication between them. By default, PageKite uses its own commercial pagekite.net service, but as it is a completely open-source project, it allows you to set up a private frontend on a publicly accessible host, such as a DigitalOcean Droplet. With this setup, you can create a vendor-independent solution for remote access to hosts behind NAT. By configuring the remote hosts with the PageKite client to connect to the frontend and exposing the SSH port, it is possible to access them via the command line interface shell using SSH. It’s also possible to access a graphical user interface using a desktop sharing system such as VNC or RDP running over an SSH connection.

      In this tutorial, you will install and set up a PageKite front-end service on a server running Debian 9. You will also set up two more Debian 9 servers to simulate a local and a remote environment. When you’re finished, you will have set up a server for multiple clients, and tested it with a practical solution for remote access using SSH and VNC.

      Prerequisites

      Before following this guide you’ll need the following:

      • A DigitalOcean account to set up the Droplets that will be used in the tutorial.
      • A server running Debian 9 with a public IP address to act as the front-end server, set up according to the Initial Server Setup with Debian 9 guide. A standard DigitalOcean Droplet with 1GB of memory is enough for testing purposes or for applications with a few connections. We’ll refer to this server by the host name front-end-server and its public IP address by Front_End_Public_IP.
      • Two hosts running Debian 9, which will play the role of a remote and local host that will connect using the PageKite service, set up according to the Initial Server Setup with Debian 9 guide. The remote host, with internet access through NAT, will be accessed by the local host using a PageKite tunnel. Remote and local hosts will be referred to by the host names remote-host and local-host and their public IP addresses by Remote_Host_Public_IP and Local_Host_Public_IP respectively. This tutorial will use two standard DigitalOcean Droplets with 1GB of memory to represent them. Alternatively, two local or virtual machines could be used to represent these hosts.
      • A fully registered domain name. This tutorial will use your_domain as an example throughout. You can purchase a domain name on Namecheap, get one for free on Freenom, or use the domain registrar of your choice.
      • Both of the following DNS records set up for your server. You can follow this introduction to DigitalOcean DNS for details on how to add them.
        • An A record with pagekite.your_domain pointing to the IP address of the front-end-server.
        • We also need to set up DNS so that every domain ending with pagekite.your_domain also points out to our front-end-server. This can be set up using wildcard DNS entries. In this case, create an A record for the wildcard DNS entry *.pagekite.your_domain to point out to the same IP address, Front_End_Public_IP. This will be used to distinguish the clients that connect to our server by domain name (client-1.pagekite.your_domain and client-2.pagekite.your_domain, for example) and tunnel the requisitions appropriately.
      • A local computer with a VNC client installed that supports VNC connections over SSH tunnels.
        • On Windows, you can use TightVNC, RealVNC, or UltraVNC.
        • On macOS, you can use the built-in Screen Sharing program, or can use a cross-platform app like RealVNC.
        • On Linux, you can choose from many options, including vinagre, krdc, RealVNC, or TightVNC.

      Step 1 — Setting Up the Servers

      In this tutorial, we are going to use three DigitalOcean Droplets to play the role of front-end-server, local-host, and remote-host. To do this, we will first set the local-host and remote-host up to have access to the graphical environment and to mimic the behavior of a remote-host under NAT, so that PageKite can be used as a solution to access its services. Besides that, we also need to configure the front-end-server Droplet firewall rules to allow it to work with PageKite and intermediate the connection between local-host and remote-host.

      As we are going to work with multiple servers, we’re going to use different colors in the command listings to identify which server we are using, as follows:

      • # Commands and outputs in the front-end-server Droplet
      • # Commands and outputs in the remote-host Droplet
      • # Commands and outputs in the local-host Droplet
      • # Commands and outputs in both the remote-host and local-host Droplets

      Let’s first go through the steps for both remote-host and local-host Droplets, to install the dependencies and set up access to the graphical environment using VNC. After that, we will cover the firewall configuration in each of the three Droplets to allow the front-end-server to run PageKite and mimic a connection using NAT on remote-host.

      Installing Dependencies

      We will need access to the graphical interface on both local-host and remote-host hosts to run through this demonstration. On local-host, we will use a VNC session to access its graphical interface and test our setup using the browser. On remote-host, we will set up a VNC session that we will access from local-host.

      To set up VNC, first we need to install some dependencies on local-host and remote-host. But before installing any package, we need to update the package list of the repositories, by running the following on both servers:

      Next, we install the VNC server and a graphical user environment, which is needed to start a VNC session. We will use the Tight VNC server and the Xfce desktop environment, which can be installed by running:

      • sudo apt-get install xfce4 xfce4-goodies tightvncserver

      In the middle of the graphical environment installation, we’ll be asked about the keyboard layout we wish to use. For a QWERTY US keyboard, select English (US).

      In addition to these, on local-host we’re going to need a VNC viewer and an internet browser to be able to perform the connection to remote-host. This tutorial will install the Firefox web browser and the xtightvncviewer. To install them, run:

      • sudo apt-get install firefox-esr xtightvncviewer

      When a graphical environment is installed, the system initializes in graphical mode by default. By using the DigitalOcean console, it is possible to visualize the graphical login manager, but it is not possible to log in or to use the command line interface. In our setup, we are mimicking the network behavior as if we were using NAT. To do this, we will need to use the DigitalOcean console, since we won’t be able to connect using SSH. Therefore, we need to disable the graphical user interface from automatically starting on boot. This can be done by disabling the login manager on both servers:

      • sudo systemctl disable lightdm.service

      After disabling the login manager, we can restart the Droplets and test if we can log in using the DigitalOcean console. To do that, run the following:

      Next, access the DigitalOcean console by navigating to the Droplet page in the DigitalOcean Control Panel, selecting your local-host Droplet, and clicking on the word Console in the top right corner, near the switch to turn the Droplet on and off:

      DigitalOcean Control Panel

      Once you press enter in the console, you will be prompted for your username and password. Enter these credentials to bring up the command line prompt:

      DigitalOcean Droplet Console

      Once you have done this for the local-host, repeat for the remote-host.

      With the console up for both Droplets, we can now set up the VNC.

      Setting Up VNC

      Here, we will put together a basic VNC setup. If you would like a more in-depth guide on how to set this up, check out our How to Install and Configure VNC on Debian 9 tutorial.

      To start a VNC session, run the following on both local-host and remote-host Droplets:

      On the first run, the system will create the configuration files and ask for the main password. Input your desired password, then verify it. The VNC server will also ask for a view-only password, used for viewing another user’s VNC session. As we won’t need a view-only VNC session, type n for this prompt.

      The ouput will look similar to this:

      Output

      sammy@remote-host:/home/sammy$ vncserver You will require a password to access your desktops. Password: Verify: Would you like to enter a view-only password (y/n)? n xauth: file /home/sammy/.Xauthority does not exist New 'X' desktop is remote-host:1 Creating default startup script /home/sammy/.vnc/xstartup Starting applications specified in /home/sammy/.vnc/xstartup Log file is /home/sammy/.vnc/remote-host:1.log

      The :1 after the host name represents the number of the VNC session. By default, the session number 1 is run on port 5901, session number 2 on port 5902, and so on. Following the previous output, we can access remote-host by using a VNC client to connect to Remote_Host_Public_IP on port 5901.

      One problem of the previous configuration is that it is not persistent, which means it won’t be started by default when the Droplet is restarted. To make it persistent, we can create a Systemd service and enable it. To do that, we will create the vncserver@.service file under /etc/systemd/system, which can be done using nano:

      • sudo nano /etc/systemd/system/vncserver@.service

      Place the following contents in the file, replacing sammy with your username:

      /etc/systemd/system/vncserver@.service

      [Unit]
      Description=Start TightVNC server at startup
      After=syslog.target network.target
      
      [Service]
      Type=forking
      User=sammy
      PAMName=login
      PIDFile=/home/sammy/.vnc/%H:%i.pid
      ExecStartPre=-/usr/bin/vncserver -kill :%i > /dev/null 2>&1
      ExecStart=/usr/bin/vncserver -depth 24 -geometry 1280x800 :%i
      ExecStop=/usr/bin/vncserver -kill :%i
      
      [Install]
      WantedBy=multi-user.target
      

      This file creates a vncserver Systemd unit, which can be configured as a system service using the systemctl tool. In this case, when the service is started, it kills the VNC session if it is already running (line ExecStartPre) and starts a new session using the resolution set to 1280x800 (line ExecStart). When the service is stopped, it kills the VNC session (line ExecStop).

      Save the file and quit nano. Next, we’ll make the system aware of the new unit file by running:

      • sudo systemctl daemon-reload

      Then, enable the service to be automatically started when the server is initialized by running:

      • sudo systemctl enable vncserver@1.service

      When we use the enable command with systemctl, symlinks are created so that the service is started automatically when the system is initialized, as informed by the output of the previous command:

      Output

      Created symlink /etc/systemd/system/multi-user.target.wants/vncserver@1.service → /etc/systemd/system/vncserver@.service.

      With the VNC server properly configured, we may restart the Droplets to test if the service is automatically started:

      After the system initializes, log in using SSH and check if VNC is running with:

      • sudo systemctl status vncserver@1.service

      The output will indicate the service is running:

      ● vncserver@1.service - Start TightVNC server at startup
         Loaded: loaded (/etc/systemd/system/vncserver@.service; enabled; vendor preset: enabled)
         Active: active (running) since Thu 2019-08-29 19:21:12 UTC; 1h 22min ago
        Process: 848 ExecStart=/usr/bin/vncserver -depth 24 -geometry 1280x800 :1 (code=exited, status=0/SUCCESS)
        Process: 760 ExecStartPre=/usr/bin/vncserver -kill :1 > /dev/null 2>&1 (code=exited, status=2)
       Main PID: 874 (Xtightvnc)
          Tasks: 0 (limit: 4915)
         CGroup: /system.slice/system-vncserver.slice/vncserver@1.service
                 ‣ 874 Xtightvnc :1 -desktop X -auth /home/sammy/.Xauthority -geometry 1280x800 -depth 24 -rfbwait
      
      Aug 29 19:21:10 remote-host systemd[1]: Starting Start TightVNC server at startup...
      Aug 29 19:21:10 remote-host systemd[760]: pam_unix(login:session): session opened for user sammy by (uid=0)
      Aug 29 19:21:11 remote-host systemd[848]: pam_unix(login:session): session opened for user sammy by (uid=0)
      Aug 29 19:21:12 remote-host systemd[1]: Started Start TightVNC server at startup.
      ~
      

      This finishes the VNC configuration. Remember to follow the previous steps on both remote-host and local-host. Now let’s cover the firewall configurations for each host.

      Configuring the Firewall

      Starting with the remote-host, we will configure the firewall to deny external connections to the Droplets’ services to mimic the behavior from behind NAT. In this tutorial, we are going to use port 8000 for HTTP connections, 22 for SSH, and 5901 for VNC, so we will configure the firewall to deny external connections to these ports.

      By following the initial setup for Debian 9, remote-host will have a firewall rule to allow connections to SSH. We can review this rule by running:

      The output will be the following:

      Output

      Status: active Logging: on (low) Default: deny (incoming), allow (outgoing), disabled (routed) New profiles: skip To Action From -- ------ ---- 22/tcp (OpenSSH) ALLOW IN Anywhere 22/tcp (OpenSSH (v6)) ALLOW IN Anywhere (v6)

      Remove these SSH rules to mimic the behavior behind NAT.

      Warning: Closing port 22 means you will no longer be able to use SSH to remotely log in to your server. For Droplets, this is not a problem because you can access the server’s console via the DigitalOcean Control Panel, as we did at the end of the Installing Dependencies section of this step. However, if you are not using a Droplet, be careful: closing off port 22 could lock you out of your server if you have no other means of accessing it.

      To deny SSH access, use ufw and run:

      • sudo ufw delete allow OpenSSH

      We can verify the SSH rules were removed by checking the status of the firewall again:

      The output will show no firewall rules, as in the following:

      Output

      Status: active Logging: on (low) Default: deny (incoming), allow (outgoing), disabled (routed) New profiles: skip

      Although the firewall is configured, the new configuration is not running until we enable it with:

      After enabling it, note that we won’t be able to access remote-host via SSH anymore, as mentioned in the output of the command:

      Output

      Command may disrupt existing ssh connections. Proceed with operation (y|n)? y Firewall is active and enabled on system startup

      Log out of the remote-host, then test the configuration by trying to establish an SSH or a VNC connection. It will not be possible. From now on, we may access remote-host exclusively by the DigitalOcean console.

      On local-host, we will leave the SSH ports open. We only need one firewall rule to allow access to the VNC session:

      After modifying the firewall rules, enable it by running:

      Now we may test the VNC connection using the prerequisite VNC client on your local machine to connect to local-host on port 5901 using the VNC password you’ve set up.

      To do this, open up your VNC client and connect to Local_Host_Public_IP:5901. Once you enter the password, you will connect to the VNC session.

      Note: If you have trouble connecting to the VNC session, restart the VNC service on local-host with sudo systemctl restart vncserver@1 and try to connect again.

      On its first start, Xfce will ask about the initial setup of the environment:

      Initial Xfce Configuration

      For this tutorial, select the Use default config option.

      Finally, we need to allow connections to port 80 on the front-end-server, which will be used by PageKite. Open up a terminal on front-end-server and use the following command:

      Additionally, allow traffic on port 443 for HTTPS:

      To enable the new firewall configuration, run the following:

      Now that we’ve set up the Droplets, let’s configure the PageKite front-end server.

      Step 2 — Installing PageKite on the Front-End Server

      Although it is possible to run PageKite using a Python script to set up the front-end server, it is more reliable to run it using a system service. To do so, we will need to install PageKite on the server.

      The recommended way to install a service on a Debian server is to use a distribution package. This way, it is possible to obtain automated updates and configure the service to start up on boot.

      First, we will configure the repository to install PageKite. To do that, update the package list of the repositories:

      Once the update is done, install the package dirmngr, which is necessary to support the key-ring import from the PageKite repository to ensure a secure installation:

      • sudo apt-get install dirmngr

      Next, add the repository to the /etc/apt/sources.list file, by running:

      • echo deb http://pagekite.net/pk/deb/ pagekite main | sudo tee -a /etc/apt/sources.list

      After setting up the repository, import the PageKite packaging key to our trusted set of keys, so that we can install packages from this repository. Packaging key management is done with the apt-key utility. In this case, we have to import the key AED248B1C7B2CAC3 from the key server keys.gnupg.net, which can be done by running:

      • sudo apt-key adv --recv-keys --keyserver keys.gnupg.net AED248B1C7B2CAC3

      Next, update the package lists of the repositories again, so that the pagekite package gets indexed:

      Finally, install it with:

      • sudo apt-get install pagekite

      Now that we have PageKite installed, let’s set up the front-end server and configure the service to run on boot.

      Step 3 — Configuring the Front-End Server

      The PageKite package we have just installed can be used to configure a connection to a PageKite front-end server. It can also be used to set up a front-end service to receive PageKite connections, which is what we want to do here. In order to do so, we have to edit PageKite’s configuration files.

      PageKite stores its configuration files in the directory /etc/pagekite.d. The first change we have to do is disable all lines in the /etc/pagekite.d/10_account.rc file, since this file is only used when PageKite is set up as a client to connect to a front-end server. We can edit the file using nano:

      • sudo nano /etc/pagekite.d/10_account.rc

      To disable the lines, add a # to disable the active lines of the file:

      /etc/pagekite.d/10_account.rc

      #################################[ This file is placed in the Public Domain. ]#
      # Replace the following with your account details.
      
      # kitename   = NAME.pagekite.me
      # kitesecret = YOURSECRET
      
      # Delete this line!
      # abort_not_configured
      

      After making the changes, save them and quit nano. Next, edit the file /etc/pagekite.d/20_frontends.rc:

      • sudo nano /etc/pagekite.d/20_frontends.rc

      Add the following highlighted lines to the file and comment out the defaults line, making sure to replace your_domain with the domain name you are using and examplepassword with a password of your choice:

      /etc/pagekite.d/20_frontends.rc

      
      #################################[ This file is placed in the Public Domain. ]#
      # Front-end selection
      #
      # Front-ends accept incoming requests on your behalf and forward them to
      # your PageKite, which in turn forwards them to the actual server.  You
      # probably need at least one, the service defaults will choose one for you.
      
      # Use the pagekite.net service defaults.
      # defaults
      
      # If you want to use your own, use something like:
      #     frontend = hostname:port
      # or:
      #     frontends = COUNT:dnsname:port
      
      isfrontend
      ports=80,443
      
      protos=http,https,raw
      domain=http,https,raw:*.pagekite.your_domain:examplepassword
      
      rawports=virtual
      

      Let’s explain these lines one by one. First, to configure PageKite as a front-end server, we added the line isfrontend. To configure the ports on which the server will be listening, we added ports=80,443. We also configured the protocols PageKite is going to proxy. To use HTTP, HTTPS, and RAW (which is used by SSH connections), we add the line protos=http,https,raw. We also disable the defaults settings so that there are no conflicting configurations for the server.

      Besides that, we configured the domain we are going to use for the front-end-server. For each client, a subdomain will be used, which is why we needed the DNS configurations in the Prerequisites section. We also set up a password that will be used to authenticate the clients. Using the placeholder password examplepassword, these configurations were done by adding the line domain=http,https,raw:*.pagekite.your_domain:examplepassword. Finally, we added an extra line in order to connect using SSH (which is not documented, as discussed here): rawports=virtual.

      Save the file and quit nano. Restart the PageKite service, by running:

      • sudo systemctl restart pagekite.service

      Then enable it to start on boot with:

      • sudo systemctl enable pagekite.service

      Now that we have front-end-server running, let’s test it by exposing an HTTP port on remote-host and connecting to it from local-host.

      Step 4 — Connecting to the Host Behind NAT

      To test the front-end-server, let’s start an HTTP service on remote-host and expose it to the internet using PageKite, so that we can connect to it from local-host. Remember, we have to connect to remote-host using the DigitalOcean console, since we have configured the firewall to deny incoming SSH connections.

      To start up an HTTP server for testing, we can use the Python 3 http.server module. Since Python is already installed even on the minimal Debian installation and http.server is part of the standard Python library, to start the HTTP server using port 8000 on remote-host we’ll run:

      • python3 -m http.server 8000 &

      As Debian 9 still uses Python 2 by default, it is necessary to invoke Python by running python3 to start the server. The ending & character indicates for the command to run in the background, so that we can still use the shell terminal. The output will indicate that the server is running:

      Output

      sammy@remote-host:~$ python3 -m http.server 8000 & [1] 1782 sammy@remote-host:~$ Serving HTTP on 0.0.0.0 port 8000 ...

      Note: The number 1782 that appears in this output refers to the ID that was assigned to the process started with this command and may be different depending on the run. Since it is running in the background, we can use this ID to terminate (kill) the process by issuing kill -9 1782.

      With the HTTP server running, we may establish the PageKite tunnel. A quick way to do this is by using the pagekite.py script. We can download it to remote-host running:

      • wget https://pagekite.net/pk/pagekite.py

      After downloading it, mark it as executable by running:

      Note: Since PageKite is written in Python 2 and this is the current default version of Python in Debian 9, the proceeding command works without errors. However, since default Python is being progressively migrated to Python 3 in several Linux distributions, it may be necessary to alter the first line of the pagekite.py script to set it to run with Python 2 (setting it to #!/usr/bin/python2).

      With pagekite.py available in the current directory, we can connect to front-end-server and expose the HTTP server on the domain remote-host.pagekite.your_domain by running the following, substituting your_domain and examplepassword with your own credentials:

      • ./pagekite.py --clean --frontend=pagekite.your_domain:80 --service_on=http:remote-host.pagekite.your_domain:localhost:8000:examplepassword

      Let’s take a look at the arguments in this command:

      • --clean is used to ignore the default configuration.
      • --frontend=pagekite.your_domain:80 specifies the address of our frontend. Note we are using port 80, since we have set the front end to run on this port in Step 3.
      • In the last argument, --service_on=http:remote-host.pagekite.your_domain:localhost:8000:examplepassword, we set up the service we are going to expose (http), the domain we are going to use (remote-host.pagekite.your_domain), the local address and port where the service is running (localhost:8000 since we are exposing a service on the same host we are using to connect to PageKite), and the password to connect to the frontend (examplepassword).

      Once this command is run, we will see the message Kites are flying and all is well displayed in the console. After that, we may open a browser window in the local-host VNC session and use it to access the HTTP server on remote-host by accessing the address http://remote-host.pagekite.your_domain. This will display the file system for remote-host:

      local-host Accessing remote-host Web Page

      To stop PageKite’s connection on remote-host, hit CTRL+C in the remote-host console.

      Now that we have tested front-end-server, let’s configure remote-host to make the connection with PageKite persistent and to start on boot.

      Step 5 — Making the Host Configuration Persistent

      The connection between the remote-host and the front-end-server we set up in Step 4 is not persistent, which means that the connection will not be re-established when the server is restarted. This will be a problem if you would like to use this solution long-term, so let’s make this setup persistent.

      It is possible to set up PageKite to run as a service on remote-host, so that it is started on boot. To do this, we can use the same distribution packages we used for the front-end-server in Step 3. In the remote-host console accessed through the DigitalOcean control panel, run the following command to install dirmngr:

      • sudo apt-get install dirmngr

      Then to add the PageKite repository and import the GPG key, run:

      • echo deb http://pagekite.net/pk/deb/ pagekite main | sudo tee -a /etc/apt/sources.list
      • sudo apt-key adv --recv-keys --keyserver keys.gnupg.net AED248B1C7B2CAC3

      To update the package list and install PageKite, run:

      • sudo apt-get update
      • sudo apt-get install pagekite

      To set up PageKite as a client, we will configure the front-end-server address and port in the file /etc/pagekite.d/20_frontends.rc. We can edit it using nano:

      • sudo nano /etc/pagekite.d/20_frontends.rc

      In this file, comment the line with defaults to avoid using pagekite.net service defaults. Also, configure the front-end-server address and port by using the parameter frontend, adding the line frontend = pagekite.your_domain:80 to the end of the file. Be sure to replace your_domain with the domain you are using.

      Here is the full file with the edited lines highlighted:

      /etc/pagekite.d/20_frontends.rc

      #################################[ This file is placed in the Public Domain. ]#
      # Front-end selection
      #
      # Front-ends accept incoming requests on your behalf and forward them to
      # your PageKite, which in turn forwards them to the actual server.  You
      # probably need at least one, the service defaults will choose one for you.
      
      # Use the pagekite.net service defaults.
      # defaults
      
      # If you want to use your own, use something like:
           frontend = pagekite.your_domain:80
      # or:
      #     frontends = COUNT:dnsname:port
      

      After saving the modifications and quitting nano, continue the configuration by editing the file /etc/pagekite.d/10_account.rc and setting the credentials to connect to front-end-server. First, open up the file by running:

      • sudo nano /etc/pagekite.d/10_account.rc

      To set up the domain we are going to use the domain name and the password to connect to our front-end-server, editing the parameters kitename and kitesecret respectively. We also have to comment out the last line of the file to enable the configuration, as highlighted next:

      /etc/pagekite.d/10_account.rc

      #################################[ This file is placed in the Public Domain. ]#
      # Replace the following with your account details.
      
      kitename   = remote-host.pagekite.your_domain
      kitesecret = examplepassword
      
      # Delete this line!
      # abort_not_configured
      

      Save and quit from the text editor.

      We will now configure our services that will be exposed to the internet. For HTTP and SSH services, PageKite includes sample configuration files with extensions ending in .sample in its configuration directory /etc/pagekite.d. Let’s start by copying the sample configuration file into a valid one for HTTP:

      • cd /etc/pagekite.d
      • sudo cp 80_httpd.rc.sample 80_httpd.rc

      The HTTP configuration file is almost set up. We only have to adjust the HTTP port, which we can do by editing the file we just copied:

      • sudo nano /etc/pagekite.d/80_httpd.rc

      The parameter service_on defines the address and port of the service we wish to expose. By default, it exposes localhost:80. As our HTTP server will be running on port 8000, we just have to change the port number, as highlighted next:

      /etc/pagekite.d/80_httpd.rc

      #################################[ This file is placed in the Public Domain. ]#
      # Expose the local HTTPD
      
      service_on = http:@kitename : localhost:8000 : @kitesecret
      
      # If you have TLS/SSL configured locally, uncomment this to enable end-to-end
      # TLS encryption instead of relying on the wild-card certificate at the relay.
      
      #service_on = https:@kitename : localhost:443 : @kitesecret
      
      #
      # Uncomment the following to globally DISABLE the request firewall.  Do this
      # if you are sure you know what you are doing, for more details please see
      #                <http://pagekite.net/support/security/>
      #
      #insecure
      #
      # To disable the firewall for one kite at a time, use lines like this::
      #
      #service_cfg = KITENAME.pagekite.me/80 : insecure : True
      

      Note: The service_on parameter syntax is similar to the one used with the pagekite.py script. However, the domain name we are going to use and the password are obtained from the /etc/pagekite.d/10_account.rc file and inserted by the markers @kitename and @kitesecret respectively.

      After saving the modifications to this configuration file, we have to restart the service so that the changes take effect:

      • sudo systemctl restart pagekite.service

      To start the service on boot, enable the service with:

      • sudo systemctl enable pagekite.service

      Just as we have done before, use the http.server Python module to emulate our HTTP server. It will be already running since we started it to run in the background in Step 4. However, if for some reason it is not running, we may start it again with:

      • python3 -m http.server 8000 &

      Now that we have the HTTP server and the PageKite service running, open a browser window in the local-host VNC session and use it to access remote-host by using the address http://remote-host.pagekite.your_domain. This will display the file system of remote-host in the browser.

      We have seen how to configure a PageKite front-end server and a client to expose a local HTTP server. Next, we’ll set up remote-host to expose SSH and allow remote connections.

      Step 6 — Exposing SSH with PageKite

      Besides HTTP, PageKite can be used to proxy other services, such as SSH, which is useful to access hosts remotely behind NAT in environments where it is not possible to modify networking and a router’s configurations.

      In this section, we are going to configure remote-host to expose its SSH service using PageKite, then open an SSH session from local-host.

      Just like we have done to configure HTTP with PageKite, for SSH we will copy the sample configuration file into a valid one to expose the SSH service on remote-host:

      • cd /etc/pagekite.d
      • sudo cp 80_sshd.rc.sample 80_sshd.rc

      This file is pre-configured to expose the SSH service running on port 22, which is the default configuration. Let’s take a look at its contents:

      This will show you the file:

      /etc/pagekite.d/80_sshd.rc

      #################################[ This file is placed in the Public Domain. ]#
      # Expose the local SSH daemon
      
      service_on = raw/22:@kitename : localhost:22 : @kitesecret
      

      This file is very similar to the one used to expose HTTP. The only differences are the port number, which is 22 for SSH, and the protocol, which must be set to raw when exposing SSH.

      Since we do not need to make any changes here, exit from the file.

      Restart the PageKite service:

      • sudo systemctl restart pagekite.service

      Note: We could also expose SSH using the pagekite.py script if the PageKite service wasn’t installed. We would just have to use the --service-on argument, setting the protocol to raw with the proper domain name and password. For example, to expose it using the same parameters we have configured in the PageKite service, we would use the command ./pagekite.py --clean --frontend=pagekite.your_domain:80 --service_on=raw:remote-host.pagekite.your_domain:localhost:22:examplepassword.

      On local-host, we will use the SSH client to connect to remote-host. PageKite tunnels the connections using HTTP, so that to use SSH over PageKite, we will need an HTTP proxy. There are several options of HTTP proxies we could use from the Debian repositories, such as Netcat(nc) and corkscrew. For this tutorial, we will use corkscrew, since it requires fewer arguments than nc.

      To install corkscrew on local-host, use apt-get install with the package of the same name:

      • sudo apt-get install corkscrew

      Next, generate an SSH key on local-host and append the public key to the .ssh/authorized_keys file of remote-host. To do this, follow the How to Set Up SSH Keys on Debian 9 guide, including the Copying Public Key Manually section in Step 2.

      To connect to an SSH server using a proxy, we will use ssh with the -o argument to pass in ProxyCommand and specify corkscrew as the HTTP proxy. This way, on local-host, we will run the following command to connect to remote-host through the PageKite tunnel:

      • ssh sammy@remote-host.pagekite.your_domain -i ~/id_rsa -o "ProxyCommand corkscrew %h 80 %h %p"

      Notice we provided some arguments to corkscrew. The %h and %p are tokens that the SSH client replaces by the remote host name (remote-host.pagekite.your_domain) and remote port (22, implicitly used by ssh) when it runs corkscrew. The 80 refers to the port on which PageKite is running. This port refers to the communication between the PageKite client and the front-end server.

      Once you run this command on local-host, the command line prompt for remote-host will appear.

      With our SSH connection working via PageKite, let’s next set a VNC session on remote_server and access it from local-host using VNC over SSH.

      Step 7 — Using VNC Over SSH

      Now we can access a remote host using a shell, which solves a lot of the problems that arise from servers hidden behind NAT. However, in some situations, we require access to the graphical user interface. SSH provides a way of tunneling any service in its connection, such as VNC, which can be used for graphical remote access.

      With remote-host configured to expose SSH using our front-end server, let’s use an SSH connection to tunnel VNC and have access to the remote-host graphical interface.

      Since we have already configured a VNC session to start automatically on remote-host, we will use local-host to connect to remote-host using ssh with the -L argument:

      • ssh sammy@remote-host.pagekite.your_domain -i ~/id_rsa -o "ProxyCommand corkscrew %h 80 %h %p" -L5902:localhost:5901

      The -L argument specifies that connections to a given local port should be forwarded to a remote host and port. Together with this argument, we provided a port number followed by a colon, then an IP address, domain, or host name, followed by another colon and a port number. Let’s take a look at this information in detail:

      • The first port number refers to the one we are going to use on the host that is starting the SSH connection (in this case local-host), to receive the tunneled connection from the remote host. In this case, from the point of view of local-host, the VNC Session from remote-host will be available locally, on port 5902. We could not use the port 5901 since it is already being used on local-host for its own VNC session.
      • After the first colon, we provide the host name (or IP address) of the device that is serving the VNC session we wish to tunnel. If we provide a host name, it will be resolved into an IP address by the host that is serving SSH. In this case, since remote-host is serving the SSH connection and the VNC session is also served by this same host, we can use localhost.
      • After the second colon, we provide the port in which the service to be tunneled is served. We use port 5901, since VNC is running on this port on the remote-host.

      After the connection is established, we will be presented with a remote shell on remote-host.

      Now we can reach the remote-host VNC session from local-host by connecting to port 5902 itself. To do so, open a shell from the local-host GUI in your VNC client, then run:

      Upon providing the remote-host VNC password, we will be able to access its graphical environment.

      Note: If the VNC session has been running for too long, you may encounter an error in which the GUI on remote-host is replaced by a gray screen with an X for a cursor. If this happens, try restarting the VNC session on remote-host with sudo systemctl restart vncserver@1. Once the service is running, try connecting again.

      This setup can be useful for support teams using remote access. It is possible to use SSH to tunnel any service that can be reached by remote-host. This way, we could set up remote-host as a gateway to a local attached network with many hosts, including some running Windows or another OS. As long as the hosts have a VNC server with a VNC session set up, it would be possible to access them with a graphical user interface through SSH tunneled by our PageKite front-end-server.

      In the final step, we will configure the PageKite frontend to support more clients with different passwords.

      Step 8 — Configuring the Front-End Server for Many Clients (Optional)

      Suppose we are going to use our front-end-server to offer remote access to many clients. In this multi-user setup, it would be a best practice to isolate them, using a different domain name and password for each one to connect to our server. One way of doing this is by running several PageKite services on our server on different ports, each one configured with its own subdomain and password, but this can be difficult to keep organized.

      Fortunately, the PageKite frontend supports the configuration of multiple clients itself, so that we can use the same service on a single port. To do this, we would configure the front end with the domain names and passwords.

      As we have configured the wildcard DNS entry *.pagekite.your_domain pointing out to our front-end-server, DNS entries in subdomains like remote-host.client-1.pagekite.your_domain can also point out to our server, so that we could use domains ending in client1.pagekite.your_domain and client2.pagekite.your_domain to identify hosts of different clients with different passwords.

      To do this on the front-end-server, open the /etc/pagekite.d/20_frontends.rc file:

      • sudo nano /etc/pagekite.d/20_frontends.rc

      Add the domains using the domain keyword and set different passwords for each one. To set up the domains we’ve mentioned, add:

      /etc/pagekite.d/20_frontends.rc

      #################################[ This file is placed in the Public Domain. ]#
      # Front-end selection
      #
      # Front-ends accept incoming requests on your behalf and forward them to
      # your PageKite, which in turn forwards them to the actual server.  You
      # probably need at least one, the service defaults will choose one for you.
      
      # Use the pagekite.net service defaults.
      # defaults
      
      # If you want to use your own, use something like:
      #     frontend = hostname:port
      # or:
      #     frontends = COUNT:dnsname:port
      
      isfrontend
      ports=80,443
      
      protos=http,https,raw
      domain=http,https,raw:*.pagekite.your_domain:examplepassword
      domain=http,https,raw:*.client-1.pagekite.your_domain:examplepassword2
      domain=http,https,raw:*.client-2.pagekite.your_domain:examplepassword3
      
      rawports=virtual
      

      Save and exit the file.

      After modifying the configuration files, restart PageKite:

      • sudo systemctl restart pagekite.service

      On the remote hosts, let’s configure the PageKite client to connect according to the new domains and passwords. For example, in remote-host, to connect using client-1.pagekite.your_domain, modify the file /etc/pagekite.d/10_account.rc, where the credentials to connect to front-end-server are stored:

      • sudo nano /etc/pagekite.d/10_account.rc

      Change kitename and kitesecret to the appropriate credentials. For the domain remote-host.client-1.pagekite.your_domain, the configuration would be:

      /etc/pagekite.d/10_account.rc

      #################################[ This file is placed in the Public Domain. ]#
      # Replace the following with your account details.
      
      kitename   = remote-host.client-1.pagekite.your_domain
      kitesecret = examplepassword2
      
      # Delete this line!
      
      

      Save and exit the file.

      After modifying the file, restart the PageKite service:

      • sudo systemctl restart pagekite.service

      Now, on local-host, we can connect to remote-host via SSH with:

      • ssh sammy@remote-host.client-1.pagekite.your_domain -i ~/id_rsa -o "ProxyCommand corkscrew %h 80 %h %p"

      We could use the domain client-2.pagekite.your-domain for another client. This way, we could administrate the services in an isolated way, with the possibility to change the password of one client or even disable one of them without affecting the other.

      Conclusion

      In this article, we set up a private PageKite front-end server on a Debian 9 Droplet and used it to expose HTTP and SSH services on a remote host behind NAT. We then connected to these services from a local-host server and verified the PageKite functionality. As we have mentioned, this could be an effective setup for remote access applications, since we can tunnel other services in the SSH connection, such as VNC.

      If you’d like to learn more about PageKite, check out the PageKite Support Info. If you would like to dive deeper into networking with Droplets, take a look through DigitalOcean’s Networking Documentation.



      Source link

      How To Set Up a Ruby on Rails Project with a React Frontend


      The author selected the Electronic Frontier Foundation to receive a donation as part of the Write for DOnations program.

      Introduction

      Ruby on Rails is a popular server-side web application framework, with over 42,000 stars on GitHub at the time of writing this tutorial. It powers a lot of the popular applications that exist on the web today, like GitHub, Basecamp, SoundCloud, Airbnb, and Twitch. With its emphasis on programmer experience and the passionate community that has built up around it, Ruby on Rails will give you the tools you need to build and maintain your modern web application.

      React is a JavaScript library used to create front-end user interfaces. Backed by Facebook, it is one of the most popular front-end libraries used on the web today. React offers features like a virtual Document Object Model (DOM), component architecture, and state management, which make the process of front-end development more organized and efficient.

      With the frontend of the web moving toward frameworks that are separate from the server-side code, combining the elegance of Rails with the efficiency of React will let you build powerful and modern applications informed by current trends. By using React to render components from within a Rails view instead of the Rails template engine, your application will benefit from the latest advancements in JavaScript and front-end development while still leveraging the expressiveness of Ruby on Rails.

      In this tutorial, you will create a Ruby on Rails application that stores your favorite recipes then displays them with a React frontend. When you are finished, you will be able to create, view, and delete recipes using a React interface styled with Bootstrap:

      Completed Recipe App

      If you would like to take a look at the code for this application, check out the companion repository for this tutorial on the DigitalOcean Community GitHub.

      Prerequisites

      To follow this tutorial, you need to have the following:

      • Node.js and npm installed on your development machine. This tutorial uses Node.js version 10.16.0 and npm version 6.9.0. Node.js is a JavaScript run-time environment that allows you to run your code outside of the browser. It comes with a pre-installed Package Manager called npm, which lets you install and update packages. To install these on macOS or Ubuntu 18.04, follow the steps in How to Install Node.js and Create a Local Development Environment on macOS or the “Installing Using a PPA” section of How To Install Node.js on Ubuntu 18.04.

      • The Yarn package manager installed on your development machine, which will allow you to download the React framework. This tutorial was tested on version 1.16.0; to install this dependency, follow the official Yarn installation guide.

      • Installation of the Ruby on Rails framework. To get this, follow our guide on How to Install Ruby on Rails with rbenv on Ubuntu 18.04, or How To Install Ruby on Rails with rbenv on CentOS 7. If you would like to develop this application on macOS, please see this tutorial on How To Install Ruby on Rails with rbenv on macOS. This tutorial was tested on version 2.6.3 of Ruby and version 5.2.3 of Rails, so make sure to specify these versions during the installation process.

      • Installation of PostgreSQL, as shown in Steps 1 and 2 of our tutorial How To Use PostgreSQL with Your Ruby on Rails Application on Ubuntu 18.04 or How To Use PostgreSQL with Your Ruby on Rails Application on macOS. To follow this tutorial, use PostgreSQL version 10. If you are looking to develop this application on a different distribution of Linux or on another OS, see the official PostgreSQL downloads page. For more information on how to use PostgreSQL, see our How To Install and Use PostgreSQL tutorials.

      Step 1 — Creating a New Rails Application

      In this step, you will build your recipe application on the Rails application framework. First, you’ll create a new Rails application, which will be set up to work with React out of the box with little configuration.

      Rails provides a number of scripts called generators that help in creating everything that’s necessary to build a modern web application. To see a full list of these commands and what they do, run the following command in your Terminal window:

      This will yield a comprehensive list of options, which will allow you to set the parameters of your application. One of the commands listed is the new command, which creates a new Rails application.

      Now, you will create a new Rails application using the new generator. Run the following command in your Terminal window:

      • rails new rails_react_recipe -d=postgresql -T --webpack=react --skip-coffee

      The preceding command creates a new Rails application in a directory named rails_react_recipe, installs the required Ruby and JavaScript dependencies, and configures Webpack. Let’s walk through the flags that are associated with this new generator command:

      • The -d flag specifies the preferred database engine, which in this case is PostgreSQL.
      • The -T flag instructs Rails to skip the generation of test files, since you won’t be writing tests for the purposes of this tutorial. This command is also suggested if you want to use a Ruby testing tool different from the one Rails provides.
      • The --webpack instructs Rails to preconfigure for JavaScript with the webpack bundler, in this case specifically for a React application.
      • The --skip-coffee asks Rails not to set up CoffeeScript, which is not needed for this tutorial.

      Once the command is done running, move into the rails_react_recipe directory, which is the root directory of your app:

      Next, list out the contents of the directory:

      This root directory has a number of auto-generated files and folders that make up the structure of a Rails application, including a package.json file containing the dependencies for a React application.

      Now that you have successfully created a new Rails application, you are ready to hook it up to a database in the next step.

      Step 2 — Setting Up the Database

      Before you run your new Rails application, you have to first connect it to a database. In this step, you'll connect the newly created Rails application to a PostgreSQL database, so recipe data can be stored and fetched when needed.

      The database.yml file found in config/database.yml contains database details like database name for different development environments. Rails specifies a database name for the different development environments by appending an underscore (_) followed by the environment name to your app’s name. You can always change any environment database name to whatever you prefer.

      Note: At this point, you can alter config/database.yml to set up which PostgreSQL role you would like Rails to use to create your database. If you followed the Prerequisite How To Use PostgreSQL with Your Ruby on Rails Application and created a role that is secured by a password, you can follow the instructions in Step 4 for macOS or Ubuntu 18.04.

      As earlier stated, Rails offers a lot of commands to make developing web applications easy. This includes commands to work with databases, such as create, drop, and reset. To create a database for your application, run the following command in your Terminal window:

      This command creates a development and test database, yielding the following output:

      Output

      Created database 'rails_react_recipe_development' Created database 'rails_react_recipe_test'

      Now that the application is connected to a database, start the application by running the following command in you Terminal window:

      • rails s --binding=127.0.0.1

      The s or server command fires up Puma, which is a web server distributed with Rails by default, and --binding=127.0.0.1 binds the server to your localhost.

      Once you run this command, your command prompt will disappear, and you will see the following output:

      Output

      => Booting Puma => Rails 5.2.3 application starting in development => Run `rails server -h` for more startup options Puma starting in single mode... * Version 3.12.1 (ruby 2.6.3-p62), codename: Llamas in Pajamas * Min threads: 5, max threads: 5 * Environment: development * Listening on tcp://127.0.0.1:3000 Use Ctrl-C to stop

      To see your application, open a browser window and navigate to http://localhost:3000. You will see the Rails default welcome page:

      Rails welcome page

      This means that you have properly set up your Rails application.

      To stop the web server at anytime, press CTRL+C in the Terminal window where the server is running. Go ahead and do this now; you will get a goodbye message from Puma:

      Output

      ^C- Gracefully stopping, waiting for requests to finish === puma shutdown: 2019-07-31 14:21:24 -0400 === - Goodbye! Exiting

      Your prompt will then reappear.

      You have successfully set up a database for your food recipe application. In the next step, you will install all the extra JavaScript dependencies you need to put together your React frontend.

      Step 3 — Installing Frontend Dependencies

      In this step, you will install the JavaScript dependencies needed on the frontend of your food recipe application. They include:

      Run the following command in your Terminal window to install these packages with the Yarn package manager:

      • yarn add react-router-dom bootstrap jquery popper.js

      This command uses Yarn to install the specified packages and adds them to the package.json file. To verify this, take a look at the package.json file located in the root directory of the project:

      You'll see the installed packages listed under the dependencies key:

      ~/rails_react_recipe/package.json

      {
        "name": "rails_react_recipe",
        "private": true,
        "dependencies": {
          "@babel/preset-react": "^7.0.0",
          "@rails/webpacker": "^4.0.7",
          "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
          "bootstrap": "^4.3.1",
          "jquery": "^3.4.1",
          "popper.js": "^1.15.0",
          "prop-types": "^15.7.2",
          "react": "^16.8.6",
          "react-dom": "^16.8.6",
          "react-router-dom": "^5.0.1"
        },
        "devDependencies": {
          "webpack-dev-server": "^3.7.2"
        }
      }
      

      You have installed a few front-end dependencies for your application. Next, you’ll set up a homepage for your food recipe application.

      Step 4 — Setting Up the Homepage

      With all the required dependencies installed, in this step you will create a homepage for the application. The homepage will serve as the landing page when users first visit the application.

      Rails follows the Model-View-Controller architectural pattern for applications. In the MVC pattern, a controller's purpose is to receive specific requests and pass them along to the appropriate model or view. Right now the application displays the Rails welcome page when the root URL is loaded in the browser. To change this, you will create a controller and view for the homepage and match it to a route.

      Rails provides a controller generator for creating a controller. The controller generator receives a controller name, along with a matching action. For more on this, check out the official Rails documentation.

      This tutorial will call the controller Homepage. Run the following command in your Terminal window to create a Homepage controller with an index action.

      • rails g controller Homepage index

      Note:
      On Linux, if you run into the error FATAL: Listen error: unable to monitor directories for changes., this is due to a system limit on the number of files your machine can monitor for changes. Run the following command to fix it:

      • echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

      This will permanently increase the amount of directories that you can monitor with Listen to 524288. You can change this again by running the same command and replacing 524288 with your desired number.

      Running this command generates the following files:

      • A homepage_controller.rb file for receiving all homepage-related requests. This file contains the index action you specified in the command.
      • A homepage.js file for adding any JavaScript behavior related to the Homepage controller.
      • A homepage.scss file for adding styles related to the Homepage controller.
      • A homepage_helper.rb file for adding helper methods related to the Homepage controller.
      • An index.html.erb file which is the view page for rendering anything related to the homepage.

      Apart from these new pages created by running the Rails command, Rails also updates your routes file which is located at config/routes.rb. It adds a get route for your homepage which you will modify as your root route.

      A root route in Rails specifies what will show up when users visit the root URL of your application. In this case, you want your users to see your homepage. Open the routes file located at config/routes.rb in your favorite editor:

      Inside this file, replace get 'homepage/index' with root 'homepage#index' so that the file looks like the following:

      ~/rails_react_recipe/config/routes.rb

      Rails.application.routes.draw do
        root 'homepage#index'
        # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
      end
      

      This modification instructs Rails to map requests to the root of the application to the index action of the Homepage controller, which in turn renders whatever is in the index.html.erb file located at app/views/homepage/index.html.erb on to the browser.

      To verify that this is working, start your application:

      • rails s --binding=127.0.0.1

      Opening the application in the browser, you will see a new landing page for your application:

      Application Homepage

      Once you have verified that your application is working, press CTRL+C to stop the server.

      Next, delete the contents of the ~/rails_react_recipe/app/views/homepage/index.html.erb file. By doing this, you will ensure that the contents of index.html.erb do not interfere with the React rendering of your frontend.

      Now that you have set up your homepage for your application, you can move to the next section, where you will configure the frontend of your application to use React.

      Step 5 — Configuring React as Your Rails Frontend

      In this step, you will configure Rails to use React on the frontend of the application, instead of its template engine. This will allow you to take advantage of React rendering to create a more visually appealing homepage.

      Rails, with the help of the Webpacker gem, bundles all your JavaScript code into packs. These can be found in the packs directory at app/javascript/packs. You can link these packs in Rails views using the javascript_pack_tag helper, and you can link stylesheets imported into the packs using the stylesheet_pack_tag helper. To create an entry point to your React environment, you will add one of these packs to your application layout.

      First, rename the ~/rails_react_recipe/app/javascript/packs/hello_react.jsx file to ~/rails_react_recipe/app/javascript/packs/Index.jsx.

      • mv ~/rails_react_recipe/app/javascript/packs/hello_react.jsx ~/rails_react_recipe/app/javascript/packs/Index.jsx

      After renaming the file, open application.html.erb, the application layout file:

      • nano ~/rails_react_recipe/app/views/layouts/application.html.erb

      Add the following highlighted lines of code at the end of the head tag in the application layout file:

      ~/rails_react_recipe/app/views/layouts/application.html.erb

      <!DOCTYPE html>
      <html>
        <head>
          <title>RailsReactRecipe</title>
          <%= csrf_meta_tags %>
          <%= csp_meta_tag %>
      
          <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
          <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
          <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
          <%= javascript_pack_tag 'Index' %>
        </head>
      
        <body>
          <%= yield %>
        </body>
      </html>
      

      Adding the JavaScript pack to your application’s header makes all your JavaScript code available and executes the code in your Index.jsx file on the page whenever you run the app. Along with the JavaScript pack, you also added a meta viewport tag to control the dimensions and scaling of pages on your application.

      Save and exit the file.

      Now that your entry file is loaded onto the page, create a React component for your homepage. Start by creating a components directory in the app/javascript directory:

      • mkdir ~/rails_react_recipe/app/javascript/components

      The components directory will house the component for the homepage, along with other React components in the application. The homepage will contain some text and a call to action button to view all recipes.

      In your editor, create a Home.jsx file in the components directory:

      • nano ~/rails_react_recipe/app/javascript/components/Home.jsx

      Add the following code to the file:

      ~/rails_react_recipe/app/javascript/components/Home.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      
      export default () => (
        <div className="vw-100 vh-100 primary-color d-flex align-items-center justify-content-center">
          <div className="jumbotron jumbotron-fluid bg-transparent">
            <div className="container secondary-color">
              <h1 className="display-4">Food Recipes</h1>
              <p className="lead">
                A curated list of recipes for the best homemade meal and delicacies.
              </p>
              <hr className="my-4" />
              <Link
                to="/recipes"
                className="btn btn-lg custom-button"
                role="button"
              >
                View Recipes
              </Link>
            </div>
          </div>
        </div>
      );
      

      In this code, you imported React and also the Link component from React Router. The Link component creates a hyperlink to navigate from one page to another. You then created and exported a functional component containing some Markup language for your homepage, styled with Bootstrap classes.

      With your Home component in place, you will now set up routing using React Router. Create a routes directory in the app/javascript directory:

      • mkdir ~/rails_react_recipe/app/javascript/routes

      The routes directory will contain a few routes with their corresponding components. Whenever any specified route is loaded, it will render its corresponding component to the browser.

      In the routes directory, create an Index.jsx file:

      • nano ~/rails_react_recipe/app/javascript/routes/Index.jsx

      Add the following code to it:

      ~/rails_react_recipe/app/javascript/routes/Index.jsx

      import React from "react";
      import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
      import Home from "../components/Home";
      
      export default (
        <Router>
          <Switch>
            <Route path="/" exact component={Home} />
          </Switch>
        </Router>
      );
      

      In this Index.jsx route file, you imported a couple of modules: the React module that allows us to use React, and the BrowserRouter, Route, and Switch modules from React Router, which together help us navigate from one route to another. Lastly, you imported your Home component, which will be rendered whenever a request matches the root (/) route. Whenever you want to add more pages to your application, all you need to do is declare a route in this file and match it to the component you want to render for that page.

      Save and exit the file.

      You have now successfully set up routing using React Router. For React to be aware of the available routes and use them, the routes have to be available at the entry point to the application. To achieve this, you will render your routes in a component that React will render in your entry file.

      Create an App.jsx file in the app/javascript/components directory:

      • nano ~/rails_react_recipe/app/javascript/components/App.jsx

      Add the following code into the App.jsx file:

      ~/rails_react_recipe/app/javascript/components/App.jsx

      import React from "react";
      import Routes from "../routes/Index";
      
      export default props => <>{Routes}</>;
      

      In the App.jsx file, you imported React and the route files you just created. You then exported a component that renders the routes within fragments. This component will be rendered at the entry point of the aplication, thereby making the routes available whenever the application is loaded.

      Now that you have your App.jsx set up, it's time to render it in your entry file. Open the entry Index.jsx file:

      • nano ~/rails_react_recipe/app/javascript/packs/Index.jsx

      Replace the code there with the following code:

      ~/rails_react_recipe/app/javascript/packs/Index.jsx

      import React from "react";
      import { render } from "react-dom";
      import 'bootstrap/dist/css/bootstrap.min.css';
      import $ from 'jquery';
      import Popper from 'popper.js';
      import 'bootstrap/dist/js/bootstrap.bundle.min';
      import App from "../components/App";
      
      document.addEventListener("DOMContentLoaded", () => {
        render(
          <App />,
          document.body.appendChild(document.createElement("div"))
        );
      });
      

      In this code snippet, you imported React, the render method from ReactDOM, Bootstrap, jQuery, Popper.js, and your App component. Using ReactDOM's render method, you rendered your App component in a div element, which was appended to the body of the page. Whenever the application is loaded, React will render the content of the App component inside the div element on the page.

      Save and exit the file.

      Finally, add some CSS styles to your homepage.

      Open up your application.css in your ~/rails_react_recipe/app/assets/stylesheets directory:

      • nano ~/rails_react_recipe/app/assets/stylesheets/application.css

      Next, replace the contents of the application.css file with the follow code:

      ~/rails_react_recipe/app/assets/stylesheets/application.css

      .bg_primary-color {
        background-color: #FFFFFF;
      }
      .primary-color {
        background-color: #FFFFFF;
      }
      .bg_secondary-color {
        background-color: #293241;
      }
      .secondary-color {
        color: #293241;
      }
      .custom-button.btn {
        background-color: #293241;
        color: #FFF;
        border: none;
      }
      .custom-button.btn:hover {
        color: #FFF !important;
        border: none;
      }
      .hero {
        width: 100vw;
        height: 50vh;
      }
      .hero img {
        object-fit: cover;
        object-position: top;
        height: 100%;
        width: 100%;
      }
      .overlay {
        height: 100%;
        width: 100%;
        opacity: 0.4;
      }
      

      This creates the framework for a hero image, or a large web banner on the front page of your website, that you will add later. Additionally, this styles the button that the user will use to enter the application.

      With your CSS styles in place, save and exit the file. Next, restart the web server for your application, then reload the application in your browser. You will see a brand new homepage:

      Homepage Style

      In this step, you configured your application so that it uses React as its frontend. In the next section, you will create models and controllers that will allow you to create, read, update, and delete recipes.

      Step 6 — Creating the Recipe Controller and Model

      Now that you have set up a React frontend for your application, in this step you'll create a Recipe model and controller. The recipe model will represent the database table that will hold information about the user's recipes while the controller will receive and handle requests to create, read, update, or delete recipes. When a user requests a recipe, the recipe controller receives this request and passes it to the recipe model, which retrieves the requested data from the database. The model then returns the recipe data as a response to the controller. Finally, this information is displayed in the browser.

      Start by creating a Recipe model by using the generate model subcommand provided by Rails and by specifying the name of the model along with its columns and data types. Run the following command in your Terminal window to create a Recipe model:

      • rails generate model Recipe name:string ingredients:text instruction:text image:string

      The preceding command instructs Rails to create a Recipe model together with a name column of type string, an ingredients and instruction column of type text, and an image column of type string. This tutorial has named the model Recipe, because by convention models in Rails use a singular name while their corresponding database tables use a plural name.

      Running the generate model command creates two files:

      • A recipe.rb file that holds all the model related logic.
      • A 20190407161357_create_recipes.rb file (the number at the beginning of the file may differ depending on the date when you run the command). This is a migration file that contains the instruction for creating the database structure.

      Next, edit the recipe model file to ensure that only valid data is saved to the database. You can achieve this by adding some database validation to your model. Open your recipe model located at app/models/recipe.rb:

      • nano ~/rails_react_recipe/app/models/recipe.rb

      Add the following highlighted lines of code to the file:

      class Recipe < ApplicationRecord
        validates :name, presence: true
        validates :ingredients, presence: true
        validates :instruction, presence: true
      end
      

      In this code, you added model validation which checks for the presence of a name, ingredients, and instruction field. Without the presence of these three fields, a recipe is invalid and won’t be saved to the database.

      Save and quit the file.

      For Rails to create the recipes table in your database, you have to run a migration, which in Rails is a way to make changes to your database programmatically. To make sure that the migration works with the database you set up, it is necessary to make changes to the 20190407161357_create_recipes.rb file.

      Open this file in your editor:

      • nano ~/rails_react_recipe/db/migrate/20190407161357_create_recipes.rb

      Add the following highlighted lines, so that the file looks like this:

      db/migrate/20190407161357_create_recipes.rb

      class CreateRecipes < ActiveRecord::Migration[5.2]
        def change
          create_table :recipes do |t|
            t.string :name, null: false
            t.text :ingredients, null: false
            t.text :instruction, null: false
            t.string :image, default: 'https://raw.githubusercontent.com/do-community/react_rails_recipe/master/app/assets/images/Sammy_Meal.jpg'
            t.timestamps
          end
        end
      end
      

      This migration file contains a Ruby class with a change method, and a command to create a table called recipes along with the columns and their data types. You also updated 20190407161357_create_recipes.rb with a NOT NULL constraint on the name, ingredients, and instruction columns by adding null: false, ensuring that these columns have a value before changing the database. Finally, you added a default image URL for your image column; this could be another URL if you wanted to use a different image.

      With these changes, save and exit the file. You’re now ready to run your migration and actually create your table. In your Terminal window, run the following command:

      Here you used the database migrate command, which executes the instructions in your migration file. Once the command runs successfully, you will receive an output similar to the following:

      Output

      == 20190407161357 CreateRecipes: migrating ==================================== -- create_table(:recipes) -> 0.0140s == 20190407161357 CreateRecipes: migrated (0.0141s) ===========================

      With your recipe model in place, create your recipes controller and add the logic for creating, reading, and deleting recipes. In your Terminal window, run the following command:

      • rails generate controller api/v1/Recipes index create show destroy -j=false -y=false --skip-template-engine --no-helper

      In this command, you created a Recipes controller in an api/v1 directory with an index, create, show, and destroy action. The index action will handle fetching all your recipes, the create action will be responsible for creating new recipes, the show action will fetch a single recipe, and the destroy action will hold the logic for deleting a recipe.

      You also passed some flags to make the controller more lightweight, including:

      • -j=false which instructs Rails to skip generating associated JavaScript files.
      • -y=false which instructs Rails to skip generating associated stylesheet files.
      • --skip-template-engine, which instructs Rails to skip generating Rails view files, since React is handling your front-end needs.
      • --no-helper, which instructs Rails to skip generating a helper file for your controller.

      Running the command also updated your routes file with a route for each action in the Recipes controller. To use these routes, make changes to your config/routes.rb file.

      Open up the routes file in your text editor:

      • nano ~/rails_react_recipe/config/routes.rb

      Once it is open, update it to look like the following code, altering or adding the highlighted lines:

      ~/rails_react_recipe/config/routes.rb

      Rails.application.routes.draw do
        namespace :api do
          namespace :v1 do
            get 'recipes/index'
            post 'recipes/create'
            get '/show/:id', to: 'recipes#show'
            delete '/destroy/:id', to: 'recipes#destroy'
          end
        end
        root 'homepage#index'
        get '/*path' => 'homepage#index'
        # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
      end
      

      In this route file, you modified the HTTP verb of the create and destroy routes so that it can post and delete data. You also modified the routes for the show and destroy action by adding an :id parameter into the route. :id will hold the identification number of the recipe you want to read or delete.

      You also added a catch all route with get '/*path' that will direct any other request that doesn’t match the existing routes to the index action of the homepage controller. This way, the routing on the frontend will handle requests that are not related to creating, reading, or deleting recipes.

      Save and exit the file.

      To see a list of routes available in your application, run the following command in your Terminal window:

      Running this command displays a list of URI patterns, verbs, and matching controllers or actions for your project.

      Next, add the logic for getting all recipes at once. Rails uses the ActiveRecord library to handle database-related tasks like this. ActiveRecord connects classes to relational database tables and provides a rich API for working with them.

      To get all recipes, you'll use ActiveRecord to query the recipes table and fetch all the recipes that exist in the database.

      Open the recipes_controller.rb file with the following command:

      • nano ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb

      Add the following highlighted lines of code to the recipes controller:

      ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb

      class Api::V1::RecipesController < ApplicationController
        def index
          recipe = Recipe.all.order(created_at: :desc)
          render json: recipe
        end
      
        def create
        end
      
        def show
        end
      
        def destroy
        end
      end
      

      In your index action, using the all method provided by ActiveRecord, you get all the recipes in your database. Using the order method, you order them in descending order by their created date. This way, you have the newest recipes first. Lastly, you send your list of recipes as a JSON response with render.

      Next, add the logic for creating new recipes. As with fetching all recipes, you'll rely on ActiveRecord to validate and save the provided recipe details. Update your recipe controller with the following highlighted lines of code:

      ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb

      class Api::V1::RecipesController < ApplicationController
        def index
          recipe = Recipe.all.order(created_at: :desc)
          render json: recipe
        end
      
        def create
          recipe = Recipe.create!(recipe_params)
          if recipe
            render json: recipe
          else
            render json: recipe.errors
          end
        end
      
        def show
        end
      
        def destroy
        end
      
        private
      
        def recipe_params
          params.permit(:name, :image, :ingredients, :instruction)
        end
      end
      

      In the create action, you use ActiveRecord’s create method to create a new recipe. The create method has the ability to assign all controller parameters provided into the model at once. This makes it easy to create records, but also opens the possibility of malicious use. This can be prevented by using a feature provided by Rails known as strong parameters. This way, parameters can’t be assigned unless they’ve been whitelisted. In your code, you passed a recipe_params parameter to the create method. The recipe_params is a private method where you whitelisted your controller parameters to prevent wrong or malicious content from getting into your database. In this case, you are permitting a name, image, ingredients, and instruction parameter for valid use of the create method.

      Your recipe controller can now read and create recipes. All that’s left is the logic for reading and deleting a single recipe. Update your recipes controller with the following code:

      ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb

      class Api::V1::RecipesController < ApplicationController
        def index
          recipe = Recipe.all.order(created_at: :desc)
          render json: recipe
        end
      
        def create
          recipe = Recipe.create!(recipe_params)
          if recipe
            render json: recipe
          else
            render json: recipe.errors
          end
        end
      
        def show
          if recipe
            render json: recipe
          else
            render json: recipe.errors
          end
        end
      
        def destroy
          recipe&.destroy
          render json: { message: 'Recipe deleted!' }
        end
      
        private
      
        def recipe_params
          params.permit(:name, :image, :ingredients, :instruction)
        end
      
        def recipe
          @recipe ||= Recipe.find(params[:id])
        end
      end
      

      In the new lines of code, you created a private recipe method. The recipe method uses ActiveRecord’s find method to find a recipe whose idmatches the id provided in the params and assigns it to an instance variable @recipe. In the show action, you checked if a recipe is returned by the recipe method and sent it as a JSON response, or sent an error if it was not.

      In the destroy action, you did something similar using Ruby’s safe navigation operator &., which avoids nil errors when calling a method. This let's you delete a recipe only if it exists, then send a message as a response.

      Now that you have finished making these changes to recipes_controller.rb, save the file and exit your text editor.

      In this step, you created a model and controller for your recipes. You’ve written all the logic needed to work with recipes on the backend. In the next section, you'll create components to view your recipes.

      Step 7 — Viewing Recipes

      In this section, you will create components for viewing recipes. First you’ll create a page where you can view all existing recipes, and then another to view individual recipes.

      You’ll start off by creating a page to view all recipes. However, before you can do this, you need recipes to work with, since your database is currently empty. Rails affords us the opportunity to create seed data for your application.

      Open up the seed file seeds.rb to edit:

      • nano ~/rails_react_recipe/db/seeds.rb

      Replace the contents of this seed file with the following code:

      ~/rails_react_recipe/db/seeds.rb

      9.times do |i|
        Recipe.create(
          name: "Recipe #{i + 1}",
          ingredients: '227g tub clotted cream, 25g butter, 1 tsp cornflour,100g parmesan, grated nutmeg, 250g fresh fettuccine or tagliatelle, snipped chives or chopped parsley to serve (optional)',
          instruction: 'In a medium saucepan, stir the clotted cream, butter, and cornflour over a low-ish heat and bring to a low simmer. Turn off the heat and keep warm.'
        )
      end
      

      In this code, you are using a loop to instruct Rails to create nine recipes with a name, ingredients, and instruction. Save and exit the file.

      To seed the database with this data, run the following command in your Terminal window:

      Running this command adds nine recipes to your database. Now you can fetch them and render them on the frontend.

      The component to view all recipes will make a HTTP request to the index action in the RecipesController to get a list of all recipes. These recipes will then be displayed in cards on the page.

      Create a Recipes.jsx file in the app/javascript/components directory:

      • nano ~/rails_react_recipe/app/javascript/components/Recipes.jsx

      Once the file is open, import the React and Link modules into it by adding the following lines:

      ~/rails_react_recipe/app/javascript/components/Recipes.jsx

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

      Next, create a Recipes class that extends the React.Component class. Add the following highlighted code to create a React component that extends React.Component:

      ~/rails_react_recipe/app/javascript/components/Recipes.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      
      class Recipes extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            recipes: []
          };
        }
      
      }
      export default Recipes;
      

      Inside the constructor, we are initializing a state object that holds the state of your recipes, which on initialization is an empty array ([]).

      Next, add a componentDidMount method in the Recipe class. The componentDidMount method is a React lifecycle method that is called immediately after a component is mounted. In this lifecycle method, you will make a call to fetch all your recipes. To do this, add the following lines:

      ~/rails_react_recipe/app/javascript/components/Recipes.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      
      class Recipes extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            recipes: []
          };
        }
      
        componentDidMount() {
            const url = "/api/v1/recipes/index";
            fetch(url)
              .then(response => {
                if (response.ok) {
                  return response.json();
                }
                throw new Error("Network response was not ok.");
              })
              .then(response => this.setState({ recipes: response }))
              .catch(() => this.props.history.push("/"));
        }
      
      }
      export default Recipes;
      

      In your componentDidMount method, you made an HTTP call to fetch all recipes using the Fetch API. If the response is successful, the application saves the array of recipes to the recipe state. If there’s an error, it will redirect the user to the homepage.

      Finally, add a render method in the Recipe class. The render method holds the React elements that will be evaluated and displayed on the browser page when a component is rendered. In this case, the render method will render cards of recipes from the component state. Add the following highlighted lines to Recipes.jsx:

      ~/rails_react_recipe/app/javascript/components/Recipes.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      
      class Recipes extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            recipes: []
          };
        }
      
        componentDidMount() {
          const url = "/api/v1/recipes/index";
          fetch(url)
            .then(response => {
              if (response.ok) {
                return response.json();
              }
              throw new Error("Network response was not ok.");
            })
            .then(response => this.setState({ recipes: response }))
            .catch(() => this.props.history.push("/"));
        }
        render() {
          const { recipes } = this.state;
          const allRecipes = recipes.map((recipe, index) => (
            <div key={index} className="col-md-6 col-lg-4">
              <div className="card mb-4">
                <img
                  src={recipe.image}
                  className="card-img-top"
                  alt={`${recipe.name} image`}
                />
                <div className="card-body">
                  <h5 className="card-title">{recipe.name}</h5>
                  <Link to={`/recipe/${recipe.id}`} className="btn custom-button">
                    View Recipe
                  </Link>
                </div>
              </div>
            </div>
          ));
          const noRecipe = (
            <div className="vw-100 vh-50 d-flex align-items-center justify-content-center">
              <h4>
                No recipes yet. Why not <Link to="/new_recipe">create one</Link>
              </h4>
            </div>
          );
      
          return (
            <>
              <section className="jumbotron jumbotron-fluid text-center">
                <div className="container py-5">
                  <h1 className="display-4">Recipes for every occasion</h1>
                  <p className="lead text-muted">
                    We’ve pulled together our most popular recipes, our latest
                    additions, and our editor’s picks, so there’s sure to be something
                    tempting for you to try.
                  </p>
                </div>
              </section>
              <div className="py-5">
                <main className="container">
                  <div className="text-right mb-3">
                    <Link to="/recipe" className="btn custom-button">
                      Create New Recipe
                    </Link>
                  </div>
                  <div className="row">
                    {recipes.length > 0 ? allRecipes : noRecipe}
                  </div>
                  <Link to="/" className="btn btn-link">
                    Home
                  </Link>
                </main>
              </div>
            </>
          );
        }
      }
      export default Recipes;
      

      Save and exit Recipes.jsx.

      Now that you have created a component to display all the recipes, the next step is to create a route for it. Open the front-end route file located at app/javascript/routes/Index.jsx:

      • nano app/javascript/routes/Index.jsx

      Add the following highlighted lines to the file:

      ~/rails_react_recipe/app/javascript/routes/Index.jsx

      import React from "react";
      import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
      import Home from "../components/Home";
      import Recipes from "../components/Recipes";
      
      export default (
        <Router>
          <Switch>
            <Route path="/" exact component={Home} />
            <Route path="/recipes" exact component={Recipes} />
          </Switch>
        </Router>
      );
      

      Save and exit the file.

      At this point, it's a good idea to verify that your code is working correctly. As you did before, use the following command to start your server:

      • rails s --binding=127.0.0.1

      Go ahead and open the app in your browser. By clicking the View Recipe button on the homepage, you will see a display with your seed recipes:

      Recipes Page

      Use CTRL+C in your Terminal window to stop the server and get your prompt back.

      Now that you can view all the recipes that exist in your application, it's time to create a second component to view individual recipes. Create a Recipe.jsx file in the app/javascript/components directory:

      • nano app/javascript/components/Recipe.jsx

      As with the Recipes component, import the React and Link modules by adding the following lines:

      ~/rails_react_recipe/app/javascript/components/Recipe.jsx

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

      Next create a Recipe class that extends React.Component class by adding the highlighted lines of code:

      ~/rails_react_recipe/app/javascript/components/Recipe.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      
      class Recipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = { recipe: { ingredients: "" } };
      
          this.addHtmlEntities = this.addHtmlEntities.bind(this);
        }
      }
      
      export default Recipe;
      

      Like with your Recipes component, in the constructor, you initialized a state object that holds the state of a recipe. You also bound an addHtmlEntities method to this so it can be accessible within the component. The addHtmlEntities method will be used to replace character entities with HTML entities in the component.

      In order to find a particular recipe, your application needs the id of the recipe. This means your Recipe component expects an id param. You can access this via the props passed into the component.

      Next, add a componentDidMount method where you will access the id param from the match key of the props object. Once you get the id, you will then make an HTTP request to fetch the recipe. Add the following highlighted lines to your file:

      ~/rails_react_recipe/app/javascript/components/Recipe.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      
      class Recipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = { recipe: { ingredients: "" } };
      
          this.addHtmlEntities = this.addHtmlEntities.bind(this);
        }
      
        componentDidMount() {
          const {
            match: {
              params: { id }
            }
          } = this.props;
      
          const url = `/api/v1/show/${id}`;
      
          fetch(url)
            .then(response => {
              if (response.ok) {
                return response.json();
              }
              throw new Error("Network response was not ok.");
            })
            .then(response => this.setState({ recipe: response }))
            .catch(() => this.props.history.push("/recipes"));
        }
      
      }
      
      export default Recipe;
      

      In the componentDidMount method, using object destructuring, you get the id param from the props object, then using the Fetch API, you make a HTTP request to fetch the recipe that owns the id and save it to the component state using the setState method. If the recipe does not exist, the app redirects the user to the recipes page.

      Now add the addHtmlEntities method, which takes a string and replaces all escaped opening and closing brackets with their HTML entities. This will help us convert whatever escaped character was saved in your recipe instruction:

      ~/rails_react_recipe/app/javascript/components/Recipe.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      
      class Recipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = { recipe: { ingredients: "" } };
      
          this.addHtmlEntities = this.addHtmlEntities.bind(this);
        }
      
        componentDidMount() {
          const {
            match: {
              params: { id }
            }
          } = this.props;
      
          const url = `/api/v1/show/${id}`;
      
          fetch(url)
            .then(response => {
              if (response.ok) {
                return response.json();
              }
              throw new Error("Network response was not ok.");
            })
            .then(response => this.setState({ recipe: response }))
            .catch(() => this.props.history.push("/recipes"));
        }
      
        addHtmlEntities(str) {
          return String(str)
            .replace(/&lt;/g, "<")
            .replace(/&gt;/g, ">");
        }
      }
      
      export default Recipe;
      

      Finally, add a render method that gets the recipe from the state and renders it on the page. To do this, add the following highlighted lines:

      ~/rails_react_recipe/app/javascript/components/Recipe.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      
      class Recipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = { recipe: { ingredients: "" } };
      
          this.addHtmlEntities = this.addHtmlEntities.bind(this);
        }
      
        componentDidMount() {
          const {
            match: {
              params: { id }
            }
          } = this.props;
      
          const url = `/api/v1/show/${id}`;
      
          fetch(url)
            .then(response => {
              if (response.ok) {
                return response.json();
              }
              throw new Error("Network response was not ok.");
            })
            .then(response => this.setState({ recipe: response }))
            .catch(() => this.props.history.push("/recipes"));
        }
      
        addHtmlEntities(str) {
          return String(str)
            .replace(/&lt;/g, "<")
            .replace(/&gt;/g, ">");
        }
      
        render() {
          const { recipe } = this.state;
          let ingredientList = "No ingredients available";
      
          if (recipe.ingredients.length > 0) {
            ingredientList = recipe.ingredients
              .split(",")
              .map((ingredient, index) => (
                <li key={index} className="list-group-item">
                  {ingredient}
                </li>
              ));
          }
          const recipeInstruction = this.addHtmlEntities(recipe.instruction);
      
          return (
            <div className="">
              <div className="hero position-relative d-flex align-items-center justify-content-center">
                <img
                  src={recipe.image}
                  alt={`${recipe.name} image`}
                  className="img-fluid position-absolute"
                />
                <div className="overlay bg-dark position-absolute" />
                <h1 className="display-4 position-relative text-white">
                  {recipe.name}
                </h1>
              </div>
              <div className="container py-5">
                <div className="row">
                  <div className="col-sm-12 col-lg-3">
                    <ul className="list-group">
                      <h5 className="mb-2">Ingredients</h5>
                      {ingredientList}
                    </ul>
                  </div>
                  <div className="col-sm-12 col-lg-7">
                    <h5 className="mb-2">Preparation Instructions</h5>
                    <div
                      dangerouslySetInnerHTML={{
                        __html: `${recipeInstruction}`
                      }}
                    />
                  </div>
                  <div className="col-sm-12 col-lg-2">
                    <button type="button" className="btn btn-danger">
                      Delete Recipe
                    </button>
                  </div>
                </div>
                <Link to="/recipes" className="btn btn-link">
                  Back to recipes
                </Link>
              </div>
            </div>
          );
        }
      
      }
      
      export default Recipe;
      

      In this render method, you split your comma separated ingredients into an array and mapped over it, creating a list of ingredients. If there are no ingredients, the app displays a message that says No ingredients available. It also displays the recipe image as a hero image, adds a delete recipe button next to the recipe instruction, and adds a button that links back to the recipes page.

      Save and exit the file.

      To view the Recipe component on a page, add it to your routes file. Open your route file to edit:

      • nano app/javascript/routes/Index.jsx

      Now, add the following highlighted lines to the file:

      ~/rails_react_recipe/app/javascript/routes/Index.jsx

      import React from "react";
      import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
      import Home from "../components/Home";
      import Recipes from "../components/Recipes";
      import Recipe from "../components/Recipe";
      
      export default (
        <Router>
          <Switch>
            <Route path="/" exact component={Home} />
            <Route path="/recipes" exact component={Recipes} />
            <Route path="/recipe/:id" exact component={Recipe} />
          </Switch>
        </Router>
      );
      

      In this route file, you imported your Recipe component and added a route for it. Its route has an :id param that will be replaced by the id of the recipe you want to view.

      Use the rails s command to start your server again, then visit http://localhost:3000 in your browser. Click the View Recipes button to navigate to the recipes page. On the recipes page, view any recipe by clicking its View Recipe button. You will be greeted with a page populated with the data from your database:

      Single Recipe Page

      In this section, you added nine recipes to your database and created components to view these recipes, both individually and as a collection. In the next section, you will add a component to create recipes.

      Step 8 — Creating Recipes

      The next step to having a usable food recipe application is the ability to create new recipes. In this step, you will create a component for creating recipes. This component will contain a form for collecting the required recipe details from the user and will make a request to the create action in the Recipe controller to save the recipe data.

      Create a NewRecipe.jsx file in the app/javascript/components directory:

      • nano app/javascript/components/NewRecipe.jsx

      In the new file, import the React and Link modules you have used so far in other components:

      ~/rails_react_recipe/app/javascript/components/NewRecipe.jsx

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

      Next create a NewRecipe class that extends React.Component class. Add the following highlighted code to create a React component that extends react.Component:

      ~/rails_react_recipe/app/javascript/components/NewRecipe.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      
      class NewRecipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            name: "",
            ingredients: "",
            instruction: ""
          };
      
          this.onChange = this.onChange.bind(this);
          this.onSubmit = this.onSubmit.bind(this);
          this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
        }
      }
      
      export default NewRecipe;
      

      In the NewRecipe component’s constructor, you initialized your state object with empty name, ingredients, and instruction fields. These are the fields you need to create a valid recipe. You also have three methods; onChange, onSubmit, and stripHtmlEntities, which you bound to this. These methods will handle updating the state, form submissions, and converting special characters (like <) into their escaped/encoded values (like &lt;), respectively.

      Next, create the stripHtmlEntities method itself by adding the highlighted lines to the NewRecipe component:

      ~/rails_react_recipe/app/javascript/components/NewRecipe.jsx

      class NewRecipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            name: "",
            ingredients: "",
            instruction: ""
          };
      
          this.onChange = this.onChange.bind(this);
          this.onSubmit = this.onSubmit.bind(this);
          this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
        }
      
        stripHtmlEntities(str) {
          return String(str)
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");
        }
      
      }
      
      export default NewRecipe;
      

      In the stripHtmlEntities method, you’re replacing the < and > characters with their escaped value. This way you’re not storing raw HTML in your database.

      Next add the onChange and onSubmit methods to the NewRecipe component to handle editing and submission of the form:

      ~/rails_react_recipe/app/javascript/components/NewRecipe.jsx

      class NewRecipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            name: "",
            ingredients: "",
            instruction: ""
          };
      
          this.onChange = this.onChange.bind(this);
          this.onSubmit = this.onSubmit.bind(this);
          this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
        }
      
        stripHtmlEntities(str) {
          return String(str)
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");
        }
      
        onChange(event) {
          this.setState({ [event.target.name]: event.target.value });
        }
      
        onSubmit(event) {
          event.preventDefault();
          const url = "/api/v1/recipes/create";
          const { name, ingredients, instruction } = this.state;
      
          if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
            return;
      
          const body = {
            name,
            ingredients,
            instruction: instruction.replace(/n/g, "<br> <br>")
          };
      
          const token = document.querySelector('meta[name="csrf-token"]').content;
          fetch(url, {
            method: "POST",
            headers: {
              "X-CSRF-Token": token,
              "Content-Type": "application/json"
            },
            body: JSON.stringify(body)
          })
            .then(response => {
              if (response.ok) {
                return response.json();
              }
              throw new Error("Network response was not ok.");
            })
            .then(response => this.props.history.push(`/recipe/${response.id}`))
            .catch(error => console.log(error.message));
        }
      
      }
      
      export default NewRecipe;
      

      In the onChange method, you used the ES6 computed property names to set the value of every user input to its corresponding key in your state. In the onSubmit method, you checked that none of the required inputs are empty. You then build an object that contains the parameters required by the recipe controller to create a new recipe. Using regular expression, you replace every new line character in the instruction with a break tag, so you can retain the text format entered by the user.

      To protect against Cross-Site Request Forgery (CSRF) attacks, Rails attaches a CSRF security token to the HTML document. This token is required whenever a non-GET request is made. With the token constant in the preceding code, your application verifies the token on the server and throws an exception if the security token doesn't match what is expected. In the onSubmit method, the application retrieves the CSRF token embedded in your HTML document by Rails and makes a HTTP request with a JSON string. If the recipe is successfully created, the application redirects the user to the recipe page where they can view their newly created recipe.

      Lastly, add a render method that renders a form for the user to enter the details for the recipe the user wishes to create:

      ~/rails_react_recipe/app/javascript/components/NewRecipe.jsx

      class NewRecipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = {
            name: "",
            ingredients: "",
            instruction: ""
          };
      
          this.onChange = this.onChange.bind(this);
          this.onSubmit = this.onSubmit.bind(this);
          this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
        }
      
        stripHtmlEntities(str) {
          return String(str)
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");
        }
      
        onChange(event) {
          this.setState({ [event.target.name]: event.target.value });
        }
      
        onSubmit(event) {
          event.preventDefault();
          const url = "/api/v1/recipes/create";
          const { name, ingredients, instruction } = this.state;
      
          if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
            return;
      
          const body = {
            name,
            ingredients,
            instruction: instruction.replace(/n/g, "<br> <br>")
          };
      
          const token = document.querySelector('meta[name="csrf-token"]').content;
          fetch(url, {
            method: "POST",
            headers: {
              "X-CSRF-Token": token,
              "Content-Type": "application/json"
            },
            body: JSON.stringify(body)
          })
            .then(response => {
              if (response.ok) {
                return response.json();
              }
              throw new Error("Network response was not ok.");
            })
            .then(response => this.props.history.push(`/recipe/${response.id}`))
            .catch(error => console.log(error.message));
        }
      
        render() {
          return (
            <div className="container mt-5">
              <div className="row">
                <div className="col-sm-12 col-lg-6 offset-lg-3">
                  <h1 className="font-weight-normal mb-5">
                    Add a new recipe to our awesome recipe collection.
                  </h1>
                  <form onSubmit={this.onSubmit}>
                    <div className="form-group">
                      <label htmlFor="recipeName">Recipe name</label>
                      <input
                        type="text"
                        name="name"
                        id="recipeName"
                        className="form-control"
                        required
                        onChange={this.onChange}
                      />
                    </div>
                    <div className="form-group">
                      <label htmlFor="recipeIngredients">Ingredients</label>
                      <input
                        type="text"
                        name="ingredients"
                        id="recipeIngredients"
                        className="form-control"
                        required
                        onChange={this.onChange}
                      />
                      <small id="ingredientsHelp" className="form-text text-muted">
                        Separate each ingredient with a comma.
                      </small>
                    </div>
                    <label htmlFor="instruction">Preparation Instructions</label>
                    <textarea
                      className="form-control"
                      id="instruction"
                      name="instruction"
                      rows="5"
                      required
                      onChange={this.onChange}
                    />
                    <button type="submit" className="btn custom-button mt-3">
                      Create Recipe
                    </button>
                    <Link to="/recipes" className="btn btn-link mt-3">
                      Back to recipes
                    </Link>
                  </form>
                </div>
              </div>
            </div>
          );
        }
      
      }
      
      export default NewRecipe;
      

      In the render method, you have a form that contains three input fields; one for the recipeName, recipeIngredients, and instruction. Each input field has an onChange event handler that calls the onChange method. Also, there's an onSubmit event handler on the submit button that calls the onSubmit method which then submits the form data.

      Save and exit the file.

      To access this component in the browser, update your route file with its route:

      • nano app/javascript/routes/Index.jsx

      Update your route file to include these highlighted lines:

      ~/rails_react_recipe/app/javascript/routes/Index.jsx

      import React from "react";
      import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
      import Home from "../components/Home";
      import Recipes from "../components/Recipes";
      import Recipe from "../components/Recipe";
      import NewRecipe from "../components/NewRecipe";
      
      export default (
        <Router>
          <Switch>
            <Route path="/" exact component={Home} />
            <Route path="/recipes" exact component={Recipes} />
            <Route path="/recipe/:id" exact component={Recipe} />
            <Route path="/recipe" exact component={NewRecipe} />
          </Switch>
        </Router>
      );
      

      With the route in place, save and exit your file. Restart your development server and visit http://localhost:3000 in your browser. Navigate to the recipes page and click the Create New Recipe button. You will find a page with a form to add recipes to your database:

      Create Recipe Page

      Enter the required recipe details and click the Create Recipe button; you will see the newly created recipe on the page.

      In this step, you brought your food recipe application to life by adding the ability to create recipes. In the next step, you’ll add the functionality to delete recipes.

      Step 9 — Deleting Recipes

      In this section, you will modify your Recipe component to be able to delete recipes.

      When you click the delete button on the recipe page, the application will send a request to delete a recipe from the database. To do this, open up your Recipe.jsx file:

      • nano app/javascript/components/Recipe.jsx

      In the constructor of the Recipe component, bind this to the deleteRecipe method:

      ~/rails_react_recipe/app/javascript/components/Recipe.jsx

      class Recipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = { recipe: { ingredients: "" } };
          this.addHtmlEntities = this.addHtmlEntities.bind(this);
          this.deleteRecipe = this.deleteRecipe.bind(this);
        }
      ...
      

      Now add a deleteRecipe method to the Recipe component:

      ~/rails_react_recipe/app/javascript/components/Recipe.jsx

      class Recipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = { recipe: { ingredients: "" } };
      
          this.addHtmlEntities = this.addHtmlEntities.bind(this);
          this.deleteRecipe = this.deleteRecipe.bind(this);
        }
      
        componentDidMount() {
          const {
            match: {
              params: { id }
            }
          } = this.props;
          const url = `/api/v1/show/${id}`;
          fetch(url)
            .then(response => {
              if (response.ok) {
                return response.json();
              }
              throw new Error("Network response was not ok.");
            })
            .then(response => this.setState({ recipe: response }))
            .catch(() => this.props.history.push("/recipes"));
        }
      
        addHtmlEntities(str) {
          return String(str)
            .replace(/&lt;/g, "<")
            .replace(/&gt;/g, ">");
        }
      
        deleteRecipe() {
          const {
            match: {
              params: { id }
            }
          } = this.props;
          const url = `/api/v1/destroy/${id}`;
          const token = document.querySelector('meta[name="csrf-token"]').content;
      
          fetch(url, {
            method: "DELETE",
            headers: {
              "X-CSRF-Token": token,
              "Content-Type": "application/json"
            }
          })
            .then(response => {
              if (response.ok) {
                return response.json();
              }
              throw new Error("Network response was not ok.");
            })
            .then(() => this.props.history.push("/recipes"))
            .catch(error => console.log(error.message));
        }
      
        render() {
          const { recipe } = this.state;
          let ingredientList = "No ingredients available";
      ... 
      

      In the deleteRecipe method, you get the id of the recipe to be deleted, then build your url and grab the CSRF token. Next, you make a DELETE request to the Recipes controller to delete the recipe. If the recipe is successfully deleted, the application redirects the user to the recipes page.

      To run the code in the deleteRecipe method whenever the delete button is clicked, pass it as the click event handler to the button. Add an onClick event to the delete button in the render method:

      ~/rails_react_recipe/app/javascript/components/Recipe.jsx

      ...
      return (
        <div className="">
          <div className="hero position-relative d-flex align-items-center justify-content-center">
            <img
              src={recipe.image}
              alt={`${recipe.name} image`}
              className="img-fluid position-absolute"
            />
            <div className="overlay bg-dark position-absolute" />
            <h1 className="display-4 position-relative text-white">
              {recipe.name}
            </h1>
          </div>
          <div className="container py-5">
            <div className="row">
              <div className="col-sm-12 col-lg-3">
                <ul className="list-group">
                  <h5 className="mb-2">Ingredients</h5>
                  {ingredientList}
                </ul>
              </div>
              <div className="col-sm-12 col-lg-7">
                <h5 className="mb-2">Preparation Instructions</h5>
                <div
                  dangerouslySetInnerHTML={{
                    __html: `${recipeInstruction}`
                  }}
                />
              </div>
              <div className="col-sm-12 col-lg-2">
                <button type="button" className="btn btn-danger" onClick={this.deleteRecipe}>
                  Delete Recipe
                </button>
              </div>
            </div>
            <Link to="/recipes" className="btn btn-link">
              Back to recipes
            </Link>
          </div>
        </div>
      );
      ...
      

      At this point in the tutorial, your complete Recipe.jsx file will look like this:

      ~/rails_react_recipe/app/javascript/components/Recipe.jsx

      import React from "react";
      import { Link } from "react-router-dom";
      
      class Recipe extends React.Component {
        constructor(props) {
          super(props);
          this.state = { recipe: { ingredients: "" } };
      
          this.addHtmlEntities = this.addHtmlEntities.bind(this);
          this.deleteRecipe = this.deleteRecipe.bind(this);
        }
      
        addHtmlEntities(str) {
          return String(str)
            .replace(/&lt;/g, "<")
            .replace(/&gt;/g, ">");
        }
      
        componentDidMount() {
          const {
            match: {
              params: { id }
            }
          } = this.props;
          const url = `/api/v1/show/${id}`;
          fetch(url)
            .then(response => {
              if (response.ok) {
                return response.json();
              }
              throw new Error("Network response was not ok.");
            })
            .then(response => this.setState({ recipe: response }))
            .catch(() => this.props.history.push("/recipes"));
        }
      
        deleteRecipe() {
          const {
            match: {
              params: { id }
            }
          } = this.props;
          const url = `/api/v1/destroy/${id}`;
          const token = document.querySelector('meta[name="csrf-token"]').content;
          fetch(url, {
            method: "DELETE",
            headers: {
              "X-CSRF-Token": token,
              "Content-Type": "application/json"
            }
          })
            .then(response => {
              if (response.ok) {
                return response.json();
              }
              throw new Error("Network response was not ok.");
            })
            .then(() => this.props.history.push("/recipes"))
            .catch(error => console.log(error.message));
        }
      
        render() {
          const { recipe } = this.state;
          let ingredientList = "No ingredients available";
          if (recipe.ingredients.length > 0) {
            ingredientList = recipe.ingredients
              .split(",")
              .map((ingredient, index) => (
                <li key={index} className="list-group-item">
                  {ingredient}
                </li>
              ));
          }
      
          const recipeInstruction = this.addHtmlEntities(recipe.instruction);
      
          return (
            <div className="">
              <div className="hero position-relative d-flex align-items-center justify-content-center">
                <img
                  src={recipe.image}
                  alt={`${recipe.name} image`}
                  className="img-fluid position-absolute"
                />
                <div className="overlay bg-dark position-absolute" />
                <h1 className="display-4 position-relative text-white">
                  {recipe.name}
                </h1>
              </div>
              <div className="container py-5">
                <div className="row">
                  <div className="col-sm-12 col-lg-3">
                    <ul className="list-group">
                      <h5 className="mb-2">Ingredients</h5>
                      {ingredientList}
                    </ul>
                  </div>
                  <div className="col-sm-12 col-lg-7">
                    <h5 className="mb-2">Preparation Instructions</h5>
                    <div
                      dangerouslySetInnerHTML={{
                        __html: `${recipeInstruction}`
                      }}
                    />
                  </div>
                  <div className="col-sm-12 col-lg-2">
                    <button type="button" className="btn btn-danger" onClick={this.deleteRecipe}>
                      Delete Recipe
                    </button>
                  </div>
                </div>
                <Link to="/recipes" className="btn btn-link">
                  Back to recipes
                </Link>
              </div>
            </div>
          );
        }
      }
      
      export default Recipe;
      

      Save and exit the file.

      Restart the application server and navigate to the homepage. Click the View Recipes button to view all existing recipes, view any individual recipe, and click the Delete Recipe button on the page to delete the article. You will be redirected to the recipes page, and the deleted recipe will no longer exists.

      With the delete button working, you now have a fully functional recipe application!

      Conclusion

      In this tutorial, you created a food recipe application with Ruby on Rails and a React frontend, using PostgreSQL as your database and Bootstrap for styling. If you'd like to run through more Ruby on Rails content, take a look at our Securing Communications in a Three-tier Rails Application Using SSH Tunnels tutorial, or head to our How To Code in Ruby series to refresh your Ruby skills. To dive deeper into React, try out our How To Display Data from the DigitalOcean API with React article.



      Source link