happy: add working backend and frontend

Current version:
* has user generation (but no real validation)
* allows user to select a mood
* handles a few error situations, but not enough
* looks plain
This commit is contained in:
Chris Sexton 2019-10-27 22:08:30 -04:00
parent 104af726f8
commit 2e472c4993
14 changed files with 601 additions and 47 deletions

1
.gitignore vendored
View File

@ -260,3 +260,4 @@ $RECYCLE.BIN/
.idea .idea
happy happy
frontend/dist frontend/dist
*.db

View File

@ -8,6 +8,6 @@ frontend-watch:
cd frontend && yarn build --watch cd frontend && yarn build --watch
run: build run: build
./happy ./happy -develop
.PHONY: run frontend frontend-watch .PHONY: run frontend frontend-watch

View File

@ -8,8 +8,13 @@
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"axios": "^0.19.0",
"bootstrap": "^4.3.1",
"bootstrap-vue": "^2.0.4",
"core-js": "^3.1.2", "core-js": "^3.1.2",
"vue": "^2.6.10" "vue": "^2.6.10",
"vue-cookies": "^1.5.13",
"vuex": "^3.1.1"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^4.0.0", "@vue/cli-plugin-babel": "^4.0.0",

View File

@ -1,18 +1,26 @@
<template> <template>
<div id="app"> <div id="app">
<img alt="Vue logo" src="./assets/logo.png"> <div v-show="err">{{err}}</div>
<HelloWorld msg="Welcome to Your Vue.js App"/> <user-info class="fixed-bottom userInfo"/>
<Chooser/>
</div> </div>
</template> </template>
<script> <script>
import HelloWorld from './components/HelloWorld.vue' import Chooser from './components/Chooser.vue'
import UserInfo from "./components/UserInfo";
import {mapState} from 'vuex';
export default { export default {
name: 'app', name: 'app',
components: { components: {
HelloWorld Chooser,
} UserInfo
},
computed: mapState({
userInfo: state => state.userInfo,
err: state => state.err
}),
} }
</script> </script>
@ -25,4 +33,7 @@ export default {
color: #2c3e50; color: #2c3e50;
margin-top: 60px; margin-top: 60px;
} }
.userInfo {
margin-bottom: 1em;
}
</style> </style>

23
frontend/src/api.js Normal file
View File

@ -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")
}
}

View File

@ -0,0 +1,24 @@
<template>
<b-button v-on:click="click" variant="outline-primary" class="moodButton">{{mood.Key}}</b-button>
</template>
<script>
export default {
name: "ChoiceButton",
props: {
mood: Object
},
methods: {
click: function(e) {
this.$emit('selected', e, this.mood)
}
}
}
</script>
<style scoped>
.moodButton {
font-size: 24pt;
margin: 1em;
}
</style>

View File

