Home Servers Products Newletters Sponsers Donate

Contents









Servers

To run a website or a webapp, you will need a server to run it. There are a number of (low cost or free) hosting services you can employ for this. I am currently using Google Compute Engine instances. Google is developer friendly and offers a f1-micro instance for free. The f1-micro is adequate for running a node-js server and persistantly storing a modest amount of application data. The f1-micro runs a Debian Linux operating system. If you are not a Linux Guru, don't worry about it, I will show you what you need to know.

I have also used Heroku, but Heroku does not offer persistent data storage and I had to resort to storing data elsewhere.

You may postpone signing up for a Google account until you have your application tested on your local machine, which may be a windows, macintosh, or linux based machine. I do my development on a HP Windows 10 laptop. My son is using a $200 HP Stream 14 laptop running Windows 10 which is entirely adequate.

I recommend that you start by installing nodejs on your local machine and running the demo they provide.

The following is a walk through of installing nodejs on Windows 10.

Go to https://nodejs.org/en/ and download the latest Long Term Support version for your operating system. For Windows, this is currently 8.9.0 LTS. Run this MSI file, accepting the license agreement and defaults. When the installation is complete open a new command window and type: "node -v" followed by <enter>, the response should be the version number. Then type "npm -v" <enter>, it should also respond with its version number. This process may be repeated anytime to upgrade to a more recent version.

The two commands, node and npm, that have been added to your windows path are the javascript engine and the package manager, respectively. You will use node to run your servers. You will use npm to install node packages.

Node.js uses the V8 javascript engine that is at the heart of the Chrome browser, so there is some advantage to using Chrome as your browser; you will be using the same version of javascript both client-side and server-side. The Chrome browser has good developer tools. Of course, it is a good idea to test with various browsers, since browsers have a terrible history of not being entirely compatible with each other. Chrome's conformance to "standards" has been particularly good.

The Simplest Server

The simplest server is listed at https://nodejs.org/en/about/:

const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

Read the description of the code on the site.

You may download this code at this link simplest_server.js.

Create a new directory "servers", copy the downloaded "simplest_server.js" file to the new directory. Open a new command window, cd to the new servers directory, then enter the command "node simplest_server.js". It should look something like this:

C:\Users\joe\servers>node simplest_server.js
Server running at http://127.0.0.1:3000/

Now open a browser window and type "localhost:3000" into the address bar. It should display "Hello World".

The server may be stopped by keying control-C or closing the command window.

This "Hello World" server always sends the same message to any request to 127.0.0.1:3000, where 127.0.0.1 is the default address of your localhost, and 3000 is the port number.

This is not a particularly useful server, but it does have several points of interest:

This server is a little too simple to be practially useful. It would not be convenient to include all the html for an application in the code for the server. The servers that I am about to present are more practical and provide more power.

The Static Server

The practical servers that I present here are powered by the Express module. The express module provides for serving static assets such as html pages, images, movies, sounds, and interfacing to the resources of the server through routing.

A node application server is typically a directory containing subdirectories and files with a "package.json" in its root directory which ties the assembly together and gives it identity. The npm command stores package dependency information in the package.json file and enables rebuilding with package with the proper versions of packages and enables the movement of a package from server to server.

To build the Static Server:

The source for "static_server.js" follows:

var express = require('express');
const http = require('http');
var app = express();
//app.use(express.static(__dirname + '/public'));
app.use(express.static(__dirname));

const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer(app);
server.listen(port, hostname, () => {
   console.log(`Server running at http://${hostname}:${port}/`);
});

As you can see this is a small program. The first line "requires" "express". The second line "requires" http. The third line creates an instance of express and calls it "app". The forth line is a comment. The fifth line instructs app to "use" the middleware module "static" that is built into express and names the directory from which static components are to be retrieved. The comment above it is similar and may be used instead if you want to store your static components in a "public" subdirectory of the directory in which the server resides. Lines 7 and 8 specify the host and port to listen to. Line 9 creates a server object passing it the app object. Line 10 starts the server. The arrow function passed to listen announces that the server is running.

If you are new to html, css, and javascript, you may wish to put server development aside at this point and focus on learning those things using static_server as your playground. I recommend w3school's free tutorials as an efficient way to learn. Return here after your confidence has grown.

Hosting Your Site

It is probably easier to signup for Google Compute Engine if you already have a Google Account. If you do not have a gmail account, I suggest that you get one and use it for this purpose.

When you have developed a site on your local machine that you now want to show to the world, it is time to secure a hosting account. I use and recommend Google Compute Engine. Visit https://cloud.google.com/compute/. Click "Sign Up for Free Trial" or "Try the Quickstart". Accept the "terms of service". Create a payment profile (you will not be billed for the free trial). Follow the directions for the quick start. When you are presented a "Create Instance" link, click it. In the "VM Instances" panel click the "Create" button. Fill in the form. "Name" can be left as "instance-1", for zone I chose "us-east1-b" (Charlotte, USA), "Machine type "micro", leave "New 10 GB standard persistent disk", scroll down, check "allow HTTP traffic" and "Allow HTTPS traffic", click "Create". Spins for a minute or so. Scroll right and note the external IP address that has been assigned. Then click "SSH". Then select "view gcloud command". Copy and save the gcloud command. (looks something like 'gcloud compute --project "lustrous-oasis-186320" ssh --zone "us-east1-b" "instance-1"'). Hit escape key.

In another browser window, go to "https://cloud.google.com/sdk/downloads", and download the Google Cloud SDK. Run the "interactive installer". You will not need "App Engine extensions". The installer check boxes "bundled Python" and "Cloud Tools for PowerShell" should be checked.

When the download finishes, you will have the opportunity to configure. Use your gmail email account. Configure your Google Compute engine defaults for zone (us-east1-b) and region(us-east-1).

At this point there will be several browser windows open and it is easy to get confused. Close all browser windows and open a fresh one at console.cloud.google.com.

At this point there should be one project. You may change the project name to "Node Server" and click "save

You should now have a Google Cloud Instance defined and running. Open a new cmd window and issue the the gcloud command you copied and saved above. Another command window should open after several seconds that looks a bit like this:

Using username "joe".
Authenticating with public key "JOES-LAPTOP\joe@joes-laptop"
Linux instance-1 4.9.0-4-amd64 #1 SMP Debian 4.9.51-1 (2017-09-28) x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law.

Now that you have an instance running, you can upload you server and other files from you local machine to the instance. This may be done most conveniently on windows with WINSCP.

Install and configure WINSCP

Open a browser window and go to https://winscp.net/eng/download.php and follow the directions. Select the "Commander" interface.

When you have installed the Google Cloud SDK, a key pair was generated so that the gcloud command could ssh (secure shell) to your new instance.

WINSCP must have the private key to connect a session to the instance.

While defining your session, select "SCP" as the File Protocol, use the external ip address for the Host Name, use your windows user name for User Name, then click "Advanced" dropdown, select "Advanced", click "Authentication". Uncheck any boxes down to "Private Key File". Enter "C:\Users\<your windows user name>\.ssh\google_compute_engine.ppk" into the "Private Key File" field. Then click "save". Then click "login". After login, the left hand panel is the file system on your local machine and the right hand panel is the file system on the host. You may drag files and directories from the left to the right to upload files.

Since port 80 is the default port number for http, edit your static_server.js file and change port 3000 to 80. While you are there change the host from 127.0.0.1 to 0.0.0.0 (So it will listen on all interfaces). After you save this and restart your server, you will be able to access hello and goodbye with localhost, rather than localhost:3000.

Using WINSCP, drag the servers directory from you local machine in the left hand panel to your new Google Compute Instance in the right hand panel.

Open a new cmd window and issue the the gcloud command you copied and saved above.

In the gcloud window that opens, install nodejs with the following commands "curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -", "sudo apt-get install -y nodejs" (per https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions). Verify the installation with "node -v" and "npm -v".

In the gcloud window key in the following commands: "cd servers","cd static_server","nohup sudo node static_server.js > log.txt&" (then hit <enter>), "tail -f log.txt'

This should look like this:

