├── .gitignore
├── README.md
├── mongodb_version
├── config.json
├── private
│ ├── addModal.css
│ ├── addModal.hb
│ ├── addModal.js
│ ├── app.js
│ ├── dataList.css
│ ├── dataList.hb
│ ├── dataList.js
│ ├── editModal.css
│ ├── editModal.hb
│ ├── editModal.js
│ ├── nav.css
│ ├── nav.hb
│ └── nav.js
├── setenv.sh
├── src
│ ├── admin
│ │ └── main.go
│ └── webapp
│ │ ├── auth.go
│ │ ├── authroutes.go
│ │ ├── config.go
│ │ ├── dataroutes.go
│ │ ├── db.go
│ │ ├── fileroutes.go
│ │ ├── main.go
│ │ ├── router.go
│ │ └── tplroutes.go
└── templates
│ ├── app.tpl
│ └── main.tpl
├── mysql_version
├── config.json
├── private
│ └── app.js
├── sampledata.csv
├── setenv.sh
├── src
│ ├── admin
│ │ └── main.go
│ ├── statichttpserver
│ │ └── main.go
│ └── webapp
│ │ ├── auth.go
│ │ ├── authroutes.go
│ │ ├── config.go
│ │ ├── dataroutes.go
│ │ ├── db.go
│ │ ├── fileroutes.go
│ │ ├── main.go
│ │ ├── router.go
│ │ └── tplroutes.go
└── templates
│ ├── app.tpl
│ └── main.tpl
└── tutorial
├── css
├── beige-mfs.css
├── pdf.css
├── reveal.css
└── zenburn.css
├── include
├── admin_001.go
├── admin_final.go
└── statichttpserver.go
├── index.html
├── js
├── head.min.js
├── jquery-2.1.1.min.js
├── loadcode.js
└── reveal.js
└── plugin
├── highlight
└── highlight.js
├── math
└── math.js
├── notes
├── notes.html
└── notes.js
├── svg-js
└── svg.min.js
└── zoom-js
└── zoom.js
/.gitignore:
--------------------------------------------------------------------------------
1 | *.exe
2 | webapp_config.json
3 | */pkg/*
4 | */src/golang.org/*
5 | */src/google.golang.org/*
6 | */src/github.com/*
7 | */src/gopkg.in/*
8 | myconfig.json
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Golang Webapp Tutorial
2 |
3 | This repository shows how to build a simple web application using Go.
4 |
5 | There are two subfolders, depending on which back-end you prefer to use
6 | (MongoDB or MySQL). The original tutorial was written in MySQL, and the
7 | latest changes to the MongoDB version haven't been back-ported. Unless you
8 | have a reason to use MySQL, I recommend using the MongoDB version.
9 |
10 | ## Key Features
11 |
12 | This tutorial is designed to introduce the technologies needed for full-stack
13 | web development. The technologies involved are:
14 |
15 | - Back End: MongoDB or MySQL
16 | - Middle End: Go
17 | - Front End: HTML/JavaScript/jQuery/Bootstrap/Handlebars
18 |
19 | The app also is a customer to one web service: Google's OAuth 2.0 provider.
20 |
21 | ## App Overview
22 |
23 | The app itself is very simple: users can register using their Google Id, and
24 | then can modify a single table of data by adding, removing, and updating
25 | rows.
26 |
27 | I contend that if you can do this, then being able to manage multiple tables
28 | is mostly just cut-and-paste in the middle and front ends, and
29 | straightforward database design in the back end. If you can do one table,
30 | you can do 10 tables. If you can do 10 tables, you can figure out how to
31 | make them look less like tables and more like cool web content. And if you
32 | can do that, you can probably build a pretty cool and useful app.
33 |
34 | ## Getting Started
35 |
36 | Everything in this tutorial should be able to work on a developer machine or
37 | in the cloud. To get started, it's probably easiest to install golang,
38 | install mongodb, and then work locally.
39 |
40 | The only caveat is that you'll need to set up a project in Google Developer
41 | Console in order to get authentication working. There are many tutorials
42 | online for doing this, so I won't bother explaining in much detail.
43 |
44 | ### Example: Getting Started on Windows
45 |
46 | Since this is a Git repository, I'm going to assume that you installed "Git
47 | Bash for Windows". Assuming that is the case, here are the steps for getting
48 | started. We'll check out the code to a folder on the desktop, build
49 | everything and get it running.
50 |
51 | #### Get the code, configure it
52 |
53 | 1. Open Git Bash
54 | 2. Navigate to your desktop, check out the code, and move to the code directory:
55 |
56 | cd ~/Desktop
57 | git clone https://github.com/mfs409/golang-webapp-tutorial.git
58 | cd golang-webapp-tutorial
59 |
60 | 3. Set up a Google OAuth project in the Google Developer Console
61 | - Be sure to enable the Google+ API
62 | - Set up credentials for a web client
63 | - For now, when developing locally, use http://localhost:8080/auth/google/callback as your Redirect URL
64 | - Don't forget to configure your OAuth consent screen
65 | - Make note of your Client ID and your Client Secret
66 |
67 | 4. Set up a file with your configuration information
68 |
69 | A good "12 factor" app uses environment variables for configuration. You'll
70 | do that eventually, but for now, it is easier to put your configuration in a
71 | file. But you should **not** put your actual configuration information into
72 | a repository. In golang-webapp-tutorial, the .gitignore file specifies that
73 | "myconfig.json" won't accidentally get checked in. Let's set it up:
74 |
75 | cd mongodb_version
76 | cp config.json myconfig.json
77 |
78 | Now go ahead and edit myconfig.json in your favorite editor. Be sure to copy
79 | your Client ID and Client Secret exactly as they appear in the Google
80 | Developer Console. For now, also set up your MongoDB information like this:
81 |
82 | "MongoHost" : "127.0.0.1",
83 | "MongoPort" : "27017",
84 | "MongoDbname" : "webapp",
85 |
86 | #### Start your database
87 |
88 | I assume that you installed MongoDB in the "normal" way. If you did, then to
89 | start your database, first start a second Git Bash for Windows prompt, and
90 | navigate to the mongodb_version folder.
91 |
92 | cd ~/Desktop/golang-webapp-tutorial/mongodb_version
93 |
94 | Second, create a folder called "db":
95 |
96 | mkdir db
97 |
98 | Third, launch MongoDB from the command line, and instruct it to store its
99 | data in the "db" folder:
100 |
101 | /c/Program\ Files/MongoDB\ 2.6\ Standard/bin/mongod --dbpath ./db
102 |
103 | (That last bit is a tad ugly... the backslashes are necessary, because of the
104 | spaces in the names of the folders you are using. I typically put this
105 | single line in a shell script for convenience. Alternatively, you could add
106 | the MongoDB "bin" folder to your PATH.)
107 |
108 | #### Build the code
109 |
110 | The Go language expects you to have your build environment configured in a
111 | certain way. If you've used Go before, you're familiar with the idea of
112 | setting your "GOPATH". If not, trust me that all you need to do is type this
113 | line from the mongodb_version folder:
114 |
115 | . setenv.sh
116 |
117 | This will make mongodb_version the root of your go project, so that you
118 | can build code easily.
119 |
120 | Normally, you build by typing "go build XXX", where XXX is the name of the
121 | project to build (for example, "webapp" or "admin"). But we can't do that
122 | quite yet, because we need to fetch the dependencies first. We use "go get"
123 | for that purpose. Let's fetch the MongoDB driver, and then build our admin
124 | module:
125 |
126 | go get gopkg.in/mgo.v2
127 | go build admin
128 |
129 | At this point, you can type "admin -h" to see a list of command-line options
130 | to your program. Let's not do any work yet. Instead, let's build the webapp
131 | server:
132 |
133 | go get golang.org/x/oauth2
134 | go get golang.org/x/oauth2/google
135 | go build webapp
136 |
137 | #### Run the code
138 |
139 | Our code has an administrative component, and a main server. You might want
140 | to have two windows open for this part.
141 |
142 | Assuming your MongoDB instance is still running, you can start by
143 | initializing the database tables. Strictly speaking, this isn't necessary in
144 | MongoDB, but sometimes it feels nice to know that you can use a tool like
145 | RoboMongo to inspect your database, even when it has no data yet.
146 |
147 | ./admin.exe -configfile myconfig.json -resetdb
148 |
149 | Next, start up your server:
150 |
151 | ./webapp.exe -configfile myconfig.json
152 |
153 | Point a browser at http://localhost:8080, click "register", and walk through
154 | the OAuth process. You should get a message that your account is registered,
155 | but needs to be activated. You can list accounts with the admin program:
156 |
157 | ./admin.exe -configfile myconfig.json -listnewusers
158 |
159 | And, of course, you can activate them. Use the ObjectIdHex value to do so:
160 |
161 | ./admin.exe -configfile myconfig.json -activatenewuser 59c3e3cda12d7b0610aa440
162 |
163 | Now you should be able to log in and use the app. Don't forget to run admin
164 | with the "-h" flag to see other features, like pre-populating a table from a
165 | CSV.
166 |
167 | ## Feature Requests
168 |
169 | The main goal of this repository is educational. This is designed to help
170 | students get acquainted with web technologies, so that they can start
171 | building web apps. To that end, I gladly will accept pull requests that are
172 | in keeping with those goals. I'd love for someone to rewrite the backend in
173 | Node.js and also in some not-too-dissimilar Java framework. However, I will
174 | not accept code that is poorly commented. This repository is first and
175 | foremost an educational tool. Reading this code should be educational,
176 | informative, and enjoyable for novice and intermediate programmers.
177 |
--------------------------------------------------------------------------------
/mongodb_version/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "OauthGoogleClientId" : "",
3 | "OauthGoogleClientSecret" : "",
4 | "OauthGoogleScopes" : ["https://www.googleapis.com/auth/userinfo.email"],
5 | "OauthGoogleRedirectUrl" : "http://localhost:8080/auth/google/callback",
6 |
7 | "MongoHost" : "",
8 | "MongoPort" : "27017",
9 | "MongoDbname" : "",
10 |
11 | "MemcachedHost" : "",
12 | "MemcachedPort" : "11211",
13 |
14 | "AppPort" : "8080"
15 | }
16 |
--------------------------------------------------------------------------------
/mongodb_version/private/addModal.css:
--------------------------------------------------------------------------------
1 | /* The addModal object does not require any custom styling */
2 |
--------------------------------------------------------------------------------
/mongodb_version/private/addModal.hb:
--------------------------------------------------------------------------------
1 | {{! This file provides a Bootstrap modal for allowing the user to add a new}}
2 | {{! row of data}}
3 |
4 |
5 |
6 |
7 |
10 |
Add New Data
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/mongodb_version/private/addModal.js:
--------------------------------------------------------------------------------
1 | // The addModal object is a singleton web component for soliciting input in
2 | // order to create a new row of data
3 | //
4 | // NB: addModal is very similar to editModal. We have them as different
5 | // objects for conceptual clarity, but you could overload one modal to do
6 | // both.
7 | var addModal = {
8 |
9 | // track if we've loaded and configured this modal yet
10 | init : false,
11 |
12 | // reset and show the modal
13 | Show : function() {
14 | // inject the modal into the DOM if we've never displayed it before
15 | if (!addModal.init) {
16 | $("body").append(hb_t.addModal());
17 | // wire up the buttons, and set the behavior on modal show
18 | $("#addModal").on("shown.bs.modal", function() {
19 | $("#addModal-smallnote").focus()
20 | });
21 | $("#addModal-save").click(addModal.save);
22 | $("#addModal-cancel").click(addModal.cancel);
23 | addModal.init = true;
24 | }
25 | // clear modal content and show it
26 | $("#addModal input").val("");
27 | $("#addModal").modal("show");
28 | },
29 |
30 | // This runs when the "cancel" button of the modal is clicked
31 | cancel : function() {
32 | $("#addModal").modal("hide");
33 | },
34 |
35 | // Send the new content to the server, and on success, update the page
36 | save : function() {
37 | // gather data from "required" fields
38 | var newdata = {
39 | smallnote : $('#addModal-smallnote').val()+"",
40 | bignote : $('#addModal-bignote').val()+"",
41 | favint : parseInt($('#addModal-favint').val()),
42 | favfloat : parseFloat($('#addModal-favfloat').val()),
43 | }
44 | // stop now if any data missing
45 | if (newdata.smallnote+"" === "" || newdata.bignote === "" ||
46 | newdata.favint+"" === "" || newdata.favfloat+"" === "")
47 | {
48 | window.alert("Missing field");
49 | return;
50 | }
51 | // get the optional field:
52 | if ($('#addModal-trickfloat').val() + "" !== "")
53 | newdata.trickfloat = parseFloat( $('#addModal-trickfloat').val());
54 | // send everything to the server, re-load the page on success
55 | $.ajax({
56 | type: "POST",
57 | url: "/data",
58 | contentType: "application/json",
59 | data: JSON.stringify(newdata),
60 | success : function(data) {
61 | dataList.DisplayLatest();
62 | $("#addModal").modal("hide");
63 | },
64 | error : function(data) {
65 | window.alert("Unspecified error: " + data)
66 | $("#addModal").modal("hide");
67 | }
68 | });
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/mongodb_version/private/app.js:
--------------------------------------------------------------------------------
1 | // All our templates go in here
2 | var hb_t = {};
3 |
4 | // This list holds all of the template names. Names matter... for entry
5 | // "abc" in this list, we will create field hb_t.abc in the above map, using
6 | // file "private/abc.hb"
7 | var hblist = [ "dataList", "editModal", "addModal", "nav" ];
8 |
9 | // When the page has been loaded, and we know all the javascript code is
10 | // available, call loadTemplates to fetch and parse the templates. When all
11 | // templates are loaded, buildPage will run
12 | $(document).ready(function() {
13 | loadTemplates(buildPage);
14 | });
15 |
16 | // Load the handlebar template files. Once all the templates are loaded, run "action"
17 | function loadTemplates(action) {
18 | // keep track of the number of templates we still need to load
19 | var remain = hblist.length;
20 | // Function to load template then call action() iff all templates loaded
21 | var loader = function(i) {
22 | $.get("private/" + hblist[i]+".hb", function(data) {
23 | hb_t[hblist[i]] = Handlebars.compile(data);
24 | remain--;
25 | if (remain == 0) action();
26 | }, "html");
27 | }
28 | // load the templates
29 | for (var i = 0; i < hblist.length; i++) { loader(i); }
30 | }
31 |
32 | // We build the main logged-in page by adding the initial components, which
33 | // are a navbar and the data list
34 | function buildPage() {
35 | navbar.Inject();
36 | dataList.DisplayLatest();
37 | }
38 |
--------------------------------------------------------------------------------
/mongodb_version/private/dataList.css:
--------------------------------------------------------------------------------
1 | /* The dataList object does not require any custom styling */
2 |
--------------------------------------------------------------------------------
/mongodb_version/private/dataList.hb:
--------------------------------------------------------------------------------
1 | {{! This file provides a template for generating a table of data. We use a}}
2 | {{! little bit of Bootstrap styling on the table.}}
3 |
38 |
39 |
--------------------------------------------------------------------------------
/mongodb_version/private/dataList.js:
--------------------------------------------------------------------------------
1 | // The dataList object is a singleton web component for presenting all of the
2 | // data currently in the database.
3 | //
4 | // NB: The component is very simple... on any change to the data, we're going
5 | // to re-fetch the latest data and regenerate the entire component. It
6 | // would be straightforward to extend this component so that it could
7 | // update individual rows, but in the interest of keeping the tutorial
8 | // simple, we won't.
9 |
10 | var dataList = {
11 |
12 | // Fetch the latest content, re-create the component, and (re)place it in
13 | // the DOM.
14 | DisplayLatest : function() {
15 | $.ajax({
16 | url: "/data",
17 | success: function(data) {
18 | // Put the data into the DOM
19 | $(".container").html(hb_t.dataList({d:data}));
20 |
21 | // wire up click handlers for buttons
22 | //
23 | // NB: there is one add button, but potentially many edit and
24 | // delete buttons
25 | $(".dataList-addbtn").click(addModal.Show);
26 | $(".dataList-deletebtn").click(dataList.deleteRow);
27 | $(".dataList-editbtn").click(dataList.editRow);
28 | },
29 | dataType: "json"
30 | })
31 | },
32 |
33 | // when a 'delete' button is clicked, we use the data-id of the
34 | // button to know what row to delete
35 | deleteRow : function() {
36 | var id = $(this).data("id");
37 | $.ajax({
38 | type: "DELETE",
39 | url: "/data/"+id,
40 | contentType: "application/json",
41 | success : function(data) {
42 | dataList.DisplayLatest();
43 | location.reload();
44 | },
45 | error : function(data) {
46 | window.alert("Unspecified error: " + data)
47 | }
48 | });
49 |
50 | },
51 |
52 | // when an 'edit' button is clicked, we need to extract the data from the
53 | // row, and then use it to populate the edit modal
54 | editRow: function() {
55 | var id = $(this).data("id");
56 | // find the row that holds this button, get its cells
57 | var tds = $(this).closest("tr").find("td");
58 | // pull text from the cells to produce the default values for the
59 | // modal, then show the modal with the data.
60 | var content = {
61 | id : id,
62 | smallnote : $(tds[0]).text(),
63 | bignote : $(tds[1]).text(),
64 | favint : $(tds[2]).text(),
65 | favfloat : $(tds[3]).text(),
66 | trickfloat : $(tds[4]).text()
67 | };
68 | editModal.Show(content);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/mongodb_version/private/editModal.css:
--------------------------------------------------------------------------------
1 | /* The editModal object does not require any custom styling */
2 |
--------------------------------------------------------------------------------
/mongodb_version/private/editModal.hb:
--------------------------------------------------------------------------------
1 | {{! This file provides a Bootstrap modal for allowing the user to edit a}}
2 | {{! row of data}}
3 |
4 |
5 |
6 |
7 |
10 |
Edit Data
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/mongodb_version/private/editModal.js:
--------------------------------------------------------------------------------
1 | // The editModal object is a singleton web component for soliciting input in
2 | // order to change the content of a row of data
3 | //
4 | // NB: editModal is very similar to addModal. We have them as different
5 | // objects for conceptual clarity, but you could overload one modal to do
6 | // both.
7 | var editModal = {
8 |
9 | // track if we've loaded and configured this modal yet
10 | init : false,
11 |
12 | // populate and show the modal
13 | Show : function(data) {
14 | // inject the modal into the DOM if we've never displayed it before
15 | if (!editModal.init) {
16 | $("body").append(hb_t.editModal());
17 | // wire up the buttons, and set the behavior on modal show
18 | $("#editModal").on("shown.bs.modal", function() {
19 | $("#editModal-smallnote").focus()
20 | });
21 | $("#editModal-save").click(editModal.save);
22 | $("#editModal-cancel").click(editModal.cancel);
23 | editModal.init = true;
24 | }
25 | // set modal content and show it
26 | $("#editModal-smallnote").val(data.smallnote);
27 | $("#editModal-bignote").val(data.bignote);
28 | $("#editModal-favint").val(data.favint);
29 | $("#editModal-favfloat").val(data.favfloat);
30 | $("#editModal-trickfloat").val(data.trickfloat);
31 | $("#editModal-id").val(data.id);
32 | $("#editModal").modal("show");
33 | },
34 |
35 | // This runs when the "cancel" button of the modal is clicked
36 | cancel : function() {
37 | $("#editModal").modal("hide");
38 | },
39 |
40 | // Send the new content to the server, and on success, update the page
41 | save : function() {
42 | // gather data from "required" fields
43 | var newdata = {
44 | smallnote : $('#editModal-smallnote').val()+"",
45 | bignote : $('#editModal-bignote').val()+"",
46 | favint : parseInt($('#editModal-favint').val()),
47 | favfloat : parseFloat($('#editModal-favfloat').val()),
48 | id : $("#editModal-id").val() // NB: shouldn't ever be blank
49 | }
50 | // stop now if any data missing
51 | if (newdata.smallnote+"" === "" || newdata.bignote === "" ||
52 | newdata.favint+"" === "" || newdata.favfloat+"" === "")
53 | {
54 | window.alert("Missing field");
55 | return;
56 | }
57 | // get the optional field:
58 | if ($('#editModal-trickfloat').val() + "" !== "")
59 | newdata.trickfloat = parseFloat( $('#editModal-trickfloat').val());
60 | // send everything to the server, re-load the page on success
61 | $.ajax({
62 | type: "PUT",
63 | url: "/data/"+newdata.id,
64 | contentType: "application/json",
65 | data: JSON.stringify(newdata),
66 | success : function(data) {
67 | dataList.DisplayLatest();
68 | $("#editModal").modal("hide");
69 | },
70 | error : function(data) {
71 | window.alert("Unspecified error: " + data)
72 | $("#editModal").modal("hide");
73 | }
74 | });
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/mongodb_version/private/nav.css:
--------------------------------------------------------------------------------
1 | /* The nav object does not require any custom styling */
2 |
--------------------------------------------------------------------------------
/mongodb_version/private/nav.hb:
--------------------------------------------------------------------------------
1 | {{! This file provides a template for generating the navbar}}
2 | {{! It's just a Bootstrap navbar... see the Bootstrap docs for ideas}}
3 | {{! about how to make it fancier}}
4 |
31 |
--------------------------------------------------------------------------------
/mongodb_version/private/nav.js:
--------------------------------------------------------------------------------
1 | // The nav object is a singleton web component for providing a simple
2 | // bootstrap navbar on the page.
3 | //
4 | // NB: This doesn't really need to be a component with an Handlebars
5 | // template, but it's easier to work this way. If we embedded the nav in
6 | // the app.tpl file, we'd have to re-start the web server any time we
7 | // edited the navbar. Also, it's easier to keep track of what's been put
8 | // in the DOM when the document body is pretty much empty to start with.
9 | var navbar = {
10 |
11 | // Put the navbar into the DOM as the first child of the body
12 | Inject : function() {
13 | $("body").prepend(hb_t.nav());
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/mongodb_version/setenv.sh:
--------------------------------------------------------------------------------
1 | # This script is a convenience. When working on Windows using 'git bash' as
2 | # the shell, we can enter the golang-webapp-tutorial folder and then type
3 | # '. setenv.sh' to set our GOPATH. Doing so ensures that 'go build ...'
4 | # works as desired.
5 | export GOPATH=`pwd`
6 |
--------------------------------------------------------------------------------
/mongodb_version/src/admin/main.go:
--------------------------------------------------------------------------------
1 | // Admin app for managing aspects of the program. The program administers an
2 | // app according to the information provided in a config file.
3 | // Administrative tasks include:
4 | // - Create or Drop the entire Database
5 | // - Reset the Users or Data table
6 | // - Populate the Data table with data from a CSV
7 | // - Activate new user registrations
8 | // There is also a simple function to show how to update this file to run
9 | // arbitrary one-off operations on the database
10 | package main
11 |
12 | import (
13 | "encoding/csv"
14 | "encoding/json"
15 | "flag"
16 | "fmt"
17 | "gopkg.in/mgo.v2"
18 | "gopkg.in/mgo.v2/bson"
19 | "io"
20 | "log"
21 | "os"
22 | "strconv"
23 | "time"
24 | )
25 |
26 | // Configuration information for Google OAuth, MySQL, and Memcached. We
27 | // parse this from a JSON config file
28 | //
29 | // NB: field names must start with Capital letter for JSON parse to work
30 | //
31 | // NB: between the field names and JSON mnemonics, it should be easy to
32 | // figure out what each field does
33 | //
34 | // NB: to keep things simple, we're omitting the memory cache in this
35 | // tutorial
36 | type Config struct {
37 | ClientId string `json:"OauthGoogleClientId"`
38 | ClientSecret string `json:"OauthGoogleClientSecret"`
39 | Scopes []string `json:"OauthGoogleScopes"`
40 | RedirectUrl string `json:"OauthGoogleRedirectUrl"`
41 | DbHost string `json:"MongoHost"`
42 | DbPort string `json:"MongoPort"`
43 | DbName string `json:"MongoDbname"`
44 | McdHost string `json:"MemcachedHost"`
45 | McdPort string `json:"MemcachedPort"`
46 | AppPort string `json:"AppPort"`
47 | }
48 |
49 | // The configuration information for the app we're administering
50 | var cfg Config
51 |
52 | // The type for data in the "users" table
53 | type UserEntry struct {
54 | ID bson.ObjectId `bson:"_id"`
55 | State int `bson:"state"`
56 | Googleid string `bson:"googleid"`
57 | Name string `bson:"name"`
58 | Email string `bson:"email"`
59 | Create time.Time `bson:"create"`
60 | }
61 |
62 | // The type for data in the "data" table
63 | type DataEntry struct {
64 | ID bson.ObjectId `bson:"_id"`
65 | SmallNote string `bson:"smallnote"`
66 | BigNote string `bson:"bignote"`
67 | FavInt int `bson:"favint"`
68 | FavFloat float64 `bson:"favfloat"`
69 | TrickFloat *float64 `bson:"trickfloat"`
70 | Create time.Time `bson:"create"`
71 | }
72 |
73 | // Load a JSON file that has all the config information for our app, and put
74 | // the JSON contents into the cfg variable
75 | func loadConfig(cfgFileName string) {
76 | f, err := os.Open(cfgFileName)
77 | if err != nil {
78 | log.Fatal(err)
79 | }
80 | defer f.Close()
81 | jsonParser := json.NewDecoder(f)
82 | if err = jsonParser.Decode(&cfg); err != nil {
83 | log.Fatal(err)
84 | }
85 | }
86 |
87 | // Reset the database by dropping the entire database, re-creating it, and
88 | // then creating the collections
89 | //
90 | // NB: since the schemas for the collections are determined by the
91 | // insertions, this isn't very exciting
92 | func resetDb() {
93 | // Connect or fail
94 | db, err := mgo.Dial(cfg.DbHost)
95 | if err != nil {
96 | log.Fatal(err)
97 | }
98 | defer db.Close()
99 |
100 | // We'll use monotonic mode for now...
101 | db.SetMode(mgo.Monotonic, true)
102 |
103 | // drop the database
104 | err = db.DB(cfg.DbName).DropDatabase()
105 | if err != nil {
106 | log.Fatal(err)
107 | }
108 |
109 | // create the database by putting the collections into it
110 | //
111 | // NB: we must insert and then remove a row in order for the
112 | // collection to actually exist
113 | u := db.DB(cfg.DbName).C("users")
114 | id := bson.NewObjectId()
115 | err = u.Insert(
116 | &UserEntry{
117 | ID: id,
118 | State: 0,
119 | Googleid: "",
120 | Name: "",
121 | Email: "",
122 | Create: time.Now()})
123 | if err != nil {
124 | log.Fatal(err)
125 | }
126 | q := bson.M{"_id": id}
127 | err = u.Remove(q)
128 | if err != nil {
129 | log.Fatal(err)
130 | }
131 |
132 | d := db.DB(cfg.DbName).C("data")
133 | id = bson.NewObjectId()
134 | err = d.Insert(
135 | &DataEntry{
136 | ID: id,
137 | SmallNote: "",
138 | BigNote: "",
139 | FavInt: 72,
140 | FavFloat: 2.23,
141 | TrickFloat: nil,
142 | Create: time.Now()})
143 | if err != nil {
144 | log.Fatal(err)
145 | }
146 | q = bson.M{"_id": id}
147 | err = d.Remove(q)
148 | if err != nil {
149 | log.Fatal(err)
150 | }
151 | }
152 |
153 | // Connect to the database, and return the corresponding database object
154 | //
155 | // NB: will fail if the database hasn't been created
156 | func openDB() *mgo.Database {
157 | // Connect or fail
158 | s, err := mgo.Dial(cfg.DbHost)
159 | if err != nil {
160 | log.Fatal(err)
161 | }
162 | return s.DB(cfg.DbName)
163 | }
164 |
165 | // Parse a CSV so that each line becomes an array of strings, and then use
166 | // the array of strings to push a row to the data table
167 | //
168 | // NB: This is hard-coded for our "data" table.
169 | func loadCsv(csvname *string, db *mgo.Database) {
170 | // load the csv file
171 | file, err := os.Open(*csvname)
172 | if err != nil {
173 | log.Fatal(err)
174 | }
175 | defer file.Close()
176 |
177 | // get the right collection from the database
178 | d := db.C("data")
179 |
180 | // parse the csv, one record at a time
181 | reader := csv.NewReader(file)
182 | reader.Comma = ','
183 | count := 0 // count insertions, for the sake of nice output
184 | for {
185 | // get next row... exit on EOF
186 | row, err := reader.Read()
187 | if err == io.EOF {
188 | break
189 | } else if err != nil {
190 | log.Fatal(err)
191 | }
192 | // We have an array of strings, representing the data. We
193 | // need to move it into a DataEntry struct, then we can send
194 | // it to mgo. Be careful about nulls!
195 | fi, err := strconv.Atoi(row[2])
196 | if err != nil {
197 | log.Fatal(err)
198 | }
199 | ff, err := strconv.ParseFloat(row[3], 64)
200 |
201 | var trick *float64 = nil
202 | if row[4] != "" {
203 | tf, err := strconv.ParseFloat(row[4], 64)
204 | if err != nil {
205 | log.Fatal(err)
206 | }
207 | trick = &tf
208 | }
209 | // Create the id for this record
210 | id := bson.NewObjectId()
211 | err = d.Insert(
212 | &DataEntry{
213 | ID: id,
214 | SmallNote: row[0],
215 | BigNote: row[1],
216 | FavInt: fi,
217 | FavFloat: ff,
218 | TrickFloat: trick,
219 | Create: time.Now()})
220 | if err != nil {
221 | log.Fatal(err)
222 | }
223 | count++
224 | }
225 | log.Println("Added", count, "rows")
226 | }
227 |
228 | // When a user registers, the new account is not active until the
229 | // administrator activates it. This function lists registrations that are
230 | // not yet activated
231 | func listNewAccounts(db *mgo.Database) {
232 | // get all inactive rows from the database
233 | var results []UserEntry
234 | err := db.C("users").Find(bson.M{"state": 0}).Sort("create").All(&results)
235 | if err != nil {
236 | log.Fatal(err)
237 | }
238 | // print a header
239 | fmt.Println("New Users:")
240 | fmt.Println("[id googleid name email]")
241 | for i, v := range results {
242 | fmt.Println("[", i, "]", v.ID, v.Googleid, v.Name, v.Email)
243 | }
244 | }
245 |
246 | // Since this is an administrative interface, we don't need to do anything
247 | // too fancy for the account activation: the flags include the ID to update,
248 | // we just use it to update the database, and we don't worry about the case
249 | // where the account is already activated
250 | func activateAccount(db *mgo.Database, id string) {
251 | q := bson.M{"_id": bson.ObjectIdHex(id)}
252 | change := bson.M{"$set": bson.M{"state": 1}}
253 | err := db.C("users").Update(q, change)
254 | if err != nil {
255 | log.Fatal(err)
256 | }
257 | }
258 |
259 | // We occasionally need to do one-off queries that can't really be predicted
260 | // ahead of time. When that time comes, we can edit this function,
261 | // recompile, and then run with the "oneoff" flag to do the corresponding
262 | // action. For now, it's a no-op.
263 | func doOneOff(db *mgo.Database) {
264 | // TODO: do something in here!
265 | }
266 |
267 | // Main routine: Use the command line options to determine what action to
268 | // take, then forward to the appropriate function. Since this program is for
269 | // quick admin tasks, all of the above functions terminate on any error.
270 | // That means we don't need error checking in this code... any function we
271 | // call will only return on success.
272 | func main() {
273 | // parse command line options
274 | configPath := flag.String("configfile", "config.json", "Path to the configuration (JSON) file")
275 | csvName := flag.String("csvfile", "data.csv", "The csv file to parse")
276 | opResetDb := flag.Bool("resetdb", false, "Reset the Mongo database?")
277 | opCsv := flag.Bool("loadcsv", false, "Load a csv into the data table?")
278 | opListNewReg := flag.Bool("listnewusers", false, "List new registrations?")
279 | opRegister := flag.String("activatenewuser", "", "Complete pending registration for a user")
280 | opOneOff := flag.Bool("oneoff", false, "Run a one-off query")
281 | flag.Parse()
282 |
283 | // load the JSON config file
284 | loadConfig(*configPath)
285 |
286 | // Reset the database?
287 | if *opResetDb {
288 | resetDb()
289 | return
290 | }
291 |
292 | // open the database
293 | db := openDB()
294 |
295 | // all other ops are handled below:
296 | if *opCsv {
297 | loadCsv(csvName, db)
298 | }
299 | if *opListNewReg {
300 | listNewAccounts(db)
301 | }
302 | if *opRegister != "" {
303 | activateAccount(db, *opRegister)
304 | }
305 | if *opOneOff {
306 | doOneOff(db)
307 | }
308 | }
309 |
--------------------------------------------------------------------------------
/mongodb_version/src/webapp/auth.go:
--------------------------------------------------------------------------------
1 | // This file contains functions and objects that are used in order to
2 | // authenticate the user
3 | package main
4 |
5 | import (
6 | "crypto/rand"
7 | "encoding/base64"
8 | "encoding/json"
9 | "golang.org/x/oauth2"
10 | "io"
11 | "io/ioutil"
12 | "log"
13 | "net/http"
14 | "sync"
15 | "time"
16 | )
17 |
18 | // create an "enum" for dealing with responses from a login request
19 | const (
20 | registerOk = iota // registration success
21 | registerErr = iota // unspecified registration error
22 | registerExist = iota // register a registered user
23 | loginOk = iota // login success
24 | loginErr = iota // unspecified login error
25 | loginNotReg = iota // log in of unregistered user
26 | loginNotActive = iota // log in of unactivated registered user
27 | )
28 |
29 | // When the user logs in, we get the Google User ID, Email, and Name.
30 | //
31 | // When we get the ID, it's from Google, so we can trust it. We also put it
32 | // in a cookie on the client browser, so that subsequent requests can prove
33 | // their identity.
34 | //
35 | // The problem is that anyone who knows the ID can spoof the user. To be
36 | // more secure, we don't just save the ID, we also save a token that we
37 | // randomly generate upon login. If the ID and Token match, then we trust
38 | // you to be who you say you are.
39 | //
40 | // Note that an attacker can still assume your identity if it can access your
41 | // cookies, but it can't assume your identity just by knowing your ID
42 | //
43 | // Note, too, that multiple requests can access this map, so we need it to be
44 | // synchronized
45 | //
46 | // Lastly, note that if you have multiple servers running, and a user
47 | // migrates among servers, their login info will be lost, and they'll have to
48 | // re-log in. The only way to avoid that is to persist this map to some
49 | // location that is global across nodes, and we're not going to do that in
50 | // this simple example.
51 | var cookieStore = struct {
52 | sync.RWMutex
53 | m map[string]string
54 | }{m: make(map[string]string)}
55 |
56 | // Check if a request is being made from an authenticated context
57 | func checkLogin(r *http.Request) bool {
58 | // grab the "id" cookie, fail if it doesn't exist
59 | cookie, err := r.Cookie("id")
60 | if err == http.ErrNoCookie {
61 | return false
62 | }
63 |
64 | // grab the "key" cookie, fail if it doesn't exist
65 | key, err := r.Cookie("key")
66 | if err == http.ErrNoCookie {
67 | return false
68 | }
69 |
70 | // make sure we've got the right stuff in the hash
71 | cookieStore.RLock()
72 | defer cookieStore.RUnlock()
73 | return cookieStore.m[cookie.Value] == key.Value
74 | }
75 |
76 | // Generate 256 bits of randomness
77 | func sessionId() string {
78 | b := make([]byte, 32)
79 | if _, err := io.ReadFull(rand.Reader, b); err != nil {
80 | return ""
81 | }
82 | return base64.URLEncoding.EncodeToString(b)
83 | }
84 |
85 | // To in order to log the user out, we need to remove the corresponding
86 | // cookie from our local cookie store, and then erase the cookies on the
87 | // client browser.
88 | //
89 | // WARNING: you must call this before any other code in the logout route, or
90 | // else there is a risk that the header will already be sent.
91 | func processLogoutRequest(w http.ResponseWriter, r *http.Request) {
92 | // grab the "ID" cookie, erase from map if it is found
93 | id, err := r.Cookie("id")
94 | if err != http.ErrNoCookie {
95 | cookieStore.Lock()
96 | delete(cookieStore.m, id.Value)
97 | cookieStore.Unlock()
98 | // create a log-out (info) flash
99 | flash := http.Cookie{Name: "iflash", Value: "Logout successful", Path: "/"}
100 | http.SetCookie(w, &flash)
101 | }
102 |
103 | // clear the cookies on the client
104 | clearID := http.Cookie{Name: "id", Value: "-1", Expires: time.Now(), Path: "/"}
105 | http.SetCookie(w, &clearID)
106 | clearVal := http.Cookie{Name: "key", Value: "-1", Expires: time.Now(), Path: "/"}
107 | http.SetCookie(w, &clearVal)
108 | }
109 |
110 | // This is used in the third and final step of the OAuth dance. Google is
111 | // sending back a URL whose QueryString encodes a "state" and a "code". The
112 | // "state" is something we sent to Google, that we expect to get back... it
113 | // helps prevent spoofing. The "code" is something we can send to Google to
114 | // get the user's info. From there, we save some info in a session cookie to
115 | // keep the user logged in.
116 | //
117 | // NB: return values will be based on the "enum" at the top of this file
118 | func processLoginReply(w http.ResponseWriter, r *http.Request) int {
119 | // extract the code and state from the querystring
120 | code := r.FormValue("code")
121 | state := r.FormValue("state")
122 |
123 | // choose a default error code to return, depending on login or
124 | // register attempt. First character of the 'state' string is 'r'
125 | // for register, 'l' for login.
126 | errorCode := loginErr
127 | if state[:1] == "r" {
128 | errorCode = registerErr
129 | }
130 |
131 | // validate state... it needs to match the secret we sent
132 | if state[1:] != oauthStateString {
133 | log.Println("state didn't match", oauthStateString)
134 | return errorCode
135 | }
136 |
137 | // convert the authorization code into a token
138 | token, err := oauthConf.Exchange(oauth2.NoContext, code)
139 | if err != nil {
140 | log.Println("token exchange error", code)
141 | return errorCode
142 | }
143 |
144 | // send the token to Google, get a JSON blob back
145 | response, err := http.Get(googleIdApi + token.AccessToken)
146 | if err != nil {
147 | log.Println("token lookup error", response)
148 | return errorCode
149 | }
150 |
151 | // the JSON blob has Google ID, Name, and Email... convert to a map
152 | //
153 | // NB: we don't convert via jsonParser.Decode, because we don't know
154 | // exactly what fields we'll get back
155 | defer response.Body.Close()
156 | contents, err := ioutil.ReadAll(response.Body)
157 | if err != nil {
158 | log.Println("Error reading JSON reply")
159 | return errorCode
160 | }
161 | var f interface{}
162 | err = json.Unmarshal([]byte(string(contents)), &f)
163 | if err != nil {
164 | log.Println("Error unmarshaling JSON reply")
165 | return errorCode
166 | }
167 | m := f.(map[string]interface{})
168 | // NB: m should now hold the following interfaces:
169 | // m["id"], m["email"], m["given_name"], m["name"], and m["family_name"]
170 |
171 | // look up the user in the database
172 | //
173 | // NB: check err first, otherwise we might mistake a 'nil' for
174 | // 'unregistered' when we should return registerExist
175 | u, err := getUserById(m["id"].(string))
176 | if err != nil {
177 | log.Println("Unspecified SQL error during user lookup")
178 | return errorCode
179 | }
180 | if u == nil {
181 | // no user... let's hope this is a registration request
182 | if state[:1] != "r" {
183 | log.Println("Attempt to log in an unregistered user")
184 | return loginNotReg
185 | }
186 | // add a registration (0 == not active)
187 | err = addNewUser(m["id"].(string), m["name"].(string), m["email"].(string), 0)
188 | if err != nil {
189 | log.Println("error adding new user")
190 | return registerErr
191 | }
192 | return registerOk
193 | } else {
194 | // we have a user... let's hope this is a login request
195 | if state[:1] == "r" {
196 | log.Println("Attempt to register an existing user")
197 | return registerExist
198 | }
199 | // is the user allowed to log in?
200 | if u.State == 0 {
201 | log.Println("Attempt to log in unactivated account")
202 | return loginNotActive
203 | }
204 | // it's a valid login!
205 |
206 | // To keep the user logged in, we save two cookies. The
207 | // first has the ID, the second has a random value. We then
208 | // put ID->rand in our cookieStore map. Subsequent requests
209 | // can grab the cookies and check the map
210 | //
211 | // NB: no Expires ==> it's a session cookie
212 | cookie := http.Cookie{Name: "id", Value: m["id"].(string), Path: "/"}
213 | http.SetCookie(w, &cookie)
214 | unique := sessionId()
215 | cookie = http.Cookie{Name: "key", Value: unique, Path: "/"}
216 | http.SetCookie(w, &cookie)
217 | cookieStore.Lock()
218 | cookieStore.m[m["id"].(string)] = unique
219 | cookieStore.Unlock()
220 | return loginOk
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/mongodb_version/src/webapp/authroutes.go:
--------------------------------------------------------------------------------
1 | // Routes related to OAuth, logging in, logging out, and registering
2 | package main
3 |
4 | import (
5 | "golang.org/x/oauth2"
6 | "log"
7 | "net/http"
8 | )
9 |
10 | // The route for '/login' starts the OAuth dance by redirecting to Google
11 | //
12 | // This is the first step of the OAuth dance. Via oauthConf, we send
13 | // ClientID, ClientSecret, and RedirectURL. Step 2 is for google to check
14 | // these fields and get the user to log in.
15 | func handleGoogleLogin(w http.ResponseWriter, r *http.Request) {
16 | // NB: prefix the state string with 'l' so we can tell it's a login
17 | // later. That's much easier than dealing with two different
18 | // redirect routes.
19 | url := oauthConf.AuthCodeURL("l"+oauthStateString, oauth2.AccessTypeOnline)
20 | http.Redirect(w, r, url, http.StatusTemporaryRedirect)
21 | }
22 |
23 | // The route for '/register' is identical to '/login', except we change the
24 | // state string to know it's a request to register.
25 | func handleGoogleRegister(w http.ResponseWriter, r *http.Request) {
26 | url := oauthConf.AuthCodeURL("r"+oauthStateString, oauth2.AccessTypeOnline)
27 | http.Redirect(w, r, url, http.StatusTemporaryRedirect)
28 | }
29 |
30 | // The route for '/auth/google/callback' finishes the OAuth dance: It
31 | // processes the Google response, and only sends us to the app page if
32 | // the dance was successful.
33 | func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
34 | res := processLoginReply(w, r)
35 | if res == loginOk {
36 | http.Redirect(w, r, "/app", http.StatusTemporaryRedirect)
37 | return
38 | }
39 |
40 | // we can't let the user in yet. Set a flash cookie to explain, then send to '/'
41 | name := "eflash" // i or e for info or error
42 | val := ""
43 | if res == registerOk {
44 | name = "iflash"
45 | val = "Registration succeeded. Please wait for an administrator to confirm your account."
46 | } else if res == registerErr {
47 | val = "Registration error. Please try again later."
48 | } else if res == registerExist {
49 | val = "The account you specified has already been registered."
50 | } else if res == loginNotReg {
51 | val = "The account you specified has not been registered."
52 | } else if res == loginNotActive {
53 | val = "The account you specified has not yet been activated by the administrator."
54 | } else if res == loginErr {
55 | val = "Login error. Please try again later."
56 | } else {
57 | log.Fatal("processLoginReply() returned invalid status")
58 | }
59 | http.SetCookie(w, &http.Cookie{Name: name, Value: val, Path: "/"})
60 | http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
61 | }
62 |
63 | // The route for '/logout' logs the user out and redirects to home
64 | func handleLogout(w http.ResponseWriter, r *http.Request) {
65 | processLogoutRequest(w, r)
66 | http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
67 | }
68 |
--------------------------------------------------------------------------------
/mongodb_version/src/webapp/config.go:
--------------------------------------------------------------------------------
1 | // All hard-coded app configuration is in this file, as is all code for
2 | // interacting with config information that is stored in a JSON file.
3 | package main
4 |
5 | import (
6 | "encoding/json"
7 | "golang.org/x/oauth2"
8 | "golang.org/x/oauth2/google"
9 | "log"
10 | "os"
11 | )
12 |
13 | // Information regarding the Google OAuth provider... most of this can't be
14 | // set until we load the JSON file
15 | var oauthConf = &oauth2.Config{
16 | ClientID: "",
17 | ClientSecret: "",
18 | Endpoint: google.Endpoint,
19 | RedirectURL: "",
20 | }
21 |
22 | // For extra security, we use this random string in OAuth calls... we pass it
23 | // to the server, and we expect to get it back when we get the reply
24 | //
25 | // NB: We can't re-generate this on the fly, because we need all instances of
26 | // the server to have the same string, or else they can't all satisfy the
27 | // same client
28 | const oauthStateString = "FJDKSIE7S88dhjflsid83kdlsHp7TEbpg6TwHBWdJzNwYod1i5ZTbrIF5bEoO3oP"
29 |
30 | // This is the service to which we request identifying info for a google ID
31 | const googleIdApi = "https://www.googleapis.com/oauth2/v2/userinfo?access_token="
32 |
33 | // Configuration information for Google OAuth, MySQL, and Memcached. We
34 | // parse this from a JSON config file
35 | //
36 | // NB: field names must start with Capital letter for JSON parse to work
37 | //
38 | // NB: between the field names and JSON mnemonics, it should be easy to
39 | // figure out what each field does
40 | type Config struct {
41 | ClientId string `json:"OauthGoogleClientId"`
42 | ClientSecret string `json:"OauthGoogleClientSecret"`
43 | Scopes []string `json:"OauthGoogleScopes"`
44 | RedirectUrl string `json:"OauthGoogleRedirectUrl"`
45 | DbHost string `json:"MongoHost"`
46 | DbPort string `json:"MongoPort"`
47 | DbName string `json:"MongoDbname"`
48 | McdHost string `json:"MemcachedHost"`
49 | McdPort string `json:"MemcachedPort"`
50 | AppPort string `json:"AppPort"`
51 | }
52 |
53 | // The configuration information for the app we're administering
54 | var cfg Config
55 |
56 | // Load a JSON file that has all the config information for our app, and put
57 | // the JSON contents into the cfg variable
58 | func loadConfig(cfgFileName string) {
59 | // first, load the JSON file and parse it into /cfg/
60 | f, err := os.Open(cfgFileName)
61 | if err != nil {
62 | log.Fatal(err)
63 | }
64 | defer f.Close()
65 | jsonParser := json.NewDecoder(f)
66 | if err = jsonParser.Decode(&cfg); err != nil {
67 | log.Fatal(err)
68 | }
69 |
70 | // second, update our OAuth configuration object
71 | oauthConf.ClientID = cfg.ClientId
72 | oauthConf.ClientSecret = cfg.ClientSecret
73 | oauthConf.Scopes = cfg.Scopes
74 | oauthConf.RedirectURL = cfg.RedirectUrl
75 | }
76 |
--------------------------------------------------------------------------------
/mongodb_version/src/webapp/dataroutes.go:
--------------------------------------------------------------------------------
1 | // Routes related to REST paths for accessing the DATA table
2 | package main
3 |
4 | import (
5 | "encoding/json"
6 | "gopkg.in/mgo.v2/bson"
7 | "io/ioutil"
8 | "log"
9 | "net/http"
10 | )
11 |
12 | // a helper function to send HTTP 403 / Forbidden when the user is not logged
13 | // in
14 | func do403(w http.ResponseWriter, r *http.Request) {
15 | http.Error(w, "Forbidden", http.StatusForbidden)
16 | }
17 |
18 | // Helper routine for sending JSON back to the client a bit more cleanly
19 | func jResp(w http.ResponseWriter, data interface{}) {
20 | payload, err := json.Marshal(data)
21 | if err != nil {
22 | log.Println("Internal Server Error:", err.Error())
23 | http.Error(w, "Internal Server Error", http.StatusInternalServerError)
24 | return
25 | }
26 | w.Header().Set("Content-Type", "application/json; charset=utf-8")
27 | w.Write([]byte(string(payload)))
28 | }
29 |
30 | // The GET route for all rows of the data table
31 | func handleGetAllData(w http.ResponseWriter, r *http.Request) {
32 | // if authentication passes, use getAllRows to get a big JSON blob to
33 | // send back
34 | if !checkLogin(r) {
35 | do403(w, r)
36 | return
37 | }
38 | w.Write(getAllRows())
39 | }
40 |
41 | // The PUT route for updating a row of the data table
42 | func handlePutData(w http.ResponseWriter, r *http.Request) {
43 | // check authentication
44 | if !checkLogin(r) {
45 | do403(w, r)
46 | return
47 | }
48 |
49 | // get the ID from the querystring
50 | id := bson.ObjectIdHex(r.URL.Path[6:])
51 |
52 | // Get the user's JSON blob as raw bytes, then marshal into a DataRow
53 | defer r.Body.Close()
54 | contents, err := ioutil.ReadAll(r.Body)
55 | if err != nil {
56 | log.Println("Error reading body of PUT request")
57 | jResp(w, "error")
58 | return
59 | }
60 | var d DataRow
61 | err = json.Unmarshal(contents, &d)
62 | if err != nil {
63 | log.Println("Error unmarshaling JSON reply", err)
64 | jResp(w, "error")
65 | return
66 | }
67 |
68 | // send the new data to the database
69 | ok := updateDataRow(id, d)
70 | if ok {
71 | jResp(w, "{res: 'ok'}")
72 | } else {
73 | jResp(w, "{res: 'error'}")
74 | }
75 | }
76 |
77 | // The GET route for viewing one row of the data table
78 | func handleGetDataOne(w http.ResponseWriter, r *http.Request) {
79 | // check authentication
80 | if !checkLogin(r) {
81 | do403(w, r)
82 | return
83 | }
84 |
85 | // get the ID from the querystring
86 | id := bson.ObjectIdHex(r.URL.Path[6:])
87 |
88 | // get a big JSON blob from the database via getRow, send it back
89 | w.Write(getRow(id))
90 | }
91 |
92 | // The DELETE route for removing one row from the data table
93 | func handleDeleteData(w http.ResponseWriter, r *http.Request) {
94 | // authenticate, then get ID from querystring
95 | if !checkLogin(r) {
96 | do403(w, r)
97 | return
98 | }
99 |
100 | // get the ID from the querystring
101 | id := bson.ObjectIdHex(r.URL.Path[6:])
102 |
103 | // delete the row
104 | ok := deleteDataRow(id)
105 | if ok {
106 | jResp(w, "{res: 'ok'}")
107 | } else {
108 | jResp(w, "{res: 'error'}")
109 | }
110 | }
111 |
112 | // The POST route for adding a new row to the data table
113 | func handlePostData(w http.ResponseWriter, r *http.Request) {
114 | // authenticate
115 | if !checkLogin(r) {
116 | do403(w, r)
117 | return
118 | }
119 |
120 | // Get the user's JSON blob as raw bytes, then marshal into a DataRow
121 | defer r.Body.Close()
122 | contents, err := ioutil.ReadAll(r.Body)
123 | if err != nil {
124 | log.Println("Error reading body of POST request")
125 | jResp(w, "error")
126 | return
127 | }
128 | var d DataRow
129 | err = json.Unmarshal(contents, &d)
130 | if err != nil {
131 | log.Println("Error unmarshaling JSON reply", err)
132 | jResp(w, "error")
133 | return
134 | }
135 |
136 | // insert the data
137 | ok := insertDataRow(d)
138 | if ok {
139 | jResp(w, "{res: 'ok'}")
140 | } else {
141 | jResp(w, "{res: 'error'}")
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/mongodb_version/src/webapp/db.go:
--------------------------------------------------------------------------------
1 | // The purpose of this file is to put all of the interaction with MongoDB in
2 | // one place. This lets us change backends without having to modify other
3 | // parts of the code, and it also makes it easier to see how the program
4 | // interacts with data, since it's all in one place.
5 | package main
6 |
7 | import (
8 | "encoding/json"
9 | "gopkg.in/mgo.v2"
10 | "gopkg.in/mgo.v2/bson"
11 | "log"
12 | "time"
13 | )
14 |
15 | // the database connection
16 | var db *mgo.Database
17 |
18 | // a user row from the database looks like this:
19 | type User struct {
20 | ID bson.ObjectId `bson:"_id"`
21 | State int `bson:"state"`
22 | Googleid string `bson:"googleid"`
23 | Name string `bson:"name"`
24 | Email string `bson:"email"`
25 | Create time.Time `bson:"create"`
26 | }
27 |
28 | // a data row from the database looks like this
29 | //
30 | // NB: BSON and JSON tags are provided, so that Go will auto-marshall to/from
31 | // BSON and JSON as needed
32 | type DataRow struct {
33 | ID bson.ObjectId `bson:"_id" json:"id"`
34 | SmallNote string `bson:"smallnote" json:"smallnote"`
35 | BigNote string `bson:"bignote" json:"bignote"`
36 | FavInt int `bson:"favint" json:"favint"`
37 | FavFloat float64 `bson:"favfloat" json:"favfloat"`
38 | TrickFloat *float64 `bson:"trickfloat" json:"trickfloat"`
39 | Create time.Time `bson:"create"`
40 | }
41 |
42 | // open the database
43 | func openDB() {
44 | var err error
45 | log.Println("opening database " + cfg.DbHost)
46 | m, err := mgo.Dial(cfg.DbHost)
47 | if err != nil {
48 | log.Fatal(err)
49 | }
50 | log.Println("database open")
51 | db = m.DB(cfg.DbName)
52 | }
53 |
54 | // close the database
55 | // NB: We defer() this from main()
56 | func closeDB() {
57 | log.Println("closing database")
58 | db.Session.Close()
59 | }
60 |
61 | // get a user's record, to make login/register decisions
62 | func getUserById(googleId string) (*User, error) {
63 | u := User{}
64 | err := db.C("users").Find(bson.M{"googleid": googleId}).Select(nil).One(&u)
65 | // NB: Findone returns an error on not found, so we need to
66 | // disambiguate between DB errors and not-found errors
67 | if err != nil {
68 | if err.Error() == "not found" {
69 | return nil, nil
70 | }
71 | log.Println("Error querying users", err)
72 | return nil, err
73 | }
74 | return &u, nil
75 | }
76 |
77 | // insert a row into the user table
78 | func addNewUser(googleid string, name string, email string, state int) error {
79 | u := User{
80 | ID: bson.NewObjectId(),
81 | State: state,
82 | Googleid: googleid,
83 | Name: name,
84 | Email: email,
85 | Create: time.Now(),
86 | }
87 | err := db.C("users").Insert(u)
88 | if err != nil {
89 | log.Println(err)
90 | }
91 | return err
92 | }
93 |
94 | // get all rows from the data table, return them as JSON
95 | func getAllRows() []byte {
96 | // query into an array of DataRow objects
97 | var results []DataRow
98 | err := db.C("data").Find(nil).Sort("create").All(&results)
99 | if err != nil {
100 | log.Fatal(err)
101 | }
102 |
103 | // marshall as JSON, which will produce a byte stream
104 | jsonData, err := json.Marshal(results)
105 | if err != nil {
106 | return []byte("error")
107 | }
108 | return jsonData
109 | }
110 |
111 | // get one row from the data table, return it as JSON
112 | func getRow(id bson.ObjectId) []byte {
113 | d := DataRow{}
114 | err := db.C("users").Find(bson.M{"_id": id}).Select(nil).One(&d)
115 | // NB: Findone returns an error on not found, so we need to
116 | // disambiguate between DB errors and not-found errors
117 | if err != nil {
118 | if err.Error() == "not found" {
119 | return []byte("not found")
120 | }
121 | log.Println("Error querying users", err)
122 | return []byte("internal error")
123 | }
124 |
125 | jsonData, err := json.Marshal(d)
126 | if err != nil {
127 | return []byte("internal error")
128 | }
129 | return jsonData
130 | }
131 |
132 | // Update a row in the data table
133 | func updateDataRow(id bson.ObjectId, data DataRow) bool {
134 | q := bson.M{"_id": id}
135 | fields := bson.M{"smallnote": data.SmallNote,
136 | "bignote": data.BigNote, "favint": data.FavInt,
137 | "favfloat": data.FavFloat, "trickfloat": nil}
138 | if data.TrickFloat != nil {
139 | fields["trickfloat"] = data.TrickFloat
140 | }
141 | change := bson.M{"$set": fields}
142 | err := db.C("data").Update(q, change)
143 | if err != nil {
144 | log.Println(err)
145 | return false
146 | }
147 | return true
148 | }
149 |
150 | // Delete a row from the data table
151 | func deleteDataRow(id bson.ObjectId) bool {
152 | q := bson.M{"_id": id}
153 | err := db.C("data").Remove(q)
154 | if err != nil {
155 | log.Println(err)
156 | return false
157 | }
158 | return true
159 | }
160 |
161 | // Insert a row into the data table
162 | func insertDataRow(data DataRow) bool {
163 | data.ID = bson.NewObjectId()
164 | err := db.C("data").Insert(data)
165 | if err != nil {
166 | log.Println(err)
167 | return false
168 | }
169 | return true
170 | }
171 |
--------------------------------------------------------------------------------
/mongodb_version/src/webapp/fileroutes.go:
--------------------------------------------------------------------------------
1 | // Routes for handling file requests. This corresponds to a static file
2 | // server, with public and private subfolders.
3 | package main
4 |
5 | import (
6 | "net/http"
7 | "os"
8 | )
9 |
10 | // ensure that the given path is a valid file, and not a directory
11 | func isValidFile(path string) bool {
12 | file, err := os.Open(path)
13 | if err != nil {
14 | return false
15 | } // no file
16 | defer file.Close()
17 | stat, err := file.Stat()
18 | if err != nil {
19 | return false
20 | } // couldn't stat
21 | if stat.IsDir() {
22 | return false
23 | } // directory
24 | return true
25 | }
26 |
27 | // route for serving public static files... they are prefixed with 'public'
28 | func handlePublicFile(w http.ResponseWriter, r *http.Request) {
29 | // serve only if valid file, else notfound
30 | path := r.URL.Path[1:]
31 | if isValidFile(path) {
32 | http.ServeFile(w, r, r.URL.Path[1:])
33 | } else {
34 | http.NotFound(w, r)
35 | }
36 | }
37 |
38 | // route for serving private static files... they are prefixed with 'private'
39 | func handlePrivateFile(w http.ResponseWriter, r *http.Request) {
40 | // validate login
41 | if !checkLogin(r) {
42 | do403(w, r)
43 | return
44 | }
45 | // serve only if valid file, else notfound
46 | path := r.URL.Path[1:]
47 | if isValidFile(path) {
48 | http.ServeFile(w, r, r.URL.Path[1:])
49 | } else {
50 | http.NotFound(w, r)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/mongodb_version/src/webapp/main.go:
--------------------------------------------------------------------------------
1 | // A demo web application to show how to use OAuth 2.0 (Google+ Provider) and
2 | // MongoDB from Go.
3 | package main
4 |
5 | import (
6 | "flag"
7 | "log"
8 | "net/http"
9 | )
10 |
11 | // The main function just configures resources and starts listening for new
12 | // web requests
13 | func main() {
14 | // parse command line options
15 | configPath := flag.String("configfile", "config.json", "Path to the configuration (JSON) file")
16 | flag.Parse()
17 |
18 | // load the JSON config file
19 | loadConfig(*configPath)
20 |
21 | // open the database
22 | openDB()
23 | defer closeDB()
24 |
25 | // set up templates
26 | buildTemplates()
27 |
28 | // set up the routes... it's good to have these all in one place,
29 | // since we need to be cautious about orders when there is a common
30 | // prefix
31 | router := new(Router)
32 | // REST routes for the DATA table
33 | router.Register("/data/[0-9a-z]+$", "PUT", handlePutData)
34 | router.Register("/data/[0-9a-z]+$", "GET", handleGetDataOne)
35 | router.Register("/data/[0-9a-z]+$", "DELETE", handleDeleteData)
36 | router.Register("/data$", "POST", handlePostData)
37 | router.Register("/data$", "GET", handleGetAllData)
38 | // OAuth and login/out routes
39 | router.Register("/auth/google/callback$", "GET", handleGoogleCallback)
40 | router.Register("/register", "GET", handleGoogleRegister)
41 | router.Register("/logout", "GET", handleLogout)
42 | router.Register("/login", "GET", handleGoogleLogin)
43 | // Static files
44 | router.Register("/public/", "GET", handlePublicFile) // NB: regexp
45 | router.Register("/private/", "GET", handlePrivateFile) // NB: regexp
46 | // The logged-in main page
47 | router.Register("/app", "GET", handleApp)
48 | // The not-logged-in main page
49 | router.Register("/", "GET", handleMain)
50 |
51 | // print a diagnostic message and start the server
52 | log.Println("Server running on port " + cfg.AppPort)
53 | http.ListenAndServe(":"+cfg.AppPort, router)
54 | }
55 |
--------------------------------------------------------------------------------
/mongodb_version/src/webapp/router.go:
--------------------------------------------------------------------------------
1 | // Set up an HTTP router that can manage REST requests a little more cleanly.
2 | // In particular, we want to route as early as possible based on
3 | // GET/PUT/POST/DELETE verbs, and we want to use regexps to match routes.
4 | package main
5 |
6 | import (
7 | "net/http"
8 | "regexp"
9 | )
10 |
11 | // each route consists of a pattern, the HTTP verb, and a function to run
12 | type route struct {
13 | rxp *regexp.Regexp // the regexp describing the route
14 | verb string // the verb
15 | handler http.Handler // the handler
16 | }
17 |
18 | // The router's data is just an array of routes
19 | type Router struct {
20 | routes []*route
21 | }
22 |
23 | // extend Router with a function to register a new Route
24 | func (this *Router) Register(regex string, verb string,
25 | handler func(http.ResponseWriter, *http.Request)) {
26 | // NB: compile the regexp before saving it
27 | this.routes = append(this.routes, &route{regexp.MustCompile(regex), verb,
28 | http.HandlerFunc(handler)})
29 | }
30 |
31 | // Handle a request by forwarding to the appropriate route
32 | //
33 | // NB: http.ListenAndServe() requires this to be called ServeHTTP
34 | func (this *Router) ServeHTTP(res http.ResponseWriter, req *http.Request) {
35 | for _, route := range this.routes {
36 | if route.verb == req.Method && route.rxp.MatchString(req.URL.Path) {
37 | route.handler.ServeHTTP(res, req)
38 | return
39 | }
40 | }
41 | http.NotFound(res, req)
42 | }
43 |
--------------------------------------------------------------------------------
/mongodb_version/src/webapp/tplroutes.go:
--------------------------------------------------------------------------------
1 | // Routes for serving content that is generated from templates
2 | package main
3 |
4 | import (
5 | "html/template"
6 | "log"
7 | "net/http"
8 | "time"
9 | )
10 |
11 | // The template for generating the main page (the '/' route)
12 | var mainPage *template.Template
13 |
14 | // The template for generating the logged in page (the '/app' route)
15 | var appPage *template.Template
16 |
17 | // We use these Flash structs to send a message, via a cookie, back to the
18 | // browser when we redirect home on login/logout/registration events.
19 | type Flash struct {
20 | Inf bool // true if there's an info message
21 | InfText string // info message to print
22 | Err bool // true if there's an error message
23 | ErrText string // error message to print
24 | }
25 |
26 | // We call this in order to initialize the templates when we start the app
27 | func buildTemplates() {
28 | var err error
29 | mainPage, err = template.ParseFiles("templates/main.tpl")
30 | if err != nil {
31 | log.Fatal("main template parse error", err)
32 | }
33 | appPage, err = template.ParseFiles("templates/app.tpl")
34 | if err != nil {
35 | log.Fatal("app template parse error", err)
36 | }
37 | }
38 |
39 | // The route for '/' checks for flash cookies (i == Info; e == Error) and
40 | // uses them when generating content via the main template
41 | func handleMain(w http.ResponseWriter, r *http.Request) {
42 | // Prepare to consume flash messages
43 | flash := Flash{false, "", false, ""}
44 |
45 | // if we have an 'iflash' cookie, grab its contents then erase it
46 | cookie, err := r.Cookie("iflash")
47 | if err != http.ErrNoCookie {
48 | flash.InfText = cookie.Value
49 | flash.Inf = true
50 | http.SetCookie(w, &http.Cookie{Name: "iflash", Value: "-1", Expires: time.Now(), Path: "/"})
51 | }
52 |
53 | // if we have an 'eflash' cookie, grab its contents then erase it
54 | cookie, err = r.Cookie("eflash")
55 | if err != http.ErrNoCookie {
56 | flash.ErrText = cookie.Value
57 | flash.Err = true
58 | http.SetCookie(w, &http.Cookie{Name: "eflash", Value: "-1", Expires: time.Now(), Path: "/"})
59 | }
60 |
61 | // Render the template
62 | mainPage.Execute(w, flash)
63 | }
64 |
65 | // The route for '/app' ensures the user is logged in, and then renders the
66 | // app page via a template
67 | func handleApp(w http.ResponseWriter, r *http.Request) {
68 | if !checkLogin(r) {
69 | do403(w, r)
70 | return
71 | }
72 | appPage.Execute(w, nil)
73 | }
74 |
--------------------------------------------------------------------------------
/mongodb_version/templates/app.tpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
10 |
11 |
12 | Example Web App
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/mysql_version/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "OauthGoogleClientId" : "",
3 | "OauthGoogleClientSecret" : "",
4 | "OauthGoogleScopes" : ["https://www.googleapis.com/auth/userinfo.email"],
5 | "OauthGoogleRedirectUrl" : "http://localhost:8080/auth/google/callback",
6 |
7 | "MysqlUsername" : "",
8 | "MysqlPassword" : "",
9 | "MysqlHost" : "",
10 | "MysqlPort" : "3306",
11 | "MysqlDbname" : "",
12 |
13 | "MemcachedHost" : "",
14 | "MemcachedPort" : "11211",
15 |
16 | "AppPort" : "8080"
17 | }
18 |
--------------------------------------------------------------------------------
/mysql_version/private/app.js:
--------------------------------------------------------------------------------
1 | // store all data from the server, so that we can easily populate the form
2 | // when 'edit' is pressed
3 | var origData;
4 |
5 | // Request some code to run when the page is done loading
6 | $(document).ready(function() {
7 | fetch();
8 | });
9 |
10 | // fetch the data from the server and use it to build the table
11 | function fetch() {
12 | $.ajax({
13 | url: "/data",
14 | success: function(data) {
15 | origData = data; // save it, to avoid server accesses later
16 | for (var i = 0; i < data.length; ++i) {
17 | makeRow(i);
18 | }
19 | },
20 | dataType: "json"
21 | })
22 | }
23 |
24 | // build a row in the table, using an index into origData
25 | function makeRow(i) {
26 | // add a row
27 | $('#dataTable').append('
');
28 | // add cells for data
29 | $('#row'+i).append('
'+origData[i].smallnote+'
')
30 | $('#row'+i).append('
'+origData[i].bignote+'
')
31 | $('#row'+i).append('
'+origData[i].favint+'
')
32 | $('#row'+i).append('
'+origData[i].favfloat+'
')
33 | $('#row'+i).append('
'+origData[i].trickfloat+'
')
34 | // add buttons for edit and delete
35 | $('#row'+i).append('
' + '' + '' + '
');
36 | }
37 |
38 | // wire up a click handler for 'edit' buttons
39 | $(document).on("click", ".edit-btn", function() {
40 | // populate the fields of the modal form
41 | var i = $(this).data("id");
42 | $('#id').val(origData[i].id)
43 | $('#smallnote').val(origData[i].smallnote)
44 | $('#bignote').val(origData[i].bignote)
45 | $('#favint').val(origData[i].favint)
46 | $('#favfloat').val(origData[i].favfloat)
47 | $('#trickfloat').val(origData[i].trickfloat)
48 |
49 | // set modal title
50 | $("#modal-header").text("Edit Entry")
51 |
52 | // wire up the modal "save" button
53 | $("#modal-save").unbind();
54 | $("#modal-save").click(function(){
55 | // wrap up the new data in an object that is easy to send
56 | var newdata = {
57 | id : $('#id').val()+"",
58 | smallnote : $('#smallnote').val()+"",
59 | bignote : $('#bignote').val()+"",
60 | favint : $('#favint').val()+"",
61 | favfloat : $('#favfloat').val()+"",
62 | trickfloat : $('#trickfloat').val()+"",
63 | }
64 | $.ajax({
65 | type: "PUT",
66 | url: "/data/"+newdata.id,
67 | contentType: "application/json",
68 | data: JSON.stringify(newdata),
69 | success : function(data) {
70 | location.reload();
71 | },
72 | error : function(data) {
73 | window.alert("Unspecified error: " + data)
74 | }
75 | });
76 | });
77 | });
78 |
79 | // wire up a click handler for 'delete' buttons
80 | $(document).on("click", ".delete-btn", function() {
81 | // get the id
82 | var i = $(this).data("id")
83 | var id = origData[i].id
84 |
85 | // send a DELETE
86 | $.ajax({
87 | type: "DELETE",
88 | url: "/data/"+id,
89 | contentType: "application/json",
90 | success : function(data) {
91 | location.reload();
92 | },
93 | error : function(data) {
94 | window.alert("Unspecified error: " + data)
95 | }
96 | });
97 | });
98 |
99 | // wire up a click handler for 'add' button
100 | $(document).on("click", ".add-btn", function() {
101 | // populate the fields of the modal form
102 | var i = $(this).data("id");
103 | $('#id').val("")
104 | $('#smallnote').val("")
105 | $('#bignote').val("")
106 | $('#favint').val("")
107 | $('#favfloat').val("")
108 | $('#trickfloat').val("")
109 |
110 | // set the title
111 | $("#modal-header").text("Add Entry")
112 |
113 | // wire up the modal "save" button
114 | $("#modal-save").unbind();
115 | $("#modal-save").click(function(){
116 | // wrap up the new data in an object that is easy to send
117 | var newdata = {
118 | smallnote : $('#smallnote').val()+"",
119 | bignote : $('#bignote').val()+"",
120 | favint : $('#favint').val()+"",
121 | favfloat : $('#favfloat').val()+"",
122 | trickfloat : $('#trickfloat').val()+"",
123 | }
124 | $.ajax({
125 | type: "POST",
126 | url: "/data",
127 | contentType: "application/json",
128 | data: JSON.stringify(newdata),
129 | success : function(data) {
130 | location.reload();
131 | },
132 | error : function(data) {
133 | window.alert("Unspecified error: " + data)
134 | }
135 | });
136 | });
137 | });
138 |
--------------------------------------------------------------------------------
/mysql_version/sampledata.csv:
--------------------------------------------------------------------------------
1 | "Short text","Long text, or at least text that could be long.",7,-2.6,6
2 | "More text","Let's just have short text, so this doesn't get too crazy",-8,99.3,-42
3 | "No third option","This example will trigger the ""null"" 5th field",-8,63.9,""
4 | "Comprehensiveness","We should take care to ensure that negatives, positives, decimals, nulls, and special text (like quotation marks) all work",22,9.4,-22.9
5 |
--------------------------------------------------------------------------------
/mysql_version/setenv.sh:
--------------------------------------------------------------------------------
1 | # This script is a convenience. When working on Windows using 'git bash' as
2 | # the shell, we can enter the golang-webapp-tutorial folder and then type
3 | # '. setenv.sh' to set our GOPATH. Doing so ensures that 'go build ...'
4 | # works as desired.
5 | export GOPATH=`pwd`
6 |
--------------------------------------------------------------------------------
/mysql_version/src/admin/main.go:
--------------------------------------------------------------------------------
1 | // Admin app for managing aspects of the program. The program administers an
2 | // app according to the information provided in a config file.
3 | // Administrative tasks include:
4 | // - Create or Drop the entire Database
5 | // - Reset the Users or Data table
6 | // - Populate the Data table with data from a CSV
7 | // - Activate new user registrations
8 | // There is also a simple function to show how to update this file to run
9 | // arbitrary one-off operations on the database
10 | package main
11 |
12 | import (
13 | "database/sql"
14 | "encoding/csv"
15 | "encoding/json"
16 | "flag"
17 | "fmt"
18 | _ "github.com/go-sql-driver/mysql"
19 | "io"
20 | "log"
21 | "os"
22 | )
23 |
24 | // Configuration information for Google OAuth, MySQL, and Memcached. We
25 | // parse this from a JSON config file
26 | //
27 | // NB: field names must start with Capital letter for JSON parse to work
28 | //
29 | // NB: between the field names and JSON mnemonics, it should be easy to
30 | // figure out what each field does
31 | type Config struct {
32 | ClientId string `json:"OauthGoogleClientId"`
33 | ClientSecret string `json:"OauthGoogleClientSecret"`
34 | Scopes []string `json:"OauthGoogleScopes"`
35 | RedirectUrl string `json:"OauthGoogleRedirectUrl"`
36 | DbUser string `json:"MysqlUsername"`
37 | DbPass string `json:"MysqlPassword"`
38 | DbHost string `json:"MysqlHost"`
39 | DbPort string `json:"MysqlPort"`
40 | DbName string `json:"MysqlDbname"`
41 | McdHost string `json:"MemcachedHost"`
42 | McdPort string `json:"MemcachedPort"`
43 | AppPort string `json:"AppPort"`
44 | }
45 |
46 | // The configuration information for the app we're administering
47 | var cfg Config
48 |
49 | // Load a JSON file that has all the config information for our app, and put
50 | // the JSON contents into the cfg variable
51 | func loadConfig(cfgFileName string) {
52 | f, err := os.Open(cfgFileName)
53 | if err != nil {
54 | log.Fatal(err)
55 | }
56 | defer f.Close()
57 | jsonParser := json.NewDecoder(f)
58 | if err = jsonParser.Decode(&cfg); err != nil {
59 | log.Fatal(err)
60 | }
61 | }
62 |
63 | // Create the database that will be used by our program. The database name
64 | // is read from the config file, so this code is very generic.
65 | func createDatabase() {
66 | // NB: trailing '/' is necessary to indicate that we aren't
67 | // specifying any database
68 | db, err := sql.Open("mysql",
69 | cfg.DbUser+":"+cfg.DbPass+"@("+cfg.DbHost+":"+cfg.DbPort+")/")
70 | if err != nil {
71 | log.Fatal(err)
72 | }
73 | defer db.Close()
74 | _, err = db.Exec("CREATE DATABASE `" + cfg.DbName + "`;")
75 | if err != nil {
76 | log.Fatal(err)
77 | }
78 | }
79 |
80 | // Delete the database that was being used by our program
81 | // NB: code is almost identical to createDatabase()
82 | func deleteDatabase() {
83 | db, err := sql.Open("mysql",
84 | cfg.DbUser+":"+cfg.DbPass+"@("+cfg.DbHost+":"+cfg.DbPort+")/")
85 | if err != nil {
86 | log.Fatal(err)
87 | }
88 | defer db.Close()
89 | _, err = db.Exec("DROP DATABASE `" + cfg.DbName + "`;")
90 | if err != nil {
91 | log.Fatal(err)
92 | }
93 | }
94 |
95 | // Connect to the database, and return the corresponding database object
96 | //
97 | // NB: will fail if the database hasn't been created
98 | func openDB() *sql.DB {
99 | db, err := sql.Open("mysql", cfg.DbUser+":"+cfg.DbPass+"@tcp("+cfg.DbHost+":"+cfg.DbPort+")/"+cfg.DbName)
100 | if err != nil {
101 | log.Fatal(err)
102 | }
103 | // Ping the database to be sure it's live
104 | err = db.Ping()
105 | if err != nil {
106 | log.Fatal(err)
107 | }
108 | return db
109 | }
110 |
111 | // drop and re-create the Users table. The table name, field names, and
112 | // field types are hard-coded into this function... for Google OAuth, you
113 | // don't need to change this unless you want to add profile pics or other
114 | // user-defined columns
115 | func resetUserTable(db *sql.DB) {
116 | // drop the old users table
117 | stmt, err := db.Prepare("DROP TABLE IF EXISTS users")
118 | if err != nil {
119 | log.Fatal(err)
120 | }
121 | defer stmt.Close()
122 | _, err = stmt.Exec()
123 | if err != nil {
124 | log.Fatal(err)
125 | }
126 |
127 | // create the new users table
128 | //
129 | // NB: state is 0, 1 for pending, active
130 | //
131 | // NB: we technically don't need a prepared statement for a one-time
132 | // query, but knowing the statement is legit makes debugging
133 | // database interactions a tad easier
134 | stmt, err = db.Prepare(`CREATE TABLE users (
135 | id int not null auto_increment PRIMARY KEY,
136 | state int not null,
137 | googleid varchar(100) not null,
138 | name varchar(100) not null,
139 | email varchar(100) not null
140 | )`)
141 | if err != nil {
142 | log.Fatal(err)
143 | }
144 | _, err = stmt.Exec()
145 | if err != nil {
146 | log.Fatal(err)
147 | }
148 | }
149 |
150 | // drop and re-create the Data table. The table name, field names, and field
151 | // types are hard-coded into this function. For any realistic app, you'll
152 | // need more complex code, which creates multiple tables, any foreign key
153 | // constraints, views, etc. For our demo app, this + resetUserTable() is all
154 | // we need to configure the data model.
155 | func resetDataTable(db *sql.DB) {
156 | // drop the old table
157 | stmt, err := db.Prepare("DROP TABLE IF EXISTS data")
158 | if err != nil {
159 | log.Fatal(err)
160 | }
161 | defer stmt.Close()
162 | _, err = stmt.Exec()
163 | if err != nil {
164 | log.Fatal(err)
165 | }
166 |
167 | // create the new objectives table
168 | stmt, err = db.Prepare(`CREATE TABLE data (
169 | id int not null auto_increment PRIMARY KEY,
170 | smallnote varchar(2000) not null,
171 | bignote text not null,
172 | favint int not null,
173 | favfloat float not null,
174 | trickfloat float)`)
175 | if err != nil {
176 | log.Fatal(err)
177 | }
178 | _, err = stmt.Exec()
179 | if err != nil {
180 | log.Fatal(err)
181 | }
182 | }
183 |
184 | // Parse a CSV so that each line becomes an array of strings, and then use
185 | // the array of strings to push a row to the data table
186 | //
187 | // NB: It goes without saying that this is hard-coded for our Data table.
188 | // However, the hard-coding is quite simple: we just need to hard-code
189 | // the INSERT statement itself, and then the mapping of array positions
190 | // to VALUES in that INSERT statement.
191 | func loadCsv(csvname *string, db *sql.DB) {
192 | // load the csv file
193 | file, err := os.Open(*csvname)
194 | if err != nil {
195 | log.Fatal(err)
196 | }
197 | defer file.Close()
198 |
199 | // create the prepared statement for inserting into the database
200 | //
201 | // I chose to have a single statement with 5 parameters, instead of
202 | // two statements to reflect the possible null-ness of the last
203 | // column. That's not an objectively better approach, but for this
204 | // example, it results in less code.
205 | stmt, err := db.Prepare("INSERT INTO data(smallnote, bignote, favint, favfloat, trickfloat) VALUES(?, ?, ?, ?, ?)")
206 | if err != nil {
207 | log.Fatal(err)
208 | }
209 |
210 | // parse the csv, one record at a time
211 | reader := csv.NewReader(file)
212 | reader.Comma = ','
213 | count := 0 // count insertions, for the sake of nice output
214 | for {
215 | // get next row... exit on EOF
216 | row, err := reader.Read()
217 | if err == io.EOF {
218 | break
219 | } else if err != nil {
220 | log.Fatal(err)
221 | }
222 | // We have an array of strings, representing the data. We
223 | // can dump it into the database by matching array indices
224 | // with parameters to the INSERT statement. The only tricky
225 | // part is that our last column can be null, so we need a way
226 | // to handle null values, which are '""' in the CSV
227 | var lastcol *string = nil
228 | if row[4] != "" {
229 | lastcol = &row[4]
230 | }
231 | // NB: no need for casts... the MySQL driver will figure out
232 | // the types
233 | _, err = stmt.Exec(row[0], row[1], row[2], row[3], lastcol)
234 | if err != nil {
235 | log.Fatal(err)
236 | }
237 | count++
238 | }
239 | log.Println("Added", count, "rows")
240 | }
241 |
242 | // When a user registers, the new account is not active until the
243 | // administrator activates it. This function lists registrations that are
244 | // not yet activated
245 | func listNewAccounts(db *sql.DB) {
246 | // get all inactive rows from the database
247 | rows, err := db.Query("SELECT * FROM users WHERE state = ?", 0)
248 | if err != nil {
249 | log.Fatal(err)
250 | }
251 | defer rows.Close()
252 | // scan into these vars, which get reused on each loop iteration
253 | var (
254 | id int
255 | state int
256 | googleid string
257 | name string
258 | email string
259 | )
260 | // print a header
261 | fmt.Println("New Users:")
262 | fmt.Println("[id googleid name email]")
263 | // print the rows
264 | for rows.Next() {
265 | err = rows.Scan(&id, &state, &googleid, &name, &email)
266 | if err != nil {
267 | log.Fatal(err)
268 | }
269 | fmt.Println(id, googleid, name, email)
270 | }
271 | // On error, rows.Next() returns false... but we still need to check
272 | // for errors
273 | err = rows.Err()
274 | if err != nil {
275 | log.Fatal(err)
276 | }
277 | }
278 |
279 | // Since this is an administrative interface, we don't need to do anything
280 | // too fancy for the account activation: the flags include the ID to update,
281 | // we just use it to update the database, and we don't worry about the case
282 | // where the account is already activated
283 | func activateAccount(db *sql.DB, id int) {
284 | _, err := db.Exec("UPDATE users SET state = 1 WHERE id = ?", id)
285 | if err != nil {
286 | log.Fatal(err)
287 | }
288 | }
289 |
290 | // We occasionally need to do one-off queries that can't really be predicted
291 | // ahead of time. When that time comes, we can edit this function,
292 | // recompile, and then run with the "oneoff" flag to do the corresponding
293 | // action. For now, it's hard coded to delete userid=1 from the Users table
294 | func doOneOff(db *sql.DB) {
295 | _, err := db.Exec("DELETE FROM users WHERE id = ?", 1)
296 | if err != nil {
297 | log.Fatal(err)
298 | }
299 | }
300 |
301 | // Main routine: Use the command line options to determine what action to
302 | // take, then forward to the appropriate function. Since this program is for
303 | // quick admin tasks, all of the above functions terminate on any error.
304 | // That means we don't need error checking in this code... any function we
305 | // call will only return on success.
306 | func main() {
307 | // parse command line options
308 | configPath := flag.String("configfile", "config.json", "Path to the configuration (JSON) file")
309 | csvName := flag.String("csvfile", "data.csv", "The csv file to parse")
310 | opCreateDb := flag.Bool("createschema", false, "Create the MySQL database into which we will create tables?")
311 | opDeleteDb := flag.Bool("deleteschema", false, "Delete the entire MySQL database?")
312 | opResetUserTable := flag.Bool("resetuserstable", false, "Delete and re-create the users table?")
313 | opResetDataTable := flag.Bool("resetdatatable", false, "Delete and re-create the data table?")
314 | opCsv := flag.Bool("loadcsv", false, "Load a csv into the data table?")
315 | opListNewReg := flag.Bool("listnewusers", false, "List new registrations?")
316 | opRegister := flag.Int("activatenewuser", -1, "Complete pending registration for a user")
317 | opOneOff := flag.Bool("oneoff", false, "Run a one-off query")
318 | flag.Parse()
319 |
320 | // load the JSON config file
321 | loadConfig(*configPath)
322 |
323 | // if we are creating or deleting a database, do the op and return
324 | if *opCreateDb {
325 | createDatabase()
326 | return
327 | }
328 | if *opDeleteDb {
329 | deleteDatabase()
330 | return
331 | }
332 |
333 | // open the database
334 | db := openDB()
335 | defer db.Close()
336 |
337 | // all other ops are handled below:
338 | if *opResetUserTable {
339 | resetUserTable(db)
340 | }
341 | if *opResetDataTable {
342 | resetDataTable(db)
343 | }
344 | if *opCsv {
345 | loadCsv(csvName, db)
346 | }
347 | if *opListNewReg {
348 | listNewAccounts(db)
349 | }
350 | if *opRegister != -1 {
351 | activateAccount(db, *opRegister)
352 | }
353 | if *opOneOff {
354 | doOneOff(db)
355 | }
356 | }
357 |
--------------------------------------------------------------------------------
/mysql_version/src/statichttpserver/main.go:
--------------------------------------------------------------------------------
1 | // When you need to serve a folder's contents via HTTP, this is a
2 | // satisfactory technique. It's not super-resilient, but it is about as easy
3 | // as the corresponding Python one-liner, without requiring us to also have
4 | // Python installed.
5 | //
6 | // This is not intended for use in a production setting. EVER. It is useful
7 | // when you've got some HTML5 that CORS forbids from rendering correctly via
8 | // file://. In such cases, you can use this to serve it via
9 | // http://localhost:8080
10 | package main
11 |
12 | import (
13 | "flag"
14 | "fmt"
15 | "net/http"
16 | )
17 |
18 | func main() {
19 |
20 | // use a command-line flag (-p) to set the port on which to serve
21 | port := flag.String("p", "8080",
22 | "The port on which to install the http server")
23 | // use a command-line flag (-f) to specify the root folder to serve
24 | folder := flag.String("f", "./",
25 | "The folder from which to serve requests")
26 |
27 | flag.Parse()
28 |
29 | // print our configuration
30 | fmt.Println("Serving " + *folder + " on port " + *port)
31 |
32 | // On any request, we add the folder prefix and then attempt to serve
33 | // the file that results. Note that this approach will display
34 | // folders, too.
35 | http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
36 | http.ServeFile(res, req, *folder+req.URL.Path)
37 | })
38 | http.ListenAndServe(":"+*port, nil)
39 | }
40 |
--------------------------------------------------------------------------------
/mysql_version/src/webapp/auth.go:
--------------------------------------------------------------------------------
1 | // Functions and objects that are used in order to authenticate the user
2 | package main
3 |
4 | import (
5 | "crypto/rand"
6 | "encoding/base64"
7 | "encoding/json"
8 | "golang.org/x/oauth2"
9 | "io"
10 | "io/ioutil"
11 | "log"
12 | "net/http"
13 | "sync"
14 | "time"
15 | )
16 |
17 | // create an "enum" for dealing with responses from a login request
18 | const (
19 | registerOk = iota // registration success
20 | registerErr = iota // unspecified registration error
21 | registerExist = iota // register a registered user
22 | loginOk = iota // login success
23 | loginErr = iota // unspecified login error
24 | loginNotReg = iota // log in of unregistered user
25 | loginNotActive = iota // log in of unactivated registered user
26 | )
27 |
28 | // When the user logs in, we get the Google User ID, Email, and Name.
29 | //
30 | // When we get the ID, it's from Google, so we can trust it. We also put it
31 | // in a cookie on the client browser, so that subsequent requests can prove
32 | // their identity.
33 | //
34 | // The problem is that anyone who knows the ID can spoof the user. To
35 | // secure, we don't just save the ID, we also save a token that we randomly
36 | // generate upon login. If the ID and Token match, then we trust you to be
37 | // who you say you are.
38 | //
39 | // Note that an attacker can still assume your identity if it can access your
40 | // cookies, but it can't assume your identity just by knowing your ID
41 | //
42 | // Note, too, that multiple requests can access this map, so we need it to be
43 | // synchronized
44 | //
45 | // Lastly, note that if you have multiple servers running, and a user
46 | // migrates among servers, their login info will be lost, and they'll have to
47 | // re-log in. The only way to avoid that is to persist this map to some
48 | // location that is global across nodes, and we're not going to do that in
49 | // this simple example.
50 | var cookieStore = struct {
51 | sync.RWMutex
52 | m map[string]string
53 | }{m: make(map[string]string)}
54 |
55 | // Check if a request is being made from an authenticated context
56 | func checkLogin(r *http.Request) bool {
57 | // grab the "id" cookie, fail if it doesn't exist
58 | cookie, err := r.Cookie("id")
59 | if err == http.ErrNoCookie {
60 | return false
61 | }
62 |
63 | // grab the "key" cookie, fail if it doesn't exist
64 | key, err := r.Cookie("key")
65 | if err == http.ErrNoCookie {
66 | return false
67 | }
68 |
69 | // make sure we've got the right stuff in the hash
70 | cookieStore.RLock()
71 | defer cookieStore.RUnlock()
72 | return cookieStore.m[cookie.Value] == key.Value
73 | }
74 |
75 | // Generate 256 bits of randomness
76 | func sessionId() string {
77 | b := make([]byte, 32)
78 | if _, err := io.ReadFull(rand.Reader, b); err != nil {
79 | return ""
80 | }
81 | return base64.URLEncoding.EncodeToString(b)
82 | }
83 |
84 | // To in order to log the user out, we need to remove the corresponding
85 | // cookie from our local cookie store, and then erase the cookies on the
86 | // client browser.
87 | //
88 | // WARNING: you must call this before any other code in the logout route, or
89 | // else there is a risk that the header will already be sent.
90 | func processLogoutRequest(w http.ResponseWriter, r *http.Request) {
91 | // grab the "ID" cookie, erase from map if it is found
92 | id, err := r.Cookie("id")
93 | if err != http.ErrNoCookie {
94 | cookieStore.Lock()
95 | delete(cookieStore.m, id.Value)
96 | cookieStore.Unlock()
97 | // create a log-out (info) flash
98 | flash := http.Cookie{Name: "iflash", Value: "Logout successful", Path: "/"}
99 | http.SetCookie(w, &flash)
100 | }
101 |
102 | // clear the cookies on the client
103 | clearID := http.Cookie{Name: "id", Value: "-1", Expires: time.Now(), Path: "/"}
104 | http.SetCookie(w, &clearID)
105 | clearVal := http.Cookie{Name: "key", Value: "-1", Expires: time.Now(), Path: "/"}
106 | http.SetCookie(w, &clearVal)
107 | }
108 |
109 | // This is used in the third and final step of the OAuth dance. Google is
110 | // sending back a URL whose QueryString encodes a "state" and a "code". The
111 | // "state" is something we sent to Google, that we expect to get back... it
112 | // helps prevent spoofing. The "code" is something we can send to Google to
113 | // get the user's info. From there, we save some info in a session cookie to
114 | // keep the user logged in.
115 | //
116 | // NB: return values will be based on the "enum" at the top of this file
117 | func processLoginReply(w http.ResponseWriter, r *http.Request) int {
118 | // extract the code and state from the querystring
119 | code := r.FormValue("code")
120 | state := r.FormValue("state")
121 |
122 | // choose a default error code to return, depending on login or
123 | // register attempt. First character of the 'state' string is 'r'
124 | // for register, 'l' for login.
125 | errorCode := loginErr
126 | if state[:1] == "r" {
127 | errorCode = registerErr
128 | }
129 |
130 | // validate state... it needs to match the secret we sent
131 | if state[1:] != oauthStateString {
132 | log.Println("state didn't match", oauthStateString)
133 | return errorCode
134 | }
135 |
136 | // convert the authorization code into a token
137 | token, err := oauthConf.Exchange(oauth2.NoContext, code)
138 | if err != nil {
139 | log.Println("token exchange error", code)
140 | return errorCode
141 | }
142 |
143 | // send the token to Google, get a JSON blob back
144 | response, err := http.Get(googleIdApi + token.AccessToken)
145 | if err != nil {
146 | log.Println("token lookup error", response)
147 | return errorCode
148 | }
149 |
150 | // the JSON blob has Google ID, Name, and Email... convert to a map
151 | //
152 | // NB: we don't convert via jsonParser.Decode, because we don't know
153 | // exactly what fields we'll get back
154 | defer response.Body.Close()
155 | contents, err := ioutil.ReadAll(response.Body)
156 | if err != nil {
157 | log.Println("Error reading JSON reply")
158 | return errorCode
159 | }
160 | var f interface{}
161 | err = json.Unmarshal([]byte(string(contents)), &f)
162 | if err != nil {
163 | log.Println("Error unmarshaling JSON reply")
164 | return errorCode
165 | }
166 | m := f.(map[string]interface{})
167 | // NB: m should now hold the following interfaces:
168 | // m["id"], m["email"], m["given_name"], m["name"], and m["family_name"]
169 |
170 | // look up the user in the database
171 | //
172 | // NB: check err first, otherwise we might mistake a 'nil' for
173 | // 'unregistered' when we should return registerExist
174 | u, err := getUserById(m["id"].(string))
175 | if err != nil {
176 | log.Println("Unspecified SQL error during user lookup")
177 | return errorCode
178 | }
179 | if u == nil {
180 | // no user... let's hope this is a registration request
181 | if state[:1] != "r" {
182 | log.Println("Attempt to log in an unregistered user")
183 | return loginNotReg
184 | }
185 | // add a registration (0 == not active)
186 | err = addNewUser(m["id"].(string), m["name"].(string), m["email"].(string), 0)
187 | if err != nil {
188 | log.Println("error adding new user")
189 | return registerErr
190 | }
191 | return registerOk
192 | } else {
193 | // we have a user... let's hope this is a login request
194 | if state[:1] == "r" {
195 | log.Println("Attempt to register an existing user")
196 | return registerExist
197 | }
198 | // is the user allowed to log in?
199 | if u.state == 0 {
200 | log.Println("Attempt to log in unactivated account")
201 | return loginNotActive
202 | }
203 | // it's a valid login!
204 |
205 | // To keep the user logged in, we save two cookies. The
206 | // first has the ID, the second has a random value. We then
207 | // put ID->rand in our cookieStore map. Subsequent requests
208 | // can grab the cookies and check the map
209 | //
210 | // NB: no Expires ==> it's a session cookie
211 | cookie := http.Cookie{Name: "id", Value: m["id"].(string), Path: "/"}
212 | http.SetCookie(w, &cookie)
213 | unique := sessionId()
214 | cookie = http.Cookie{Name: "key", Value: unique, Path: "/"}
215 | http.SetCookie(w, &cookie)
216 | cookieStore.Lock()
217 | cookieStore.m[m["id"].(string)] = unique
218 | cookieStore.Unlock()
219 | return loginOk
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/mysql_version/src/webapp/authroutes.go:
--------------------------------------------------------------------------------
1 | // Routes related to OAuth, logging in, logging out, and registering
2 | package main
3 |
4 | import (
5 | "golang.org/x/oauth2"
6 | "log"
7 | "net/http"
8 | )
9 |
10 | // The route for '/login' starts the OAuth dance by redirecting to Google
11 | //
12 | // This is the first step of the OAuth dance. Via oauthConf, we send
13 | // ClientID, ClientSecret, and RedirectURL. Step 2 is for google to check
14 | // these fields and get the user to log in.
15 | func handleGoogleLogin(w http.ResponseWriter, r *http.Request) {
16 | // NB: prefix the state string with 'l' so we can tell it's a login
17 | // later. That's much easier than dealing with two different
18 | // redirect routes.
19 | url := oauthConf.AuthCodeURL("l"+oauthStateString, oauth2.AccessTypeOnline)
20 | http.Redirect(w, r, url, http.StatusTemporaryRedirect)
21 | }
22 |
23 | // The route for '/register' is identical to '/login', except we change the
24 | // state string to know it's a request to register.
25 | func handleGoogleRegister(w http.ResponseWriter, r *http.Request) {
26 | url := oauthConf.AuthCodeURL("r"+oauthStateString, oauth2.AccessTypeOnline)
27 | http.Redirect(w, r, url, http.StatusTemporaryRedirect)
28 | }
29 |
30 | // The route for '/auth/google/callback' finishes the OAuth dance: It
31 | // processes the Google response, and only sends us to the app page if
32 | // the dance was successful.
33 | func handleGoogleCallback(w http.ResponseWriter, r *http.Request) {
34 | res := processLoginReply(w, r)
35 | if res == loginOk {
36 | http.Redirect(w, r, "/app", http.StatusTemporaryRedirect)
37 | return
38 | }
39 |
40 | // we can't let the user in yet. Set a flash cookie to explain, then send to '/'
41 | name := "eflash" // i or e for info or error
42 | val := ""
43 | if res == registerOk {
44 | name = "iflash"
45 | val = "Registration succeeded. Please wait for an administrator to confirm your account."
46 | } else if res == registerErr {
47 | val = "Registration error. Please try again later."
48 | } else if res == registerExist {
49 | val = "The account you specified has already been registered."
50 | } else if res == loginNotReg {
51 | val = "The account you specified has not been registered."
52 | } else if res == loginNotActive {
53 | val = "The account you specified has not yet been activated by the administrator."
54 | } else if res == loginErr {
55 | val = "Login error. Please try again later."
56 | } else {
57 | log.Fatal("processLoginReply() returned invalid status")
58 | }
59 | http.SetCookie(w, &http.Cookie{Name: name, Value: val, Path: "/"})
60 | http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
61 | }
62 |
63 | // The route for '/logout' logs the user out and redirects to home
64 | func handleLogout(w http.ResponseWriter, r *http.Request) {
65 | processLogoutRequest(w, r)
66 | http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
67 | }
68 |
--------------------------------------------------------------------------------
/mysql_version/src/webapp/config.go:
--------------------------------------------------------------------------------
1 | // All hard-coded app configuration is in this file, as is all code for
2 | // interacting with config information that is stored in a JSON file.
3 | package main
4 |
5 | import (
6 | "encoding/json"
7 | "golang.org/x/oauth2"
8 | "golang.org/x/oauth2/google"
9 | "log"
10 | "os"
11 | )
12 |
13 | // Information regarding the Google OAuth provider... most of this can't be
14 | // set until we load the JSON file
15 | var oauthConf = &oauth2.Config{
16 | ClientID: "",
17 | ClientSecret: "",
18 | Endpoint: google.Endpoint,
19 | RedirectURL: "",
20 | }
21 |
22 | // For extra security, we use this random string in OAuth calls... we pass it
23 | // to the server, and we expect to get it back when we get the reply
24 | //
25 | // NB: We can't re-generate this on the fly, because we need all instances of
26 | // the server to have the same string, or else they can't all satisfy the
27 | // same client
28 | const oauthStateString = "FJDKSIE7S88dhjflsid83kdlsHp7TEbpg6TwHBWdJzNwYod1i5ZTbrIF5bEoO3oP"
29 |
30 | // This is the service to which we request identifying info for a google ID
31 | const googleIdApi = "https://www.googleapis.com/oauth2/v2/userinfo?access_token="
32 |
33 | // Configuration information for Google OAuth, MySQL, and Memcached. We
34 | // parse this from a JSON config file
35 | //
36 | // NB: field names must start with Capital letter for JSON parse to work
37 | //
38 | // NB: between the field names and JSON mnemonics, it should be easy to
39 | // figure out what each field does
40 | type Config struct {
41 | ClientId string `json:"OauthGoogleClientId"`
42 | ClientSecret string `json:"OauthGoogleClientSecret"`
43 | Scopes []string `json:"OauthGoogleScopes"`
44 | RedirectUrl string `json:"OauthGoogleRedirectUrl"`
45 | DbUser string `json:"MysqlUsername"`
46 | DbPass string `json:"MysqlPassword"`
47 | DbHost string `json:"MysqlHost"`
48 | DbPort string `json:"MysqlPort"`
49 | DbName string `json:"MysqlDbname"`
50 | McdHost string `json:"MemcachedHost"`
51 | McdPort string `json:"MemcachedPort"`
52 | AppPort string `json:"AppPort"`
53 | }
54 |
55 | // The configuration information for the app we're administering
56 | var cfg Config
57 |
58 | // Load a JSON file that has all the config information for our app, and put
59 | // the JSON contents into the cfg variable
60 | func loadConfig(cfgFileName string) {
61 | // first, load the JSON file and parse it into /cfg/
62 | f, err := os.Open(cfgFileName)
63 | if err != nil {
64 | log.Fatal(err)
65 | }
66 | defer f.Close()
67 | jsonParser := json.NewDecoder(f)
68 | if err = jsonParser.Decode(&cfg); err != nil {
69 | log.Fatal(err)
70 | }
71 |
72 | // second, update our OAuth stuff
73 | oauthConf.ClientID = cfg.ClientId
74 | oauthConf.ClientSecret = cfg.ClientSecret
75 | oauthConf.Scopes = cfg.Scopes
76 | oauthConf.RedirectURL = cfg.RedirectUrl
77 | }
78 |
--------------------------------------------------------------------------------
/mysql_version/src/webapp/dataroutes.go:
--------------------------------------------------------------------------------
1 | // Routes related to REST paths for accessing the DATA table
2 | package main
3 |
4 | import (
5 | "encoding/json"
6 | "io/ioutil"
7 | "log"
8 | "net/http"
9 | "strconv"
10 | )
11 |
12 | // a helper function to send HTTP 403 / Forbidden when the user is not logged
13 | // in
14 | func do403(w http.ResponseWriter, r *http.Request) {
15 | http.Error(w, "Forbidden", http.StatusForbidden)
16 | }
17 |
18 | // Helper routine for sending JSON back to the client a bit more cleanly
19 | func jResp(w http.ResponseWriter, data interface{}) {
20 | payload, err := json.Marshal(data)
21 | if err != nil {
22 | log.Println("Internal Server Error:", err.Error())
23 | http.Error(w, "Internal Server Error", http.StatusInternalServerError)
24 | return
25 | }
26 | w.Header().Set("Content-Type", "application/json; charset=utf-8")
27 | w.Write([]byte(string(payload)))
28 | }
29 |
30 | // The GET route for all rows of DATA
31 | func handleGetAllData(w http.ResponseWriter, r *http.Request) {
32 | // if authentication passes, use getAllRows to get a big JSON blob to
33 | // send back
34 | if !checkLogin(r) {
35 | do403(w, r)
36 | return
37 | }
38 | w.Write([]byte(getAllRows()))
39 | }
40 |
41 | // The PUT route for updating a row of DATA
42 | func handlePutData(w http.ResponseWriter, r *http.Request) {
43 | // check authentication
44 | if !checkLogin(r) {
45 | do403(w, r)
46 | return
47 | }
48 |
49 | // get the ID from the querystring
50 | id, err := strconv.Atoi(r.URL.Path[6:])
51 | if err != nil {
52 | jResp(w, "invalid id: "+r.URL.Path+" "+r.URL.Path[6:])
53 | return
54 | }
55 |
56 | // the JSON blob should have smallnote, bignote, favint, favfloat,
57 | // and trickfloat... read into a map of interfaces
58 | defer r.Body.Close()
59 | contents, err := ioutil.ReadAll(r.Body)
60 | if err != nil {
61 | log.Println("Error reading body of PUT request")
62 | jResp(w, "error")
63 | return
64 | }
65 | var f interface{}
66 | err = json.Unmarshal([]byte(string(contents)), &f)
67 | if err != nil {
68 | log.Println("Error unmarshaling JSON reply")
69 | jResp(w, "error")
70 | return
71 | }
72 | m := f.(map[string]interface{})
73 |
74 | // send the new data to the database
75 | ok := updateDataRow(id, m)
76 | if ok {
77 | jResp(w, "{res: 'ok'}")
78 | } else {
79 | jResp(w, "{res: 'error'}")
80 | }
81 | }
82 |
83 | // The GET route for viewing one row of DATA
84 | func handleGetDataOne(w http.ResponseWriter, r *http.Request) {
85 | // check authentication
86 | if !checkLogin(r) {
87 | do403(w, r)
88 | return
89 | }
90 |
91 | // get the ID from the querystring
92 | id, err := strconv.Atoi(r.URL.Path[6:])
93 | if err != nil {
94 | jResp(w, "invalid id: "+r.URL.Path+" "+r.URL.Path[6:])
95 | return
96 | }
97 |
98 | // get a big JSON blob via getRow, send it back
99 | w.Write([]byte(getRow(id)))
100 | }
101 |
102 | // The DELETE route for removing one row of DATA
103 | func handleDeleteData(w http.ResponseWriter, r *http.Request) {
104 | // authenticate, then get ID from querystring
105 | if !checkLogin(r) {
106 | do403(w, r)
107 | return
108 | }
109 |
110 | id, err := strconv.Atoi(r.URL.Path[6:])
111 | if err != nil {
112 | jResp(w, "invalid id: "+r.URL.Path+" "+r.URL.Path[6:])
113 | return
114 | }
115 |
116 | // delete the row
117 | ok := deleteDataRow(id)
118 | if ok {
119 | jResp(w, "{res: 'ok'}")
120 | } else {
121 | jResp(w, "{res: 'error'}")
122 | }
123 | }
124 |
125 | // The POST route for adding a new row of DATA
126 | func handlePostData(w http.ResponseWriter, r *http.Request) {
127 | // authenticate
128 | if !checkLogin(r) {
129 | do403(w, r)
130 | return
131 | }
132 |
133 | // the JSON blob should have smallnote, bignote, favint, favfloat,
134 | // and trickfloat... turn it into a map of interfaces
135 | defer r.Body.Close()
136 | contents, err := ioutil.ReadAll(r.Body)
137 | if err != nil {
138 | log.Println("Error reading body of POST request")
139 | jResp(w, "error")
140 | return
141 | }
142 | var f interface{}
143 | err = json.Unmarshal([]byte(string(contents)), &f)
144 | if err != nil {
145 | log.Println("Error unmarshaling JSON reply")
146 | jResp(w, "error")
147 | return
148 | }
149 | m := f.(map[string]interface{})
150 |
151 | // insert the data
152 | ok := insertDataRow(m)
153 | if ok {
154 | jResp(w, "{res: 'ok'}")
155 | } else {
156 | jResp(w, "{res: 'error'}")
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/mysql_version/src/webapp/db.go:
--------------------------------------------------------------------------------
1 | // All code for direct interaction with the database is in this file
2 | package main
3 |
4 | import (
5 | "database/sql"
6 | "encoding/json"
7 | _ "github.com/go-sql-driver/mysql"
8 | "log"
9 | )
10 |
11 | // prepared statements
12 | var selectRows *sql.Stmt // get all data rows
13 | var selectRow *sql.Stmt // get one data row by ID
14 | var updateRow *sql.Stmt // update one data row
15 | var insertRow *sql.Stmt // insert one row in data table
16 | var deleteRow *sql.Stmt // delete one row from data table
17 | var selectUser *sql.Stmt // get one user
18 | var insertUser *sql.Stmt // add one user
19 |
20 | // the database connection
21 | var db *sql.DB
22 |
23 | // a user row from the database looks like this:
24 | type User struct {
25 | id int
26 | state int
27 | googleid string
28 | name string
29 | email string
30 | }
31 |
32 | // open the database
33 | func openDB() {
34 | // assumes we've run 'CREATE SCHEMA `cfg.MysqlDbname` ;'
35 | db, err := sql.Open("mysql",
36 | cfg.DbUser+":"+cfg.DbPass+"@("+cfg.DbHost+":"+cfg.DbPort+")/"+cfg.DbName)
37 | if err != nil {
38 | log.Fatal(err)
39 | }
40 | err = db.Ping() // ensure alive...
41 | if err != nil {
42 | log.Fatal(err)
43 | }
44 |
45 | // create prepared statements for getting and creating users
46 | selectUser, err = db.Prepare("SELECT * FROM users WHERE googleid = ?")
47 | if err != nil {
48 | log.Fatal(err)
49 | }
50 | insertUser, err = db.Prepare("INSERT INTO users(state, googleid, name, email) VALUES (?, ?, ?, ?)")
51 | if err != nil {
52 | log.Fatal(err)
53 | }
54 |
55 | // create prepared statements for REST routes on the 'data' table
56 | selectRows, err = db.Prepare("SELECT * FROM data")
57 | if err != nil {
58 | log.Fatal(err)
59 | }
60 | selectRow, err = db.Prepare("SELECT * FROM data WHERE id = ?")
61 | if err != nil {
62 | log.Fatal(err)
63 | }
64 | updateRow, err = db.Prepare("UPDATE data SET smallnote = ?, bignote = ?, favint = ?, favfloat = ?, trickfloat = ? WHERE id = ?")
65 | if err != nil {
66 | log.Fatal(err)
67 | }
68 | insertRow, err = db.Prepare("INSERT INTO data(smallnote, bignote, favint, favfloat, trickfloat) VALUES(?, ?, ?, ?, ?)")
69 | if err != nil {
70 | log.Fatal(err)
71 | }
72 | deleteRow, err = db.Prepare("DELETE FROM data WHERE id = ?")
73 | if err != nil {
74 | log.Fatal(err)
75 | }
76 | }
77 |
78 | // get a user's record, to make login/register decisions
79 | func getUserById(googleId string) (*User, error) {
80 | // should never fail if DB is reachable:
81 | rows, err := selectUser.Query(googleId)
82 | if err != nil {
83 | log.Println("query error", err)
84 | return nil, err
85 | }
86 | defer rows.Close()
87 | if !rows.Next() {
88 | return nil, nil
89 | } // no record exists
90 |
91 | // copy row into 'user' and return it
92 | var user User
93 | err = rows.Scan(&user.id, &user.state, &user.googleid, &user.name, &user.email)
94 | if err != nil {
95 | log.Println("scan error", err)
96 | return nil, err
97 | }
98 | return &user, nil
99 | }
100 |
101 | // insert a row into the user table
102 | func addNewUser(id string, name string, email string, state int) error {
103 | _, err := insertUser.Exec(state, id, name, email)
104 | if err != nil {
105 | log.Println(err)
106 | return err
107 | }
108 | return nil
109 | }
110 |
111 | // get all rows from DATA, return them as JSON
112 | func getAllRows() string {
113 | // get all the data, return an empty blob on failure
114 | rows, err := selectRows.Query()
115 | if err != nil {
116 | return ""
117 | }
118 | defer rows.Close()
119 |
120 | // get column names
121 | columns, err := rows.Columns()
122 | if err != nil {
123 | return "error"
124 | }
125 |
126 | // we ultimately want to marshal an array into a JSON string, because
127 | // order matters
128 | allData := make([]map[string]interface{}, 0)
129 |
130 | // When we parse a row, we need a pointer for where each column goes.
131 | // To save some pain, we'll have an array to hold the rows, and
132 | // another array of pointers to those array entries. That way, we
133 | // can scan to ptrs, and then use values
134 | //
135 | // NB: '6' is a magic number, representing the number of columns
136 | bytestreams := make([]interface{}, 6)
137 | ptrs := make([]interface{}, 6)
138 | for i := 0; i < 6; i++ {
139 | ptrs[i] = &bytestreams[i]
140 | }
141 |
142 | // parse the rows, copy them into the array as string:string maps
143 | for rows.Next() {
144 | // get data into bytestreams as a bunch of byte streams
145 | err = rows.Scan(ptrs...)
146 | if err != nil {
147 | return "error"
148 | }
149 |
150 | // we're going to shuffle it into here
151 | rowAsMap := make(map[string]interface{})
152 |
153 | // for each column, create a string from the byte stream,
154 | // match it with its column name, and put it all in rowAsMap
155 | for i, bytes := range bytestreams {
156 | // if the row type isn't text, we can have trouble,
157 | // but this seems to work for ints (not sure about floats)
158 | var v interface{}
159 | b, ok := bytes.([]byte)
160 | if ok {
161 | v = string(b)
162 | } else {
163 | v = bytes
164 | }
165 | rowAsMap[columns[i]] = v
166 | }
167 |
168 | // send the parsed row into the table
169 | allData = append(allData, rowAsMap)
170 | }
171 |
172 | // marshall as JSON, then return it as a string
173 | jsonData, err := json.Marshal(allData)
174 | if err != nil {
175 | return "error"
176 | }
177 | return string(jsonData)
178 | }
179 |
180 | // get one row from DATA, return it as JSON
181 | //
182 | // NB: for better or worse, we're doing this in the same way as getRows(), so
183 | // we'll skip commenting the redundant code
184 | func getRow(id int) string {
185 | // get all the data, return an empty blob on failure
186 | rows, err := selectRow.Query(id)
187 | if err != nil {
188 | return "internal error"
189 | }
190 | defer rows.Close()
191 |
192 | // get column names
193 | columns, err := rows.Columns()
194 | if err != nil {
195 | return "internal error"
196 | }
197 |
198 | // parse the data... note the magic number 6 again...
199 | rows.Next()
200 | bytestreams := make([]interface{}, 6)
201 | ptrs := make([]interface{}, 6)
202 | for i := 0; i < 6; i++ {
203 | ptrs[i] = &bytestreams[i]
204 | }
205 |
206 | // get data into bytestreams as a bunch of byte streams
207 | err = rows.Scan(ptrs...)
208 | if err != nil {
209 | return "not found"
210 | }
211 | rowAsMap := make(map[string]interface{})
212 | for i, bytes := range bytestreams {
213 | var v interface{}
214 | b, ok := bytes.([]byte)
215 | if ok {
216 | v = string(b)
217 | } else {
218 | v = bytes
219 | }
220 | rowAsMap[columns[i]] = v
221 | }
222 |
223 | // marshall as JSON, then return it as a string
224 | jsonData, err := json.Marshal(rowAsMap)
225 | if err != nil {
226 | return "internal error"
227 | }
228 | return string(jsonData)
229 | }
230 |
231 | // Update a row in DATA
232 | func updateDataRow(id int, data map[string]interface{}) bool {
233 | // special case for trickfloat
234 | var special *string = nil
235 | tf := data["trickfloat"].(string)
236 | if tf != "" {
237 | special = &tf
238 | }
239 | // do the update
240 | _, err := updateRow.Exec(data["smallnote"], data["bignote"], data["favint"], data["favfloat"], special, id)
241 | if err != nil {
242 | log.Println(err)
243 | return false
244 | }
245 | return true
246 | }
247 |
248 | // Delete a row from DATA
249 | func deleteDataRow(id int) bool {
250 | _, err := deleteRow.Exec(id)
251 | if err != nil {
252 | log.Println(err)
253 | return false
254 | }
255 | return true
256 | }
257 |
258 | // Insert a row into DATA
259 | func insertDataRow(data map[string]interface{}) bool {
260 | // special case for trickfloat
261 | var special *string = nil
262 | tf := data["trickfloat"].(string)
263 | if tf != "" {
264 | special = &tf
265 | }
266 | // do the insert
267 | _, err := insertRow.Exec(data["smallnote"], data["bignote"], data["favint"], data["favfloat"], special)
268 | if err != nil {
269 | log.Println(err)
270 | return false
271 | }
272 | return true
273 | }
274 |
--------------------------------------------------------------------------------
/mysql_version/src/webapp/fileroutes.go:
--------------------------------------------------------------------------------
1 | // Routes for handling file requests
2 | package main
3 |
4 | import (
5 | "net/http"
6 | "os"
7 | )
8 |
9 | // ensure that the given path is a valid file, and not a directory
10 | func isValidFile(path string) bool {
11 | file, err := os.Open(path)
12 | if err != nil {
13 | return false
14 | } // no file
15 | defer file.Close()
16 | stat, err := file.Stat()
17 | if err != nil {
18 | return false
19 | } // couldn't stat
20 | if stat.IsDir() {
21 | return false
22 | } // directory
23 | return true
24 | }
25 |
26 | // route for serving public static files... they are prefixed with 'public'
27 | func handlePublicFile(w http.ResponseWriter, r *http.Request) {
28 | // serve only if valid file, else notfound
29 | path := r.URL.Path[1:]
30 | if isValidFile(path) {
31 | http.ServeFile(w, r, r.URL.Path[1:])
32 | } else {
33 | http.NotFound(w, r)
34 | }
35 | }
36 |
37 | // route for serving private static files... they are prefixed with 'private'
38 | func handlePrivateFile(w http.ResponseWriter, r *http.Request) {
39 | // validate login
40 | if !checkLogin(r) {
41 | do403(w, r)
42 | return
43 | }
44 | // serve only if valid file, else notfound
45 | path := r.URL.Path[1:]
46 | if isValidFile(path) {
47 | http.ServeFile(w, r, r.URL.Path[1:])
48 | } else {
49 | http.NotFound(w, r)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/mysql_version/src/webapp/main.go:
--------------------------------------------------------------------------------
1 | // A demo web application to show how to use OAuth 2.0 (Google+ Provider) and
2 | // MySQL from Go.
3 | package main
4 |
5 | import (
6 | "flag"
7 | "log"
8 | "net/http"
9 | )
10 |
11 | // main function configures resources and launches the app
12 | func main() {
13 | // parse command line options
14 | configPath := flag.String("configfile", "config.json", "Path to the configuration (JSON) file")
15 | flag.Parse()
16 |
17 | // load the JSON config file
18 | loadConfig(*configPath)
19 |
20 | // open the database
21 | openDB()
22 |
23 | // set up templates
24 | buildTemplates()
25 |
26 | // set up the routes... it's good to have these all in one place,
27 | // since we need to be cautious about orders when there is a common
28 | // prefix
29 | router := new(Router)
30 | // REST routes for the DATA table
31 | router.Register("/data/[0-9]+$", "PUT", handlePutData)
32 | router.Register("/data/[0-9]+$", "GET", handleGetDataOne)
33 | router.Register("/data/[0-9]+$", "DELETE", handleDeleteData)
34 | router.Register("/data$", "POST", handlePostData)
35 | router.Register("/data$", "GET", handleGetAllData)
36 | // OAuth and login/out routes
37 | router.Register("/auth/google/callback$", "GET", handleGoogleCallback)
38 | router.Register("/register", "GET", handleGoogleRegister)
39 | router.Register("/logout", "GET", handleLogout)
40 | router.Register("/login", "GET", handleGoogleLogin)
41 | // Static files
42 | router.Register("/public/", "GET", handlePublicFile) // NB: regexp
43 | router.Register("/private/", "GET", handlePrivateFile) // NB: regexp
44 | // The logged-in main page
45 | router.Register("/app", "GET", handleApp)
46 | // The not-logged-in main page
47 | router.Register("/", "GET", handleMain)
48 |
49 | // print a diagnostic message and start the server
50 | log.Println("Server running on port " + cfg.AppPort)
51 | http.ListenAndServe(":"+cfg.AppPort, router)
52 | }
53 |
--------------------------------------------------------------------------------
/mysql_version/src/webapp/router.go:
--------------------------------------------------------------------------------
1 | // Set up an HTTP router that can manage REST requests correctly.
2 | //
3 | // We want to route as early as possible based on GET, PUT, POST, and DELETE
4 | // verbs, so we don't want to use the built-in go router. This also lets use
5 | // use Regexps to match routes.
6 | package main
7 |
8 | import (
9 | "net/http"
10 | "regexp"
11 | )
12 |
13 | // each route consists of a pattern, the HTTP verb, and a function to run
14 | type route struct {
15 | rxp *regexp.Regexp // the regexp describing the route
16 | verb string // the verb
17 | handler http.Handler // the handler
18 | }
19 |
20 | // The router's data is just an array of routes
21 | type Router struct {
22 | routes []*route
23 | }
24 |
25 | // extend Router with a function to register a new Route
26 | func (this *Router) Register(regex string, verb string,
27 | handler func(http.ResponseWriter, *http.Request)) {
28 | // NB: compile the regexp before saving it
29 | this.routes = append(this.routes, &route{regexp.MustCompile(regex), verb,
30 | http.HandlerFunc(handler)})
31 | }
32 |
33 | // Handle a request by forwarding to the appropriate route
34 | //
35 | // NB: http.ListenAndServe() requires this to be called ServeHTTP
36 | func (this *Router) ServeHTTP(res http.ResponseWriter, req *http.Request) {
37 | for _, route := range this.routes {
38 | if route.verb == req.Method && route.rxp.MatchString(req.URL.Path) {
39 | route.handler.ServeHTTP(res, req)
40 | return
41 | }
42 | }
43 | http.NotFound(res, req)
44 | }
45 |
--------------------------------------------------------------------------------
/mysql_version/src/webapp/tplroutes.go:
--------------------------------------------------------------------------------
1 | // Routes for serving content that is generated from templates
2 | package main
3 |
4 | import (
5 | "html/template"
6 | "log"
7 | "net/http"
8 | "time"
9 | )
10 |
11 | // The template for generating the main page (the '/' route)
12 | var mainPage *template.Template
13 |
14 | // The template for generating the logged in page (the '/app' route)
15 | var appPage *template.Template
16 |
17 | // Flash structs are used to send a message, via a cookie, back to the
18 | // browser when we redirect home on login/logout/registration events.
19 | type Flash struct {
20 | Inf bool // true if there's an info message
21 | InfText string // info message to print
22 | Err bool // true if there's an error message
23 | ErrText string // error message to print
24 | }
25 |
26 | // We call this in order to initialize all templates when we start the app
27 | func buildTemplates() {
28 | var err error
29 | mainPage, err = template.ParseFiles("templates/main.tpl")
30 | if err != nil {
31 | log.Fatal("main template parse error", err)
32 | }
33 | appPage, err = template.ParseFiles("templates/app.tpl")
34 | if err != nil {
35 | log.Fatal("app template parse error", err)
36 | }
37 | }
38 |
39 | // The route for '/' checks for flash cookies (i == Info; e == Error) and
40 | // uses them when generating content via the main template
41 | func handleMain(w http.ResponseWriter, r *http.Request) {
42 | // Prepare to consume flash messages
43 | flash := Flash{false, "", false, ""}
44 |
45 | // if we have an 'iflash' cookie, grab its contents then erase it
46 | cookie, err := r.Cookie("iflash")
47 | if err != http.ErrNoCookie {
48 | flash.InfText = cookie.Value
49 | flash.Inf = true
50 | http.SetCookie(w, &http.Cookie{Name: "iflash", Value: "-1", Expires: time.Now(), Path: "/"})
51 | }
52 |
53 | // if we have an 'eflash' cookie, grab its contents then erase it
54 | cookie, err = r.Cookie("eflash")
55 | if err != http.ErrNoCookie {
56 | flash.ErrText = cookie.Value
57 | flash.Err = true
58 | http.SetCookie(w, &http.Cookie{Name: "eflash", Value: "-1", Expires: time.Now(), Path: "/"})
59 | }
60 |
61 | // Render the template
62 | mainPage.Execute(w, flash)
63 | }
64 |
65 | // The route for '/app' ensures the user is logged in, and then renders the
66 | // app page via a template
67 | func handleApp(w http.ResponseWriter, r *http.Request) {
68 | if !checkLogin(r) {
69 | do403(w, r)
70 | return
71 | }
72 | appPage.Execute(w, nil)
73 | }
74 |
--------------------------------------------------------------------------------
/mysql_version/templates/app.tpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
10 |
11 |
12 | Example Web App
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
59 | This tutorial will describe the steps for making a web app that
60 | employs the following technologies:
61 |
62 |
63 |
The Go programming language
64 |
MySQL
65 |
Memcached
66 |
OAuth 2.0 (via Google)
67 |
Bootstrap
68 |
69 |
70 | By the time we're done, you should have a fully-functional (though not
71 | particularly useful) web app that you can customize.
72 |
73 |
74 |
75 |
76 |
77 |
Why This Tutorial?
78 |
79 |
80 | I've written tutorials for much of this before, using a MEAN stack.
81 | Why do it again?
82 |
83 |
84 |
For small apps, it's easier to deal with relational data (MySQL) than NoSQL data (MongoDB)
85 |
Go is much faster than JavaScript
86 |
User management is harder than Passport.js makes it seem
87 |
88 |
89 | Furthermore, the old tutorials never quite finished. This
90 | time around, the tutorial won't stop until everything is integrated
91 | and working together.
92 |
93 |
94 |
95 |
96 |
97 |
Part #0: The Big Picture
98 |
99 | What are all of the pieces, and how do they fit together?
100 |
101 |
102 |
103 |
104 |
Components
105 |
106 |
107 |
Our app has three types of components:
108 |
109 |
110 |
158 |
159 |
160 |
161 |
162 |
Components: Why
163 |
164 |
165 |
Different tasks are best satisfied using different languages
166 |
Separation of functionality enables multiple front-ends
167 |
Easier to test and upgrade each part
168 |
169 |
170 |
171 |
172 |
173 |
The Database
174 |
175 |
176 |
Store all data entered by users in a way that is never lost
177 |
Data is organized as tables
178 |
Each row is like an object,
179 | each column is like a field
180 |
Relationships between tables (i.e., a row
181 | in users table can relate to many rows
182 | in comments table)
183 |
We use MySQL in this tutorial
184 |
185 |
186 |
187 |
188 |
189 |
The App Server
190 |
191 |
192 |
Manages authentication via Google+ OAuth 2.0
193 |
Creates "REST" routes to the database
194 |
195 |
196 | Use
197 | HTTP POST, GET, PUT,
198 | and DELETE verbs to
199 | CREATE, READ, UPDATE,
200 | and DELETE data
201 |
202 |
Note: we don't adhere to a 100% strict RESTful interface
203 |
204 |
Prevents app users from needing accounts to the database
205 |
Allows custom logic that's hard to achieve in MySQL
206 |
Written in Go, but could be in any language
207 |
208 |
209 |
210 |
211 |
212 |
The Web App
213 |
214 |
215 |
HTML5 and JavaScript code that enables any browser to interact
216 | with the database, mediated by the App Server
217 |
218 |
We're lazy: the App Server is also the web server for the Web App
219 |
220 |
Design goal: all logic is in JavaScript and HTML that is executed
221 | on the client
222 |
223 |
We don't want the App Server to do anything special for the Web
224 | App, vs. Mobile Apps
225 |
Ultimately, this goal will make mobile development easier
226 |
227 |
We use bootstrap for all styling in this tutorial... it gives us
228 | an app that looks good on mobile and desktop devices
229 |
230 |
231 |
232 |
233 |
234 |
The Mobile App
235 |
236 |
237 |
Android and iOS versions
238 |
Use Cordova: HTML+JavaScript to produce "app"
239 |
Use CSS to make it look like a native app
240 |
No need to re-implement app server's logic
241 |
Note: we aren't going to cover this in the tutorial
242 |
243 |
244 |
245 |
246 |
247 |
The Admin App
248 |
249 |
250 |
Activate user registrations
251 |
Reset the database's tables
252 |
Easy to add "one-off" queries
253 |
Only accessible from developer machines!
254 |
255 |
256 |
257 |
258 |
259 |
Part #1: Getting Started
260 |
261 | To start, let's set up folders, environments and a few simple Go
262 | programs.
263 |
264 |
265 |
266 |
267 |
Requirements
268 |
269 |
270 |
Bash shell
271 |
(I use Git Bash for Windows)
272 |
Golang compiler
273 |
MySQL and Memcached
274 |
275 |
Local installation is fine for now
276 |
277 |
A Google Developer Console account
278 |
279 |
280 |
281 |
282 |
283 |
Environment
284 |
285 |
286 |
287 | The setenv.sh script configures your environment for building
288 | in Go. Load it like this:
289 |
290 |
source ~/setenv.sh
291 |
292 | All of your Go code will go in subfolders of src/
293 |
294 |
295 |
296 | This may seem strange if you're coming from C++, or even from node.js.
297 | For better or for worse, in Go, sometimes you don't get a choice in
298 | how you do things.
299 |
300 |
301 |
302 |
303 |
304 |
Applications
305 |
306 |
307 |
We will make three applications:
308 |
309 |
webapp: the main web application
310 |
admin: a swiss army knife of admin tools for our app
311 |
statichttpserver: a debugging tool for testing html5 code
312 |
313 |
314 |
315 | This layout is, in part, an attempt to
316 | follow 12-factor design.
317 | Specifically, we want all admin work to be housed outside of the
318 | folder for the program we'll deploy.
319 |
320 |
321 |
322 |
323 |
324 |
App #1: statichttpserver
325 |
326 |
Put this in src/statichttpserver/main.go
327 |
328 |
329 |
330 |
Build like this:
331 |
go build statichttpserver
332 |
Run like this:
333 |
./statichttpserver.exe -h
334 |
Or, better:
335 |
./statichttpserver.exe -p 8080 -f ./tutorial
336 |
337 | (After the third command,
338 | this link
339 | should be live. Use ctrl-c from your shell to stop the server.)
340 |
341 |
342 |
343 |
344 |
345 |
App #2: admin
346 |
347 |
For now, we'll just make a shell for our admin tasks:
348 |
349 |
350 |
351 |
Build like this:
352 |
go build admin
353 |
Run like this:
354 |
./admin.exe
355 |
356 |
357 |
358 |
359 |
Part #2: Non-Code Configuration
360 |
361 | In this part, we'll set up our database and OAuth information, so that
362 | the rest of the tutorial works more cleanly.
363 |
364 |
365 |
366 |
367 |
Database Configuration
368 |
369 |
370 |
371 | You should have MySQL set up, with some kind of account and password
372 | information
373 |
374 |
We're going to build everything programatically, without the
375 | GUI, so the admin program needs to know about the database
376 |
Copy config.json to some other file (perhaps webapp_config.json?) and add it to
377 | your .gitignore
378 |
379 |
You do NOT want to put your config information
380 | into a repository that might become public.
381 |
382 |
383 |
384 |
385 |
386 |
387 |
Database Configuration
388 |
389 |
390 |
In your copy of config.json, set the five
391 | fields that start with 'Mysql':
Create a new project... I called mine 'GolangWebappTutorial'
466 |
Enable the Google+ API and create an OAuth 2.0 Client ID
467 |
For now, add a redirect uri of
468 | http://localhost:8080/auth/google/callback in the console, and in
469 | your JSON config file
470 |
471 |
This address will change after we get the whole app up and running
472 |
473 |
Copy your Client ID and Client Secret into your JSON config
474 | file
475 |
In the JSON config file, add "https://www.googleapis.com/auth/userinfo.email"
476 | as a scope.
477 |
478 |
479 |
480 |
481 |
482 |
Part #3: The Data Model
483 |
484 | Let's talk about the data that our app will store.
485 |
486 |
487 |
488 |
489 |
Tables
490 |
491 |
492 | Tables store data in Rows. When we create a database, we define
493 | columns that are represented each row. It's good to avoid 'null'
494 | values whenever possible. It's also good to have a unique ID for each
495 | row:
496 |
497 |
498 |
511 |
512 |
The "Users" table stores a user name and email address
513 |
The 'id' field is a unique identifier generated by MySQL
514 |
'googleid' is a unique identifier from Google
515 |
'state' lets us track if the account has been activated
516 |
517 |
518 |
519 |
520 |
521 |
Data Types
522 |
523 |
524 |
In the Users table, everything is a string, and no string is too
525 | long, so we can use 'varchar'
526 |
We won't concern ourselves with the "data" type
527 |
But it would be good to have int, float, and both long and short
528 | strings in our Data table
529 |
We'll also have one field that CAN be null, just
530 | to make sure the example is complete
531 |
532 |
533 |
534 |
535 |
536 |
The Data Table
537 |
538 |
Apart from the Users table, we'll have a Data table that holds some
539 | pointless information. Note the types:
540 |
541 |
555 |
Now we can write code for creating and managing our
556 | tables from the admin program.
557 |
558 |
559 |
560 |
561 |
562 |
563 |
Part #4: Setting Up All The Code
564 |
565 | The code is well commented... really! For example,
566 | the src/admin/main.go file is 268 lines, of which almost 100
567 | are comments. That being the case, there's no point in walking through
568 | how to write the code. Instead, let's put it in place, then discuss
569 | how it works.
570 |
571 |
572 |
573 |
574 |
Folders
575 |
576 |
577 |
Our program has the following folder structure:
578 |
579 |
private/ and public/
Static web content
580 |
templates/
Templates for generating HTML pages
581 |
src/admin/
The code for our app's admin program
582 |
src/webapp/
The code for our main web app
583 |
584 |
Note: in the repository, tutorial/
585 | and src/statichttpserver support this tutorial
586 |
Note: the pkg/ and other src/* folders are
587 | managed by Go
588 |
589 |
590 |
591 |
592 |
593 |
Source Code: src/admin/main.go
594 |
595 |
596 |
All administrative code lives in a single file
597 |
598 |
599 |
600 |
601 |
602 |
To build and run:
603 |
604 |
go get github.com/go-sql-driver/mysql
605 | go build admin
606 | ./admin.exe -h
607 |
608 |
Note: there are quite a few more options now!
609 |
610 |
611 |
612 |
613 |
614 |
Source Code: src/admin/???.go
615 |
616 |
617 |
618 |
619 |
620 |
621 |
go get golang.org/x/oauth2
622 | go get golang.org/x/oauth2/google
623 |
624 |
625 |
626 |
Part #X: Database Admin Tasks
627 |
628 | We'll update admin/main.go to create our tables and support one-off
629 | queries.
630 |
631 |
632 |
633 |
634 |
Part #X: Authentication
635 |
636 | Now it's time to set up OAuth with Google.
637 |
638 |
639 |
640 |
641 |
Part #X: Routing Requests
642 |
643 | Let's look at how the different HTML requests get served.
644 |
645 |
646 |
647 |
648 |
Part #X: Database CRUD / HTML REST
649 |
650 | Next, we discuss how to interact with the database.
651 |
652 |
653 |
654 |
655 |
Part #X: Front End and Templates
656 |
657 | Without a front-end, the whole exercise is for naught!
658 |
659 |
660 |
661 |
662 |
Part #X: Launching the App
663 |
664 | We'll use the admin app to initialize the database, then we'll register
665 | a user and try things out.
666 |
667 |
668 |
669 |
670 |
Part #X: Wrap-Up
671 |
672 | We'll review what we did, and where to go from here.
673 |