diff --git a/.gitignore b/.gitignore index 3e5ff48..fb9d891 100644 --- a/.gitignore +++ b/.gitignore @@ -260,3 +260,4 @@ $RECYCLE.BIN/ .idea happy frontend/dist +*.db diff --git a/Makefile b/Makefile index 0174a1b..5c6f4ac 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,6 @@ frontend-watch: cd frontend && yarn build --watch run: build - ./happy + ./happy -develop .PHONY: run frontend frontend-watch diff --git a/frontend/package.json b/frontend/package.json index 3d23619..015e57d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,8 +8,13 @@ "lint": "vue-cli-service lint" }, "dependencies": { + "axios": "^0.19.0", + "bootstrap": "^4.3.1", + "bootstrap-vue": "^2.0.4", "core-js": "^3.1.2", - "vue": "^2.6.10" + "vue": "^2.6.10", + "vue-cookies": "^1.5.13", + "vuex": "^3.1.1" }, "devDependencies": { "@vue/cli-plugin-babel": "^4.0.0", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index fcc5662..9e693ea 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,28 +1,39 @@ diff --git a/frontend/src/api.js b/frontend/src/api.js new file mode 100644 index 0000000..c2c338f --- /dev/null +++ b/frontend/src/api.js @@ -0,0 +1,23 @@ +import axios from 'axios'; + +export default { + getMoods(userInfo) { + return axios.get("/v1/moods", { + headers: { + 'X-user-id': userInfo ? userInfo.ID : null, + 'X-user-validation': userInfo ? userInfo.Validation : null + } + }) + }, + setMood(userInfo, mood) { + return axios.post("/v1/moods", mood, { + headers: { + 'X-user-id': userInfo.ID, + 'X-user-validation': userInfo.Validation + } + }) + }, + getNewUser() { + return axios.get("/v1/register/code") + } +} diff --git a/frontend/src/components/ChoiceButton.vue b/frontend/src/components/ChoiceButton.vue new file mode 100644 index 0000000..e7be908 --- /dev/null +++ b/frontend/src/components/ChoiceButton.vue @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/Chooser.vue b/frontend/src/components/Chooser.vue new file mode 100644 index 0000000..5adff76 --- /dev/null +++ b/frontend/src/components/Chooser.vue @@ -0,0 +1,37 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/UserInfo.vue b/frontend/src/components/UserInfo.vue new file mode 100644 index 0000000..2b6cc4f --- /dev/null +++ b/frontend/src/components/UserInfo.vue @@ -0,0 +1,43 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/main.js b/frontend/src/main.js index 63eb05f..8df337f 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -1,8 +1,19 @@ import Vue from 'vue' -import App from './App.vue' +Vue.config.productionTip = false; -Vue.config.productionTip = false +import VueCookies from 'vue-cookies' +Vue.use(VueCookies); +VueCookies.config('30d'); + +import BootstrapVue from 'bootstrap-vue' +Vue.use(BootstrapVue) +import 'bootstrap/dist/css/bootstrap.css' +import 'bootstrap-vue/dist/bootstrap-vue.css' + +import store from './store.js'; +import App from './App.vue'; new Vue({ render: h => h(App), -}).$mount('#app') + store, +}).$mount('#app'); diff --git a/frontend/src/store.js b/frontend/src/store.js new file mode 100644 index 0000000..aedb119 --- /dev/null +++ b/frontend/src/store.js @@ -0,0 +1,47 @@ +import Vue from 'vue'; +import Vuex from "vuex"; + +import api from './api.js' + +Vue.use(Vuex); + +const store = new Vuex.Store({ + state: { + err: null, + userInfo: null, + moods: [], + }, + actions: { + getMoods({commit, state}) { + return api.getMoods(state.userInfo) + .then(resp => { + commit('setMoods', resp.data) + }) + }, + getNewUser({commit}) { + return api.getNewUser() + .then(resp => { + commit('setUser', resp.data); + }) + }, + setMood({state}, mood) { + return api.setMood(state.userInfo, mood) + }, + }, + mutations: { + setUser(state, userInfo) { + state.userInfo = userInfo; + }, + setMoods(state, moods) { + state.moods = moods; + }, + clearError(state) { + state.err = null + }, + setError(state, err) { + state.err = err + } + } +}); + +export default store diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 05ae48a..790be44 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -759,6 +759,15 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== +"@nuxt/opencollective@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@nuxt/opencollective/-/opencollective-0.3.0.tgz#11d8944dcf2d526e31660bb69570be03f8fb72b7" + integrity sha512-Vf09BxCdj1iT2IRqVwX5snaY2WCTkvM0O4cWWSO1ThCFuc4if0Q/nNwAgCxRU0FeYHJ7DdyMUNSdswCLKlVqeg== + dependencies: + chalk "^2.4.2" + consola "^2.10.1" + node-fetch "^2.6.0" + "@soda/friendly-errors-webpack-plugin@^1.7.1": version "1.7.1" resolved "https://registry.yarnpkg.com/@soda/friendly-errors-webpack-plugin/-/friendly-errors-webpack-plugin-1.7.1.tgz#706f64bcb4a8b9642b48ae3ace444c70334d615d" @@ -1450,6 +1459,14 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== +axios@^0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8" + integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ== + dependencies: + follow-redirects "1.5.10" + is-buffer "^2.0.2" + babel-eslint@^10.0.1: version "10.0.3" resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a" @@ -1593,6 +1610,22 @@ boolbase@^1.0.0, boolbase@~1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= +bootstrap-vue@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/bootstrap-vue/-/bootstrap-vue-2.0.4.tgz#9bf99754a22acece72e02adda3c7965b818ae2bc" + integrity sha512-/5WXa3ir5uajcs7ze7jz7QXpYuJGWHjvJ8biMq0+e0IIIxw2jSdh4LsiFKD7C7qtgKre28hhXOfx79RmZX4wcQ== + dependencies: + "@nuxt/opencollective" "^0.3.0" + bootstrap ">=4.3.1 <5.0.0" + popper.js "^1.15.0" + portal-vue "^2.1.6" + vue-functional-data-merge "^3.1.0" + +"bootstrap@>=4.3.1 <5.0.0", bootstrap@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.3.1.tgz#280ca8f610504d99d7b6b4bfc4b68cec601704ac" + integrity sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -2175,6 +2208,11 @@ connect-history-api-fallback@^1.6.0: resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== +consola@^2.10.1: + version "2.10.1" + resolved "https://registry.yarnpkg.com/consola/-/consola-2.10.1.tgz#4693edba714677c878d520e4c7e4f69306b4b927" + integrity sha512-4sxpH6SGFYLADfUip4vuY65f/gEogrzJoniVhNUYkJHtng0l8ZjnDCqxxrSVRHOHwKxsy8Vm5ONZh1wOR3/l/w== + console-browserify@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" @@ -2574,6 +2612,13 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" +debug@=3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + debug@^3.0.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" @@ -3481,6 +3526,13 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + follow-redirects@^1.0.0: version "1.9.0" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.9.0.tgz#8d5bcdc65b7108fe1508649c79c12d732dcedb4f" @@ -4241,6 +4293,11 @@ is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== +is-buffer@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" + integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== + is-callable@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" @@ -5195,6 +5252,11 @@ no-case@^2.2.0: dependencies: lower-case "^1.1.1" +node-fetch@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" + integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== + node-forge@0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" @@ -5863,6 +5925,16 @@ pkg-up@^2.0.0: dependencies: find-up "^2.1.0" +popper.js@^1.15.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.0.tgz#2e1816bcbbaa518ea6c2e15a466f4cb9c6e2fbb3" + integrity sha512-+G+EkOPoE5S/zChTpmBSSDYmhXJ5PsW8eMhH8cP/CQHMFPBG/kC9Y5IIw6qNYgdJ+/COf0ddY2li28iHaZRSjw== + +portal-vue@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/portal-vue/-/portal-vue-2.1.6.tgz#a7d4790b14a79af7fd159a60ec88c30cddc6c639" + integrity sha512-lvCF85D4e8whd0nN32D8FqKwwkk7nYUI3Ku8UAEx4Z1reomu75dv5evRUTZNaj1EalxxWNXiNl0EHRq36fG8WA== + portfinder@^1.0.20, portfinder@^1.0.24: version "1.0.25" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.25.tgz#254fd337ffba869f4b9d37edc298059cb4d35eca" @@ -7828,6 +7900,11 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019" integrity sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw== +vue-cookies@^1.5.13: + version "1.5.13" + resolved "https://registry.yarnpkg.com/vue-cookies/-/vue-cookies-1.5.13.tgz#b1ca79a2663bf9a4f36982557011cab5ee2196c0" + integrity sha512-8pjpXnvbNWx1Lft0t3MJnW+ylv0Wa2Tb6Ch617u/pQah3+SfXUZZdkh3EL3bSpe/Sw2Wdw3+DhycgQsKANSxXg== + vue-eslint-parser@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-5.0.0.tgz#00f4e4da94ec974b821a26ff0ed0f7a78402b8a1" @@ -7840,6 +7917,11 @@ vue-eslint-parser@^5.0.0: esquery "^1.0.1" lodash "^4.17.11" +vue-functional-data-merge@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz#08a7797583b7f35680587f8a1d51d729aa1dc657" + integrity sha512-leT4kdJVQyeZNY1kmnS1xiUlQ9z1B/kdBFCILIjYYQDqZgLqCLa0UhjSSeRX6c3mUe6U5qYeM8LrEqkHJ1B4LA== + vue-hot-reload-api@^2.3.0: version "2.3.4" resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2" @@ -7882,6 +7964,11 @@ vue@^2.6.10: resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637" integrity sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ== +vuex@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.1.1.tgz#0c264bfe30cdbccf96ab9db3177d211828a5910e" + integrity sha512-ER5moSbLZuNSMBFnEBVGhQ1uCBNJslH9W/Dw2W7GZN23UQA69uapP5GTT9Vm8Trc0PzBSVt6LzF3hGjmv41xcg== + watchpack@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" diff --git a/go.mod b/go.mod index 2f1a47d..bff4fd7 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,11 @@ module github.com/chrissexton/happy go 1.12 require ( - github.com/carbocation/interpose v0.0.0-20161206215253-723534742ba3 - github.com/gorilla/handlers v1.4.2 github.com/gorilla/mux v1.7.3 - github.com/julienschmidt/httprouter v1.3.0 + github.com/jmoiron/sqlx v1.2.0 + github.com/mattn/go-sqlite3 v1.9.0 github.com/rs/zerolog v1.15.0 + github.com/speps/go-hashids v2.0.0+incompatible github.com/stretchr/graceful v1.2.15 + google.golang.org/appengine v1.6.5 // indirect ) diff --git a/go.sum b/go.sum index 7c69276..aef80f5 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,36 @@ -github.com/carbocation/interpose v0.0.0-20161206215253-723534742ba3 h1:RtCys6GUprNaPOP04Zuo65wS10PMbSPPZNvIb9xYYLE= -github.com/carbocation/interpose v0.0.0-20161206215253-723534742ba3/go.mod h1:4PGcghc3ZjA/uozANO8lCHo/gnHyMsm8iFYppSkVE/M= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= -github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/speps/go-hashids v2.0.0+incompatible h1:kSfxGfESueJKTx0mpER9Y/1XHl+FVQjtCqRyYcviFbw= +github.com/speps/go-hashids v2.0.0+incompatible/go.mod h1:P7hqPzMdnZOfyIk+xrlG1QaSMw+gCBdHKsBDnhpaZvc= github.com/stretchr/graceful v1.2.15 h1:vmXbwPGfe8bI6KkgmHry/P1Pk63bM3TDcfi+5mh+VHg= github.com/stretchr/graceful v1.2.15/go.mod h1:IxdGAOTZueMKoBr3oJIzdeg5CCCXbHXfV44sLhfAXXI= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= diff --git a/serve.go b/serve.go index bd7cb9a..aff7541 100644 --- a/serve.go +++ b/serve.go @@ -1,47 +1,297 @@ package main import ( + "database/sql" + "encoding/json" "flag" "fmt" + "math/rand" "net/http" "os" "path" "time" + "github.com/speps/go-hashids" + + _ "github.com/mattn/go-sqlite3" + "github.com/gorilla/mux" + "github.com/jmoiron/sqlx" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/stretchr/graceful" ) var ( - distPath = flag.String("dist", "frontend/dist", "path to dist files") + distPath = flag.String("dist", "frontend/dist", "path to dist files") + salt = flag.String("salt", "happy", "salt for IDs") + minHashLen = flag.Int("minHashLen", 4, "minimum ID hash size") + develop = flag.Bool("develop", false, "turn on develop mode") ) func main() { flag.Parse() log.Logger = log.Logger.Output(zerolog.ConsoleWriter{Out: os.Stderr}) zerolog.SetGlobalLevel(zerolog.DebugLevel) - s := server{"0.0.0.0:8080", "pub"} + s := server{ + addr: "0.0.0.0:8080", + assetPath: "pub", + salt: *salt, + } + db, err := sqlx.Connect("sqlite3", "happy.db") + if err != nil { + log.Fatal(). + Err(err). + Msg("could not connect to database") + } + s.db = db + hd := hashids.NewData() + hd.Salt = s.salt + hd.MinLength = *minHashLen + s.h, _ = hashids.NewWithData(hd) + if err := s.makeDB(); err != nil { + log.Fatal(). + Err(err). + Msg("could not create database") + } s.serve() } -func logger(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Info(). - Str("Path", r.URL.Path). - Str("Method", r.Method). - Msg("HTTP Request") - }) -} - type server struct { addr string assetPath string + db *sqlx.DB + salt string + h *hashids.HashID +} + +func (s *server) makeDB() error { + q := `create table if not exists users ( + id integer primary key, + email text unique, + verification integer not null, + dateCreated integer, + lastLogin integer + )` + if _, err := s.db.Exec(q); err != nil { + return err + } + q = `create table if not exists mood_categories ( + id integer primary key, + name text + )` + if _, err := s.db.Exec(q); err != nil { + return err + } + q = `create table if not exists mood_category_texts ( + id integer primary key, + mood_category_id integer, + key text, + value integer, + foreign key(mood_category_id) references mood_categories(id) + + )` + if _, err := s.db.Exec(q); err != nil { + return err + } + q = `create table if not exists moods ( + id integer primary key, + user_id integer, + mood_category_id integer, + value integer, + time integer, + foreign key(user_id) references users(id), + foreign key(mood_category_id) references mood_categories(id) + )` + if _, err := s.db.Exec(q); err != nil { + return err + } + return nil +} + +func (s *server) recordMood(mood getMoodsResponse, who UserID) error { + q := `insert into moods (user_id,mood_category_id,value,time) values (?,?,?,?)` + _, err := s.db.Exec(q, who.ID, mood.CategoryID, mood.Value, time.Now().Unix()) + return err +} + +type UserID struct { + db *sqlx.DB + + ID int64 + Email sql.NullString + Verification int64 + DateCreated sql.NullInt64 `db:"dateCreated"` + LastLogin sql.NullInt64 `db:"lastLogin"` + + salt int64 + str string +} + +func (s *server) NewUserID() (UserID, error) { + uid := UserID{ + db: s.db, + DateCreated: sql.NullInt64{time.Now().Unix(), true}, + LastLogin: sql.NullInt64{time.Now().Unix(), true}, + Verification: rand.Int63(), + } + q := `insert into users (verification,dateCreated) values (?, ?)` + res, err := s.db.Exec(q, uid.Verification, uid.DateCreated) + if err != nil { + return uid, err + } + id, err := res.LastInsertId() + if err != nil { + return uid, err + } + uid.ID = id + idstr, err := s.h.EncodeInt64([]int64{id}) + if err != nil { + return uid, err + } + uid.str = idstr + return uid, nil +} + +func (s *server) FromStr(uid, verification string) (UserID, error) { + id := UserID{db: s.db} + idInt, err := s.h.DecodeInt64WithError(uid) + if err != nil { + return id, err + } + q := `select id,email,verification,datecreated,lastlogin from users where id=?` + if err := s.db.Get(&id, q, idInt[0]); err != nil { + log.Error(). + Err(err). + Msg("unable to select") + return id, err + } + verify, err := s.h.EncodeInt64([]int64{id.Verification}) + if err != nil { + log.Error(). + Err(err). + Str("verify", verify). + Str("id.Verification", verification). + Msg("unable to encode") + return id, err + } + if verify != verification { + log.Debug(). + Str("verify", verify). + Str("id.Verification", verification). + Msg("verification mismatch") + return id, fmt.Errorf("Invalid verification token") + } + return id, nil +} + +func (u UserID) String() string { + return u.str +} + +type RegisterResponse struct { + ID string + DateCreated int64 + Validation string `json:",omitempty"` +} + +func (s *server) handlerRegisterCode(w http.ResponseWriter, r *http.Request) { + uid, err := s.NewUserID() + if err != nil { + w.WriteHeader(500) + log.Error().Err(err).Msg("error from NewUserID") + fmt.Fprintf(w, "Error registering user: %s", err) + return + } + if err != nil { + w.WriteHeader(500) + log.Error().Err(err).Msg("error converting date") + fmt.Fprintf(w, "Error registering user: %s", err) + return + } + resp := RegisterResponse{ + ID: uid.String(), + DateCreated: uid.DateCreated.Int64, + } + if *develop { + resp.Validation, _ = s.h.EncodeInt64([]int64{uid.Verification}) + } + out, err := json.Marshal(resp) + if err != nil { + w.WriteHeader(500) + log.Error().Err(err).Msg("error from json.Marshal") + fmt.Fprintf(w, "Error registering user: %s", err) + return + } + fmt.Fprintf(w, "%s", string(out)) +} + +type getMoodsResponse struct { + CategoryID int64 `db:"category_id"` + CategoryName string `db:"category_name"` + Key string + Value int64 +} + +func (s *server) getMoods(w http.ResponseWriter, r *http.Request) { + log.Debug().Interface("req", r).Msg("getMoods") + q := `select mc.id category_id,mc.name category_name,mct.key,mct.value + from mood_categories mc + inner join mood_category_texts mct + on mc.id=mct.mood_category_id` + recs := []getMoodsResponse{} + err := s.db.Select(&recs, q) + if err != nil { + log.Error().Err(err).Msg("could not retrieve mood categories and texts") + w.WriteHeader(500) + fmt.Fprintf(w, "Error getting moods: %s", err) + return + } + resp, err := json.Marshal(recs) + if err != nil { + log.Error().Err(err).Msg("error from json.Marshal") + w.WriteHeader(500) + fmt.Fprintf(w, "Error getting moods: %s", err) + return + } + fmt.Fprintf(w, "%s", string(resp)) } func (s *server) handleMood(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "hello") + uid := r.Header.Get("X-user-id") + verify := r.Header.Get("X-user-validation") + log.Debug(). + Str("uid", uid). + Str("verify", verify). + Msg("handleMood") + dec := json.NewDecoder(r.Body) + happyReq := getMoodsResponse{} + err := dec.Decode(&happyReq) + if err != nil { + log.Error().Err(err).Msg("error with happy") + w.WriteHeader(400) + fmt.Fprintf(w, err.Error()) + return + } + log.Debug(). + Interface("mood", happyReq). + Msg("mood") + user, err := s.FromStr(uid, verify) + if err != nil { + log.Error(). + Err(err). + Msg("error getting user") + w.WriteHeader(403) + fmt.Fprintf(w, "Error: %s", err) + return + } + if err := s.recordMood(happyReq, user); err != nil { + log.Error().Err(err).Msg("error saving mood") + w.WriteHeader(500) + fmt.Fprintf(w, "Error: %s", err) + return + } + fmt.Fprintf(w, "ok") } func (s *server) indexHandler(entryPoint string) func(w http.ResponseWriter, r *http.Request) { @@ -65,13 +315,15 @@ func (s *server) indexHandler(entryPoint string) func(w http.ResponseWriter, r * func (s *server) routeSetup() *mux.Router { r := mux.NewRouter() api := r.PathPrefix("/v1/").Subrouter() - api.HandleFunc("/mood", s.handleMood).Methods("POST") + api.HandleFunc("/moods", s.getMoods).Methods("GET") + api.HandleFunc("/moods", s.handleMood).Methods("POST") + api.HandleFunc("/register/code", s.handlerRegisterCode).Methods("GET") r.PathPrefix("/").HandlerFunc(s.indexHandler("/index.html")) return r } func (s *server) serve() { middle := s.routeSetup() - log.Info().Str("addr", s.addr).Msg("serving HTTP") + log.Info().Str("addr", "http://"+s.addr).Msg("serving HTTP") graceful.Run(s.addr, 10*time.Second, middle) }