joe@instance-1:~$ cd servers
joe@instance-1:~/servers$ cd static_server
joe@instance-1:~/servers/static_server$ nohup sudo node static_server.js > log.txt&
[1] 8153
joe@instance-1:~/servers/static_server$ nohup: ignoring input and redirecting stderr to stdout
joe@instance-1:~/servers/static_server$ tail -f log.txt
Server running at http://0.0.0.0:80/

What you have just done is start your server on you new host as a daemon which will continue to run even if you close the gcloud window, and started a display following the log. If you wish to do something else, you may interrupt the display of the log with control-C. Hint: to reissue a command, use the up arrow until the command you want is showing, edit it if necessary using the arrow keys and backspace keys, and hit enter.

To stop the server: type the command "ps aux|grep node". A list of three processes will be shown. Kill the process running "sudo node static_server.js" with the "sudo kill" command. Looks like this:

joe@instance-1:~$ ps aux|grep node
root 8153 0.0 0.5 44832 3536 ? S Nov20 0:00 sudo node static_srver.js
root 8154 0.0 5.8 917368 35680 ? Sl Nov20 0:00 node static_server.js
joe 16324 0.0 0.1 12784 932 pts/0 S+ 17:19 0:00 grep node
joe@instance-1:~$ sudo kill 8153
joe@instance-1:~$ ps aux|grep node
joe 16334 0.0 0.1 12784 936 pts/0 S+ 17:20 0:00 grep node

You will have to restart it later as above. You must stop/restart the server every time you change it.

An alternative way of opening a command window on your instance is to use the WINSCP Commands>PuTTY selection, rather than the gcloud command.

Domain names and DNS

Fewer people remember numeric ip addresses than remember telephone numbers. People are much better at remembering names. Almost no one remembers that google.com's address is 2607:f8b0:4008:805::200e.

The Domain Name Service (DNS), is the telephone book of the internet. To access your site by name, you must obtain a domain name, then tell the DSN system about it. Domain names must be unique and "owned". This is what domain name registration is all about. First you must pick a domain name that no one else owns, then you must register it with a registrar. I use namecheap.com as my registrar. The name says it all. They have pretty good documentation online and a free DNS editor (freedns). They also have a subsidiary, SSLS, which issues certificates for HTTPS.

Picking a domain name is a matter of picking a Top Level Domain (TLD, .com, .org, ...) and a subdomain of that TLD for example "example.com" that no one else owns. The rules for the subdomain name are that is may be at most 63 characters. The allowed characters are ASCII letters, numbers, and hyphen (-). Perhaps the easiest way to determine if someone else owns the name you desire is to attempt registration. If you succeed, then name is yours.

Subdomains of your subdomain are possible and frequent, for example the "www" in "www.example.com". However you do not register them.

If you use Namecheap to register, do not sign up for hosting, you are hosting on Google Compute Engine. Once you have registered your domain, it may take a while before you can access it. You must use the freedns editor to create an "a" record to associate the name with the external ip address. One thing I found a bit confusing in the Name Cheap documentation was that the freedns editor is the same as their BasicDNS. DNS propogation may take a while, but if it takes more than a day or two, engage Namecheap support in a chat session, they can quickly tell you what is wrong and how to fix it.

If you are in a hurry, you may place your domain in your hosts file (C:\Windows\System32\drivers\etc\hosts) and access it immediately. You will have to open your editor as administrator to edit the hosts file. (Enter the name of your editor in the cortana search bar, right click the editor name, select "run as administrator"). Lines that begin with "#" are comments. Active lines consist of an ip address followed by a fully qualified domain name. For example " 35.190.180.148 javascriptrules.org". The ip address 127.0.0.1 is localhost.

After DNS propagation is complete you can switch between running tests on localhost and production by having an entry " 127.0.0.1 your domain". When you want to run production make this entry a comment by replacing the leading space with '#'. Before DNS propagation is complete, you can access the new site by name by replacing 127.0.0.1 with the external ip address.

Persistant File Storage - File Server

If your application requires that data be stored on the host, then the server presented below will help. Nodejs has a builtin module "fs", which provides file system functionality: read, write, browse. I will demonstrate here reading, writing, and appending to a file. For browsing, I recommend the server in the free preview of my book.

The File Server is very similar to the static server above. It adds access to the file server as routings for http "GET" and "PUT" calls.

To build the File Server:

You should see an input field labeled "File Path:", prepopulated with "data/hello.txt", with buttons "Read", and "clear" next to it. Below these is a text area containing the contents of the "data/hello.txt" file. The text area may be used to enter/edit text. The "File Path" input field, is used to enter the path including filename relative to the directory containing the server. "Read", "Write", and "append" invoke the basic file operations. "clear" clears the input area.

I encourage you to play with this a bit.

The source for "file_server.js" follows:

var express = require('express');
const http = require('http');
var app = express();
var fs = require("fs");
//app.use(express.static(__dirname + '/public'));
app.use(express.static(__dirname));

app.put('/xhr-write', function(req, res){
  var fileName=req.query.filename;
  var mydata=req.query.mydata;
  mydata = mydata.replace(/[\r]/gm,'');
  fs.writeFile (fileName, mydata,(err) => {
    if (err) {
      console.log('err='+err);
      if (err.code === 'ENOENT'){
       res.send('error='+err);
      } else {
       throw err; //server dies
      }
    }
    res.send("ok");
  });
});

app.put('/xhr-append', function(req, res){
  var fileName=req.query.filename;
  var mydata=req.query.mydata;
  mydata = mydata.replace(/[\r]/gm,'');
  fs.appendFile (fileName, mydata,(err) => {
    if (err) {
      console.log('err='+err);
      if (err.code === 'ENOENT'){
       res.send('error='+err)
      } else {
       throw err; //server dies
      }
    }
    res.send("ok");
  });
});

app.get('/xhr-read', function(req, res){
  fileName=req.query.filename;
  fs.readFile(fileName, (err, data) => {
    if (err) {
      console.log('err='+err);
      if (err.code === 'ENOENT'){
       res.send('error='+err)
      } else {
       throw err; // server dies
      }
    } else {
      mydata = data.toString();
      res.send(mydata);
    }
  });
});

const hostname = '0.0.0.0';
const port = 80;

const server = http.createServer(app);

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

This server is the same as "static_server.js", with the addition of Express "routes", which begin with "app.put" and "app.get". The "put" and "get" are HTTP "verbs". The first parameter of the "app.put" and "app.get" functions are the urls which are to be processed by the second parameter which is a function which is called with the (req, res) parameter list. Within these functions are calls to functions within the "fs" package which perform the I/O operations asynchronously. The "xhr-" prefix is a signal that the entries are to support "XMLHttpRequest" or AJAX technology: see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest.

I illustrate the use of this server with "index.html". "index.html" follows:

