Add login

This commit is contained in:
2020-01-05 23:20:38 +01:00
parent dc396a300f
commit c0926ffea0
35 changed files with 1647 additions and 39 deletions

View File

@@ -83,7 +83,7 @@ local-launch-golang: ## Build the server and run it
kill $$PID || true kill $$PID || true
$(MAKE) $(BUILD_GOLANG_CMD) $(MAKE) $(BUILD_GOLANG_CMD)
DB_PORT=`docker-compose -p $(DOCKER_IMAGE_NAME)-uuid -f containers/docker-compose.local.yml port database 5432 | cut -f2 -d':'`; \ DB_PORT=`docker-compose -p $(DOCKER_IMAGE_NAME)-uuid -f containers/docker-compose.local.yml port database 5432 | cut -f2 -d':'`; \
$(LAUNCH_GOLANG_CMD) serve --loglevel debug --logformat text --postgreshost localhost:$$DB_PORT $(LAUNCH_GOLANG_CMD) serve --loglevel debug --logformat text --dbconnectionuri "postgresql://postgres:postgres@localhost:$$DB_PORT/hamster_tycoon?sslmode=disable"
.PHONY: local-run .PHONY: local-run
local-run: local-run-dependencies local-run-golang ## Run the server with its dependencies local-run: local-run-dependencies local-run-golang ## Run the server with its dependencies

102
cmd/root.go Executable file
View File

@@ -0,0 +1,102 @@
package cmd
import (
"fmt"
"hamster-tycoon/handlers"
"hamster-tycoon/utils"
"os"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var (
config = &handlers.Config{}
cfgFile string
)
const (
parameterConfigurationFile = "config"
parameterLogLevel = "loglevel"
parameterMock = "mock"
parameterLogFormat = "logformat"
parameterDBConnectionURI = "dbconnectionuri"
parameterPort = "port"
)
var (
defaultLogLevel = logrus.WarnLevel.String()
defaultLogFormat = utils.LogFormatText
defaultDBConnectionURI = ""
defaultPort = 8080
)
var rootCmd = &cobra.Command{
Use: "hamster-tycoon",
Short: "hamster-tycoon",
Run: func(cmd *cobra.Command, args []string) {
utils.InitLogger(config.LogLevel, config.LogFormat)
logrus.
WithField(parameterConfigurationFile, cfgFile).
WithField(parameterMock, config.Mock).
WithField(parameterLogLevel, config.LogLevel).
WithField(parameterLogFormat, config.LogFormat).
WithField(parameterPort, config.Port).
WithField(parameterDBConnectionURI, config.DBConnectionURI).
Warn("Configuration")
router := handlers.NewRouter(config)
router.Run(fmt.Sprintf(":%d", config.Port))
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&cfgFile, parameterConfigurationFile, "", "Config file. All flags given in command line will override the values from this file.")
rootCmd.Flags().String(parameterLogLevel, defaultLogLevel, "Use this flag to set the logging level")
viper.BindPFlag(parameterLogLevel, rootCmd.Flags().Lookup(parameterLogLevel))
rootCmd.Flags().String(parameterLogFormat, defaultLogFormat, "Use this flag to set the logging format")
viper.BindPFlag(parameterLogFormat, rootCmd.Flags().Lookup(parameterLogFormat))
rootCmd.Flags().String(parameterDBConnectionURI, defaultDBConnectionURI, "Use this flag to set the db connection URI")
viper.BindPFlag(parameterDBConnectionURI, rootCmd.Flags().Lookup(parameterDBConnectionURI))
rootCmd.Flags().Int(parameterPort, defaultPort, "Use this flag to set the listening port of the api")
viper.BindPFlag(parameterPort, rootCmd.Flags().Lookup(parameterPort))
rootCmd.Flags().Bool(parameterMock, false, "Use this flag to enable the mock mode")
viper.BindPFlag(parameterMock, rootCmd.Flags().Lookup(parameterMock))
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
}
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
}
config.LogLevel = viper.GetString(parameterLogLevel)
config.LogFormat = viper.GetString(parameterLogFormat)
config.Mock = viper.GetBool(parameterMock)
config.DBConnectionURI = viper.GetString(parameterDBConnectionURI)
config.Port = viper.GetInt(parameterPort)
}

View File

@@ -0,0 +1,35 @@
version: "2.1"
services:
database:
image: postgres:10-alpine
environment:
- POSTGRES_DB=hamster_tycoon
ports:
- "5432"
healthcheck:
test: ["CMD-SHELL", "psql -U postgres -c 'SELECT 1;'"]
interval: 10s
timeout: 30s
retries: 3
liquibase:
image: kilna/liquibase-postgres
depends_on:
database:
condition: service_healthy
links:
- database
environment:
- LIQUIBASE_DATABASE=postgres
- LIQUIBASE_HOST=database
- LIQUIBASE_USERNAME=postgres
- LIQUIBASE_PASSWORD=password
- LIQUIBASE_CHANGELOG=/changelogs/changelog-master.xml
- LIQUIBASE_URL=jdbc:postgresql://database:5432/postgres
volumes:
- $PWD/liquibase/changelogs/:/workspace
command:
- liquibase
- --defaultsFile=/workspace/liquibase.properties
- update

13
go.mod
View File