@ -0,0 +1,37 @@
<template>
<div>
<choice-button v-for="(mood, index) in moods"
v-bind:key="mood.key"
v-bind:index="index"
:mood="mood"
v-on:selected="doIt"
/>
</div>
</template>
<script>
import ChoiceButton from "./ChoiceButton";
import { mapState } from 'vuex';
export default {
name: "Chooser",
components: {ChoiceButton},
created() {
this.$store.dispatch('getMoods');
},
computed: mapState({
moods: state => state.moods
}),
methods: {
doIt: function(e, mood) {
this.$store.dispatch("setMood", mood)
.catch(resp => {
this.$store.commit('setError', resp.message);
})
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,43 @@
<template>
<div>
{{userInfo.ID}} &#x25CF; <b-button variant="link" class="logOut" v-on:click="logOut">Log Out</b-button>
</div>
</template>
<script>
import {mapState} from 'vuex';
export default {
name: "UserInfo",
computed: mapState({
userInfo: state => state.userInfo
}),
created() {
this.newUser();
},
methods: {
newUser: function () {
let userInfo = this.$cookies.get("userInfo");
if (!userInfo) {
this.$store.dispatch('getNewUser')
.then(() => {
this.$cookies.set("userInfo", this.$store.state.userInfo);
});
} else {
this.$store.commit('setUser', userInfo);
}
},
logOut: function () {
this.$cookies.remove("userInfo");
this.$store.commit('clearError');
this.newUser()
},
}
}
</script>
<style scoped>
.logOut {
padding: 0;
margin: 0;
}
</style>

View File

@ -1,8 +1,19 @@
import Vue from 'vue' 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({ new Vue({
render: h => h(App), render: h => h(App),
}).$mount('#app') store,
}).$mount('#app');

47
frontend/src/store.js Normal file
View File

@ -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

View File

@ -759,6 +759,15 @@
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== 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": "@soda/friendly-errors-webpack-plugin@^1.7.1":
version "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" 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" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== 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: babel-eslint@^10.0.1:
version "10.0.3" version "10.0.3"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a" 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" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= 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: brace-expansion@^1.1.7:
version "1.1.11" version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 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" resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc"
integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== 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: console-browserify@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" 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: dependencies:
ms "2.0.0" 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: debug@^3.0.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6:
version "3.2.6" version "3.2.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" 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" inherits "^2.0.3"
readable-stream "^2.3.6" 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: follow-redirects@^1.0.0:
version "1.9.0" version "1.9.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.9.0.tgz#8d5bcdc65b7108fe1508649c79c12d732dcedb4f" 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" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== 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: is-callable@^1.1.4:
version "1.1.4" version "1.1.4"
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75"
@ -5195,6 +5252,11 @@ no-case@^2.2.0:
dependencies: dependencies:
lower-case "^1.1.1" 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: node-forge@0.9.0:
version "0.9.0" version "0.9.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
@ -5863,6 +5925,16 @@ pkg-up@^2.0.0:
dependencies: dependencies:
find-up "^2.1.0" 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: portfinder@^1.0.20, portfinder@^1.0.24:
version "1.0.25" version "1.0.25"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.25.tgz#254fd337ffba869f4b9d37edc298059cb4d35eca" 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" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019"
integrity sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw== 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: vue-eslint-parser@^5.0.0:
version "5.0.0" version "5.0.0"
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-5.0.0.tgz#00f4e4da94ec974b821a26ff0ed0f7a78402b8a1" 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" esquery "^1.0.1"
lodash "^4.17.11" 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: vue-hot-reload-api@^2.3.0:
version "2.3.4" version "2.3.4"
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2" 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" resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637"
integrity sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ== 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: watchpack@^1.6.0:
version "1.6.0" version "1.6.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"

7
go.mod
View File

@ -3,10 +3,11 @@ module github.com/chrissexton/happy
go 1.12 go 1.12
require ( 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/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/rs/zerolog v1.15.0
github.com/speps/go-hashids v2.0.0+incompatible
github.com/stretchr/graceful v1.2.15 github.com/stretchr/graceful v1.2.15
google.golang.org/appengine v1.6.5 // indirect
) )

24
go.sum
View File

@ -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/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/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= 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 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 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/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 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/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/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 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 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 h1:vmXbwPGfe8bI6KkgmHry/P1Pk63bM3TDcfi+5mh+VHg=
github.com/stretchr/graceful v1.2.15/go.mod h1:IxdGAOTZueMKoBr3oJIzdeg5CCCXbHXfV44sLhfAXXI= github.com/stretchr/graceful v1.2.15/go.mod h1:IxdGAOTZueMKoBr3oJIzdeg5CCCXbHXfV44sLhfAXXI=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 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/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 h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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/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/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 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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= 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=

278
serve.go
View File

@ -1,14 +1,22 @@
package main package main
import ( import (
"database/sql"
"encoding/json"
"flag" "flag"
"fmt" "fmt"
"math/rand"
"net/http" "net/http"
"os" "os"
"path" "path"
"time" "time"
"github.com/speps/go-hashids"
_ "github.com/mattn/go-sqlite3"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/stretchr/graceful" "github.com/stretchr/graceful"
@ -16,32 +24,274 @@ import (
var ( 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() { func main() {
flag.Parse() flag.Parse()
log.Logger = log.Logger.Output(zerolog.ConsoleWriter{Out: os.Stderr}) log.Logger = log.Logger.Output(zerolog.ConsoleWriter{Out: os.Stderr})
zerolog.SetGlobalLevel(zerolog.DebugLevel) zerolog.SetGlobalLevel(zerolog.DebugLevel)
s := server{"0.0.0.0:8080", "pub"} s := server{
s.serve() addr: "0.0.0.0:8080",
assetPath: "pub",
salt: *salt,
} }
db, err := sqlx.Connect("sqlite3", "happy.db")
func logger(next http.Handler) http.Handler { if err != nil {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Fatal().
log.Info(). Err(err).
Str("Path", r.URL.Path). Msg("could not connect to database")
Str("Method", r.Method). }
Msg("HTTP Request") 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()
} }
type server struct { type server struct {
addr string addr string
assetPath 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) { 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) { 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 { func (s *server) routeSetup() *mux.Router {
r := mux.NewRouter() r := mux.NewRouter()
api := r.PathPrefix("/v1/").Subrouter() 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")) r.PathPrefix("/").HandlerFunc(s.indexHandler("/index.html"))
return r return r
} }
func (s *server) serve() { func (s *server) serve() {
middle := s.routeSetup() 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) graceful.Run(s.addr, 10*time.Second, middle)
} }