001 <!DOCTYPE html>
002 <html>
003 <head>
004
005 <!--The following is necessary for some browsers-->
006 <script src="url_search_params.js"></script>
007 <script>
008
009 window.onerror = function (errorMsg, url, lineNumber, column, errorObj) {
010    alert('Error: ' + errorMsg + ' Script: ' + url + ' Line: ' + lineNumber
011    + ' Column: ' + column + ' StackTrace: ' + errorObj);
012 }
013
014 function f_clear(){
015   document.getElementById("textarea").value = '';
016 }
017
018 function f_append(){
019   var myfile=document.getElementById("file_name").value;
020   var mydata=document.getElementById("textarea").value;
021   var params="filename="+myfile+"&mydata="+encodeURIComponent(mydata);
022   var url="xhr-append?"+params;
023   var http=new XMLHttpRequest();
024   http.open("PUT", url, true);
025   http.onreadystatechange = function()
026   {
027    if(http.readyState == 4 && http.status == 200)
028    {
029    responseText = http.responseText;
030    responseText = responseText.replace(/\\/g,'/');
031    var urlParams = new URLSearchParams (responseText);
032    if (urlParams.has('error')) {
033    document.getElementById("response").innerHTML = responseText;
034    } else {
035    f_load();
036    }
037    }
038   }
039   http.send(null);
040 }
041
042 function f_write(){
043   var myfile=document.getElementById("file_name").value;
044   var mydata=document.getElementById("textarea").value;
045   var params="filename="+myfile+"&mydata="+encodeURIComponent(mydata);
046   var url="xhr-write?"+params;
047   var http=new XMLHttpRequest();
048   http.open("PUT", url, true);
049   http.onreadystatechange = function()
050   {
051    if(http.readyState == 4)
052    {
053    responseText = http.responseText;
054    responseText = responseText.replace(/\\/g,'/');
055    var urlParams = new URLSearchParams (responseText);
056    if (urlParams.has('error')) {
057    document.getElementById("response").innerHTML = responseText;
058    } else {
059    f_load();
060    }
061    }
062   }
063   http.send(null);
064 }
065
066 function f_read_file () {
067   document.getElementById("response").innerHTML = '';
068   var selfile = document.getElementById("file_name").value;
069   var url = "xhr-read";
070   var params = "filename=" + selfile;
071   var http=new XMLHttpRequest();
072   http.open("GET", url+"?"+params, true);
073   http.onreadystatechange = function()
074   {
075    if(http.readyState == 4 && http.status == 200)
076    {
077    responseText = http.responseText;
078    var urlParams = new URLSearchParams (responseText);
079    if (urlParams.has('error')) {
080    document.getElementById("response").innerHTML = responseText;
081    document.getElementById("textarea").value = '';
082    } else {
083    document.getElementById("textarea").value = responseText;
084    }
085    }
086   }
087   http.send(null);
088 }
089
090 function f_load () {
091   document.getElementById("response").innerHTML = '';
092   if (window.localStorage["filename"] !== undefined) {
093    document.getElementById("file_name").value =window.localStorage["filename"];
094   } else {
095    window.localStorage["filename"] = document.getElementById("file_name").value;
096   }
097   f_read_file();
098 }
099
100 function f_save () {
101   window.localStorage["filename"]=document.getElementById("file_name").value;
102 }
103
104 </script>
105 </head>
106 <body onload="f_load();">
107 <h1>File API</h1>
108 File Path: <input type="text" size="80" id="file_name" value="data/hello.txt" >
109 <button type="button" onclick="f_save();f_read_file();">Read</button>
110 <button type="button" onclick="f_clear();">clear</button>
111 <div id="response"></div>
112 <textarea id="textarea" rows="18" cols="97"></textarea><br>
113 <button type="button" onclick="f_save();f_write();">Write</button>
114 <button type="button" onclick="f_save();f_append();">append</button><br>

The numbers at the beginning of the lines are not part of the source. They are included here only for reference below.

A few comments on the code:

001 This is a html5 document.
002 html begin tag.
005 html comment
006 Some browsers (notably Microsoft IE and Edge) do not have industry standard object URLSearchParams. This includes support code for that function.
007 Script begin tag.
009 Define an error alert.
014 Function definition "f_clear". This function clears the text area.
018 Function definition "f_append". This function appends the contents of the text area to the file.
021 encodeURIComponent encodes the text from the text area for transmission over a http channel. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent.
022 Construct the url for the XMLHttpRequest, that is "xhr-append?filename=...&mydata=...". This is processed by the server code routed to "xhr-append".
023 Construct a XMLHttpRequest object and call it "http".
024 Open the XMLHttpRequest object with the url.
025 Set the event handler for the XMLHttpRequest object.
026 Beginning of onreadystatechange function.
027 The onreadystatechange function is called back at each stage of http processing. We are only interested in the final callback when the request is complete. This is characterized as readyState 4 and status 200.
029 The text of the response is in http.responseText.
030 Replace all '\' with '/' (Windows vs the rest of the world).
031 Convert responseText to URLSearchParams to conveniently process the parameter 'error='.
032 If there was an "error=' in the response,
033   display it in the response div.
034 else
035   Invoke "f_load" to read the resulting file into the text area.
039 "send" the url with parameters to the server.
040 End of "f_append" function.
042 The "f_write" is very similar to "f_append".
066 The "f_read_file" is very similar to "f_write" and "f_append", except that instead of sending data, it is receiving data.
083 The received data goes into the text area.
090 Function "f_load" is invoked when the document loads and anytime the window is refreshed. It invokes "f_read_file" to load the file contents into the text area.
092 The browser's localStorage is used to persist the file name within and between sessions. See https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API. I always use localStorage instead of cookies.
097 Read the file into the text area.
100 The function "f_save" saves the file name to localStorage.
104 End of javascript.
105 End of html head.
106 HTML document body begins here. "f_load" is invoked when the document load is complete.
109 "Read" button.
110 "clear" button.
111 "response" division.
112 The text area.
113 "Write" button.
114 "append" button.
I leave it to the reader to dream up applications for persistent storage. A few ideas:

Template server

If you are a php developer, one of the things you are probably missing is the php "include" functionality. This is useful for the inclusion of components like headers, menus, sidebars, and footings on all of a site's pages. In nodejs, this void is filled with "templates", which come in several flavors:

I will be using EJS in this server.

This server and the servers that follow each build upon the prior server, so that the last server has all the capabilities of the others.

To build the Template Server:

The source for "template_server.js" follows:

001 var express = require('express');
002 const http = require('http');
003 var app = express();
004 var fs = require("fs");
005 app.use(express.static(__dirname + '/public'));
006 app.set('view engine', 'ejs');
007
008 app.get('/', function(req,res) {
009   res.render('index.ejs');
010 });
011
012 app.get('*ejs', function(req,res) {
013   var url=req.url;
014   if (url.charAt(0) == '/') {
015     url = url.substr(1);
016   }    
017   res.render(url);
018 });
019       .
             . same as file_server.js 007-066
077       .

The numbers at the beginning of the lines are not part of the source. They are included here only for reference below.

A few comments on the code:

005 Use a "public" directory for static files.
007 Use the Embedded Javascript view engine.
008 The root or home page of the site is "index.ejs".
012 All ".ejs" files are "rendered" stripping the initial "/".

I illustrate the use of this server with three pages which share a common heading. The heading consists of menu line at the top with links to the three pages. These components follow:

"heading.ejs":

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <title>My Server</title>
    <!--The following is necessary for some browsers-->
    <script src="url_search_params.js"></script>
  </head>
<body onload="f_load();">
<a href="./">Hello</a>
<a href="singing.ejs">Sing</a>
<a href="goodbye.ejs">Goodbye</a><br>

"index.ejs":

<% include heading %>
<h1>Hello World!</h1>

"singing.ejs":

<% include heading %>
Zippity Do Dah!<br>
Zippity Dahhhh!<br>
My oh my, what a wonderful day!

"goodbye.ejs":

<% include heading %>
<h1>Goodbye Cruel World!</h1>

Email Server

The ability to send email from your application can be a valuable way to alert your users to new features and promote community. Here I demonstate sending mail from your gmail account using a Google api.

Assuming that you followed my advice above to open a google account with gmail and Google Compute Engine, you may establish the capability to send gmail from you application by following the following directions:

To build the Gmail Server:

Log in to your gmail account.

Get started with the following link: https://developers.google.com/gmail/api/quickstart/nodejs

You will not follow this quick start all the way through, as the quick start does not demonstrate sending email.

At step 1.a click the "this wizard" link

Select your "Node Servers" project.

Click "Continue", then click "Go to Credentials".

Click the "Cancel" button at the bottom of the page.

Click the download button on the right hand side of the screen. This is a downward pointing arrow icon. This will place a "client secret" file in your downloads directory,
e.g. "client_secret_798265666119-g1p01s35qu0fo0e1c4q5r3td5ggntk6l.apps.googleusercontent.com.json". Copy this file to your servers/gmail_server directory and rename it "client_secret_gmail.json".

Down load the following files from javascriptrules.org by clicking the following links and move them to the indicated directories.

The source for quickstart-gmail.js follows:

