├── .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 | 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 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | {{#each d}} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 34 | 35 | {{/each}} 36 | 37 |
Small NoteBig NoteFavorite IntFavorite FloatExtra Float? 13 | 14 |
{{smallnote}}{{bignote}}{{favint}}{{favfloat}}{{trickfloat}} 26 | 28 | 29 | 30 | 32 | 33 |
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 | 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 |
35 |
36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /mongodb_version/templates/main.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 |

Example Web App

25 |
26 | 27 | {{if .Inf}} 28 | 32 | {{end}} 33 | 34 | {{if .Err}} 35 | 39 | {{end}} 40 | 41 |
42 |
43 |
44 |
45 |

Register

46 |
47 |
48 |

49 | This site uses Google IDs for authentication. Use the following link 50 | to register your ID with our system. 51 |

52 | 53 | Register 54 | 55 |
56 |
57 |
58 |
59 |
60 |
61 |

Log In

62 |
63 |
64 |

65 | If you have already registered, you can use this link to log in, 66 | using your Google ID. 67 |

68 | 69 | Log-In 70 | 71 |
72 |
73 |
74 |
75 |
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 |
24 | 25 |
26 |

Example Web App

27 | 28 | Log-Out 29 | 30 |
31 | 32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | 45 | 46 | 47 | 48 |
Small NoteBig NoteFavorite IntFavorite FloatExtra Float?
49 |
50 |
51 |
52 | 53 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /mysql_version/templates/main.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 |

Example Web App

25 |
26 | 27 | {{if .Inf}} 28 | 31 | {{end}} 32 | {{if .Err}} 33 | 36 | {{end}} 37 | 38 |
39 |
40 |
41 |
42 |

Register

43 |
44 |
45 |

46 | This site uses Google IDs for authentication. Use the following link 47 | to register your ID with our system. 48 |

49 | 50 | Register 51 | 52 |
53 |
54 |
55 |
56 |
57 |
58 |

Log In

59 |
60 |
61 |

62 | If you have already registered, you can use this link to log in, 63 | using your Google ID. 64 |

65 | 66 | 67 | 68 |
69 |
70 |
71 |
72 |
73 | 74 | 75 | -------------------------------------------------------------------------------- /tutorial/css/beige-mfs.css: -------------------------------------------------------------------------------- 1 | /* Hacked version of the 'beige.css' template */ 2 | 3 | /* Fonts */ 4 | @import url(https://fonts.googleapis.com/css?family=Anonymous+Pro:400,700,400italic,700italic); 5 | @import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic); 6 | 7 | /* Global styles */ 8 | body { /* see http://lea.verou.me/css3patterns */ 9 | background: #EADCA6; 10 | background: 11 | linear-gradient(115deg, transparent 75%, rgba(234,220,166,.8) 75%) 0 0, 12 | linear-gradient(245deg, transparent 75%, rgba(234,220,166,.8) 75%) 0 0, 13 | linear-gradient(115deg, transparent 75%, rgba(234,220,166,.8) 75%) 7px -15px, 14 | linear-gradient(245deg, transparent 75%, rgba(234,220,166,.8) 75%) 7px -15px, 15 | #ECDEA8; 16 | background-size: 15px 30px 17 | } 18 | 19 | .reveal { 20 | font-family: 'Lato'; 21 | font-size: 36px; 22 | font-weight: normal; 23 | color: #875C36; 24 | } 25 | 26 | ::selection { 27 | color: #fff; 28 | background: #875C36; 29 | text-shadow: none; 30 | } 31 | 32 | .reveal .slides > section, .reveal .slides > section > section { 33 | font-weight: inherit; 34 | } 35 | 36 | /* Headers */ 37 | .reveal h1, .reveal h2, .reveal h3, .reveal h4, .reveal h5, .reveal h6 { 38 | margin: 0px; 39 | font-family: 'Lato', sans-serif; 40 | word-wrap: break-word; 41 | } 42 | 43 | .reveal h1 { 44 | font-size: 2.5em; 45 | text-transform: uppercase; 46 | line-height: 1.3; 47 | padding-bottom: 20px; 48 | } 49 | 50 | .reveal h2 { 51 | font-size: 1.5em; 52 | } 53 | 54 | .reveal h3 { 55 | font-size: 1.25em; 56 | } 57 | 58 | .reveal h4 { 59 | font-size: 1em; 60 | } 61 | 62 | .reveal h5 { 63 | padding-top: 20px; 64 | font-size: .8em; 65 | } 66 | 67 | /* Other */ 68 | .reveal p { 69 | margin: 20px 0; 70 | line-height: 1.3; 71 | text-align: left; 72 | } 73 | 74 | /* default to a more powerpoint-esque bullet style */ 75 | .reveal ul { 76 | width: 100%; 77 | } 78 | .reveal li { 79 | margin: 0 0 .4em 0; 80 | } 81 | 82 | p.nb { 83 | font-style: italic; 84 | font-size: .8em; 85 | } 86 | 87 | /* Ensure certain elements are never larger than the slide itself */ 88 | .reveal img, .reveal video, .reveal iframe { 89 | max-width: 95%; 90 | max-height: 95%; } 91 | 92 | .reveal strong, .reveal b { 93 | font-weight: bold; } 94 | 95 | .reveal em { 96 | font-style: italic; } 97 | 98 | .reveal ol, .reveal dl, .reveal ul { 99 | display: inline-block; 100 | text-align: left; 101 | margin: 0 0 0 1em; } 102 | 103 | .reveal ol { 104 | list-style-type: decimal; } 105 | 106 | .reveal ul { 107 | list-style-type: disc; } 108 | 109 | .reveal ul ul { 110 | list-style-type: square; 111 | font-size: .8em; 112 | } 113 | 114 | .reveal ul ul ul { 115 | list-style-type: circle; 116 | font-size: .6em; 117 | } 118 | 119 | .reveal ul ul, .reveal ul ol, .reveal ol ol, .reveal ol ul { 120 | display: block; 121 | margin-left: 1em; } 122 | 123 | .reveal dt { 124 | font-weight: bold; } 125 | 126 | .reveal dd { 127 | margin-left: 40px; } 128 | 129 | .reveal q, .reveal blockquote { 130 | quotes: none; } 131 | 132 | .reveal blockquote { 133 | display: block; 134 | position: relative; 135 | width: 70%; 136 | margin: 20px auto; 137 | padding: 5px; 138 | font-style: italic; 139 | background: rgba(255, 255, 255, 0.05); 140 | box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2); } 141 | 142 | .reveal blockquote p:first-child, .reveal blockquote p:last-child { 143 | display: inline-block; } 144 | 145 | .reveal q { 146 | font-style: italic; } 147 | 148 | .reveal pre { 149 | display: block; 150 | position: relative; 151 | width: 90%; 152 | margin: 20px auto; 153 | text-align: left; 154 | font-size: 0.55em; 155 | font-family: monospace; 156 | line-height: 1.2em; 157 | word-wrap: break-word; 158 | box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.3); } 159 | 160 | .reveal tt { 161 | font-family: 'Anonymous Pro'; 162 | } 163 | 164 | .reveal code { 165 | font-family: 'Anonymous Pro'; 166 | font-size: .9em; 167 | } 168 | 169 | .reveal pre code { 170 | display: block; 171 | padding: 5px; 172 | overflow: auto; 173 | word-wrap: normal; 174 | } 175 | 176 | .reveal table { 177 | margin: auto; 178 | border-collapse: collapse; 179 | border-spacing: 0; } 180 | 181 | .reveal table th { 182 | font-weight: bold; } 183 | 184 | .reveal table th, .reveal table td { 185 | text-align: left; 186 | padding: 0.2em 0.5em 0.2em 0.5em; 187 | border-bottom: 1px solid; } 188 | 189 | .reveal table tr:last-child td { 190 | border-bottom: none; } 191 | 192 | .reveal sup { 193 | vertical-align: super; } 194 | 195 | .reveal sub { 196 | vertical-align: sub; } 197 | 198 | .reveal small { 199 | display: inline-block; 200 | font-size: 0.6em; 201 | line-height: 1.2em; 202 | vertical-align: top; } 203 | 204 | .reveal small * { 205 | vertical-align: top; } 206 | 207 | /* Links */ 208 | .reveal a { 209 | color: #976C46; 210 | text-decoration: none; 211 | -webkit-transition: color 0.15s ease; 212 | -moz-transition: color 0.15s ease; 213 | transition: color 0.15s ease; 214 | } 215 | 216 | .reveal a:hover { 217 | text-decoration: underline; 218 | 219 | border: none; 220 | } 221 | 222 | .reveal .roll span:after { 223 | color: #fff; 224 | background: #564726; } 225 | 226 | /* Images */ 227 | .reveal section img { 228 | margin: 15px 0px; 229 | background: rgba(255, 255, 255, 0.12); 230 | border: 1px solid #dedede; 231 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); } 232 | 233 | .reveal a img { 234 | -webkit-transition: all 0.15s linear; 235 | -moz-transition: all 0.15s linear; 236 | transition: all 0.15s linear; } 237 | 238 | .reveal a:hover img { 239 | background: rgba(255, 255, 255, 0.2); 240 | border-color: #8b743d; 241 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.55); } 242 | 243 | /********************************************* 244 | * NAVIGATION CONTROLS 245 | *********************************************/ 246 | .reveal .controls div.navigate-left, .reveal .controls div.navigate-left.enabled { 247 | border-right-color: #8b743d; } 248 | 249 | .reveal .controls div.navigate-right, .reveal .controls div.navigate-right.enabled { 250 | border-left-color: #8b743d; } 251 | 252 | .reveal .controls div.navigate-up, .reveal .controls div.navigate-up.enabled { 253 | border-bottom-color: #8b743d; } 254 | 255 | .reveal .controls div.navigate-down, .reveal .controls div.navigate-down.enabled { 256 | border-top-color: #8b743d; } 257 | 258 | .reveal .controls div.navigate-left.enabled:hover { 259 | border-right-color: #c0a76e; } 260 | 261 | .reveal .controls div.navigate-right.enabled:hover { 262 | border-left-color: #c0a76e; } 263 | 264 | .reveal .controls div.navigate-up.enabled:hover { 265 | border-bottom-color: #c0a76e; } 266 | 267 | .reveal .controls div.navigate-down.enabled:hover { 268 | border-top-color: #c0a76e; } 269 | 270 | /* Progress bar */ 271 | .reveal .progress { 272 | background: rgba(0, 0, 0, 0.2); } 273 | 274 | .reveal .progress span { 275 | background: #8b743d; 276 | -webkit-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); 277 | -moz-transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); 278 | transition: width 800ms cubic-bezier(0.26, 0.86, 0.44, 0.985); } 279 | 280 | /* Slide Number */ 281 | .reveal .slide-number { 282 | color: #8b743d; } 283 | -------------------------------------------------------------------------------- /tutorial/css/pdf.css: -------------------------------------------------------------------------------- 1 | /* Default Print Stylesheet Template 2 | by Rob Glazebrook of CSSnewbie.com 3 | Last Updated: June 4, 2008 4 | 5 | Feel free (nay, compelled) to edit, append, and 6 | manipulate this file as you see fit. */ 7 | 8 | 9 | /* SECTION 1: Set default width, margin, float, and 10 | background. This prevents elements from extending 11 | beyond the edge of the printed page, and prevents 12 | unnecessary background images from printing */ 13 | 14 | * { 15 | -webkit-print-color-adjust: exact; 16 | } 17 | 18 | body { 19 | margin: 0 auto !important; 20 | border: 0; 21 | padding: 0; 22 | float: none !important; 23 | overflow: visible; 24 | } 25 | 26 | html { 27 | width: 100%; 28 | height: 100%; 29 | overflow: visible; 30 | } 31 | 32 | /* SECTION 2: Remove any elements not needed in print. 33 | This would include navigation, ads, sidebars, etc. */ 34 | .nestedarrow, 35 | .reveal .controls, 36 | .reveal .progress, 37 | .reveal .slide-number, 38 | .reveal .playback, 39 | .reveal.overview, 40 | .fork-reveal, 41 | .share-reveal, 42 | .state-background { 43 | display: none !important; 44 | } 45 | 46 | /* SECTION 3: Set body font face, size, and color. 47 | Consider using a serif font for readability. */ 48 | body, p, td, li, div { 49 | 50 | } 51 | 52 | /* SECTION 4: Set heading font face, sizes, and color. 53 | Differentiate your headings from your body text. 54 | Perhaps use a large sans-serif for distinction. */ 55 | h1,h2,h3,h4,h5,h6 { 56 | text-shadow: 0 0 0 #000 !important; 57 | } 58 | 59 | .reveal pre code { 60 | overflow: hidden !important; 61 | font-family: Courier, 'Courier New', monospace !important; 62 | } 63 | 64 | 65 | /* SECTION 5: more reveal.js specific additions by @skypanther */ 66 | ul, ol, div, p { 67 | visibility: visible; 68 | position: static; 69 | width: auto; 70 | height: auto; 71 | display: block; 72 | overflow: visible; 73 | margin: auto; 74 | } 75 | .reveal { 76 | width: auto !important; 77 | height: auto !important; 78 | overflow: hidden !important; 79 | } 80 | .reveal .slides { 81 | position: static; 82 | width: 100%; 83 | height: auto; 84 | 85 | 86 | left: auto; 87 | top: auto; 88 | margin: 0 !important; 89 | padding: 0 !important; 90 | 91 | overflow: visible; 92 | display: block; 93 | 94 | -webkit-perspective: none; 95 | -moz-perspective: none; 96 | -ms-perspective: none; 97 | perspective: none; 98 | 99 | -webkit-perspective-origin: 50% 50%; /* there isn't a none/auto value but 50-50 is the default */ 100 | -moz-perspective-origin: 50% 50%; 101 | -ms-perspective-origin: 50% 50%; 102 | perspective-origin: 50% 50%; 103 | /* added this from the beige theme for printing. not sure if print preview supports gradient CSS.*/ 104 | 105 | 106 | background-color: #f7f3de; 107 | } 108 | .reveal .slides section { 109 | page-break-after: always !important; 110 | 111 | visibility: visible !important; 112 | position: relative !important; 113 | display: block !important; 114 | position: relative !important; 115 | 116 | margin: 0 !important; 117 | padding: 0 !important; 118 | box-sizing: border-box !important; 119 | min-height: 1px; 120 | 121 | opacity: 1 !important; 122 | 123 | -webkit-transform-style: flat !important; 124 | -moz-transform-style: flat !important; 125 | -ms-transform-style: flat !important; 126 | transform-style: flat !important; 127 | 128 | -webkit-transform: none !important; 129 | -moz-transform: none !important; 130 | -ms-transform: none !important; 131 | transform: none !important; 132 | } 133 | .reveal section.stack { 134 | margin: 0 !important; 135 | padding: 0 !important; 136 | page-break-after: avoid !important; 137 | height: auto !important; 138 | min-height: auto !important; 139 | } 140 | .reveal img { 141 | box-shadow: none; 142 | } 143 | .reveal .roll { 144 | overflow: visible; 145 | line-height: 1em; 146 | } 147 | 148 | /* Slide backgrounds are placed inside of their slide when exporting to PDF */ 149 | .reveal section .slide-background { 150 | display: block !important; 151 | position: absolute; 152 | top: 0; 153 | left: 0; 154 | width: 100%; 155 | z-index: -1; 156 | } 157 | /* All elements should be above the slide-background */ 158 | .reveal section>* { 159 | position: relative; 160 | z-index: 1; 161 | } 162 | 163 | -------------------------------------------------------------------------------- /tutorial/css/zenburn.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Zenburn style from voldmar.ru (c) Vladimir Epifanov 4 | based on dark.css by Ivan Sagalaev 5 | 6 | */ 7 | 8 | .hljs { 9 | display: block; padding: 0.5em; 10 | background: #3F3F3F; 11 | color: #DCDCDC; 12 | } 13 | 14 | .hljs-keyword, 15 | .hljs-tag, 16 | .css .hljs-class, 17 | .css .hljs-id, 18 | .lisp .hljs-title, 19 | .nginx .hljs-title, 20 | .hljs-request, 21 | .hljs-status, 22 | .clojure .hljs-attribute { 23 | color: #E3CEAB; 24 | } 25 | 26 | .django .hljs-template_tag, 27 | .django .hljs-variable, 28 | .django .hljs-filter .hljs-argument { 29 | color: #DCDCDC; 30 | } 31 | 32 | .hljs-number, 33 | .hljs-date { 34 | color: #8CD0D3; 35 | } 36 | 37 | .dos .hljs-envvar, 38 | .dos .hljs-stream, 39 | .hljs-variable, 40 | .apache .hljs-sqbracket { 41 | color: #EFDCBC; 42 | } 43 | 44 | .dos .hljs-flow, 45 | .diff .hljs-change, 46 | .python .exception, 47 | .python .hljs-built_in, 48 | .hljs-literal, 49 | .tex .hljs-special { 50 | color: #EFEFAF; 51 | } 52 | 53 | .diff .hljs-chunk, 54 | .hljs-subst { 55 | color: #8F8F8F; 56 | } 57 | 58 | .dos .hljs-keyword, 59 | .python .hljs-decorator, 60 | .hljs-title, 61 | .haskell .hljs-type, 62 | .diff .hljs-header, 63 | .ruby .hljs-class .hljs-parent, 64 | .apache .hljs-tag, 65 | .nginx .hljs-built_in, 66 | .tex .hljs-command, 67 | .hljs-prompt { 68 | color: #efef8f; 69 | } 70 | 71 | .dos .hljs-winutils, 72 | .ruby .hljs-symbol, 73 | .ruby .hljs-symbol .hljs-string, 74 | .ruby .hljs-string { 75 | color: #DCA3A3; 76 | } 77 | 78 | .diff .hljs-deletion, 79 | .hljs-string, 80 | .hljs-tag .hljs-value, 81 | .hljs-preprocessor, 82 | .hljs-pragma, 83 | .hljs-built_in, 84 | .sql .hljs-aggregate, 85 | .hljs-javadoc, 86 | .smalltalk .hljs-class, 87 | .smalltalk .hljs-localvars, 88 | .smalltalk .hljs-array, 89 | .css .hljs-rules .hljs-value, 90 | .hljs-attr_selector, 91 | .hljs-pseudo, 92 | .apache .hljs-cbracket, 93 | .tex .hljs-formula, 94 | .coffeescript .hljs-attribute { 95 | color: #CC9393; 96 | } 97 | 98 | .hljs-shebang, 99 | .diff .hljs-addition, 100 | .hljs-comment, 101 | .java .hljs-annotation, 102 | .hljs-template_comment, 103 | .hljs-pi, 104 | .hljs-doctype { 105 | color: #7F9F7F; 106 | } 107 | 108 | .coffeescript .javascript, 109 | .javascript .xml, 110 | .tex .hljs-formula, 111 | .xml .javascript, 112 | .xml .vbscript, 113 | .xml .css, 114 | .xml .hljs-cdata { 115 | opacity: 0.5; 116 | } 117 | 118 | -------------------------------------------------------------------------------- /tutorial/include/admin_001.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ("fmt") 4 | 5 | // Use command line to determine what action to take 6 | func main() { 7 | // No command line options yet... 8 | 9 | // Print a message: 10 | fmt.Println("admin is not yet implemented") 11 | } 12 | -------------------------------------------------------------------------------- /tutorial/include/admin_final.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 ("encoding/csv"; "flag"; "io"; "os"; "database/sql"; "encoding/json" 13 | _ "github.com/go-sql-driver/mysql"; "log"; "fmt") 14 | 15 | // Configuration information for Google OAuth, MySQL, and Memcached. We 16 | // parse this from a JSON config file 17 | // 18 | // NB: field names must start with Capital letter for JSON parse to work 19 | // 20 | // NB: between the field names and JSON mnemonics, it should be easy to 21 | // figure out what each field does 22 | type Config struct { 23 | ClientId string `json:"OauthGoogleClientId"` 24 | ClientSecret string `json:"OauthGoogleClientSecret"` 25 | Scopes []string `json:"OauthGoogleScopes"` 26 | RedirectUrl string `json:"OauthGoogleRedirectUrl"` 27 | DbUser string `json:"MysqlUsername"` 28 | DbPass string `json:"MysqlPassword"` 29 | DbHost string `json:"MysqlHost"` 30 | DbPort string `json:"MysqlPort"` 31 | DbName string `json:"MysqlDbname"` 32 | McdHost string `json:"MemcachedHost"` 33 | McdPort string `json:"MemcachedPort"` 34 | } 35 | 36 | // The configuration information for the app we're administering 37 | var cfg Config 38 | 39 | // Load a JSON file that has all the config information for our app, and put 40 | // the JSON contents into the cfg variable 41 | func loadConfig(cfgFileName string) { 42 | f, err := os.Open(cfgFileName) 43 | if err != nil { log.Fatal(err) } 44 | defer f.Close() 45 | jsonParser := json.NewDecoder(f) 46 | if err = jsonParser.Decode(&cfg); err != nil { log.Fatal(err) } 47 | } 48 | 49 | // Create the database that will be used by our program. The database name 50 | // is read from the config file, so this code is very generic. 51 | func createDatabase() { 52 | // NB: trailing '/' is necessary to indicate that we aren't 53 | // specifying any database 54 | db, err := sql.Open("mysql", 55 | cfg.DbUser+":"+cfg.DbPass+"@("+cfg.DbHost+":"+cfg.DbPort+")/") 56 | if err != nil { log.Fatal(err) } 57 | defer db.Close() 58 | _, err = db.Exec("CREATE DATABASE `"+cfg.DbName+"`;") 59 | if err != nil { log.Fatal(err) } 60 | } 61 | 62 | // Delete the database that was being used by our program 63 | // NB: code is almost identical to createDatabase() 64 | func deleteDatabase() { 65 | db, err := sql.Open("mysql", 66 | cfg.DbUser+":"+cfg.DbPass+"@("+cfg.DbHost+":"+cfg.DbPort+")/") 67 | if err != nil { log.Fatal(err) } 68 | defer db.Close() 69 | _, err = db.Exec("DROP DATABASE `"+cfg.DbName+"`;") 70 | if err != nil { log.Fatal(err) } 71 | } 72 | 73 | // Connect to the database, and return the corresponding database object 74 | // 75 | // NB: will fail if the database hasn't been created 76 | func openDB() *sql.DB { 77 | db, err := sql.Open("mysql", cfg.DbUser+":"+cfg.DbPass+"@tcp("+cfg.DbHost+":"+cfg.DbPort+")/"+cfg.DbName) 78 | if err != nil { log.Fatal(err) } 79 | // Ping the database to be sure it's live 80 | err = db.Ping() 81 | if err != nil { log.Fatal(err) } 82 | return db 83 | } 84 | 85 | // drop and re-create the Users table. The table name, field names, and 86 | // field types are hard-coded into this function... for Google OAuth, you 87 | // don't need to change this unless you want to add profile pics or other 88 | // user-defined columns 89 | func resetUserTable(db *sql.DB) { 90 | // drop the old users table 91 | stmt, err := db.Prepare("DROP TABLE IF EXISTS users") 92 | if err != nil { log.Fatal(err) } 93 | defer stmt.Close() 94 | _, err = stmt.Exec() 95 | if err != nil {log.Fatal(err) } 96 | 97 | // create the new users table 98 | // 99 | // NB: state is 0, 1 for pending, active 100 | // 101 | // NB: we technically don't need a prepared statement for a one-time 102 | // query, but knowing the statement is legit makes debugging 103 | // database interactions a tad easier 104 | stmt, err = db.Prepare(`CREATE TABLE users ( 105 | id int not null auto_increment PRIMARY KEY, 106 | state int not null, 107 | googleid varchar(100) not null, 108 | name varchar(100) not null, 109 | email varchar(100) not null 110 | )`) 111 | if err != nil { log.Fatal(err) } 112 | _, err = stmt.Exec() 113 | if err != nil { log.Fatal(err) } 114 | } 115 | 116 | // drop and re-create the Data table. The table name, field names, and field 117 | // types are hard-coded into this function. For any realistic app, you'll 118 | // need more complex code, which creates multiple tables, any foreign key 119 | // constraints, views, etc. For our demo app, this + resetUserTable() is all 120 | // we need to configure the data model. 121 | func resetDataTable(db *sql.DB) { 122 | // drop the old table 123 | stmt, err := db.Prepare("DROP TABLE IF EXISTS data") 124 | if err != nil { log.Fatal(err) } 125 | defer stmt.Close() 126 | _, err = stmt.Exec() 127 | if err != nil { log.Fatal(err) } 128 | 129 | // create the new objectives table 130 | stmt, err = db.Prepare(`CREATE TABLE data ( 131 | id int not null auto_increment PRIMARY KEY, 132 | smallnote varchar(2000) not null, 133 | bignote text not null, 134 | favint int not null, 135 | favfloat float not null, 136 | trickfloat float)`) 137 | if err != nil { log.Fatal(err) } 138 | _, err = stmt.Exec() 139 | if err != nil { log.Fatal(err) } 140 | } 141 | 142 | // Parse a CSV so that each line becomes an array of strings, and then use 143 | // the array of strings to push a row to the data table 144 | // 145 | // NB: It goes without saying that this is hard-coded for our Data table. 146 | // However, the hard-coding is quite simple: we just need to hard-code 147 | // the INSERT statement itself, and then the mapping of array positions 148 | // to VALUES in that INSERT statement. 149 | func loadCsv(csvname *string, db *sql.DB) { 150 | // load the csv file 151 | file, err := os.Open(*csvname) 152 | if err != nil { log.Fatal(err) } 153 | defer file.Close() 154 | 155 | // create the prepared statement for inserting into the database 156 | // 157 | // I chose to have a single statement with 5 parameters, instead of 158 | // two statements to reflect the possible null-ness of the last 159 | // column. That's not an objectively better approach, but for this 160 | // example, it results in less code. 161 | stmt, err := db.Prepare("INSERT INTO data(smallnote, bignote, favint, favfloat, trickfloat) VALUES(?, ?, ?, ?, ?)") 162 | if err != nil { log.Fatal(err) } 163 | 164 | // parse the csv, one record at a time 165 | reader := csv.NewReader(file) 166 | reader.Comma = ',' 167 | count := 0 // count insertions, for the sake of nice output 168 | for { 169 | // get next row... exit on EOF 170 | row, err := reader.Read() 171 | if err == io.EOF { break } else if err != nil { log.Fatal(err) } 172 | // We have an array of strings, representing the data. We 173 | // can dump it into the database by matching array indices 174 | // with parameters to the INSERT statement. The only tricky 175 | // part is that our last column can be null, so we need a way 176 | // to handle null values, which are '""' in the CSV 177 | var lastcol *string = nil 178 | if row[4] != "" { lastcol = &row[4] } 179 | // NB: no need for casts... the MySQL driver will figure out 180 | // the types 181 | _, err = stmt.Exec(row[0], row[1], row[2], row[3], lastcol) 182 | if err != nil { log.Fatal(err) } 183 | count++ 184 | } 185 | log.Println("Added", count, "rows") 186 | } 187 | 188 | // When a user registers, the new account is not active until the 189 | // administrator activates it. This function lists registrations that are 190 | // not yet activated 191 | func listNewAccounts(db *sql.DB) { 192 | // get all inactive rows from the database 193 | rows, err := db.Query("SELECT * FROM users WHERE state = ?", 0) 194 | if err != nil { log.Fatal(err) } 195 | defer rows.Close() 196 | // scan into these vars, which get reused on each loop iteration 197 | var (id int; state int; googleid string; name string; email string) 198 | // print a header 199 | fmt.Println("New Users:") 200 | fmt.Println("[id googleid name email]") 201 | // print the rows 202 | for rows.Next() { 203 | err = rows.Scan(&id, &state, &googleid, &name, &email) 204 | if err != nil { log.Fatal(err) } 205 | fmt.Println(id, googleid, name, email) 206 | } 207 | // On error, rows.Next() returns false... but we still need to check 208 | // for errors 209 | err = rows.Err() 210 | if err != nil { log.Fatal(err) } 211 | } 212 | 213 | // Since this is an administrative interface, we don't need to do anything 214 | // too fancy for the account activation: the flags include the ID to update, 215 | // we just use it to update the database, and we don't worry about the case 216 | // where the account is already activated 217 | func activateAccount(db *sql.DB, id int) { 218 | _, err := db.Exec("UPDATE users SET state = 1 WHERE id = ?", id) 219 | if err != nil { log.Fatal(err) } 220 | } 221 | 222 | // We occasionally need to do one-off queries that can't really be predicted 223 | // ahead of time. When that time comes, we can edit this function, 224 | // recompile, and then run with the "oneoff" flag to do the corresponding 225 | // action. For now, it's hard coded to delete userid=1 from the Users table 226 | func doOneOff(db *sql.DB) { 227 | _, err := db.Exec("DELETE FROM users WHERE id = ?", 1) 228 | if err != nil { log.Fatal(err) } 229 | } 230 | 231 | // Main routine: Use the command line options to determine what action to 232 | // take, then forward to the appropriate function. Since this program is for 233 | // quick admin tasks, all of the above functions terminate on any error. 234 | // That means we don't need error checking in this code... any function we 235 | // call will only return on success. 236 | func main() { 237 | // parse command line options 238 | configPath := flag.String("configfile", "config.json", "Path to the configuration (JSON) file") 239 | csvName := flag.String("csvfile", "data.csv", "The csv file to parse") 240 | opCreateDb := flag.Bool("createschema", false, "Create the MySQL database into which we will create tables?") 241 | opDeleteDb := flag.Bool("deleteschema", false, "Delete the entire MySQL database?") 242 | opResetUserTable := flag.Bool("resetuserstable", false, "Delete and re-create the users table?") 243 | opResetDataTable := flag.Bool("resetdatatable", false, "Delete and re-create the data table?") 244 | opCsv := flag.Bool("loadcsv", false, "Load a csv into the data table?") 245 | opListNewReg := flag.Bool("listnewusers", false, "List new registrations?") 246 | opRegister := flag.Int("activatenewuser", -1, "Complete pending registration for a user") 247 | opOneOff := flag.Bool("oneoff", false, "Run a one-off query") 248 | flag.Parse() 249 | 250 | // load the JSON config file 251 | loadConfig(*configPath) 252 | 253 | // if we are creating or deleting a database, do the op and return 254 | if *opCreateDb { createDatabase(); return } 255 | if *opDeleteDb { deleteDatabase(); return } 256 | 257 | // open the database 258 | db := openDB() 259 | defer db.Close() 260 | 261 | // all other ops are handled below: 262 | if *opResetUserTable { resetUserTable(db) } 263 | if *opResetDataTable { resetDataTable(db) } 264 | if *opCsv { loadCsv(csvName, db) } 265 | if *opListNewReg { listNewAccounts(db) } 266 | if *opRegister != -1 { activateAccount(db, * opRegister) } 267 | if *opOneOff { doOneOff(db) } 268 | } 269 | -------------------------------------------------------------------------------- /tutorial/include/statichttpserver.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 ("net/http"; "flag"; "fmt") 13 | 14 | func main() { 15 | 16 | // use a command-line flag (-p) to set the port on which to serve 17 | port := flag.String("p", "8080", 18 | "The port on which to install the http server") 19 | // use a command-line flag (-f) to specify the root folder to serve 20 | folder := flag.String("f", "./", 21 | "The folder from which to serve requests") 22 | 23 | flag.Parse() 24 | 25 | // print our configuration 26 | fmt.Println("Serving " + *folder + " on port " + *port) 27 | 28 | // On any request, we add the folder prefix and then attempt to serve 29 | // the file that results. Note that this approach will display 30 | // folders, too. 31 | http.HandleFunc("/", func (res http.ResponseWriter, req *http.Request) { 32 | http.ServeFile(res, req, *folder+req.URL.Path) 33 | }) 34 | http.ListenAndServe(":"+*port, nil) 35 | } 36 | -------------------------------------------------------------------------------- /tutorial/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Golang Webapp Tutorial 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 26 | 27 | 36 | 37 | 38 |
39 |
40 |
41 |

Golang Webapp Tutorial

42 |

Mike Spear

43 |
44 | blog 45 | ~ email 46 |
47 | print mode 48 | ~ display mode 49 |
50 | 53 |
54 | 55 |
56 |

Overview

57 |
58 |

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':
  • 392 |
393 |
{
394 |     ...
395 |     "MysqlUsername" : "bob",
396 |     "MysqlPassword" : "bob_has_@_$up3r_$tr0NG_P@sSW0rd",
397 |     "MysqlHost"     : "127.0.0.1",
398 |     "MysqlPort"     : "3306",
399 |     "MysqlDbname"   : "webapp",
400 |     ...
401 | }
402 |
    403 |
  • Note: you can choose a better name for your database than 'webapp'!
  • 404 |
405 |
406 |
407 | 408 |
409 |

Database Configuration

410 |
411 |
    412 |
  • Get the Go drivers for MySQL:
  • 413 |
414 |
go get github.com/go-sql-driver/mysql
415 |
    416 |
  • The code we need can be found in src/admin/main.go:
  • 417 |
418 |
// Configuration information for Google OAuth, MySQL, and Memcached.  We
419 | // parse this from a JSON config file
420 | //
421 | // NB: field names must start with Capital letter for JSON parse to work
422 | type Config struct {
423 | 	ClientId string `json:"OauthGoogleClientId"`
424 | 	ClientSecret string `json:"OauthGoogleClientSecret"`
425 | 	Scopes []string `json:"OauthGoogleScopes"`
426 | 	RedirectUrl string `json:"OauthGoogleRedirectUrl"`
427 | 	DbUser string `json:"MysqlUsername"`
428 | 	DbPass string `json:"MysqlPassword"`
429 | 	DbHost string `json:"MysqlHost"`
430 | 	DbPort string `json:"MysqlPort"`
431 | 	DbName string `json:"MysqlDbname"`
432 | 	McdHost string `json:"MemcachedHost"`
433 | 	McdPort string `json:"MemcachedPort"`
434 | }
435 | 
436 | // The configuration information for the app we're administering
437 | var cfg Config
438 | 
439 | // Load a JSON file that has all the config information for our app
440 | func loadConfig(cfgFileName string) {
441 | 	f, err := os.Open(cfgFileName)
442 | 	if err != nil { panic(err) }
443 | 	defer f.Close()
444 | 	jsonParser := json.NewDecoder(f)
445 | 	if err = jsonParser.Decode(&cfg); err != nil { panic(err) }
446 | }
447 | 
448 | // Create the schema that will be used by our program
449 | func createSchema() {
450 | 	// NB: trailing '/' is necessary
451 | 	db, err := sql.Open("mysql", cfg.DbUser+":"+cfg.DbPass+"@("+cfg.DbHost+":"+cfg.DbPort+")/")
452 | 	if err != nil { log.Fatal(err) }
453 | 	_, err = db.Exec("CREATE DATABASE `"+cfg.DbName+"`;")
454 | 	if err != nil { log.Fatal(err) }
455 | }
456 |
457 |
458 | 459 |
460 |

Google OAuth 2.0 Configuration

461 |
462 |
    463 |
  • Head to the Google Developer Console
  • 465 |
  • 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 |

674 |
675 | 676 |
677 |
678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 701 | 702 | 703 | -------------------------------------------------------------------------------- /tutorial/js/head.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | Head JS The only script in your 3 | Copyright Tero Piirainen (tipiirai) 4 | License MIT / http://bit.ly/mit-license 5 | Version 0.96 6 | 7 | http://headjs.com 8 | */(function(a){function z(){d||(d=!0,s(e,function(a){p(a)}))}function y(c,d){var e=a.createElement("script");e.type="text/"+(c.type||"javascript"),e.src=c.src||c,e.async=!1,e.onreadystatechange=e.onload=function(){var a=e.readyState;!d.done&&(!a||/loaded|complete/.test(a))&&(d.done=!0,d())},(a.body||b).appendChild(e)}function x(a,b){if(a.state==o)return b&&b();if(a.state==n)return k.ready(a.name,b);if(a.state==m)return a.onpreload.push(function(){x(a,b)});a.state=n,y(a.url,function(){a.state=o,b&&b(),s(g[a.name],function(a){p(a)}),u()&&d&&s(g.ALL,function(a){p(a)})})}function w(a,b){a.state===undefined&&(a.state=m,a.onpreload=[],y({src:a.url,type:"cache"},function(){v(a)}))}function v(a){a.state=l,s(a.onpreload,function(a){a.call()})}function u(a){a=a||h;var b;for(var c in a){if(a.hasOwnProperty(c)&&a[c].state!=o)return!1;b=!0}return b}function t(a){return Object.prototype.toString.call(a)=="[object Function]"}function s(a,b){if(!!a){typeof a=="object"&&(a=[].slice.call(a));for(var c=0;c/g, ">")); 11 | hljs.highlightBlock(block); 12 | }); 13 | else 14 | hljs.highlightBlock(block); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tutorial/plugin/math/math.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A plugin which enables rendering of math equations inside 3 | * of reveal.js slides. Essentially a thin wrapper for MathJax. 4 | * 5 | * @author Hakim El Hattab 6 | */ 7 | var RevealMath = window.RevealMath || (function(){ 8 | 9 | var options = Reveal.getConfig().math || {}; 10 | options.mathjax = options.mathjax || 'http://cdn.mathjax.org/mathjax/latest/MathJax.js'; 11 | options.config = options.config || 'TeX-AMS_HTML-full'; 12 | 13 | loadScript( options.mathjax + '?config=' + options.config, function() { 14 | 15 | MathJax.Hub.Config({ 16 | messageStyle: 'none', 17 | tex2jax: { inlineMath: [['$','$'],['\\(','\\)']] }, 18 | skipStartupTypeset: true 19 | }); 20 | 21 | // Typeset followed by an immediate reveal.js layout since 22 | // the typesetting process could affect slide height 23 | MathJax.Hub.Queue( [ 'Typeset', MathJax.Hub ] ); 24 | MathJax.Hub.Queue( Reveal.layout ); 25 | 26 | // Reprocess equations in slides when they turn visible 27 | Reveal.addEventListener( 'slidechanged', function( event ) { 28 | 29 | MathJax.Hub.Queue( [ 'Typeset', MathJax.Hub, event.currentSlide ] ); 30 | 31 | } ); 32 | 33 | } ); 34 | 35 | function loadScript( url, callback ) { 36 | 37 | var head = document.querySelector( 'head' ); 38 | var script = document.createElement( 'script' ); 39 | script.type = 'text/javascript'; 40 | script.src = url; 41 | 42 | // Wrapper for callback to make sure it only fires once 43 | var finish = function() { 44 | if( typeof callback === 'function' ) { 45 | callback.call(); 46 | callback = null; 47 | } 48 | } 49 | 50 | script.onload = finish; 51 | 52 | // IE 53 | script.onreadystatechange = function() { 54 | if ( this.readyState === 'loaded' ) { 55 | finish(); 56 | } 57 | } 58 | 59 | // Normal browsers 60 | head.appendChild( script ); 61 | 62 | } 63 | 64 | })(); 65 | -------------------------------------------------------------------------------- /tutorial/plugin/notes/notes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Notes 7 | 8 | 152 | 153 | 154 | 155 | 156 |
157 |
UPCOMING:
158 |
159 |
160 |

Time Click to Reset

161 |
162 | 0:00 AM 163 |
164 |
165 | 00:00:00 166 |
167 |
168 |
169 | 170 | 174 |
175 | 176 | 177 | 398 | 399 | 400 | -------------------------------------------------------------------------------- /tutorial/plugin/notes/notes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Handles opening of and synchronization with the reveal.js 3 | * notes window. 4 | * 5 | * Handshake process: 6 | * 1. This window posts 'connect' to notes window 7 | * - Includes URL of presentation to show 8 | * 2. Notes window responds with 'connected' when it is available 9 | * 3. This window proceeds to send the current presentation state 10 | * to the notes window 11 | */ 12 | var RevealNotes = (function() { 13 | 14 | function openNotes() { 15 | var jsFileLocation = document.querySelector('script[src$="notes.js"]').src; // this js file path 16 | jsFileLocation = jsFileLocation.replace(/notes\.js(\?.*)?$/, ''); // the js folder path 17 | var notesPopup = window.open( jsFileLocation + 'notes.html', 'reveal.js - Notes', 'width=1100,height=700' ); 18 | 19 | /** 20 | * Connect to the notes window through a postmessage handshake. 21 | * Using postmessage enables us to work in situations where the 22 | * origins differ, such as a presentation being opened from the 23 | * file system. 24 | */ 25 | function connect() { 26 | // Keep trying to connect until we get a 'connected' message back 27 | var connectInterval = setInterval( function() { 28 | notesPopup.postMessage( JSON.stringify( { 29 | namespace: 'reveal-notes', 30 | type: 'connect', 31 | url: window.location.protocol + '//' + window.location.host + window.location.pathname, 32 | state: Reveal.getState() 33 | } ), '*' ); 34 | }, 500 ); 35 | 36 | window.addEventListener( 'message', function( event ) { 37 | var data = JSON.parse( event.data ); 38 | if( data && data.namespace === 'reveal-notes' && data.type === 'connected' ) { 39 | clearInterval( connectInterval ); 40 | onConnected(); 41 | } 42 | } ); 43 | } 44 | 45 | /** 46 | * Posts the current slide data to the notes window 47 | */ 48 | function post() { 49 | 50 | var slideElement = Reveal.getCurrentSlide(), 51 | notesElement = slideElement.querySelector( 'aside.notes' ); 52 | 53 | var messageData = { 54 | namespace: 'reveal-notes', 55 | type: 'state', 56 | notes: '', 57 | markdown: false, 58 | state: Reveal.getState() 59 | }; 60 | 61 | // Look for notes defined in a slide attribute 62 | if( slideElement.hasAttribute( 'data-notes' ) ) { 63 | messageData.notes = slideElement.getAttribute( 'data-notes' ); 64 | } 65 | 66 | // Look for notes defined in an aside element 67 | if( notesElement ) { 68 | messageData.notes = notesElement.innerHTML; 69 | messageData.markdown = typeof notesElement.getAttribute( 'data-markdown' ) === 'string'; 70 | } 71 | 72 | notesPopup.postMessage( JSON.stringify( messageData ), '*' ); 73 | 74 | } 75 | 76 | /** 77 | * Called once we have established a connection to the notes 78 | * window. 79 | */ 80 | function onConnected() { 81 | 82 | // Monitor events that trigger a change in state 83 | Reveal.addEventListener( 'slidechanged', post ); 84 | Reveal.addEventListener( 'fragmentshown', post ); 85 | Reveal.addEventListener( 'fragmenthidden', post ); 86 | Reveal.addEventListener( 'overviewhidden', post ); 87 | Reveal.addEventListener( 'overviewshown', post ); 88 | Reveal.addEventListener( 'paused', post ); 89 | Reveal.addEventListener( 'resumed', post ); 90 | 91 | // Post the initial state 92 | post(); 93 | 94 | } 95 | 96 | connect(); 97 | } 98 | 99 | if( !/receiver/i.test( window.location.search ) ) { 100 | 101 | // If the there's a 'notes' query set, open directly 102 | if( window.location.search.match( /(\?|\&)notes/gi ) !== null ) { 103 | openNotes(); 104 | } 105 | 106 | // Open the notes when the 's' key is hit 107 | document.addEventListener( 'keydown', function( event ) { 108 | // Disregard the event if the target is editable or a 109 | // modifier is present 110 | if ( document.querySelector( ':focus' ) !== null || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey ) return; 111 | 112 | if( event.keyCode === 83 ) { 113 | event.preventDefault(); 114 | openNotes(); 115 | } 116 | }, false ); 117 | 118 | } 119 | 120 | return { open: openNotes }; 121 | 122 | })(); 123 | -------------------------------------------------------------------------------- /tutorial/plugin/zoom-js/zoom.js: -------------------------------------------------------------------------------- 1 | // Custom reveal.js integration 2 | (function(){ 3 | var isEnabled = true; 4 | 5 | document.querySelector( '.reveal' ).addEventListener( 'mousedown', function( event ) { 6 | var modifier = ( Reveal.getConfig().zoomKey ? Reveal.getConfig().zoomKey : 'alt' ) + 'Key'; 7 | 8 | var zoomPadding = 20; 9 | var revealScale = Reveal.getScale(); 10 | 11 | if( event[ modifier ] && isEnabled ) { 12 | event.preventDefault(); 13 | 14 | var bounds = event.target.getBoundingClientRect(); 15 | 16 | zoom.to({ 17 | x: ( bounds.left * revealScale ) - zoomPadding, 18 | y: ( bounds.top * revealScale ) - zoomPadding, 19 | width: ( bounds.width * revealScale ) + ( zoomPadding * 2 ), 20 | height: ( bounds.height * revealScale ) + ( zoomPadding * 2 ), 21 | pan: false 22 | }); 23 | } 24 | } ); 25 | 26 | Reveal.addEventListener( 'overviewshown', function() { isEnabled = false; } ); 27 | Reveal.addEventListener( 'overviewhidden', function() { isEnabled = true; } ); 28 | })(); 29 | 30 | /*! 31 | * zoom.js 0.3 (modified for use with reveal.js) 32 | * http://lab.hakim.se/zoom-js 33 | * MIT licensed 34 | * 35 | * Copyright (C) 2011-2014 Hakim El Hattab, http://hakim.se 36 | */ 37 | var zoom = (function(){ 38 | 39 | // The current zoom level (scale) 40 | var level = 1; 41 | 42 | // The current mouse position, used for panning 43 | var mouseX = 0, 44 | mouseY = 0; 45 | 46 | // Timeout before pan is activated 47 | var panEngageTimeout = -1, 48 | panUpdateInterval = -1; 49 | 50 | // Check for transform support so that we can fallback otherwise 51 | var supportsTransforms = 'WebkitTransform' in document.body.style || 52 | 'MozTransform' in document.body.style || 53 | 'msTransform' in document.body.style || 54 | 'OTransform' in document.body.style || 55 | 'transform' in document.body.style; 56 | 57 | if( supportsTransforms ) { 58 | // The easing that will be applied when we zoom in/out 59 | document.body.style.transition = 'transform 0.8s ease'; 60 | document.body.style.OTransition = '-o-transform 0.8s ease'; 61 | document.body.style.msTransition = '-ms-transform 0.8s ease'; 62 | document.body.style.MozTransition = '-moz-transform 0.8s ease'; 63 | document.body.style.WebkitTransition = '-webkit-transform 0.8s ease'; 64 | } 65 | 66 | // Zoom out if the user hits escape 67 | document.addEventListener( 'keyup', function( event ) { 68 | if( level !== 1 && event.keyCode === 27 ) { 69 | zoom.out(); 70 | } 71 | } ); 72 | 73 | // Monitor mouse movement for panning 74 | document.addEventListener( 'mousemove', function( event ) { 75 | if( level !== 1 ) { 76 | mouseX = event.clientX; 77 | mouseY = event.clientY; 78 | } 79 | } ); 80 | 81 | /** 82 | * Applies the CSS required to zoom in, prefers the use of CSS3 83 | * transforms but falls back on zoom for IE. 84 | * 85 | * @param {Object} rect 86 | * @param {Number} scale 87 | */ 88 | function magnify( rect, scale ) { 89 | 90 | var scrollOffset = getScrollOffset(); 91 | 92 | // Ensure a width/height is set 93 | rect.width = rect.width || 1; 94 | rect.height = rect.height || 1; 95 | 96 | // Center the rect within the zoomed viewport 97 | rect.x -= ( window.innerWidth - ( rect.width * scale ) ) / 2; 98 | rect.y -= ( window.innerHeight - ( rect.height * scale ) ) / 2; 99 | 100 | if( supportsTransforms ) { 101 | // Reset 102 | if( scale === 1 ) { 103 | document.body.style.transform = ''; 104 | document.body.style.OTransform = ''; 105 | document.body.style.msTransform = ''; 106 | document.body.style.MozTransform = ''; 107 | document.body.style.WebkitTransform = ''; 108 | } 109 | // Scale 110 | else { 111 | var origin = scrollOffset.x +'px '+ scrollOffset.y +'px', 112 | transform = 'translate('+ -rect.x +'px,'+ -rect.y +'px) scale('+ scale +')'; 113 | 114 | document.body.style.transformOrigin = origin; 115 | document.body.style.OTransformOrigin = origin; 116 | document.body.style.msTransformOrigin = origin; 117 | document.body.style.MozTransformOrigin = origin; 118 | document.body.style.WebkitTransformOrigin = origin; 119 | 120 | document.body.style.transform = transform; 121 | document.body.style.OTransform = transform; 122 | document.body.style.msTransform = transform; 123 | document.body.style.MozTransform = transform; 124 | document.body.style.WebkitTransform = transform; 125 | } 126 | } 127 | else { 128 | // Reset 129 | if( scale === 1 ) { 130 | document.body.style.position = ''; 131 | document.body.style.left = ''; 132 | document.body.style.top = ''; 133 | document.body.style.width = ''; 134 | document.body.style.height = ''; 135 | document.body.style.zoom = ''; 136 | } 137 | // Scale 138 | else { 139 | document.body.style.position = 'relative'; 140 | document.body.style.left = ( - ( scrollOffset.x + rect.x ) / scale ) + 'px'; 141 | document.body.style.top = ( - ( scrollOffset.y + rect.y ) / scale ) + 'px'; 142 | document.body.style.width = ( scale * 100 ) + '%'; 143 | document.body.style.height = ( scale * 100 ) + '%'; 144 | document.body.style.zoom = scale; 145 | } 146 | } 147 | 148 | level = scale; 149 | 150 | if( document.documentElement.classList ) { 151 | if( level !== 1 ) { 152 | document.documentElement.classList.add( 'zoomed' ); 153 | } 154 | else { 155 | document.documentElement.classList.remove( 'zoomed' ); 156 | } 157 | } 158 | } 159 | 160 | /** 161 | * Pan the document when the mosue cursor approaches the edges 162 | * of the window. 163 | */ 164 | function pan() { 165 | var range = 0.12, 166 | rangeX = window.innerWidth * range, 167 | rangeY = window.innerHeight * range, 168 | scrollOffset = getScrollOffset(); 169 | 170 | // Up 171 | if( mouseY < rangeY ) { 172 | window.scroll( scrollOffset.x, scrollOffset.y - ( 1 - ( mouseY / rangeY ) ) * ( 14 / level ) ); 173 | } 174 | // Down 175 | else if( mouseY > window.innerHeight - rangeY ) { 176 | window.scroll( scrollOffset.x, scrollOffset.y + ( 1 - ( window.innerHeight - mouseY ) / rangeY ) * ( 14 / level ) ); 177 | } 178 | 179 | // Left 180 | if( mouseX < rangeX ) { 181 | window.scroll( scrollOffset.x - ( 1 - ( mouseX / rangeX ) ) * ( 14 / level ), scrollOffset.y ); 182 | } 183 | // Right 184 | else if( mouseX > window.innerWidth - rangeX ) { 185 | window.scroll( scrollOffset.x + ( 1 - ( window.innerWidth - mouseX ) / rangeX ) * ( 14 / level ), scrollOffset.y ); 186 | } 187 | } 188 | 189 | function getScrollOffset() { 190 | return { 191 | x: window.scrollX !== undefined ? window.scrollX : window.pageXOffset, 192 | y: window.scrollY !== undefined ? window.scrollY : window.pageYOffset 193 | } 194 | } 195 | 196 | return { 197 | /** 198 | * Zooms in on either a rectangle or HTML element. 199 | * 200 | * @param {Object} options 201 | * - element: HTML element to zoom in on 202 | * OR 203 | * - x/y: coordinates in non-transformed space to zoom in on 204 | * - width/height: the portion of the screen to zoom in on 205 | * - scale: can be used instead of width/height to explicitly set scale 206 | */ 207 | to: function( options ) { 208 | 209 | // Due to an implementation limitation we can't zoom in 210 | // to another element without zooming out first 211 | if( level !== 1 ) { 212 | zoom.out(); 213 | } 214 | else { 215 | options.x = options.x || 0; 216 | options.y = options.y || 0; 217 | 218 | // If an element is set, that takes precedence 219 | if( !!options.element ) { 220 | // Space around the zoomed in element to leave on screen 221 | var padding = 20; 222 | var bounds = options.element.getBoundingClientRect(); 223 | 224 | options.x = bounds.left - padding; 225 | options.y = bounds.top - padding; 226 | options.width = bounds.width + ( padding * 2 ); 227 | options.height = bounds.height + ( padding * 2 ); 228 | } 229 | 230 | // If width/height values are set, calculate scale from those values 231 | if( options.width !== undefined && options.height !== undefined ) { 232 | options.scale = Math.max( Math.min( window.innerWidth / options.width, window.innerHeight / options.height ), 1 ); 233 | } 234 | 235 | if( options.scale > 1 ) { 236 | options.x *= options.scale; 237 | options.y *= options.scale; 238 | 239 | magnify( options, options.scale ); 240 | 241 | if( options.pan !== false ) { 242 | 243 | // Wait with engaging panning as it may conflict with the 244 | // zoom transition 245 | panEngageTimeout = setTimeout( function() { 246 | panUpdateInterval = setInterval( pan, 1000 / 60 ); 247 | }, 800 ); 248 | 249 | } 250 | } 251 | } 252 | }, 253 | 254 | /** 255 | * Resets the document zoom state to its default. 256 | */ 257 | out: function() { 258 | clearTimeout( panEngageTimeout ); 259 | clearInterval( panUpdateInterval ); 260 | 261 | magnify( { x: 0, y: 0 }, 1 ); 262 | 263 | level = 1; 264 | }, 265 | 266 | // Alias 267 | magnify: function( options ) { this.to( options ) }, 268 | reset: function() { this.out() }, 269 | 270 | zoomLevel: function() { 271 | return level; 272 | } 273 | } 274 | 275 | })(); 276 | 277 | 278 | 279 | --------------------------------------------------------------------------------