@@ -3,7 +3,14 @@ module hamster-tycoon
go 1.13 go 1.13
require ( require (
github.com/DATA-DOG/godog v0.7.13 github.com/allegro/bigcache v1.2.1
github.com/cweill/gotests v1.5.3 // indirect github.com/gin-contrib/cors v1.3.0
golang.org/x/tools v0.0.0-20190919180025-928b73f71f9b // indirect github.com/gin-gonic/gin v1.5.0
github.com/lib/pq v1.3.0
github.com/satori/go.uuid v1.2.0
github.com/sirupsen/logrus v1.4.2
github.com/spf13/cobra v0.0.5
github.com/spf13/viper v1.6.1
google.golang.org/api v0.15.0
gopkg.in/go-playground/validator.v9 v9.31.0
) )

283
go.sum
View File

@@ -1,12 +1,279 @@
github.com/DATA-DOG/godog v0.7.13 h1:JmgpKcra7Vf3yzI9vPsWyoQRx13tyKziHtXWDCUUgok= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/DATA-DOG/godog v0.7.13/go.mod h1:z2OZ6a3X0/YAKVqLfVzYBwFt3j6uSt3Xrqa7XTtcQE0= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/cweill/gotests v1.5.3 h1:k3t4wW/x/YNixWZJhUIn+mivmK5iV1tJVOwVYkx0UcU= cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo=
github.com/cweill/gotests v1.5.3/go.mod h1:XZYOJkGVkCRoymaIzmp9Wyi3rUgfA3oOnkuljYrjFV8= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc=
github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/cors v1.3.0 h1:PolezCc89peu+NgkIWt9OB01Kbzt6IP0J/JvkG6xxlg=
github.com/gin-contrib/cors v1.3.0/go.mod h1:artPvLlhkF7oG06nK8v3U8TNz6IeX+w1uzCSEId5/Vc=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc=
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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/tools v0.0.0-20190919180025-928b73f71f9b h1:9AAzeQvkIceaqgDSYbwjqLDhy3sMXirFnNCGFOH8hhQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190919180025-928b73f71f9b/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.15.0 h1:yzlyyDW/J0w8yNFJIhiAJy4kq74S+1DOLdawELNxFMA=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M=
gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -17,7 +17,7 @@ const (
const ( const (
TotalGestationPeriod = 15 TotalGestationPeriod = 15
MinChild = 5 MinChild = 5
MaxChild = 9 MaxChild = 9
GestationCooldown = (3 * 7) + 2 GestationCooldown = (3 * 7) + 2
GestationMinAge = 10 * 6 GestationMinAge = 10 * 6
MaxAge = 365 * 2 MaxAge = 365 * 2

View File

@@ -161,9 +161,9 @@ func TestHamster_Fuck(t *testing.T) {
if tc.expectedResult { if tc.expectedResult {
var female *Hamster var female *Hamster
if FEMALE == tc.hamster1.Sexe { if FEMALE == tc.hamster1.Sexe {
female=tc.hamster1 female = tc.hamster1
} else { } else {
female=tc.hamster2 female = tc.hamster2
} }
if !female.Gestation { if !female.Gestation {
t.Errorf("Female is not in gestation") t.Errorf("Female is not in gestation")
@@ -171,7 +171,7 @@ func TestHamster_Fuck(t *testing.T) {
if female.GestationPeriod != 0 { if female.GestationPeriod != 0 {
t.Errorf("Gestation period doesn't have been reseted") t.Errorf("Gestation period doesn't have been reseted")
} }
if female.GestationCooldown != TotalGestationPeriod + GestationCooldown { if female.GestationCooldown != TotalGestationPeriod+GestationCooldown {
t.Errorf("Gestation period doesn't have been reseted") t.Errorf("Gestation period doesn't have been reseted")
} }
} }
@@ -207,21 +207,20 @@ func TestBorn(t *testing.T) {
func Test_randSexe(t *testing.T) { func Test_randSexe(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
want string want string
fakeGen int fakeGen int
}{ }{
{ {
name: "Should be a male", name: "Should be a male",
fakeGen: 1, fakeGen: 1,
want: MALE, want: MALE,
}, },
{ {
name: "Should be a female", name: "Should be a female",
fakeGen: 0, fakeGen: 0,
want: FEMALE, want: FEMALE,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
randomizer.FakeRandomizer(tt.fakeGen) randomizer.FakeRandomizer(tt.fakeGen)
@@ -237,10 +236,10 @@ func Test_randNumber(t *testing.T) {
max int max int
} }
tests := []struct { tests := []struct {
name string name string
args args args args
fakeGen int fakeGen int
want int want int
}{ }{
{ {
name: "Should add min to result", name: "Should add min to result",
@@ -249,7 +248,7 @@ func Test_randNumber(t *testing.T) {
max: 2, max: 2,
}, },
fakeGen: 0, fakeGen: 0,
want: 1, want: 1,
}, },
} }
for _, tt := range tests { for _, tt := range tests {

94
handlers/handler.go Executable file
View File

@@ -0,0 +1,94 @@
package handlers
import (
"hamster-tycoon/middlewares"
"hamster-tycoon/service"
"hamster-tycoon/storage/dao"
"hamster-tycoon/storage/dao/fake"
"hamster-tycoon/storage/dao/postgresql"
"hamster-tycoon/storage/validators"
"net/http"
"time"
"reflect"
"strings"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"gopkg.in/go-playground/validator.v9"
)
type Config struct {
Mock bool
DBConnectionURI string
Port int
LogLevel string
LogFormat string
}
type handlersContext struct {
db dao.Database
validator *validator.Validate
userService *service.UserService
}
func NewRouter(config *Config) *gin.Engine {
gin.SetMode(gin.ReleaseMode)
router := gin.New()
router.HandleMethodNotAllowed = true
router.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"*"},
AllowHeaders: []string{"*"},
ExposeHeaders: []string{"*"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
router.Use(gin.Recovery())
router.Use(middlewares.GetLoggerMiddleware())
router.Use(middlewares.GetHTTPLoggerMiddleware())
hc := &handlersContext{}
if config.Mock {
hc.db = fake.NewDatabaseFake()
} else {
hc.db = postgresql.NewDatabasePostgreSQL(config.DBConnectionURI)
}
hc.validator = newValidator()
hc.userService = service.NewUserService(hc.db)
public := router.Group("/")
public.Handle(http.MethodGet, "/_health", hc.GetHealth)
public.Handle("LOGIN", "/users", hc.ConnectUser)
// start: user routes
public.Handle(http.MethodGet, "/users", hc.GetAllUsers)
public.Handle(http.MethodPost, "/users", hc.CreateUser)
public.Handle(http.MethodGet, "/users/:id", hc.GetUser)
public.Handle(http.MethodPut, "/users/:id", hc.UpdateUser)
public.Handle(http.MethodDelete, "/users/:id", hc.DeleteUser)
// end: user routes
return router
}
func newValidator() *validator.Validate {
va := validator.New()
va.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)
if len(name) < 1 {
return ""
}
return name[0]
})
for k, v := range validators.CustomValidators {
if v.Validator != nil {
va.RegisterValidationCtx(k, v.Validator)
}
}
return va
}

12
handlers/health_handler.go Executable file
View File

@@ -0,0 +1,12 @@
package handlers
import (
"hamster-tycoon/utils"
"net/http"
"github.com/gin-gonic/gin"
)
func (hc *handlersContext) GetHealth(c *gin.Context) {
utils.JSON(c.Writer, http.StatusNoContent, nil)
}

264
handlers/user_handler.go Executable file
View File

@@ -0,0 +1,264 @@
package handlers
import (
"encoding/json"
"fmt"
"hamster-tycoon/storage/dao"
"hamster-tycoon/storage/model"
"hamster-tycoon/storage/validators"
"hamster-tycoon/utils"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"google.golang.org/api/oauth2/v2"
)
var httpClient = &http.Client{}
func (hc *handlersContext) GetAllUsers(c *gin.Context) {
users, err := hc.db.GetAllUsers()
if err != nil {
utils.GetLoggerFromCtx(c).WithError(err).Error("error while getting users")
utils.JSONErrorWithMessage(c.Writer, model.ErrInternalServer, "Error while getting users")
return
}
utils.JSON(c.Writer, http.StatusOK, users)
}
func (hc *handlersContext) ConnectUser(c *gin.Context) {
authorizationHeader := c.GetHeader("Authorization")
authorizationHeaderSplitted := strings.Split(authorizationHeader, " ")
if len(authorizationHeaderSplitted) != 2 {
utils.JSONError(c.Writer, model.ErrBadRequestFormat)
return
}
oauth2Service, err := oauth2.New(httpClient)
tokenInfoCall := oauth2Service.Tokeninfo()
tokenInfoCall.IdToken(authorizationHeaderSplitted[1])
tokenInfo, err := tokenInfoCall.Do()
if err != nil {
utils.GetLogger().WithError(err).Error(err)
utils.JSONError(c.Writer, model.ErrBadRequestFormat)
return
}
user, err := hc.userService.GetUserFromGoogleId(tokenInfo.UserId)
if err != nil {
utils.GetLogger().WithError(err).Error(err)
if castedError, ok := err.(*model.APIError); ok {
if castedError.Type == model.ErrNotFound.Type {
user, err := hc.userService.CreateUserFromGoogleToken(tokenInfo.UserId, tokenInfo.Email)
if err != nil {
fmt.Println(err)
utils.JSONError(c.Writer, model.ErrInternalServer)
return
}
utils.JSON(c.Writer, 200, user)
return
}
utils.JSONError(c.Writer, *castedError)
return
}
utils.JSONError(c.Writer, model.ErrInternalServer)
return
}
utils.JSON(c.Writer, 200, user)
}
func (hc *handlersContext) CreateUser(c *gin.Context) {
b, err := c.GetRawData()
if err != nil {
utils.GetLoggerFromCtx(c).WithError(err).Error("error while creating user, read data fail")
utils.JSONError(c.Writer, model.ErrInternalServer)
return
}
userToCreate := model.UserEditable{}
err = json.Unmarshal(b, &userToCreate)
if err != nil {
utils.JSONError(c.Writer, model.ErrBadRequestFormat)
return
}
err = hc.validator.StructCtx(c, userToCreate)
if err != nil {
utils.JSONError(c.Writer, validators.NewDataValidationAPIError(err))
return
}
user := model.User{
UserEditable: userToCreate,
}
err = hc.db.CreateUser(&user)
if e, ok := err.(*dao.DAOError); ok {
switch {
case e.Type == dao.ErrTypeDuplicate:
utils.JSONErrorWithMessage(c.Writer, model.ErrAlreadyExists, "User already exists")
return
default:
utils.GetLoggerFromCtx(c).WithError(err).WithField("type", e.Type).Error("error CreateUser: Error type not handled")
utils.JSONError(c.Writer, model.ErrInternalServer)
return
}
} else if err != nil {
utils.GetLoggerFromCtx(c).WithError(err).Error("error while creating user")
utils.JSONError(c.Writer, model.ErrInternalServer)
return
}
utils.JSON(c.Writer, http.StatusCreated, user)
}
func (hc *handlersContext) GetUser(c *gin.Context) {
userID := c.Param("id")
err := hc.validator.VarCtx(c, userID, "uuid4")
if err != nil {
utils.JSONError(c.Writer, validators.NewDataValidationAPIError(err))
return
}
user, err := hc.db.GetUsersByID(userID)
if e, ok := err.(*dao.DAOError); ok {
switch {
case e.Type == dao.ErrTypeNotFound:
utils.JSONErrorWithMessage(c.Writer, model.ErrNotFound, "User not found")
return
default:
utils.GetLoggerFromCtx(c).WithError(err).WithField("type", e.Type).Error("error GetUser: get user error type not handled")
utils.JSONError(c.Writer, model.ErrInternalServer)
return
}
} else if err != nil {
utils.GetLoggerFromCtx(c).WithError(err).Error("error while get user")
utils.JSONError(c.Writer, model.ErrInternalServer)
return
}
if user == nil {
utils.JSONErrorWithMessage(c.Writer, model.ErrNotFound, "User not found")
return
}
utils.JSON(c.Writer, http.StatusOK, user)
}
func (hc *handlersContext) DeleteUser(c *gin.Context) {
userID := c.Param("id")
err := hc.validator.VarCtx(c, userID, "uuid4")
if err != nil {
utils.JSONError(c.Writer, validators.NewDataValidationAPIError(err))
return
}
// check user id given in URL exists
_, err = hc.db.GetUsersByID(userID)
if e, ok := err.(*dao.DAOError); ok {
switch {
case e.Type == dao.ErrTypeNotFound:
utils.JSONErrorWithMessage(c.Writer, model.ErrNotFound, "User to delete not found")
return
default:
utils.GetLoggerFromCtx(c).WithError(err).WithField("type", e.Type).Error("error DeleteUser: get user error type not handled")
utils.JSONError(c.Writer, model.ErrInternalServer)
return
}
} else if err != nil {
utils.GetLoggerFromCtx(c).WithError(err).Error("error while get user to delete")
utils.JSONError(c.Writer, model.ErrInternalServer)
return
}
err = hc.db.DeleteUser(userID)
if e, ok := err.(*dao.DAOError); ok {
switch {
case e.Type == dao.ErrTypeNotFound:
utils.JSONErrorWithMessage(c.Writer, model.ErrNotFound, "User to delete not found")
return
default:
utils.GetLoggerFromCtx(c).WithError(err).WithField("type", e.Type).Error("error DeleteUser: Error type not handled")
utils.JSONError(c.Writer, model.ErrInternalServer)
return
}
} else if err != nil {
utils.GetLoggerFromCtx(c).WithError(err).Error("error while deleting user")
utils.JSONError(c.Writer, model.ErrInternalServer)
return
}
utils.JSON(c.Writer, http.StatusNoContent, nil)
}
func (hc *handlersContext) UpdateUser(c *gin.Context) {
userID := c.Param("id")
err := hc.validator.VarCtx(c, userID, "uuid4")
if err != nil {
utils.JSONError(c.Writer, validators.NewDataValidationAPIError(err))
return
}
// check user id given in URL exists
user, err := hc.db.GetUsersByID(userID)
if e, ok := err.(*dao.DAOError); ok {
switch {
case e.Type == dao.ErrTypeNotFound:
utils.JSONErrorWithMessage(c.Writer, model.ErrNotFound, "User to update not found")
return
default:
utils.GetLoggerFromCtx(c).WithError(err).WithField("type", e.Type).Error("deleteUser: get user error type not handled")
utils.JSONError(c.Writer, model.ErrInternalServer)
return
}
} else if err != nil {
utils.GetLoggerFromCtx(c).WithError(err).Error("error while get user to update")
utils.JSONError(c.Writer, model.ErrInternalServer)
return
}
// get body and verify data
b, err := c.GetRawData()
if err != nil {
utils.GetLoggerFromCtx(c).WithError(err).Error("error while updating user, read data fail")
utils.JSONError(c.Writer, model.ErrInternalServer)
return
}
userToUpdate := model.UserEditable{}
err = json.Unmarshal(b, &userToUpdate)
if err != nil {
utils.JSONError(c.Writer, model.ErrBadRequestFormat)
return
}
err = hc.validator.StructCtx(c, userToUpdate)
if err != nil {
utils.JSONError(c.Writer, validators.NewDataValidationAPIError(err))
return
}
user.UserEditable = userToUpdate
// make the update
err = hc.db.UpdateUser(user)
if e, ok := err.(*dao.DAOError); ok {
switch {
case e.Type == dao.ErrTypeNotFound:
utils.JSONErrorWithMessage(c.Writer, model.ErrNotFound, "User to update not found")
return
default:
utils.GetLoggerFromCtx(c).WithError(err).WithField("type", e.Type).Error("error UpdateUser: Error type not handled")
utils.JSONError(c.Writer, model.ErrInternalServer)
return
}
} else if err != nil {
utils.GetLoggerFromCtx(c).WithError(err).Error("error while deleting user")
utils.JSONError(c.Writer, model.ErrInternalServer)
return
}
utils.JSON(c.Writer, http.StatusOK, user)
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
<include file="prepare-database.sql" relativeToChangelogFile="true"/>
<include file="changeset/create-user.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
<changeSet id="add-user-table" author="kratisto">
<createTable tableName="user">
<column name="id" type="uuid" defaultValueComputed="gen_random_uuid()">
<constraints nullable="false" unique="true"/>
</column>
<column name="created_at" type="timestamp" defaultValueComputed="now()">
<constraints nullable="true"/>
</column>
<column name="updated_at" type="timestamp">
<constraints nullable="true"/>
</column>
<column name="google_id" type="text" >
<constraints nullable="false" unique="true"/>
</column>
<column name="nickname" type="text">
<constraints nullable="false" unique="true"/>
</column>
<column name="email" type="text">
<constraints nullable="false" unique="true"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,6 @@
url=jdbc:postgresql://database:5432/hamster_tycoon
username=postgres
password=password
changeLogFile=changelog-master.xml
logLevel=info
classpath=postgresql-42.2.9.jar

Binary file not shown.

View File

@@ -0,0 +1,3 @@
CREATE SCHEMA IF NOT EXISTS hamster_tycoon;
CREATE EXTENSION IF NOT EXISTS pgcrypto;

10
main.go
View File

@@ -1,7 +1,7 @@
package main package main
import ( import (
"fmt" "hamster-tycoon/cmd"
"math/rand" "math/rand"
"time" "time"
) )
@@ -10,11 +10,5 @@ func init() {
rand.Seed(time.Now().UTC().UnixNano()) rand.Seed(time.Now().UTC().UnixNano())
} }
func main() { func main() {
cmd.Execute()
fmt.Print("hello")
for {
fmt.Println("Infinite Loop 1")
time.Sleep(time.Second)
}
} }

71
middlewares/logger.go Executable file
View File

@@ -0,0 +1,71 @@
package middlewares
import (
"hamster-tycoon/utils"
"math/rand"
"time"
"github.com/gin-gonic/gin"
)
const (
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)
var src = rand.NewSource(time.Now().UnixNano())
func randStringBytesMaskImprSrc(n int) string {
b := make([]byte, n)
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
b[i] = letterBytes[idx]
i--
}
cache >>= letterIdxBits
remain--
}
return string(b)
}
func GetLoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
correlationID := c.Request.Header.Get(utils.HeaderNameCorrelationID)
if correlationID == "" {
correlationID = randStringBytesMaskImprSrc(30)
c.Writer.Header().Set(utils.HeaderNameCorrelationID, correlationID)
}
logger := utils.GetLogger()
logEntry := logger.WithField(utils.HeaderNameCorrelationID, correlationID)
c.Set(utils.ContextKeyLogger, logEntry)
}
}
func GetHTTPLoggerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
utils.GetLoggerFromCtx(c).
WithField("method", c.Request.Method).
WithField("url", c.Request.RequestURI).
WithField("from", c.ClientIP()).
Info("start handling HTTP request")
c.Next()
d := time.Since(start)
utils.GetLoggerFromCtx(c).
WithField("status", c.Writer.Status()).
WithField("duration", d.String()).
Info("end handling HTTP request")
}
}

View File

@@ -9,7 +9,8 @@ type Random interface {
Int() int Int() int
} }
func init(){} func init() {}
type rndGenerator func(n int) int type rndGenerator func(n int) int
type Randomizer struct { type Randomizer struct {
@@ -29,13 +30,14 @@ func Rand(rand int) int {
} }
func realRand(n int) int { return int(rand.Intn(n)) } func realRand(n int) int { return int(rand.Intn(n)) }
func fakeRand(n int) func(numb int) int { func fakeRand(n int) func(numb int) int {
return func(numb int)int { return func(numb int) int {
if n >= numb { if n >= numb {
panic(fmt.Sprintf("%d Should not be superior of %d",n,numb)) panic(fmt.Sprintf("%d Should not be superior of %d", n, numb))
} }
return n return n
}
} }
}
var randGen = NewRandomizer(realRand).Int var randGen = NewRandomizer(realRand).Int
func FakeRandomizer(n int) { func FakeRandomizer(n int) {

7
service/service.go Normal file
View File

@@ -0,0 +1,7 @@
package service
import "hamster-tycoon/storage/dao"
type serviceContext struct {
db dao.Database
}

39
service/user_service.go Normal file
View File

@@ -0,0 +1,39 @@
package service
import (
"hamster-tycoon/storage/dao"
"hamster-tycoon/storage/model"
"hamster-tycoon/utils"
)
type UserService struct {
serviceContext
}
func NewUserService(database dao.Database) *UserService {
return &UserService{serviceContext{db: database}}
}
func (us *UserService) GetUserFromGoogleId(googleUserId string) (*model.User, error) {
user, err := us.db.GetUsersByGoogleID(googleUserId)
if err != nil {
if castedError, ok := err.(*dao.DAOError); ok {
switch castedError.Type {
case dao.ErrTypeNotFound:
return nil, &model.ErrNotFound
default:
return nil, &model.ErrInternalServer
}
}
return nil, &model.ErrInternalServer
}
return user, nil
}
func (us *UserService) CreateUserFromGoogleToken(id string, email string) (*model.User, error) {
randNick := utils.String(20)
user := &model.User{UserEditable: model.UserEditable{Email: email, Nickname: "player-" + randNick}, GoogleId: id}
err := us.db.CreateUser(user)
return user, err
}

18
storage/dao/database.go Executable file
View File

@@ -0,0 +1,18 @@
package dao
import (
"hamster-tycoon/storage/model"
)
type Database interface {
// start: user dao funcs
GetAllUsers() ([]*model.User, error)
GetUsersByID(string) (*model.User, error)
GetUsersByGoogleID(string) (*model.User, error)
CreateUser(*model.User) error
DeleteUser(string) error
UpdateUser(*model.User) error
// end: user dao funcs
}

32
storage/dao/database_error.go Executable file
View File

@@ -0,0 +1,32 @@
package dao
import (
"fmt"
)
type Type int
const (
ErrTypeNotFound Type = iota
ErrTypeDuplicate
ErrTypeForeignKeyViolation
)
type DAOError struct {
Cause error
Type Type
}
func NewDAOError(t Type, cause error) error {
return &DAOError{
Type: t,
Cause: cause,
}
}
func (e *DAOError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("Type %d: %s", e.Type, e.Cause.Error())
}
return fmt.Sprintf("Type %d: no cause given", e.Type)
}

View File

@@ -0,0 +1,37 @@
package fake
import (
"encoding/json"
"hamster-tycoon/storage/dao"
"hamster-tycoon/utils"
"time"
"github.com/allegro/bigcache"
)
type DatabaseFake struct {
Cache *bigcache.BigCache
}
func NewDatabaseFake() dao.Database {
cache, err := bigcache.NewBigCache(bigcache.DefaultConfig(time.Minute))
if err != nil {
utils.GetLogger().WithError(err).Fatal("Error while instantiate cache")
}
return &DatabaseFake{
Cache: cache,
}
}
func (db *DatabaseFake) save(key string, data []interface{}) {
b, err := json.Marshal(data)
if err != nil {
utils.GetLogger().WithError(err).Errorf("Error while marshal fake %s", key)
db.Cache.Set(key, []byte("[]"))
return
}
err = db.Cache.Set(key, b)
if err != nil {
utils.GetLogger().WithError(err).Errorf("Error while saving fake %s", key)
}
}

View File

@@ -0,0 +1,106 @@
package fake
import (
"encoding/json"
"errors"
"hamster-tycoon/storage/dao"
"hamster-tycoon/storage/model"
"hamster-tycoon/utils"
"time"
"github.com/satori/go.uuid"
)
const (
cacheKeyUsers = "users"
)
func (db *DatabaseFake) saveUsers(users []*model.User) {
data := make([]interface{}, 0)
for _, v := range users {
data = append(data, v)
}
db.save(cacheKeyUsers, data)
}
func (db *DatabaseFake) loadUsers() []*model.User {
users := make([]*model.User, 0)
b, err := db.Cache.Get(cacheKeyUsers)
if err != nil {
return users
}
err = json.Unmarshal(b, &users)
if err != nil {
utils.GetLogger().WithError(err).Error("Error while unmarshal fake users")
}
return users
}
func (db *DatabaseFake) GetAllUsers() ([]*model.User, error) {
return db.loadUsers(), nil
}
func (db *DatabaseFake) GetUsersByID(userID string) (*model.User, error) {
users := db.loadUsers()
for _, u := range users {
if u.ID == userID {
return u, nil
}
}
return nil, dao.NewDAOError(dao.ErrTypeNotFound, errors.New("user not found"))
}
func (db *DatabaseFake) CreateUser(user *model.User) error {
user.ID = uuid.NewV4().String()
user.CreatedAt = time.Now()
users := db.loadUsers()
users = append(users, user)
db.saveUsers(users)
return nil
}
func (db *DatabaseFake) DeleteUser(userID string) error {
users := db.loadUsers()
newUsers := make([]*model.User, 0)
for _, u := range users {
if u.ID != userID {
newUsers = append(newUsers, u)
}
}
db.saveUsers(newUsers)
return nil
}
func (db *DatabaseFake) UpdateUser(user *model.User) error {
users := db.loadUsers()
var foundUser *model.User
for _, u := range users {
if u.ID == user.ID {
foundUser = u
break
}
}
if foundUser == nil {
return dao.NewDAOError(dao.ErrTypeNotFound, errors.New("user not found"))
}
foundUser.UserEditable = user.UserEditable
now := time.Now()
foundUser.UpdatedAt = &now
db.saveUsers(users)
*user = *foundUser
return nil
}
func (db *DatabaseFake) GetUsersByGoogleID(userID string) (*model.User, error) {
users := db.loadUsers()
for _, u := range users {
if u.GoogleId == userID {
return u, nil
}
}
return nil, dao.NewDAOError(dao.ErrTypeNotFound, errors.New("user not found"))
}

View File

@@ -0,0 +1,43 @@
package postgresql
import (
"database/sql"
"fmt"
"hamster-tycoon/storage/dao"
"hamster-tycoon/utils"
"github.com/lib/pq"
)
const (
pgCodeUniqueViolation = "23505"
pgCodeForeingKeyViolation = "23503"
)
func handlePgError(e *pq.Error) error {
if e.Code == pgCodeUniqueViolation {
return dao.NewDAOError(dao.ErrTypeDuplicate, e)
}
if e.Code == pgCodeForeingKeyViolation {
return dao.NewDAOError(dao.ErrTypeForeignKeyViolation, e)
}
return e
}
type DatabasePostgreSQL struct {
session *sql.DB
}
func NewDatabasePostgreSQL(connectionURI string) dao.Database {
db, err := sql.Open("postgres", connectionURI)
if err != nil {
utils.GetLogger().WithError(err).Fatal("Unable to get a connection to the postgres db")
}
err = db.Ping()
if err != nil {
fmt.Println(err)
utils.GetLogger().WithError(err).Fatal("Unable to ping the postgres db")
}
return &DatabasePostgreSQL{session: db}
}

View File

@@ -0,0 +1,119 @@
package postgresql
import (
"database/sql"
"hamster-tycoon/storage/dao"
"hamster-tycoon/storage/model"
"github.com/lib/pq"
)
func (db *DatabasePostgreSQL) GetAllUsers() ([]*model.User, error) {
q := `
SELECT u.id, u.email, u.created_at, u.updated_at, u.nickname
FROM public.user u
`
rows, err := db.session.Query(q)
if err != nil {
return nil, err
}
defer rows.Close()
us := make([]*model.User, 0)
for rows.Next() {
u := model.User{}
err := rows.Scan(&u.ID, &u.Email, &u.CreatedAt, &u.UpdatedAt, &u.Nickname)
if err != nil {
return nil, err
}
us = append(us, &u)
}
return us, nil
}
func (db *DatabasePostgreSQL) GetUsersByID(id string) (*model.User, error) {
q := `
SELECT u.id, u.email, u.created_at, u.updated_at, u.nickname
FROM public.user u
WHERE u.id = $1
`
row := db.session.QueryRow(q, id)
u := model.User{}
err := row.Scan(&u.ID, &u.Email, &u.CreatedAt, &u.UpdatedAt, &u.Nickname)
if errPq, ok := err.(*pq.Error); ok {
return nil, handlePgError(errPq)
}
if err == sql.ErrNoRows {
return nil, dao.NewDAOError(dao.ErrTypeNotFound, err)
}
return &u, err
}
func (db *DatabasePostgreSQL) CreateUser(user *model.User) error {
q := `
INSERT INTO public.user
(email, nickname, google_id)
VALUES
($1, $2, $3)
RETURNING id, created_at
`
err := db.session.
QueryRow(q, user.Email, user.Nickname, user.GoogleId).
Scan(&user.ID, &user.CreatedAt)
if errPq, ok := err.(*pq.Error); ok {
return handlePgError(errPq)
}
return err
}
func (db *DatabasePostgreSQL) DeleteUser(id string) error {
q := `
DELETE FROM public.user
WHERE id = $1
`
_, err := db.session.Exec(q, id)
if errPq, ok := err.(*pq.Error); ok {
return handlePgError(errPq)
}
return err
}
func (db *DatabasePostgreSQL) UpdateUser(user *model.User) error {
q := `
UPDATE public.user
SET
email = $2
WHERE id = $1
RETURNING updated_at
`
err := db.session.
QueryRow(q, user.ID, user.Email).
Scan(&user.UpdatedAt)
if errPq, ok := err.(*pq.Error); ok {
return handlePgError(errPq)
}
return err
}
func (db *DatabasePostgreSQL) GetUsersByGoogleID(id string) (*model.User, error) {
q := `
SELECT u.id, u.email, u.created_at, u.updated_at, u.nickname, u.google_id
FROM public.user u
WHERE u.google_id = $1
`
row := db.session.QueryRow(q, id)
u := model.User{}
err := row.Scan(&u.ID, &u.Email, &u.CreatedAt, &u.UpdatedAt, &u.Nickname, &u.GoogleId)
if errPq, ok := err.(*pq.Error); ok {
return nil, handlePgError(errPq)
}
if err == sql.ErrNoRows {
return nil, dao.NewDAOError(dao.ErrTypeNotFound, err)
}
return &u, err
}

67
storage/model/error.go Executable file
View File

@@ -0,0 +1,67 @@
package model
import (
"fmt"
"github.com/lib/pq"
"net/http"
)
var (
// 400
ErrBadRequestFormat = APIError{
Type: "bad_format",
HTTPCode: http.StatusBadRequest,
Description: "unable to read request body, please check that the json is valid",
}
ErrDataValidation = APIError{
Type: "data_validation",
HTTPCode: http.StatusBadRequest,
Description: "the data are not valid",
}
// 404
ErrNotFound = APIError{
Type: "not_found",
HTTPCode: http.StatusNotFound,
}
// 40x
ErrAlreadyExists = APIError{
Type: "already_exists",
HTTPCode: http.StatusConflict,
}
// 50x
ErrInternalServer = APIError{
Type: "internal_server_error",
HTTPCode: http.StatusInternalServerError,
}
)
type APIError struct {
HTTPCode int `json:"-"`
Type string `json:"error"`
Description string `json:"error_description"`
Details []FieldError `json:"error_details,omitempty"`
Headers map[string][]string `json:"-"`
}
type FieldError struct {
Field string `json:"field"`
Constraint string `json:"constraint"`
Description string `json:"description"`
}
func (e *APIError) Error() string {
return fmt.Sprintf("error : %d, %s, %s, %v", e.HTTPCode, e.Type, e.Description, e.Details)
}
func FromPostgresError(err *pq.Error) *APIError {
return &APIError{
HTTPCode: 0,
Type: "",
Description: "",
Details: nil,
Headers: nil,
}
}

16
storage/model/user.go Executable file
View File

@@ -0,0 +1,16 @@
package model
import "time"
type User struct {
UserEditable
ID string `json:"id"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
GoogleId string `json:"-"`
}
type UserEditable struct {
Email string `json:"email" validate:"required"`
Nickname string `json:"nickname" validate:"required"`
}

View File

@@ -0,0 +1,17 @@
package model
import "time"
type UserData struct {
UserEditable
ID string `json:"id"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"`
}
type UserDataEditable struct {
UserId string `json:'userId'`
Type string `json:"type"`
Name string `json:"name"`
Content string `json:"content"`
}

44
storage/validators/error.go Executable file
View File

@@ -0,0 +1,44 @@
package validators
import (
"fmt"
"hamster-tycoon/storage/model"
"hamster-tycoon/utils"
"regexp"
"strings"
"gopkg.in/go-playground/validator.v9"
)
var regexpValidatorNamespacePrefix = regexp.MustCompile(`^\w+\.`)
func NewDataValidationAPIError(err error) model.APIError {
apiErr := model.ErrDataValidation
if err != nil {
if _, ok := err.(*validator.InvalidValidationError); ok {
utils.GetLogger().WithError(err).WithField("templateAPIErr", apiErr).Error("InvalidValidationError")
} else {
for _, e := range err.(validator.ValidationErrors) {
reason := e.Tag()
if _, ok := CustomValidators[e.Tag()]; ok {
reason = truncatingSprintf(CustomValidators[e.Tag()].Message, e.Param())
}
namespaceWithoutStructName := regexpValidatorNamespacePrefix.ReplaceAllString(e.Namespace(), "")
fe := model.FieldError{
Field: namespaceWithoutStructName,
Constraint: e.Tag(),
Description: reason,
}
apiErr.Details = append(apiErr.Details, fe)
}
}
}
return apiErr
}
// truncatingSprintf is used as fmt.Sprintf but allow to truncate the additional parameters given when there is more parameters than %v in str
func truncatingSprintf(str string, args ...interface{}) string {
n := strings.Count(str, "%v")
return fmt.Sprintf(str, args[:n]...)
}

View File

@@ -0,0 +1,34 @@
package validators
import (
"context"
"strings"
"gopkg.in/go-playground/validator.v9"
)
var (
CustomValidators = map[string]customValidator{
"enum": {
Message: "This field should be in: %v",
Validator: validateEnum,
},
"required": {
Message: "This field is required and cannot be empty",
},
}
)
type customValidator struct {
Message string
Validator validator.FuncCtx
}
func validateEnum(ctx context.Context, fl validator.FieldLevel) bool {
for _, v := range strings.Split(fl.Param(), " ") {
if v == fl.Field().String() {
return true
}
}
return false
}

8
utils/headers.go Executable file
View File

@@ -0,0 +1,8 @@
package utils
const (
HeaderNameContentType = "content-type"
HeaderNameCorrelationID = "correlationID"
HeaderValueApplicationJSONUTF8 = "application/json; charset=UTF-8"
)

74
utils/logger.go Executable file
View File

@@ -0,0 +1,74 @@
package utils
import (
"io"
"os"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
const (
LogFormatText = "text"
LogFormatJSON = "json"
ContextKeyLogger = "logger"
)
var (
logLevel = logrus.DebugLevel
logFormat logrus.Formatter = &logrus.TextFormatter{}
logOut io.Writer
)
func InitLogger(ll, lf string) {
logLevel = parseLogrusLevel(ll)
logrus.SetLevel(logLevel)
logFormat = parseLogrusFormat(lf)
logrus.SetFormatter(logFormat)
logOut = os.Stdout
logrus.SetOutput(logOut)
}
func GetLoggerFromCtx(c *gin.Context) *logrus.Entry {
if logger, ok := c.Get(ContextKeyLogger); ok {
logEntry, assertionOk := logger.(*logrus.Entry)
if assertionOk {
return logEntry
}
}
return logrus.NewEntry(GetLogger())
}
func GetLogger() *logrus.Logger {
logger := logrus.New()
logger.Formatter = logFormat
logger.Level = logLevel
logger.Out = logOut
return logger
}
func parseLogrusLevel(logLevelStr string) logrus.Level {
logLevel, err := logrus.ParseLevel(logLevelStr)
if err != nil {
logrus.WithError(err).Errorf("error while parsing log level. %v is set as default.", logLevel)
logLevel = logrus.DebugLevel
}
return logLevel
}
func parseLogrusFormat(logFormatStr string) logrus.Formatter {
var formatter logrus.Formatter
switch logFormatStr {
case LogFormatText:
formatter = &logrus.TextFormatter{ForceColors: true, FullTimestamp: true}
case LogFormatJSON:
formatter = &logrus.JSONFormatter{}
default:
logrus.Errorf("error while parsing log format. %v is set as default.", formatter)
formatter = &logrus.TextFormatter{ForceColors: true, FullTimestamp: true}
}
return formatter
}

24
utils/rand.go Normal file
View File

@@ -0,0 +1,24 @@
package utils
import (
"math/rand"
"time"
)
const charset = "abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var seededRand = rand.New(
rand.NewSource(time.Now().UnixNano()))
func StringWithCharset(length int, charset string) string {
b := make([]byte, length)
for i := range b {
b[i] = charset[seededRand.Intn(len(charset))]
}
return string(b)
}
func String(length int) string {
return StringWithCharset(length, charset)
}

31
utils/responses.go Executable file
View File

@@ -0,0 +1,31 @@
package utils
import (
"encoding/json"
"hamster-tycoon/storage/model"
"net/http"
)
func JSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set(HeaderNameContentType, HeaderValueApplicationJSONUTF8)
w.WriteHeader(status)
if data != nil {
json.NewEncoder(w).Encode(data)
}
}
func JSONError(w http.ResponseWriter, e model.APIError) {
if e.Headers != nil {
for k, headers := range e.Headers {
for _, headerValue := range headers {
w.Header().Add(k, headerValue)
}
}
}
JSON(w, e.HTTPCode, e)
}
func JSONErrorWithMessage(w http.ResponseWriter, e model.APIError, message string) {
e.Description = message
JSONError(w, e)
}