001 var fs = require('fs');
002 var readline = require('readline');
003 var google = require('googleapis');
004 var googleAuth = require('google-auth-library');
005
006 // If modifying these scopes, delete your previously saved credentials
007 // at ~/.credentials/gmail-nodejs-quickstart.json
008 var SCOPES = ['https://www.googleapis.com/auth/gmail.send'];
009 // var TOKEN_DIR = (process.env.HOME || process.env.HOMEPATH ||
010 //    process.env.USERPROFILE) + '/.credentials/';
011 //var TOKEN_PATH = TOKEN_DIR + 'gmail-nodejs-quickstart.json';
012 var TOKEN_PATH = 'gmail-nodejs-send-quickstart.json';
013
014 // Load client secrets from a local file.
015 fs.readFile('client_secret_gmail.json', function processClientSecrets(err, content) {
016   if (err) {
017     console.log('Error loading client secret file: ' + err);
018     return;
019   }
020   // Authorize a client with the loaded credentials, then call the
021   // Gmail API.
022   authorize(JSON.parse(content), tryIt);
023 });
024
025 /**
026  * Create an OAuth2 client with the given credentials, and then execute the
027  * given callback function.
028  *
029  * @param {Object} credentials The authorization client credentials.
030  * @param {function} callback The callback to call with the authorized client.
031  */
032 function authorize(credentials, callback) {
033   var clientSecret = credentials.installed.client_secret;
034   var clientId = credentials.installed.client_id;
035   var redirectUrl = credentials.installed.redirect_uris[0];
036   var auth = new googleAuth();
037   var oauth2Client = new auth.OAuth2(clientId, clientSecret, redirectUrl);
038
039   // Check if we have previously stored a token.
040   fs.readFile(TOKEN_PATH, function(err, token) {
041     if (err) {
042       getNewToken(oauth2Client, callback);
043     } else {
044       oauth2Client.credentials = JSON.parse(token);
045       callback(oauth2Client);
046     }
047   });
048 }
049
050 /**
051  * Get and store new token after prompting for user authorization, and then
052  * execute the given callback with the authorized OAuth2 client.
053  *
054  * @param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for.
055  * @param {getEventsCallback} callback The callback to call with the authorized
056  *     client.
057  */
058 function getNewToken(oauth2Client, callback) {
059   var authUrl = oauth2Client.generateAuthUrl({
060     access_type: 'offline',
061     scope: SCOPES
062   });
063   console.log('Authorize this app by visiting this url: ', authUrl);
064   var rl = readline.createInterface({
065     input: process.stdin,
066     output: process.stdout
067   });
068   rl.question('Enter the code from that page here: ', function(code) {
069     rl.close();
070     oauth2Client.getToken(code, function(err, token) {
071       if (err) {
072         console.log('Error while trying to retrieve access token', err);
073         return;
074       }
075       oauth2Client.credentials = token;
076       storeToken(token);
077       callback(oauth2Client);
078     });
079   });
080 }
081
082 /**
083  * Store token to disk be used in later program executions.
084  *
085  * @param {Object} token The token to store to disk.
086  */
087 function storeToken(token) {
088   fs.writeFile(TOKEN_PATH, JSON.stringify(token));
089   console.log('Token stored to ' + TOKEN_PATH);
090 }
091
092 /**
093  * Lists the labels in the user's account.
094  *
095  * @param {google.auth.OAuth2} auth An authorized OAuth2 client.
096  */
097 function tryIt(auth) {
098   console.log('now run "node gmail_server.js" and test');
099 }

The numbers at the beginning of the lines are not part of the source. They are included here only for reference below.

A few comments on the code:

001-004 Require the necessary packages.
008        SCOPES defines the gmail privileges to be authorized.    In this
              case only "send".
012        TOKEN_PATH the path for the "token" to be saved.
               Note: the comments are lines from the quickstart code I originally downloaded,
               but did not find useful.
015        Read the "client secret" file we generated earlier.
022        "authorize" using the data from "client secret".    The "authorize" function definition follows.
              The "tryit" function is to be the "callback" to be authorized
032        The "authorize" function definition.
033-035 Decode the client secret credentials.
036        create the oauth2 client
040        Attempt to read the token file, it should not yet exist.
042        If token file does not exist, create it. This is the purpose of this program.
058        Get the "token", prints out a url to visit to copy/paste a "code" from.
              Then read the pasted "code".
070        Pass the "code" to the oauth2 client to get the "token".
076        Store the "token".
077        Execute the callback, passing the oauth2 client.    In this case the callback really doesn't do anything.
088        The callback function does not exercize any gmail function.
098        Instructions on how to test the token online.

Figuring out how to send gmail was one of the most difficult puzzles I have ever solved. It was the second oauth2 interface that I used (the first was reading from/writing to google drive). The google documentation for oauth2 and these interfaces would benefit greatly from a bit more work. My thanks to those who left bread crumbs on the internet as clues.

The source for "gmail_server.js" appears below:

001var express = require('express');
002const http = require('http');
003var app = express();
004var fs = require("fs");
005var google = require('googleapis');
006var googleAuth = require('google-auth-library');
007
008app.use(express.static(    dirname + '/public'));
009app.set('view engine', 'ejs');
010
011var responder=null;
012var crlf = "\r\n";
013var base64EncodedEmail ="";
014var TOKEN_PATH_GMAIL = 'gmail-nodejs-send-quickstart.json';
015var auth = null;
016
017function sendMessage (auth) {
018    var gmail = google.gmail('v1');
019    gmail.users.messages.send({
020        auth: auth,
021        userId: 'me',
022        resource: {
023            raw: base64EncodedEmail
024        }
025    }, function(err, response) {
026        if (err) {
027            console.log('sendMessage The API returned an error: ' + err);
028            return;
029        } else {
030            responder().send('ok');
031            return;
032        }
033    });
034}
035
036app.get('/', function(req,res) {
037    res.render('index.ejs');
038});
039
040app.get('*ejs', function(req,res) {
041    var url=req.url;
042    if (url.charAt(0) == '/') {
043        url = url.substr(1);
044    }        
045    res.render(url);
046});
047app.put('/xhr-write', function(req, res){
048    var fileName=req.query.filename;
049    var mydata=req.query.mydata;
050    mydata = mydata.replace(/[\r]/gm,'');
051    fs.writeFile (fileName, mydata,(err) => {
052        if (err) {
053            console.log('err='+err);
054            if (err.code === 'ENOENT'){
055                res.send('error='+err);            
056            } else {
057                throw err; //server dies
058            }
059        }
060        res.send("ok");
061    });
062});
063
064app.put('/xhr-append', function(req, res){
065    var fileName=req.query.filename;
066    var mydata=req.query.mydata;
067    mydata = mydata.replace(/[\r]/gm,'');
068    fs.appendFile (fileName, mydata,(err) => {
069        if (err) {
070            console.log('err='+err);
071            if (err.code === 'ENOENT'){
072                res.send('error='+err)            
073            } else {
074                throw err;    //server dies
075            }
076        }
077        res.send("ok");
078    });
079});
080
081app.get('/xhr-read', function(req, res){
082    fileName=req.query.filename;
083    fs.readFile(fileName, (err, data) => {
084        if (err) {
085            console.log('err='+err);
086            if (err.code === 'ENOENT'){
087                res.send('error='+err)            
088            } else {
089                throw err; // server dies
090            }
091        } else {
092            mydata = data.toString();
093            res.send(mydata);
094        }
095    });
096});
097
098app.put('/xhr-gmail-send',
099    function (req, res) {
100        var the_date = new Date();
101    
102        var the_gmail_recipient = req.query.r;
103        var the_subject=req.query.s;
104        var the_gmail_message = req.query.m.replace(/\n/g,"
");
105      responder=(function(){
106        return function(){return res}
107        }())
108        var email_msg = "To: " + the_gmail_recipient+crlf;
109        email_msg += "Subject: "+the_subject+crlf;
110        email_msg += "Content-Type: text/html; char-set=utf-8"+crlf;
111        email_msg += "Content-Transfer-Encoding: base64"
112        email_msg += crlf + crlf;
113        email_msg += ""
114        email_msg += the_gmail_message;
115        email_msg += "";
116        var mybuffer = new Buffer(email_msg);
117
118        base64EncodedEmail = mybuffer.toString('base64');
119        base64EncodedEmail = base64EncodedEmail.replace(/\+/g,'-');
120        base64EncodedEmail = base64EncodedEmail.replace(/\//g,'_');
121        indexof = base64EncodedEmail.indexOf('=');
122        if (indexof > 0){
123            base64EncodedEmail=base64EncodedEmail.substr(0,indexof);
124        }
125
126        fs.readFile('client_secret_gmail.json',
127            function processClientSecrets(err, content){
128                if (err) {
129                    console.log('Error loading client secret file: ' + err);
130                    res.send('error='+err);              
131                    throw err;
132                }
133
134                authorize_gmail (JSON.parse(content), sendMessage);
135            } //end of processClientSecrets
136        ) // end of fs.readFile parameter list
137    })
138
139function authorize_gmail(credentials, callback) {
140    var clientSecret = credentials.installed.client_secret;
141    var clientId = credentials.installed.client_id;
142    var redirectUrl = credentials.installed.redirect_uris[0];
143    auth = new googleAuth();
144    var oauth2Client = new auth.OAuth2(clientId, clientSecret, redirectUrl);
145
146    fs.readFile(TOKEN_PATH_GMAIL, function(err, token) {
147        if (err) {
148            console.log('Error loading token file: ' + err);
149            res.send('error='+err);              
150            throw err;
151        } else {
152            oauth2Client.credentials = JSON.parse(token);
153            callback(oauth2Client);
154        }
155    });
156}
157
158const hostname = '0.0.0.0';
159const port = 80;
160
161const server = http.createServer(app);
162
163server.listen(port, hostname, () => {
164    console.log(`Server running at http://${hostname}:${port}/`);
165});

The numbers at the beginning of the lines are not part of the source. They are included here only for reference below.

A few comments on the code:

001-006"Require the necessary modules.
008"public" static directory.
009"ejs" view engine.
011-016Global variables
017Function "sendMessage". Parameter "auth".
019Invoke the "send" API.
030"ok" response to XMLHttpRequest.
036-046Handle "EJS" requests
047-096Handle XMLHttpRequest file system requests.
098Handle XMLHttpRequest request to send gmail.
102-104Retrieve the recipient, subject, and email message from the request.
105"responder" is a closure wrapping the Express response object used to send the API response.
108-115Format the email message.
116Convert the email message string to a Nodejs buffer.
128-124Encode the email message to the weird base64 encoding used by gmail.
126Read and process the "client secret" file.
134Authorize the gmail send with oauth2 and send the message.
139Invoked by the preceding function to process the credentials in the client secret file and call the sendMessage callback in the oauth2 context passing the credentials and the "token". Magic!!!
158-165Create the server and listen to port 80 on all interfaces.
164Announce that the server is running on the log.

The source for "index.html" follows:

001<!DOCTYPE html>
002<html>
003<head>
004
005<!--The following is necessary for some browsers-->
006<script src="url search params.js"></script>
007<script>
008
009window.onerror = function (errorMsg, url, lineNumber, column, errorObj) {
010        alert('Error: ' + errorMsg + ' Script: ' + url + ' Line: ' + lineNumber
011        + ' Column: ' + column + ' StackTrace: ' +    errorObj);
012}
013
014function f send gmail(){
015    var recipient=document.getElementById("recipient").value;
016    var subject=document.getElementById("subject").value;
017    var message=document.getElementById("message").value;    
018    var params="r="+recipient+"&s="+subject+"&m="+encodeURIComponent(message);
019    var url="xhr-gmail-send?"+params;
020    var http=new XMLHttpRequest();
021    http.open("PUT", url, true);
022    http.onreadystatechange = function()
023    {
024        if(http.readyState == 4)
025        {
026            responseText = http.responseText;
027            responseText = responseText.replace(/\\/g,'/');
028            var urlParams = new URLSearchParams (responseText);
029            if (urlParams.has('error')) {
030                document.getElementById("response").innerHTML = responseText;
031            } else {
032                document.getElementById("response").innerHTML = responseText;
033                f load();
034            }
035        }
036    }
037    http.send(null);    
038}
039
040function f load () {
041    // document.getElementById("response").innerHTML = '';
042    if (window.localStorage["recipient"] !== undefined) {
043        document.getElementById("recipient").value =window.localStorage["recipient"];
044    } else {
045        window.localStorage["recipient"] = document.getElementById("recipient").value;
046    }
047    if (window.localStorage["subnect"] !== undefined) {
048        document.getElementById("subject").value =window.localStorage["subject"];
049    } else {
050        window.localStorage["subject"] = document.getElementById("subject").value;
051    }
052    if (window.localStorage["message"] !== undefined) {
053        document.getElementById("message").value =window.localStorage["message"];
054    } else {
055        window.localStorage["message"] = document.getElementById("message").value;
056    }
057}
058
059function f save () {
060    window.localStorage["recipient"]=document.getElementById("recipient").value;
061    window.localStorage["subject"]=document.getElementById("subject").value;
062    window.localStorage["message"]=document.getElementById("message").value;
063}
064
065</script>
066</head>
067<body onload="f load();">
068<h1>Gmail API</h1>
069To: <input type="text" size="80" id="recipient" value="danceswithdolphin@gmail.com" ><br>
070Subject: <input type="text" size="80" id="subject" value="Testing
071Gmail Send" ><br>
072<textarea id="message" rows="18" cols="97">Testing testing testing</textarea><br>
073<button type="button" onclick="f save();f send gmail();">Send Gmail</button><br>
074<div id="response"></div>

The numbers at the beginning of the lines are not part of the source. They are included here only for reference below.

A few comments on the code:

006Some browsers (notably Microsoft IE and Edge) do not have industry standard object URLSearchParams. This includes support code for that function.
009Define an error alert.
014Function f_send_email gathers the input recipient, subject, and message and sends an XMLHttpRequest to the server.
040Function f_load retrieves recipient, subject, and message from local storage to populate the input fields.
059Function f_save saves recipient, subject and message to local storage.
069The input fields for recipient, subject, and message.
073The "send" button.
074The response to the XMLHttpRequest appears here.

Note that the email is formatted as an HTML message. This is useful for producing pretty emails and including links in the email.

HTTPS Server

For applications that record personal data and accept credit cards,HTTPS provides secure transport of the information between client and server. This functionality is therefore mandatory for most applications. With Nodejs, this is easily achieved.

From the sample servers above, we see that the structure of an application in Express is implemented by expressing the application with Express, then passing the application to a server provided by the HTTP module. For HTTPS, this is very similar, the application is passed in the creation of an HTTPS server.

HTTPS secures the client/server communication by encripting the transmissions. At the heart of this encription is private key and public key pairs obtained from certificates obtained from certified Certificate Authorities (CAs). For a discussion of how all this works, see https://en.wikipedia.org/wiki/HTTPS. I provide here instructions on obtaining certificates from Namecheap's www.ssls.com authority.

I have used two products from this source:

I am currently using the second for supporting more than one application with the same certificate. The first will serve if you only intend to develop one application.

I have found their online chat support more than adequate.

There is no escaping spending a little money for your certificates. Browsers will warn loudly of self signed certificates.

I used openSSL on my windows laptop to generate the CSR(certificate signing request) with the command "openssl req -nodes -newkey rsa:2048 -keyout josephbonds.key -out josephbonds.csr". There are other ways to do this, for example see https://www.namecheap.com/support/knowledgebase/article.aspx/9852/0/csrgoogle or https://helpdesk.ssls.com/hc/en-us/articles/115001608932-How-to-generate-a-CSR-code-on-Node-js. The private key must be kept for later use.

The CSR (certificate signing request) is used in the purchase process for the certificate. Once the purchase is complete you will receive an email with a zip file attached, which contains the components you will need to set up the server.

Part of the process is proving that you own the domain(s) the certificate is for. I used the HTTP-based validation, which involves putting a .txt file within a specific directory(/.well-known/pki-validation/) of your site. Since WINSCP does not display directories that begin with '.', I created the directory without the '.', then renamed the directory to include the '.' from a PuTTy window.

See https://helpdesk.ssls.com/hc/en-us/articles/206957109-How-can-I-complete-the-domain-control-validation-DCV-for-my-SSL-certificate- for the SSL Certificate Validation procedure.

To build the HTTPS Server:

copy the following files from servers/gmail_server to servers/https_server

Down load the following files from javascriptrules.org by clicking the following links and move them to the indicated directories.

Copy your ssl files: private key (.key), certificate (.crt), and CA bundle (.ca-bundle) files to servers/https_server. You will need to edit the https_server.js to reference these (look for "var key=", 'var cert=", and "var ca=".)

To test the https_server, it will be necessary to edit the "C:\Windows\System32\drivers\etc\hosts" file as administrator. Add a line "127.0.0.1 yourdomain.com", where "yourdomain.com" is the domain the SSL certificate covers.

To test the server:

Credit Card Server

I illustrate online credit card processing with Square.

The credit card server is built upon the HTTPS server detailed above. Begin by creating a subdirectory of your "servers" directory named "creditcard_server", then copy the entire contents of the "https_server" subdirectory to the new "creditcard_server" subdirectory.

Signup for a developers account at squareup.com https://squareup.com/signup?v=developers.

After signing up for a Squareup account. Set up a new application, by going to https://connect.squareup.com/apps. Click "new application", fill in application name, the click "Create Application". Once the application has been created, select the application, then click "Show" Personal Access Token. Take note of the Application ID and Personal Access Token, these will need to be inserted into the code below.

Download the following components from javascriptrules.org by clicking the links and then moving them to the indicated directories:

The Square interface is a "restful" one powered by the "UNIREST" module(). The unirest module is documented at http://unirest.io/nodejs.html. In a command window, execute the following commands:

To modify the code to work with your Square account:

Edit creditcard_server.js. Edit first line to include your squareAccessToken from above.

Edit buy.ejs. Edit the sixth line to include you squareApplicationId from above.

Edit products.ejs. Replace the anchors with you own products and prices. Prices are in cents. Product may be any simple string you wish to identify purchases with.

Edit the "donate.ejs" file to include the applicationId from above. Find the paymentForm in the file, which looks like this:

var paymentForm = new SqPaymentForm({
  applicationId: 'sq0idp-4qNCnmXPUlrB_9-xOY5SgQ',
  inputClass: 'sq-input',

Change the applicationId string to the value noted above.

Edit pay.ejs.

To test the creditcard_server, it will be necessary to edit the "C:\Windows\System32\drivers\etc\hosts" file as administrator. Add a line "127.0.0.1 yourdomain.com", where "yourdomain.com" is the domain the SSL certificate covers.

To test the server:

The source for "creditcard_server.js" appears below:

001var squareAccessToken = "sq0atp-Z1vTkOAh3m-gJ9iamolNcw";
002var express = require('express');
003const http = require('http');
004var app = express();
005var fs = require("fs");
006var google = require('googleapis');
007var googleAuth = require('google-auth-library');
008
009app.use(express.static(__dirname + '/public'));
010app.set('view engine', 'ejs');
011
012var responder=null;
013var crlf = "\r\n";
014var base64EncodedEmail ="";
015var TOKEN_PATH_GMAIL = 'gmail-nodejs-send-quickstart.json';
016var auth = null;
017
018// set up a route to redirect http to https
019app.get('*',function(req,res){  
020    res.redirect('https://'+ req.hostname+req.url);
021})
022
023function sendMessage (auth) {
024  var gmail = google.gmail('v1');
025  gmail.users.messages.send({
026    auth: auth,
027    userId: 'me',
028    resource: {
029      raw: base64EncodedEmail
030    }
031  }, function(err, response) {
032    if (err) {
033      console.log('sendMessage The API returned an error: ' + err);
034      return;
035    } else {
036      responder().send('ok');
037      return;
038    }
039  });
040}
041
042const hostname = '0.0.0.0';
043const port = 80;
044
045const server = http.createServer(app);
046
047server.listen(port, hostname, () => {
048  console.log(`Server running at http://${hostname}:${port}/`);
049});
050// set up https server
051var https = require ('https');
052var ca=fs.readFileSync('96630797repl_1.ca-bundle');
053var key=fs.readFileSync('josephbonds.key');
054var cert=fs.readFileSync('96630797repl_1.crt');
055var https_options = {
056    ca: ca,
057    key: key,
058    cert: cert
059};
060// global variables
061var transaction_success = false;      
062var amount = 0;
063var really_charging = true;
064var PORT = 443;
065var HOST = '0.0.0.0';
066app = express();
067
068var fs = require("fs");
069var path = require('path');
070
071var router = express.Router();
072var unirest = require('unirest');//square
073var base_url = "https://connect.squareup.com/v2";//square
074var request_params = null;
075
076
077var google = require('googleapis');
078var googleAuth = require('google-auth-library');
079var os = require('os');
080
081var fileName = '';
082var indexof = 0;
083var crlf = "\r\n";
084var base64EncodedEmail ="";
085var email_msg = "";
086var the_gmail_recipient = '';
087var the_gmail_message = '';
088var responder=null;
089var auth = null;
090var the_date = 0;
091
092
093
094
095
096app.use('/', router);
097
098router.get('/', function(req,res) {
099  res.render('index.ejs');
100});
101
102router.get('/purchase', function(req,res){
103  request_params = req.query;
104  var product = request_params.product;
105  var price = request_params.price;
106  res.render('buy.ejs', { product: product, price: price });
107})
108
109router.get('*ejs', function(req,res) {
110  var url=req.url;
111  if (url.charAt(0) == '/') {
112    url = url.substr(1);
113  }    
114  res.render(url);
115});
116
117router.get('/charges/charge_card', function(req,res,next){
118  transaction_success = false;      
119  var charge_responder=(function(){
120      return function(){return res}
121  })();
122  var location;
123  request_params = req.query;
124  if (really_charging) {
125  unirest.get(base_url + '/locations')
126  .headers({
127    'Authorization': 'Bearer ' + squareAccessToken,
128    'Accept': 'application/json'
129  })
130  .end(function (response) {
131
132    for (var i = response.body.locations.length - 1; i >= 0; i--) {
133      if(response.body.locations[i].capabilities.indexOf("CREDIT_CARD_PROCESSING")>-1){
134        location = response.body.locations[i];
135        break;
136      }
137      if(i==0){
138        return res.json({status: 400, errors: [{"detail": "No locations have credit card processing available."}] });
139      }
140    }
141
142    var token = require('crypto').randomBytes(64).toString('hex');
143
144    amount = parseInt(request_params.amount);
145
146    request_body = {
147      card_nonce: request_params.nonce,
148      amount_money: {
149        amount: amount,
150        currency: 'USD'
151      },
152      idempotency_key: token
153    }
154    unirest.post(base_url + '/locations/' + location.id + "/transactions")
155    .headers({
156      'Authorization': 'Bearer ' + squareAccessToken,
157      'Accept': 'application/json',
158      'Content-Type': 'application/json'
159    })
160    .send(request_body)
161    .end(function(response){
162      if (response.body.errors){
163        console.log('response has errors');
164        console.log(JSON.stringify(response.body.errors));
165        return res.json({status: 400, errors: response.body.errors})
166        console.log('!! cannot happen !! continuing after errors');
167      }else{
168        transaction_success = true;      
169        f_transaction_complete(charge_responder);
170      }
171    })
172
173  });
174 } else {
175 } // really_charging else
176}); // router get
177
178function f_transaction_complete(charge_responder) {
179 if (transaction_success){ 
180 var memrec= request_params.name + ','; 
181 memrec += request_params.street_address_1 + ',';
182 memrec += request_params.street_address_2 + ',';
183 memrec += request_params.city + ',';
184 memrec += request_params.state + ',';
185 memrec += request_params.zip + ',' + request_params.email + ',';
186 memrec += request_params.mobile + ',';
187 memrec += request_params.home + ',';
188 if (really_charging) {
189   memrec += request_params.product_id + ',online';
190 } else {
191   memrec += request_params.product_id + ',not_charging';
192 }
193   memrec += ","+request_params.amount;
194 var now=new Date();
195 var now_yyyy = now.getFullYear();
196 var now_mm = now.getMonth()+1;
197     now_mm = "0"+now_mm;
198     now_mm = now_mm.substring(now_mm.length -2);
199 var now_dd = now.getDate();
200     now_dd = "0"+now_dd;
201     now_dd = now_dd.substring(now_dd.length - 2);
202 var now_hr = now.getHours();   
203 var now_min = now.getMinutes();
204 now_str = now_yyyy+'.'+now_mm+'.'+now_dd+' '+now_hr+':'+now_min;
205 memrec += ","+now_str;
206 memrec += "\n";
207 fs.appendFile("public/data/members.csv",memrec,(err,data) => {
208  if (err) throw err;
209  charge_responder().json({status: 200});
210 })
211 }
212}
213
214router.put('/xhr-write', function(req, res){
215  var fileName=req.query.filename;
216  var mydata=req.query.mydata;
217  mydata = mydata.replace(/[\r]/gm,'');
218  fs.writeFile (fileName, mydata,(err) => {
219    if (err) { 
220      console.log('err='+err);
221      if (err.code === 'ENOENT'){
222        res.send('error='+err);      
223      } else {
224        throw err; //server dies
225      }
226    }
227    res.send("ok");
228  });
229});
230
231router.put('/xhr-append', function(req, res){
232  var fileName=req.query.filename;
233  var mydata=req.query.mydata;
234  mydata = mydata.replace(/[\r]/gm,'');
235  fs.appendFile (fileName, mydata,(err) => {
236    if (err) {
237      console.log('err='+err);
238      if (err.code === 'ENOENT'){
239        res.send('error='+err)      
240      } else {
241        throw err;  //server dies
242      }
243    }
244    res.send("ok");
245  });
246});
247
248router.get('/xhr-read', function(req, res){
249  fileName=req.query.filename;
250  fs.readFile(fileName, (err, data) => {
251    if (err) { 
252      console.log('err='+err);
253      if (err.code === 'ENOENT'){
254        res.send('error='+err)      
255      } else {
256        throw err; // server dies
257      }
258    } else {
259      mydata = data.toString();
260      res.send(mydata);
261    }
262  });
263});
264
265router.put('/xhr-gmail-send',
266  function (req, res) {
267    the_date = new Date();
268  
269    var the_gmail_recipient = req.query.r;
270    var the_subject=req.query.s;
271    var the_gmail_message = req.query.m;
272    responder=(function(){
273      return function(){return res}
274    }())
275    var email_msg = "To: " + the_gmail_recipient+crlf;
276    email_msg += "Subject: "+the_subject+crlf;
277    email_msg += "Content-Type: text/html; char-set=utf-8"+crlf;
278    email_msg += "Content-Transfer-Encoding: base64"
279    email_msg += crlf + crlf;
280    email_msg += ""
281    email_msg += the_gmail_message; 
282    email_msg += "";
283    var mybuffer = new Buffer(email_msg);
284
285    base64EncodedEmail = mybuffer.toString('base64');
286    base64EncodedEmail = base64EncodedEmail.replace(/\+/g,'-');
287    base64EncodedEmail = base64EncodedEmail.replace(/\//g,'_');
288    indexof = base64EncodedEmail.indexOf('=');
289    if (indexof > 0){
290      base64EncodedEmail=base64EncodedEmail.substr(0,indexof);
291    }
292
293    fs.readFile('client_secret_gmail.json', 
294      function processClientSecrets(err, content){
295        if (err) {
296          console.log('Error loading client secret file: ' + err);
297          res.send('error='+err);        
298          throw err;
299        }
300        // Authorize a client with the loaded credentials, then call the
301        //   gmail API.
302        authorize_gmail (JSON.parse(content), sendMessage);
303      } //end of processClientSecrets 
304    ) // end of fs.readFile parameter list
305  })
306
307function authorize_gmail(credentials, callback) {
308  var clientSecret = credentials.installed.client_secret;
309  var clientId = credentials.installed.client_id;
310  var redirectUrl = credentials.installed.redirect_uris[0];
311  auth = new googleAuth();
312  var oauth2Client = new auth.OAuth2(clientId, clientSecret, redirectUrl);
313
314  fs.readFile(TOKEN_PATH_GMAIL, function(err, token) {
315    if (err) {
316      console.log('Error loading token file: ' + err);
317      res.send('error='+err);        
318      throw err;
319    } else {
320      oauth2Client.credentials = JSON.parse(token);
321      callback(oauth2Client);
322    }
323  });
324}
325
326https_server = https.createServer(https_options, app).listen(PORT, HOST, () => {
327console.log('HTTPS Server listening on %s:%s', HOST, PORT);
328});
329

The numbers at the beginning of the lines are not part of the source. They are included here only for reference below.

A few comments on the code:

001The Square Access token.
002-007The node require statements.
009The static directory.
010The EJS template engine.
012-16Global javascript variables.
018-021redirect http to https.
023-040email.
042-043The http interface and port number.
045Create the http server.
047-049Http listen and announce
050-059Setup the https server.
052-054Read the ssl files
060-092Global javascript variables and modules
071Create a router.
072Require the unirest module for Square.
073Square url.
077-078Google modules for gmail.
081-90Global variables.
092For Gmail.
096Mount router on root url.
098-100Process root url, index.ejs.
102-107Process purchase url.
103-105extract parameters product and price.
106Render buy.ejs.
109-115Process *.ejs.
117-130Process /charges/charge_card Square.
119a closure wrapping the Express response object used to send the API response
125unirest get locations Square.
130unirest response received.
132Loop through locations.
133Looking for one with credit card capaility.
134Found it.
137-139If not found, error.
140end of location loop.
142Generate a random token.
146-151Construct unirest request body.
154-161unirest post to location's tranactions.
162-166Response has errors.
168-169Response success, call f_transaction_complete.
175-176Ending router get for charges/charge_card Square.
178-212Square transaction complete, append a record to member.csv.
214-263From File Server.
265-325From Email Server.
326Create and announce HTTPS server.

The source for "donate.ejs" appears below:

001<!DOCTYPE html><html><head><title>Javascript Rules Donation</title>
002   <script type="text/javascript" src="https://js.squareup.com/v2/paymentform"></script>
003    <!--script src="url_search_params.js"></script-->
004<script>
005var applicationId = 'sq0idp-4qNCnmXPUlrB_9-xOY5SgQ';
006function f_load (){
007}
008function f_validate_dollars () {
009  var dollars = document.getElementById("dollars").value;
010  var innerhtml = "";
011  if (isNaN(dollars)){
012    innerhtml = "dollars must be numeric";
013    document.getElementById("dollars_error").innerHTML = innerhtml;
014    return false; // cancel event
015  } else {
016    document.getElementById("dollars_error").innerHTML = innerhtml;
017  }
018}
019function f_validate_cents () {
020  var cents = document.getElementById("cents").value;
021  var innerhtml = "";
022  if (isNaN(cents)){
023    innerhtml = "cents must be numeric";
024    document.getElementById("cents_error").innerHTML = innerhtml;
025    return false; // cancel event
026  } else {
027    if (cents.length == 0){
028        cents = "00";    
029    }else{
030      if (cents.length == 1){
031        cents = "0"+cents;    
032      }
033    }
034    document.getElementById("cents").value = cents;    
035    //alert("cents: "+cents);
036    document.getElementById("cents_error").innerHTML = innerhtml;
037  }
038}
039</script>
040<% include heading.ejs %>
041
042<body><html></html><div class="no-boot">
043<div id="successNotification" style="display: none"><Card>Charged Succesfully!!</Card></div>
044      <h2>Donate</h2>
045      dollars: 
046      <input type="text" id="dollars" name="dollars" maxlength="5" size="5" placeholder="00000" style="color: #006400;font-size:xx-large;disp:inline-block;width:5em" onchange="return f_validate_dollars();">
047      <div id="dollars_error" style="color: #006400;fontsize:xx-large;disp:inline-block;width:25em"></div>
048      cents: <input type="text" id="cents" maxlength="2" size="2" placeholder="00" style="color: #006400;font-size:xx-large;disp:inline-block;width:5em" onchange="return f_validate_cents();" value="00">
049      <div id="cents_error" style="color: #006400;fontsize:xx-large;disp:inline-block;width:25em"></div>
050<form id="payment-form" action="#" onsubmit="return paymentFormSubmit()">
051<label>Name</label>
052<input type="text" id="name" name="name"  placeholder="Name"/><br>
053<label>Email</label>
054<input type="email" id="email" name="email"  placeholder="Email"/><br>
055<label>Mobile</label>
056<input type="text" id="mobile" name="mobile"  placeholder="Mobile"/><br>
057<label>Home</label>
058<input type="text" id="home" name="home"  placeholder="Home"/><br>
059<h3> Address </h3>
060<label>Street</label>
061<input type="text" id="street_address_1" name="street_address_1"  placeholder="Address Line 1"/><br>
062<label>Street</label>
063<input type="text" id="street_address_2" name="street_address_2"  placeholder="Address Line 2"/><br>
064<label>City</label>
065<input type="text" id="city" name="city"  placeholder="City"/><br>
066<label>State</label>
067<select id="state" name="state">
068<option value=""></option>
069<option value="AL">Alabama</option>
070<option value="AK">Alaska</option>
071<option value="AZ">Arizona</option>
072<option value="AR">Arkansas</option>
073<option value="CA">California</option>
074<option value="CO">Colorado</option>
075<option value="CT">Connecticut</option>
076<option value="DE">Delaware</option>
077<option value="DC">District of Columbia</option>
078<option value="FL">Florida</option>
079<option value="GA">Georgia</option>
080<option value="HI">Hawaii</option>
081<option value="ID">Idaho</option>
082<option value="IL">Illinois</option>
083<option value="IN">Indiana</option>
084<option value="IA">Iowa</option>
085<option value="KS">Kansas</option>
086<option value="KY">Kentucky</option>
087<option value="LA">Louisiana</option>
088<option value="ME">Maine</option>
089<option value="MD">Maryland</option>
090<option value="MA">Massachusetts</option>
091<option value="MI">Michigan</option>
092<option value="MN">Minnesota</option>
093<option value="MS">Mississippi</option>
094<option value="MO">Missouri</option>
095<option value="MT">Montana</option>
096<option value="NE">Nebraska</option>
097<option value="NV">Nevada</option>
098<option value="NH">New Hampshire</option>
099<option value="NJ">New Jersey</option>
100<option value="NM">New Mexico</option>
101<option value="NY">New York</option>
102<option value="NC">North Carolina</option>
103<option value="ND">North Dakota</option>
104<option value="OH">Ohio</option>
105<option value="OK">Oklahoma</option>
106<option value="OR">Oregon</option>
107<option value="PA">Pennsylvania</option>
108<option value="RI">Rhode Island</option>
109<option value="SC">South Carolina</option>
110<option value="SD">South Dakota</option>
111<option value="TN">Tennessee</option>
112<option value="TX">Texas</option>
113<option value="UT">Utah</option>
114<option value="VT">Vermont</option>
115<option value="VA">Virginia</option>
116<option value="WA">Washington</option>
117<option value="WV">West Virginia</option>
118<option value="WI">Wisconsin</option>
119<option value="WY">Wyoming</option>
120</select><br>
121<label>Zip</label>
122<input type="text" id="zip" name="zip"  placeholder="Zip"/><br>
123<div id="card-errors">
124</div>
125<div>
126<label>Card Number</label>
127<div  id="sq-card-number"></div>
128</div>
129<div>
130<label>CVV</label>
131<div  id="sq-cvv"></div>
132</div>
133<div>
134<label>Expiration Date</label>
135<div  id="sq-expiration-date"></div>
136</div>
137<div>
138<label>Postal Code</label>
139<div  id="sq-postal-code"></div>
140</div>
141<div>
142<input type="submit" id="submit" value="Donate Now" class="btn btn-primary">
143</div>
144</form>
145</div><script type="text/javascript">var cardNonce;
146var paymentForm = new SqPaymentForm({
147  applicationId: 'sq0idp-4qNCnmXPUlrB_9-xOY5SgQ',
148  inputClass: 'sq-input',
149  inputStyles: [
150      {
151        fontSize: 'xx-large',
152        padding: '7px 12px',
153        backgroundColor: "transparent"
154      }
155    ],
156  cardNumber: {
157    elementId: 'sq-card-number',
158    placeholder: '0000 0000 0000 0000'    
159  },
160  cvv: {
161    elementId: 'sq-cvv',
162    placeholder: 'CVV'
163  },
164  expirationDate: {
165    elementId: 'sq-expiration-date',
166    placeholder: 'MM/YY'
167  },
168  postalCode: {
169    elementId: 'sq-postal-code',
170    placeholder: '94110'
171  },
172  callbacks: {
173    cardNonceResponseReceived: function(errors, nonce, cardData) {
174      if (errors){
175        var error_html = ""
176        for (var i =0; i < errors.length; i++){
177          error_html += "<li> " + errors[i].message + " </li>";
178        }
179        document.getElementById("card-errors").innerHTML = error_html;
180        document.getElementById('submit').disabled = false;
181      }else{
182        document.getElementById("card-errors").innerHTML = "";
183        chargeCardWithNonce(nonce);
184      }
185      
186      
187    },
188    unsupportedBrowserDetected: function() {
189      // Alert the buyer
190    }
191  }
192});
193
194// build payment form after DOM load
195document.addEventListener('page:change', function(){
196  console.log('dom loaded')
197  paymentForm.build()
198});
199
200var paymentFormSubmit = function(){
201  console.log('submit clicked');
202  document.getElementById('submit').disabled = true;
203  paymentForm.requestCardNonce();
204  return false;
205}
206
207var chargeCardWithNonce = function(nonce) {
208  var product_id = 'donate';
209  var name = document.getElementById('name').value;
210  var email = document.getElementById('email').value;
211  var mobile = document.getElementById('mobile').value;
212  var home = document.getElementById('home').value;
213  var street_address_1 = document.getElementById('street_address_1').value;
214  var street_address_2 = document.getElementById('street_address_2').value;
215  var city = document.getElementById('city').value;
216  var state = document.getElementById('state').value;
217  var zip = document.getElementById('zip').value;
218  var amount = document.getElementById('dollars').value+
219               document.getElementById('cents').value;
220  //alert ('amount: '+amount);
221  
222  var http = new XMLHttpRequest();
223  var url = "/charges/charge_card";
224  var params = "product_id=" + product_id 
225  +"&name=" + name 
226  +"&email=" + email 
227  +"&mobile=" + mobile 
228  +"&home=" + home 
229  + "&nonce=" + nonce
230  + "&street_address_1=" + street_address_1
231  + "&street_address_2=" + street_address_2
232  + "&city=" + city
233  + "&state=" + state
234  + "&zip=" + zip
235  + "&amount=" + amount;
236  url += '?' + params;
237  http.open("GET", url, true);
238
239  //Send the proper header information along with the request
240  http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
241  http.setRequestHeader("X-CSRF-Token", "<%%= form_authenticity_token %>");
242
243  http.onreadystatechange = function() {//Call a function when the state changes.
244      if(http.readyState == 4 && http.status == 200) {
245        var data = JSON.parse(http.responseText)
246        if (data.status == 200) {
247          document.getElementById("successNotification").style.display = "block";
248          document.getElementById("payment-form").style.display = "none";
249          window.scrollTo(0, 0);
250          window.location = "thanks.ejs";
251        }else if (data.status == 400){
252          var error_html = "";
253          for (var i =0; i < data.errors.length; i++){
254            error_html += "<li> " + data.errors[i].detail + " </li>";
255          }
256          document.getElementById("card-errors").innerHTML = error_html;
257          document.getElementById('submit').disabled = false;
258        }
259      }
260  }
261  http.send(null);
262}</script></body></html>
263

The numbers at the beginning of the lines are not part of the source. They are included here only for reference below.

A few comments on the code:

000.