diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..374eeb9 --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,91 @@ +package auth + +import ( + "time" + + jwt "github.com/appleboy/gin-jwt/v2" + "github.com/gin-gonic/gin" + "github.com/peppermint-recipes/peppermint-server/user" +) + +type User struct { + UserName string + UserID string +} + +type login struct { + Username string `form:"username" json:"username" binding:"required"` + Password string `form:"password" json:"password" binding:"required"` +} + +func Unauthorized(c *gin.Context, code int, message string) { + c.JSON(code, gin.H{ + "code": code, + "message": message, + }) +} + +var IdentityKey = "id" + +func RegisterAuthMiddleware(JWTSigningKey string) (*jwt.GinJWTMiddleware, error) { + authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{ + Realm: "peppermint-server", + Key: []byte(JWTSigningKey), + Timeout: time.Hour, + MaxRefresh: time.Hour, + IdentityKey: IdentityKey, + PayloadFunc: func(data interface{}) jwt.MapClaims { + if v, ok := data.(*User); ok { + return jwt.MapClaims{ + IdentityKey: string(v.UserID), + } + } + return jwt.MapClaims{} + }, + IdentityHandler: func(c *gin.Context) interface{} { + claims := jwt.ExtractClaims(c) + return &User{ + UserID: claims[IdentityKey].(string), + } + }, + Unauthorized: Unauthorized, + Authenticator: func(c *gin.Context) (interface{}, error) { + var loginVals login + if err := c.ShouldBind(&loginVals); err != nil { + return "", jwt.ErrMissingLoginValues + } + userID := loginVals.Username + password := loginVals.Password + + user, err := user.IsUserAuthorized(userID, password) + if err != nil { + return nil, jwt.ErrFailedAuthentication + } + + return &User{ + UserName: user.Name, + UserID: user.ID.String(), + }, nil + }, + // TokenLookup is a string in the form of ":" that is used + // to extract token from the request. + // Optional. Default value "header:Authorization". + // Possible values: + // - "header:" + // - "query:" + // - "cookie:" + // - "param:" + TokenLookup: "header: Authorization, query: token, cookie: jwt", + // TokenLookup: "query:token", + // TokenLookup: "cookie:token", + + // TokenHeadName is a string in the header. Default value is "Bearer" + TokenHeadName: "Bearer", + + // TimeFunc provides the current time. You can override it to use another time value. + // This is useful for testing or if your server uses a different time zone than your tokens. + TimeFunc: time.Now, + }) + + return authMiddleware, err +} diff --git a/config/config.go b/config/config.go index 676e6b8..2ee2162 100644 --- a/config/config.go +++ b/config/config.go @@ -17,8 +17,9 @@ type DBConfig struct { // TODO: add GIN_MODE=release type WebServerConfig struct { - Address string - Port string + Address string + Port string + JWTSigningKey string } func GetConfig() *Config { @@ -29,8 +30,9 @@ func GetConfig() *Config { Password: getFromEnvAsString("DATABASE_PASSWORD", "example"), }, Web: &WebServerConfig{ - Address: getFromEnvAsString("WEBSERVER_ADDRESS", "127.0.0.1"), - Port: getFromEnvAsString("WEBSERVER_PORT", "1337"), + Address: getFromEnvAsString("WEBSERVER_ADDRESS", "127.0.0.1"), + Port: getFromEnvAsString("WEBSERVER_PORT", "1337"), + JWTSigningKey: getFromEnvAsString("WEBSERVER_JWT_SIGNING_KEY", "changeme"), }, } } diff --git a/go.mod b/go.mod index 3a09926..bc6b464 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/peppermint-recipes/peppermint-server go 1.16 require ( + github.com/appleboy/gin-jwt v2.5.0+incompatible + github.com/appleboy/gin-jwt/v2 v2.6.4 github.com/aws/aws-sdk-go v1.37.30 // indirect github.com/gin-gonic/gin v1.7.1 github.com/golang/protobuf v1.4.3 // indirect @@ -15,9 +17,10 @@ require ( github.com/ugorji/go v1.2.4 // indirect github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect go.mongodb.org/mongo-driver v1.5.1 - golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b // indirect + golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20210314195730-07df6a141424 // indirect google.golang.org/protobuf v1.25.0 // indirect + gopkg.in/dgrijalva/jwt-go.v3 v3.2.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 4f1a5e7..058e268 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,32 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/appleboy/gin-jwt v2.5.0+incompatible h1:oLQTP1fiGDoDKoC2UDqXD9iqCP44ABIZMMenfH/xCqw= +github.com/appleboy/gin-jwt v2.5.0+incompatible/go.mod h1:pG7tv32IEe5wEh1NSQzcyD02ZZAqZWp07RdGiIhgaRQ= +github.com/appleboy/gin-jwt/v2 v2.6.4 h1:4YlMh3AjCFnuIRiL27b7TXns7nLx8tU/TiSgh40RRUI= +github.com/appleboy/gin-jwt/v2 v2.6.4/go.mod h1:CZpq1cRw+kqi0+yD2CwVw7VGXrrx4AqBdeZnwxVmoAs= +github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= github.com/aws/aws-sdk-go v1.37.30 h1:fZeVg3QuTkWE/dEvPQbK6AL32+3G9ofJfGFSPS1XLH0= github.com/aws/aws-sdk-go v1.37.30/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 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 h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 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.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.7.1 h1:qC89GU3p8TvKWMAVhEpmpB2CIb1hnqt2UdKZaP93mS8= github.com/gin-gonic/gin v1.7.1/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= -github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -70,13 +77,11 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -89,10 +94,8 @@ github.com/klauspost/compress v1.11.12 h1:famVnQVu7QwryBN4jNseQdUKES71ZAOnB6UQQJ github.com/klauspost/compress v1.11.12/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -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.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= @@ -113,7 +116,6 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/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_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -129,9 +131,10 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ 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/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= +github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.2.4 h1:cTciPbZ/VSOzCLKclmssnfQ/jyoVyOcJ3aoJyUV1Urc= @@ -206,7 +209,6 @@ golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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= @@ -227,14 +229,13 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 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/dgrijalva/jwt-go.v3 v3.2.0/go.mod h1:hdNXC2Z9yC029rvsQ/on2ZNQ44Z2XToVhpXXbR+J05A= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/main.go b/main.go index 81c8e19..690db1d 100644 --- a/main.go +++ b/main.go @@ -1,14 +1,21 @@ package main import ( + "log" + "net/http" + "github.com/peppermint-recipes/peppermint-server/auth" + "github.com/gin-gonic/gin" "github.com/peppermint-recipes/peppermint-server/config" "github.com/peppermint-recipes/peppermint-server/database" "github.com/peppermint-recipes/peppermint-server/recipe" shoppinglist "github.com/peppermint-recipes/peppermint-server/shopping-list" + "github.com/peppermint-recipes/peppermint-server/user" "github.com/peppermint-recipes/peppermint-server/weekplan" + + jwt "github.com/appleboy/gin-jwt/v2" ) func livezHandler(c *gin.Context) { @@ -31,7 +38,7 @@ func CORSMiddleware(context *gin.Context) { } } -func setupServer(dbConfig *config.DBConfig) *gin.Engine { +func setupServer(dbConfig *config.DBConfig, JWTSigningKey string) *gin.Engine { database.RegisterConnection(dbConfig.Username, dbConfig.Password, dbConfig.Endpoint) router := gin.Default() @@ -39,31 +46,67 @@ func setupServer(dbConfig *config.DBConfig) *gin.Engine { recipeServer := recipe.NewRecipeServer() weekplanServer := weekplan.NewWeekplanServer() shoppingListServer := shoppinglist.NewShoppingListServer() + userServer := user.NewUserServer() router.GET("/livez", livezHandler) - router.GET("/recipes/:id", recipeServer.GetRecipeByIDHandler) - router.GET("/recipes/", recipeServer.GetAllRecipesHandler) - router.POST("/recipes/", recipeServer.CreateRecipeHandler) - router.PUT("/recipes/", recipeServer.UpdateRecipeHandler) - router.DELETE("/recipes/:id", recipeServer.DeleteRecipeHandler) + authMiddleware, err := auth.RegisterAuthMiddleware(JWTSigningKey) + if err != nil { + log.Fatal("JWT Error:" + err.Error()) + } + + // When you use jwt.New(), the function is already automatically called for checking, + // which means you don't need to call it again. + errInit := authMiddleware.MiddlewareInit() + if errInit != nil { + log.Fatal("authMiddleware.MiddlewareInit() Error:" + errInit.Error()) + } + + router.POST("/login", authMiddleware.LoginHandler) + router.NoRoute(authMiddleware.MiddlewareFunc(), func(c *gin.Context) { + claims := jwt.ExtractClaims(c) + log.Printf("NoRoute claims: %#v\n", claims) + c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"}) + }) + + auth := router.Group("/auth") + // Refresh time can be longer than token timeout, therefore it must be registered BEFORE the auth middleware. + auth.GET("/refresh_token", authMiddleware.RefreshHandler) + auth.Use(authMiddleware.MiddlewareFunc()) + + router.POST("/register", userServer.CreateUserHandler) + + recipes := router.Group("/recipes") + recipes.Use(authMiddleware.MiddlewareFunc()) + recipes.GET("/:id", recipeServer.GetRecipeByIDHandler) + recipes.GET("/", recipeServer.GetAllRecipesHandler) + recipes.POST("/", recipeServer.CreateRecipeHandler) + recipes.PUT("/", recipeServer.UpdateRecipeHandler) + recipes.DELETE("/:id", recipeServer.DeleteRecipeHandler) - router.GET("/weekplans/:id", weekplanServer.GetWeekplanByIDHandler) - router.GET("/weekplans/", weekplanServer.GetAllWeekplansHandler) - router.POST("/weekplans/", weekplanServer.CreateWeekplanHandler) - router.PUT("/weekplans/", weekplanServer.UpdateWeekplanHandler) - router.DELETE("/weekplans/:id", weekplanServer.DeleteWeekplanHandler) + weekplans := router.Group("/weekplans") + weekplans.Use(authMiddleware.MiddlewareFunc()) + weekplans.GET("/:id", weekplanServer.GetWeekplanByIDHandler) + weekplans.GET("/", weekplanServer.GetAllWeekplansHandler) + weekplans.POST("/", weekplanServer.CreateWeekplanHandler) + weekplans.PUT("/", weekplanServer.UpdateWeekplanHandler) + weekplans.DELETE("/:id", weekplanServer.DeleteWeekplanHandler) - router.GET("/shopping-lists/:id", shoppingListServer.GetShoppingListsByIDHandler) - router.GET("/shopping-lists/", shoppingListServer.GetAllWeekplansHandler) - router.POST("/shopping-lists/", shoppingListServer.CreateWeekplanHandler) - router.PUT("/shopping-lists/", shoppingListServer.UpdateWeekplanHandler) - router.DELETE("/shopping-lists/:id", shoppingListServer.DeleteWeekplanHandler) + shoppingLists := router.Group("/shopping-lists") + shoppingLists.Use(authMiddleware.MiddlewareFunc()) + shoppingLists.GET("/:id", shoppingListServer.GetShoppingListsByIDHandler) + shoppingLists.GET("/", shoppingListServer.GetAllWeekplansHandler) + shoppingLists.POST("/", shoppingListServer.CreateWeekplanHandler) + shoppingLists.PUT("/", shoppingListServer.UpdateWeekplanHandler) + shoppingLists.DELETE("/:id", shoppingListServer.DeleteWeekplanHandler) return router } func main() { config := config.GetConfig() - setupServer(config.DB).Run(config.Web.Address + ":" + config.Web.Port) + setupServer( + config.DB, + config.Web.JWTSigningKey, + ).Run(config.Web.Address + ":" + config.Web.Port) } diff --git a/main_test.go b/main_test.go index 0ba0c23..ddcfd0b 100644 --- a/main_test.go +++ b/main_test.go @@ -11,7 +11,7 @@ import ( func TestLivezRoute(t *testing.T) { config := config.GetConfig() - testServer := httptest.NewServer(setupServer(config.DB)) + testServer := httptest.NewServer(setupServer(config.DB, "test")) defer testServer.Close() response, err := http.Get(fmt.Sprintf("%s/livez", testServer.URL)) diff --git a/recipe/recipe-server.go b/recipe/recipe-server.go index bda0941..47403e3 100644 --- a/recipe/recipe-server.go +++ b/recipe/recipe-server.go @@ -4,9 +4,12 @@ import ( "errors" "log" "net/http" + "regexp" + "strings" "time" "github.com/gin-gonic/gin" + "github.com/peppermint-recipes/peppermint-server/auth" "github.com/peppermint-recipes/peppermint-server/database" "go.mongodb.org/mongo-driver/mongo" ) @@ -25,10 +28,26 @@ func NewRecipeServer() *recipeServer { return &recipeServer{mongoClient: mongoClient} } +func getUserIDFromContext(context *gin.Context) (string, error) { + // claims := jwt.ExtractClaims(context) + user, _ := context.Get(auth.IdentityKey) + + userID := user.(*auth.User).UserID + + pat := regexp.MustCompile(`"\w+"`) + s := pat.FindString(userID) + + test := strings.ReplaceAll(s, `"`, ``) + + return test, nil +} + func (rs *recipeServer) GetAllRecipesHandler(context *gin.Context) { var recipes []*Recipe - recipes, err := getAllRecipes() + userID, _ := getUserIDFromContext(context) + + recipes, err := getAllRecipesForUser(userID) if err != nil { context.JSON(http.StatusNotFound, gin.H{"message": err}) @@ -48,8 +67,9 @@ func (rs *recipeServer) GetAllRecipesHandler(context *gin.Context) { func (rs *recipeServer) GetRecipeByIDHandler(context *gin.Context) { recipeID := context.Param("id") + userID, _ := getUserIDFromContext(context) - var loadedRecipe, err = getRecipeByID(recipeID) + var loadedRecipe, err = getRecipeByID(recipeID, userID) if err != nil { context.JSON(http.StatusNotFound, gin.H{"message": err}) @@ -74,6 +94,9 @@ func (rs *recipeServer) CreateRecipeHandler(context *gin.Context) { return } + userID, _ := getUserIDFromContext(context) + + recipe.UserID = userID recipe.LastUpdated = time.Now() createdRecipe, err := createRecipe(&recipe) @@ -94,7 +117,9 @@ func (rs *recipeServer) UpdateRecipeHandler(context *gin.Context) { return } + userID, _ := getUserIDFromContext(context) + recipe.UserID = userID recipe.LastUpdated = time.Now() savedRecipe, err := updateRecipe(&recipe) @@ -109,8 +134,9 @@ func (rs *recipeServer) UpdateRecipeHandler(context *gin.Context) { func (rs *recipeServer) DeleteRecipeHandler(context *gin.Context) { recipeID := context.Param("id") + userID, _ := getUserIDFromContext(context) - deletedRecipe, err := deleteRecipe(recipeID) + deletedRecipe, err := deleteRecipe(recipeID, userID) if err != nil { context.JSON(http.StatusInternalServerError, gin.H{"message": err}) diff --git a/recipe/recipe-store.go b/recipe/recipe-store.go index bbd59d1..6fcd1ce 100644 --- a/recipe/recipe-store.go +++ b/recipe/recipe-store.go @@ -22,7 +22,7 @@ var ( errCouldNotCreateObjectID = errors.New("could not create object id") ) -func getAllRecipes() ([]*Recipe, error) { +func getAllRecipesForUser(userID string) ([]*Recipe, error) { var recipes []*Recipe client, ctx, cancel := database.GetConnection() @@ -31,7 +31,7 @@ func getAllRecipes() ([]*Recipe, error) { db := client.Database(database.DatabaseName) collection := db.Collection(recipeCollectionName) - cursor, err := collection.Find(ctx, bson.D{}) + cursor, err := collection.Find(ctx, bson.M{"userid": userID}) if err != nil { return nil, err } @@ -46,14 +46,14 @@ func getAllRecipes() ([]*Recipe, error) { return recipes, nil } -func getRecipeByID(id string) (*Recipe, error) { +func getRecipeByID(recipdeID string, userID string) (*Recipe, error) { var recipe *Recipe client, ctx, cancel := database.GetConnection() defer cancel() defer client.Disconnect(ctx) - mongoObjectID, err := primitive.ObjectIDFromHex(id) + mongoObjectID, err := primitive.ObjectIDFromHex(recipdeID) if err != nil { log.Printf("Could not create object id from string. %v", err) @@ -62,7 +62,7 @@ func getRecipeByID(id string) (*Recipe, error) { db := client.Database(database.DatabaseName) collection := db.Collection(recipeCollectionName) - result := collection.FindOne(ctx, bson.D{{"id", mongoObjectID}}) + result := collection.FindOne(ctx, bson.M{"id": mongoObjectID, "userid": userID}) if result == nil { return nil, errCouldNotFindRecipe } @@ -127,12 +127,12 @@ func updateRecipe(recipe *Recipe) (*Recipe, error) { return updatedRecipe, nil } -func deleteRecipe(id string) (*Recipe, error) { +func deleteRecipe(id string, userID string) (*Recipe, error) { client, ctx, cancel := database.GetConnection() defer cancel() defer client.Disconnect(ctx) - foundRecipe, err := getRecipeByID(id) + foundRecipe, err := getRecipeByID(id, userID) if err != nil { return nil, err } diff --git a/user/user-server.go b/user/user-server.go new file mode 100644 index 0000000..799b889 --- /dev/null +++ b/user/user-server.go @@ -0,0 +1,98 @@ +package user + +import ( + "errors" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/peppermint-recipes/peppermint-server/database" + "go.mongodb.org/mongo-driver/mongo" +) + +var ( + errUserIsNotValid = errors.New("user is not valid") +) + +type userServer struct { + mongoClient *mongo.Client +} + +func NewUserServer() *userServer { + mongoClient, _, _ := database.GetConnection() + return &userServer{mongoClient: mongoClient} +} + +func (us *userServer) GetUserByIDHandler(context *gin.Context) { + userID := context.Param("id") + + var loadedUser, err = getUserByID(userID) + if err != nil { + context.JSON(http.StatusNotFound, gin.H{"message": err}) + + return + } + + loadedUser.Password = "nope" + + context.JSON(http.StatusOK, gin.H{"User": loadedUser}) +} + +func (us *userServer) CreateUserHandler(context *gin.Context) { + var user User + + if err := context.ShouldBindJSON(&user); err != nil { + context.String(http.StatusBadRequest, err.Error()) + + return + } + + if !user.isValid() { + context.String(http.StatusBadRequest, errUserIsNotValid.Error()) + + return + } + + user.LastUpdated = time.Now() + + createdUser, err := createUser(&user) + if err != nil { + context.JSON(http.StatusInternalServerError, gin.H{"message": err}) + return + } + context.JSON(http.StatusOK, gin.H{"recipe": createdUser}) +} + +func (us *userServer) UpdateUserHandler(context *gin.Context) { + var user User + if err := context.ShouldBindJSON(&user); err != nil { + context.JSON(http.StatusBadRequest, gin.H{"message": err}) + return + } + + user.LastUpdated = time.Now() + + savedUser, err := updateUser(&user) + if err != nil { + context.JSON(http.StatusInternalServerError, gin.H{"message": err}) + return + } + + savedUser.Password = "nope" + + context.JSON(http.StatusOK, gin.H{"user": savedUser}) +} + +func (us *userServer) DeleteUserHandler(context *gin.Context) { + userID := context.Param("id") + + deletedUser, err := deleteUser(userID) + if err != nil { + context.JSON(http.StatusInternalServerError, gin.H{"message": err}) + return + } + + deletedUser.Password = "nope" + + context.JSON(http.StatusOK, gin.H{"user": deletedUser}) +} diff --git a/user/user-store.go b/user/user-store.go new file mode 100644 index 0000000..4de5dd1 --- /dev/null +++ b/user/user-store.go @@ -0,0 +1,195 @@ +package user + +import ( + "errors" + "log" + + "github.com/peppermint-recipes/peppermint-server/database" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" + "golang.org/x/crypto/bcrypt" +) + +const ( + userCollectionName = "users" +) + +var ( + errCouldNotMarhsallJSON = errors.New("could not marshall user to json") + errCouldNotFindUser = errors.New("could not find user") + errCouldNotCreateUser = errors.New("could not create user") + errCouldNotSaveUser = errors.New("could not save user") + errCouldNotCreateObjectID = errors.New("could not create object id") + errCouldNotCreateHashedAndSaltedPassword = errors.New("could not create hashed and salted password") + errUserNotAuthorized = errors.New("user not authorized") +) + +func hashAndSalt(pwd []byte) (string, error) { + hash, err := bcrypt.GenerateFromPassword(pwd, bcrypt.DefaultCost) + if err != nil { + log.Println(err) + return "", errCouldNotCreateHashedAndSaltedPassword + } + + return string(hash), nil +} + +func comparePasswords(hashedPwd string, plainPwd []byte) bool { + byteHash := []byte(hashedPwd) + err := bcrypt.CompareHashAndPassword(byteHash, plainPwd) + if err != nil { + log.Println(errUserNotAuthorized) + return false + } + + return true +} + +func IsUserAuthorized(userName string, userPassword string) (*User, error) { + var user *User + + user, err := getUserByName(userName) + if err != nil { + return nil, err + } + + if comparePasswords(user.Password, []byte(userPassword)) { + return user, nil + } + + return nil, errUserNotAuthorized +} + +func getUserByID(id string) (*User, error) { + var user *User + + client, ctx, cancel := database.GetConnection() + defer cancel() + defer client.Disconnect(ctx) + + mongoObjectID, err := primitive.ObjectIDFromHex(id) + if err != nil { + log.Printf("Could not create object id from string. %v", err) + + return nil, errCouldNotCreateObjectID + } + + db := client.Database(database.DatabaseName) + collection := db.Collection(userCollectionName) + result := collection.FindOne(ctx, bson.D{{"id", mongoObjectID}}) + if result == nil { + return nil, errCouldNotFindUser + } + + err = result.Decode(&user) + if err != nil { + log.Printf("Failed marshalling %v", err) + + return nil, errCouldNotMarhsallJSON + } + + return user, nil +} + +func getUserByName(name string) (*User, error) { + var user *User + + client, ctx, cancel := database.GetConnection() + defer cancel() + defer client.Disconnect(ctx) + + db := client.Database(database.DatabaseName) + collection := db.Collection(userCollectionName) + result := collection.FindOne(ctx, bson.D{{"name", name}}) + if result == nil { + return nil, errCouldNotFindUser + } + + err := result.Decode(&user) + if err != nil { + log.Printf("Failed marshalling %v", err) + + return nil, errCouldNotMarhsallJSON + } + + return user, nil +} + +func createUser(user *User) (*User, error) { + client, ctx, cancel := database.GetConnection() + defer cancel() + defer client.Disconnect(ctx) + + hashedAndSaltedPassword, err := hashAndSalt([]byte(user.Password)) + if err != nil { + return nil, errCouldNotCreateHashedAndSaltedPassword + } + user.Password = hashedAndSaltedPassword + + user.ID = primitive.NewObjectID() + + _, err = client.Database(database.DatabaseName). + Collection(userCollectionName). + InsertOne(ctx, user) + if err != nil { + log.Printf("Could not create User: %v", err) + return user, errCouldNotCreateUser + } + + return user, nil +} + +func updateUser(user *User) (*User, error) { + var updatedUser *User + + client, ctx, cancel := database.GetConnection() + defer cancel() + defer client.Disconnect(ctx) + + update := bson.M{ + "$set": user, + } + + // TODO: handle password change + + upsert := false + after := options.After + opt := options.FindOneAndUpdateOptions{ + Upsert: &upsert, + ReturnDocument: &after, + } + + err := client.Database(database.DatabaseName). + Collection(userCollectionName). + FindOneAndUpdate( + ctx, bson.M{"id": user.ID}, update, &opt). + Decode(&updatedUser) + if err != nil { + log.Printf("Could not save User: %v", err) + + return nil, errCouldNotSaveUser + } + + return updatedUser, nil +} + +func deleteUser(id string) (*User, error) { + client, ctx, cancel := database.GetConnection() + defer cancel() + defer client.Disconnect(ctx) + + foundUser, err := getUserByID(id) + if err != nil { + return nil, err + } + foundUser.Deleted = true + + _, err = updateUser(foundUser) + if err != nil { + return nil, err + } + + return foundUser, nil +} diff --git a/user/user.go b/user/user.go new file mode 100644 index 0000000..4a28a9b --- /dev/null +++ b/user/user.go @@ -0,0 +1,20 @@ +package user + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// User is a model of a user +type User struct { + ID primitive.ObjectID `json:"id"` + Name string `form:"name" json:"name" binding:"required"` + Password string `form:"password" json:"password" binding:"required"` + Deleted bool `json:"deleted"` + LastUpdated time.Time `json:"lastUpdated"` +} + +func (user *User) isValid() bool { + return user.Name != "" +}