diff options
| author | Max Resnick <max@ofmax.li> | 2020-08-14 23:13:41 -0700 |
|---|---|---|
| committer | Max Resnick <max@ofmax.li> | 2020-11-08 07:57:13 -0800 |
| commit | 689a57ec4a444f8233fe2e5ec7ceb0903218218d (patch) | |
| tree | 1bcfe6786c38b4ae11997d5d97dc3c5fba747b97 | |
| parent | 77c2e6aca2dc0f851f55e30a0f49c9ee7c2c952e (diff) | |
| download | iserv-master.tar.gz | |
Diffstat (limited to '')
| -rw-r--r-- | cmd/web/main.go | 44 | ||||
| -rw-r--r-- | go.mod | 20 | ||||
| -rw-r--r-- | go.sum | 29 | ||||
| -rw-r--r-- | internal/auth/handler.go | 105 | ||||
| -rw-r--r-- | internal/auth/handler_test.go | 60 | ||||
| -rw-r--r-- | internal/auth/model.go | 49 | ||||
| -rw-r--r-- | internal/auth/repo.go | 8 | ||||
| -rw-r--r-- | internal/auth/service.go | 97 | ||||
| -rw-r--r-- | internal/auth/service_test.go | 133 | ||||
| -rw-r--r-- | internal/db/redis/auth.go | 108 | ||||
| -rw-r--r-- | internal/image/service.go | 2 | ||||
| -rw-r--r-- | internal/mock/mock_auth/mock_auth.go | 196 | ||||
| -rw-r--r-- | internal/test/helpers.go | 14 | ||||
| -rw-r--r-- | templates/pages/image.tmpl | 2 | ||||
| -rw-r--r-- | templates/pages/login.tmpl | 17 |
15 files changed, 871 insertions, 13 deletions
diff --git a/cmd/web/main.go b/cmd/web/main.go index f215d1e..093dad6 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -1,22 +1,46 @@ package main import ( + "io/ioutil" "log" "net/http" "os" "path" + "time" + "github.com/alexedwards/scs/v2" "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "git.ofmax.li/iserv/internal/auth" "git.ofmax.li/iserv/internal/db/redis" "git.ofmax.li/iserv/internal/fs" "git.ofmax.li/iserv/internal/image" "go.ofmax.li/tmpl" ) +func serviceConfig() (*oauth2.Config, error) { + b, err := ioutil.ReadFile("credentials.json") + if err != nil { + log.Fatalf("Unable to read client secret file: %v", err) + } + scopes := []string{ + "https://www.googleapis.com/auth/userinfo.email", + "openid", + } + config, err := google.ConfigFromJSON(b, scopes...) + return config, err +} + func main() { connPool := redis.CreatePool("localhost:6379") - db := redis.NewRedisImageRepo(connPool) + + oauthClientConfig, err := serviceConfig() + if err != nil { + log.Fatal("failed to load service creds") + } renderer, err := tmpl.NewHTMLTmpl("templates") if err != nil { @@ -27,20 +51,36 @@ func main() { if err != nil { log.Fatal("couldn't find directory to write images to") } + sessionManager := scs.New() + sessionManager.Lifetime = 24 * time.Hour // Image - imageService := image.NewService(db, storagePath, renderer) + imgdb := redis.NewRedisImageRepo(connPool) + imageService := image.NewService(imgdb, storagePath, renderer) imageHandler := image.NewHandler(imageService) imageFile := fs.NewHandler(storagePath) + // Auth + authdb := redis.NewRedisAuthRepo(connPool) + authService := auth.NewService(authdb) + authHandler := auth.NewHandler(authService, sessionManager, oauthClientConfig) + // Static Files staticFiles := fs.NewHandler(path.Join(storagePath, "static")) + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Use(sessionManager.LoadAndSave) + + r.Get("/a/g", authHandler.OauthCallback) + r.Get("/l", authHandler.Login) r.Get("/i/{fileName}", imageHandler.GetImage) r.Post("/u", imageHandler.PostImage) r.Get("/f/*", imageFile) + r.Get("/static/*", staticFiles) + log.Print("starting imageserv") log.Fatal(http.ListenAndServe(":8080", r)) } @@ -3,21 +3,23 @@ module git.ofmax.li/iserv go 1.14 require ( - github.com/alexedwards/scs/v2 v2.3.0 - github.com/brianvoe/gofakeit v3.18.0+incompatible // indirect - github.com/bwmarrin/snowflake v0.3.0 - github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.2 // indirect + github.com/alexedwards/scs/v2 v2.4.0 + github.com/brianvoe/gofakeit v3.18.0+incompatible + github.com/bwmarrin/snowflake v0.3.0 // indirect + github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.2 github.com/go-chi/chi v4.1.0+incompatible + github.com/golang/mock v1.4.4 github.com/gomodule/redigo v2.0.0+incompatible github.com/matoous/go-nanoid v1.3.0 - github.com/muyo/sno v1.1.0 + github.com/muyo/sno v1.1.0 // indirect github.com/pkg/errors v0.9.1 github.com/rafaeljusto/redigomock v2.3.0+incompatible - github.com/speps/go-hashids v2.0.0+incompatible + github.com/speps/go-hashids v2.0.0+incompatible // indirect github.com/stretchr/testify v1.5.1 - gitlab.com/grumps/environ v0.0.0-20190605051324-730aa37373e1 + gitlab.com/grumps/environ v0.0.0-20190605051324-730aa37373e1 // indirect go.ofmax.li/environ v0.1.0 go.ofmax.li/tmpl v0.1.0 - golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect - google.golang.org/api v0.21.0 // indirect + golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d + golang.org/x/tools v0.0.0-20201031021630-582c62ec74d0 // indirect + google.golang.org/api v0.21.0 ) @@ -7,6 +7,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/alexedwards/scs v1.4.1 h1:/5L5a07IlqApODcEfZyMsu8Smd1S7Q4nBjEyKxIRTp0= github.com/alexedwards/scs/v2 v2.3.0 h1:V8rtn2P5QGh8C9S7T/ikBo/AdA27vDoQJPbiAaOCmFg= github.com/alexedwards/scs/v2 v2.3.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= +github.com/alexedwards/scs/v2 v2.4.0 h1:XfnMamKnvp1muJVNr1WzikQTclopsBXWZtzz0NBjOK0= +github.com/alexedwards/scs/v2 v2.4.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= github.com/brianvoe/gofakeit v1.2.0 h1:GGbzCqQx9ync4ObAUhRa3F/M73eL9VZL3X09WoTwphM= github.com/brianvoe/gofakeit v3.18.0+incompatible h1:wDOmHc9DLG4nRjUVVaxA+CEglKOW72Y5+4WNxUIkjM8= github.com/brianvoe/gofakeit v3.18.0+incompatible/go.mod h1:kfwdRA90vvNhPutZWfH7WPaDzUjz+CZFqG+rPkOjGOc= @@ -24,10 +26,13 @@ github.com/gbrlsnchs/jwt/v3 v3.0.0-rc.2/go.mod h1:AncDcjXz18xetI3A6STfXq2w+LuTx8 github.com/go-chi/chi v1.0.0 h1:s/kv1cTXfivYjdKJdyUzNGyAWZ/2t7duW1gKn5ivu+c= github.com/go-chi/chi v4.1.0+incompatible h1:ETj3cggsVIY2Xao5ExCu6YhEh5MD6JTfcBzS37R260w= github.com/go-chi/chi v4.1.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/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= @@ -48,6 +53,7 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ 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/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/magefile/mage v1.9.0 h1:t3AU2wNwehMCW97vuqQLtw6puppWXHO+O2MHo5a50XE= github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/matoous/go-nanoid v1.3.0 h1:ynznZVSo9t0E8BTYLZx9geceRYZr8yLIrkOv3C/CU8M= github.com/matoous/go-nanoid v1.3.0/go.mod h1:fvGBnhcQ+zcrB3qJIG32PAN11J/y1IYkGX2/VeHzuH0= @@ -67,6 +73,7 @@ github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= gitlab.com/grumps/environ v0.0.0-20190605051324-730aa37373e1 h1:Jjns+kL26DLZhyrxqBZHJn1ymX33jtfnmv2xufSXOaA= gitlab.com/grumps/environ v0.0.0-20190605051324-730aa37373e1/go.mod h1:GPGILReyQ0kLhYL5BcbmvTUGCmHC+upQgUZBQMqvcDY= go.ofmax.li/environ v0.1.0 h1:O5FEy8lPef4BpdiMNvDJUH7tqQaZmjQvwpDVv6rqmBI= @@ -76,7 +83,10 @@ go.ofmax.li/tmpl v0.1.0/go.mod h1:gzv8lp+KPxqP6f7FqoBdGl8UwXANmuLOXxyOaKNUaQg= go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad h1:5E5raQxcv+6CZ11RrBYQe5WRbUIWpScjh0kvHZkZIrQ= golang.org/x/crypto v0.0.0-20190927123631-a832865fa7ad/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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= @@ -84,6 +94,8 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk 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/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -93,6 +105,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 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/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -103,32 +117,46 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ 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-20201020160332-67f06af15bc9/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e h1:1xWUkZQQ9Z9UuZgNaIR6OQOE7rUFglXUUBZlO+dGg6I= golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201031021630-582c62ec74d0 h1:obBdJPIfkOi5/rVh102giHaq0G8BZGE4eGB+NU6SgBo= +golang.org/x/tools v0.0.0-20201031021630-582c62ec74d0/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.21.0 h1:zS+Q/CJJnVlXpXQVIz+lH0ZT2lBuT2ac7XD8Y/3w6hY= google.golang.org/api v0.21.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 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= @@ -139,6 +167,7 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/auth/handler.go b/internal/auth/handler.go new file mode 100644 index 0000000..992608c --- /dev/null +++ b/internal/auth/handler.go @@ -0,0 +1,105 @@ +package auth + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + + "github.com/alexedwards/scs/v2" + "golang.org/x/oauth2" +) + +var ( + profileURL = "https://www.googleapis.com/oauth2/v3/userinfo" +) + +// Handler authentication handler +// handles, login, oauth and registration +type Handler interface { + Login(w http.ResponseWriter, r *http.Request) + OauthCallback(w http.ResponseWriter, r *http.Request) +} + +type authHandler struct { + service Servicer + ses *scs.SessionManager + oclient *oauth2.Config +} + +// NewHandler create new instance of handler +// Servicer, session, and oauth client required +func NewHandler(service Servicer, + session *scs.SessionManager, + oclient *oauth2.Config) Handler { + return &authHandler{ + service, + session, + oclient, + } +} + +func (h *authHandler) Login(w http.ResponseWriter, r *http.Request) { + stateValue, err := h.service.GenerateStateToken() + if err != nil { + return + } + h.ses.Put(r.Context(), "state", stateValue) + url := h.oclient.AuthCodeURL(stateValue, oauth2.AccessTypeOnline) + http.Redirect(w, r, url, 302) +} + +func (h *authHandler) OauthCallback(w http.ResponseWriter, r *http.Request) { + // this needs to be random + stateFromSession := h.ses.GetString(r.Context(), "state") + stateFromCallback := r.FormValue("state") + // prevent arbitrary authorization exchanges + if stateFromCallback != stateFromSession { + http.Error(w, "state value miss match bad data", 400) + return + } + stateValid, err := h.service.ValidateStateToken(stateFromCallback, stateFromSession) + if err != nil { + http.Error(w, "error validating", 400) + return + } + if !stateValid { + http.Error(w, "invalid tokens", 400) + return + } + // valid and same as state + code := r.FormValue("code") + token, err := h.oclient.Exchange(r.Context(), code) + if err != nil { + http.Error(w, err.Error(), 400) + return + } + log.Printf("returned token %v", token) + // google profile + client := h.oclient.Client(r.Context(), token) + resp, err := client.Get(profileURL) + if err != nil { + http.Error(w, err.Error(), 400) + return + } + defer resp.Body.Close() + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + http.Error(w, err.Error(), 400) + return + } + gp := &GoogleAuthProfile{} + err = json.Unmarshal(data, &gp) + profileID, newProfile, err := h.service.LoginOrRegisterSessionID(token, gp) + if err != nil { + http.Error(w, err.Error(), 400) + return + } + h.ses.Put(r.Context(), "profid", profileID) + // send to registration + if newProfile == true { + http.Redirect(w, r, "/account/register", 302) + return + } + http.Redirect(w, r, "/", 302) +} diff --git a/internal/auth/handler_test.go b/internal/auth/handler_test.go new file mode 100644 index 0000000..abf82a8 --- /dev/null +++ b/internal/auth/handler_test.go @@ -0,0 +1,60 @@ +package auth_test + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/alexedwards/scs/v2" + _ "github.com/brianvoe/gofakeit" + "github.com/golang/mock/gomock" + + "git.ofmax.li/iserv/internal/auth" + "git.ofmax.li/iserv/internal/mock/mock_auth" + testhelper "git.ofmax.li/iserv/internal/test" +) + +type handlerSuite struct { + as *mock_auth.MockServicer + ses *scs.SessionManager +} + +func TestHandler(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + session := scs.New() + ts := &handlerSuite{ + as: mock_auth.NewMockServicer(ctrl), + ses: session, + } + ah := auth.NewHandler(ts.as, session, testhelper.NewConf("testhost")) + t.Run("test login", ts.testLogin(ah)) +} + +func (s *handlerSuite) testLogin(ah auth.Handler) func(t *testing.T) { + return func(t *testing.T) { + s.as.EXPECT().GenerateStateToken().Return("asfdas", nil) + r, _ := http.NewRequest("GET", "/auth/login/", nil) + w := httptest.NewRecorder() + handler := s.ses.LoadAndSave(http.HandlerFunc(ah.Login)) + handler.ServeHTTP(w, r) + response := w.Result() + if response.StatusCode != http.StatusFound { + t.Errorf("login http status not 302") + } + redirectURL, _ := response.Location() + if !strings.Contains(redirectURL.String(), "REDIRECT_URL") { + t.Errorf("redirect url not foukd %s", redirectURL) + } + if !strings.Contains(redirectURL.String(), "asfdas") { + t.Errorf("state token not found in url") + } + } +} + +func (s *handlerSuite) testCallBack(ah auth.Handler) func(t *testing.T) { + return func(t *testing.T) { + t.Logf("not doing this at the moment") + } +} diff --git a/internal/auth/model.go b/internal/auth/model.go new file mode 100644 index 0000000..c51ff05 --- /dev/null +++ b/internal/auth/model.go @@ -0,0 +1,49 @@ +package auth + +import ( + "time" + + "golang.org/x/oauth2" +) + +// GoogleAuthProfile auth'd user profile +// ProfileId, Email, Name, PictureURL +type GoogleAuthProfile struct { + ProfileID string `json:"sub"` + Email string `json:"email"` + Name string `json:"name"` + PictureURL string `json:"picture"` +} + +// Profile profile and token +// Identifier + Token +type Profile struct { + ID string `json:"sub"` + Email string `json:"email"` + AccessToken string `json:"access_token"` + TokenType string `json:"token_type,omitempty"` + RefreshToken string `json:"refresh_token,omitempty"` + Expiry time.Time `json:"expiry,omitempty"` +} + +// Token token from profile +func (ap *Profile) Token() (*oauth2.Token, error) { + return &oauth2.Token{ + AccessToken: ap.AccessToken, + TokenType: ap.TokenType, + RefreshToken: ap.RefreshToken, + Expiry: ap.Expiry, + }, nil +} + +// NewAuthProfile merge token and profile +func NewAuthProfile(t *oauth2.Token, u *GoogleAuthProfile) *Profile { + return &Profile{ + u.ProfileID, + u.Email, + t.AccessToken, + t.TokenType, + t.RefreshToken, + t.Expiry, + } +} diff --git a/internal/auth/repo.go b/internal/auth/repo.go new file mode 100644 index 0000000..f404c94 --- /dev/null +++ b/internal/auth/repo.go @@ -0,0 +1,8 @@ +package auth + +// Repo storage interface +type Repo interface { + IsAuthorized(gp *GoogleAuthProfile) (bool, error) + LookUpAuthProfileID(gp *GoogleAuthProfile) (string, error) + SaveAuthProfile(ap *Profile) error +} diff --git a/internal/auth/service.go b/internal/auth/service.go new file mode 100644 index 0000000..9997264 --- /dev/null +++ b/internal/auth/service.go @@ -0,0 +1,97 @@ +package auth + +import ( + "errors" + "log" + "time" + + "golang.org/x/oauth2" + + "github.com/gbrlsnchs/jwt/v3" +) + +var ( + singingSecret = jwt.NewHS512([]byte("the wolf says moo")) + // ErrInvalidToken error for token + ErrInvalidToken = errors.New("Invalid token provided") + // ErrInvalidJWT error for jwt + ErrInvalidJWT = errors.New("Invalid JWT") + // ErrUnauthorized error for unauthorized access + ErrUnauthorized = errors.New("Unauthorized") +) + +// Servicer access to auth functionality +type Servicer interface { + LoginOrRegisterSessionID(t *oauth2.Token, gp *GoogleAuthProfile) (string, bool, error) + GenerateStateToken() (string, error) + ValidateStateToken(token string, sessionToken string) (bool, error) +} + +// Service a container for auth deps +type Service struct { + repo Repo +} + +// NewService create auth service +func NewService(repo Repo) *Service { + return &Service{ + repo, + } +} + +// GenerateStateToken create a random token for oauth exchange +func (a *Service) GenerateStateToken() (string, error) { + now := time.Now() + pl := jwt.Payload{ + Issuer: "iserv-state", + Subject: "state param", + IssuedAt: jwt.NumericDate(now), + } + tokenBytes, err := jwt.Sign(pl, singingSecret) + if err != nil { + return "", err + } + return string(tokenBytes[:]), err +} + +// ValidateStateToken validate provided token +func (a *Service) ValidateStateToken(token string, sessionToken string) (bool, error) { + if token == sessionToken { + p := jwt.Payload{} + _, err := jwt.Verify([]byte(token), singingSecret, &p) + if err != nil { + return false, ErrInvalidJWT + } + return true, nil + } + return false, ErrInvalidToken +} + +// LoginOrRegisterSessionID create a login +func (a *Service) LoginOrRegisterSessionID(t *oauth2.Token, gp *GoogleAuthProfile) (string, bool, error) { + isAuthorized, err := a.repo.IsAuthorized(gp) + newRegistration := false + if err != nil { + return "", newRegistration, err + } + if isAuthorized != true { + return "", newRegistration, ErrUnauthorized + } + profileID, err := a.repo.LookUpAuthProfileID(gp) + if err != nil { + return "", newRegistration, err + } + if profileID == "" { + // create profile + log.Printf("creating new profile") + profile := NewAuthProfile(t, gp) + profileID = profile.ID + log.Printf("new profile %+v", profile) + err = a.repo.SaveAuthProfile(profile) + if err != nil { + return "", newRegistration, err + } + newRegistration = true + } + return profileID, newRegistration, nil +} diff --git a/internal/auth/service_test.go b/internal/auth/service_test.go new file mode 100644 index 0000000..72ff709 --- /dev/null +++ b/internal/auth/service_test.go @@ -0,0 +1,133 @@ +package auth_test + +import ( + "errors" + "strings" + "testing" + + "golang.org/x/oauth2" + + "github.com/brianvoe/gofakeit" + "github.com/golang/mock/gomock" + + "git.ofmax.li/iserv/internal/auth" + "git.ofmax.li/iserv/internal/mock/mock_auth" +) + +type serviceSuite struct { + as auth.Servicer + repo *mock_auth.MockRepo +} + +func TestServices(t *testing.T) { + ctrl := gomock.NewController(t) + repo := mock_auth.NewMockRepo(ctrl) + defer ctrl.Finish() + ts := &serviceSuite{ + auth.NewService(repo), + repo, + } + t.Run("token generation", ts.testGenStateToken()) + t.Run("token validation", ts.testValidateStateToken()) + t.Run("auth profile registration", ts.testLoginOrRegsiterSessionId()) +} + +func (s *serviceSuite) testGenStateToken() func(t *testing.T) { + return func(t *testing.T) { + token, _ := s.as.GenerateStateToken() + parts := strings.Split(token, ".") + if len(parts) != 3 { + t.Errorf("token doesn't match format") + } + } +} + +func (s *serviceSuite) testValidateStateToken() func(t *testing.T) { + validToken, _ := s.as.GenerateStateToken() + return func(t *testing.T) { + cases := []struct { + name string + token string + sessionToken string + wanted bool + errz error + }{ + {name: "valid matching token", + token: validToken, + sessionToken: validToken, + wanted: true, errz: nil}, + {name: "non matching token", + token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + sessionToken: "sadfsaf", + wanted: false, errz: auth.ErrInvalidToken}, + {name: "matching but not real", + token: "eyJhbGciOTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + sessionToken: "eyJhbGciOTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + wanted: false, errz: auth.ErrInvalidJWT}, + } + for _, tc := range cases { + isValid, err := s.as.ValidateStateToken(tc.token, tc.sessionToken) + if (isValid != tc.wanted) || (err != tc.errz) { + t.Fatalf("%s: expected: %v, got: %v err: %v errgot: %v", tc.name, tc.wanted, isValid, tc.errz, err) + } + } + } +} + +func (s *serviceSuite) testLoginOrRegsiterSessionId() func(t *testing.T) { + return func(t *testing.T) { + // is authorized err + gp := &auth.GoogleAuthProfile{} + token := &oauth2.Token{} + gofakeit.Struct(token) + gofakeit.Struct(gp) + s.repo. + EXPECT(). + IsAuthorized(gomock.Any()). + Return(false, errors.New("foo")) + id, isNew, err := s.as.LoginOrRegisterSessionID(token, gp) + if err == nil && id == "" && isNew == false { + t.Fatalf("%s", id) + } + // not authorized no error + s.repo. + EXPECT(). + IsAuthorized(gomock.Any()). + Return(false, nil) + id, isNew, err = s.as.LoginOrRegisterSessionID(token, gp) + if err != auth.ErrUnauthorized || id != "" || isNew != false { + t.Fatalf("unauthorized isnew: %v id: %v err: %v", isNew, id, err.Error()) + } + + // authorized + s.repo. + EXPECT(). + IsAuthorized(gomock.Any()). + Return(true, nil) + s.repo. + EXPECT(). + LookUpAuthProfileID(gomock.Any()). + Return("asdfsafaf", nil) + id, isNew, err = s.as.LoginOrRegisterSessionID(token, gp) + if err != nil && isNew == false && id == "" { + t.Fatalf("auth profile exists isnew: %v id: %v err: %v", isNew, id, err.Error()) + } + // new registration + s.repo. + EXPECT(). + IsAuthorized(gomock.Any()). + Return(true, nil) + s.repo. + EXPECT(). + LookUpAuthProfileID(gomock.Any()). + Return("", nil) + s.repo. + EXPECT(). + SaveAuthProfile(gomock.Any()). + Return(nil) + id, isNew, err = s.as.LoginOrRegisterSessionID(token, gp) + if err != nil && isNew == false && id == "" { + t.Fatalf("isnew: %v id: %v err: %v", isNew, id, err.Error()) + } + } +} diff --git a/internal/db/redis/auth.go b/internal/db/redis/auth.go new file mode 100644 index 0000000..0c5246c --- /dev/null +++ b/internal/db/redis/auth.go @@ -0,0 +1,108 @@ +package redis + +import ( + "log" + + "github.com/gomodule/redigo/redis" + "golang.org/x/oauth2" + + "git.ofmax.li/iserv/internal/auth" +) + +var ( + createAuthLua = redis.NewScript(2, ` + if redis.call("SISMEMBER","invites",KEYS[1]) == 1 then + return redis.call("HMSET",KEYS[2],unpack(ARGV)) + end + return nil + `) +) + +// AuthRepo supporting auth +type AuthRepo struct { + db *redis.Pool +} + +// NewRedisAuthRepo redis.Pool +// creates auth repo around redis pool +func NewRedisAuthRepo(conn *redis.Pool) *AuthRepo { + return &AuthRepo{ + conn, + } +} + +// IsAuthorized is member of invites +func (db *AuthRepo) IsAuthorized(gp *auth.GoogleAuthProfile) (bool, error) { + conn := db.db.Get() + defer conn.Close() + authProfileKey := "auth:goog:" + gp.Email + reply, err := redis.Bool(conn.Do("SISMEMBER", "invites", authProfileKey)) + if err != nil { + return reply, err + } + return reply, err +} + +// LookUpAuthProfileID get internal profileid +func (db *AuthRepo) LookUpAuthProfileID(gp *auth.GoogleAuthProfile) (string, error) { + conn := db.db.Get() + var id string + defer conn.Close() + authProfileKey := "auth:goog:" + gp.ProfileID + reply, err := redis.Values(conn.Do("HMGET", authProfileKey, "id")) + if err == redis.ErrNil { + // no profile + return "", nil + } else if err != nil { + // some other error + return "", err + } + if _, err := redis.Scan(reply, &id); err != nil { + return "", err + } + return id, nil +} + +// SaveAuthProfile save profile, validate against invites +func (db *AuthRepo) SaveAuthProfile(ap *auth.Profile) error { + // TODO this will over-write refresh tokens + conn := db.db.Get() + defer conn.Close() + authProfileKey := "auth:goog:" + ap.ID + prof := redis.Args{}.Add(ap.Email).Add(authProfileKey).AddFlat(ap) + res, err := createAuthLua.Do(conn, prof...) + log.Printf("auth save response %+v", res) + conn.Flush() + return err +} + +func (db *AuthRepo) getAuthProfile(id string) (*auth.Profile, error) { + conn := db.db.Get() + defer conn.Close() + authProfileKey := "auth:goog:" + id + authProfile := auth.Profile{} + res, err := redis.Values(conn.Do("HGETALL", authProfileKey)) + if err != nil { + log.Fatal(err) + } + log.Printf("getAuthProfile: %+v", res) + redis.ScanStruct(res, &authProfile) + return &authProfile, err +} + +// GetProfileToken token for profile +func (db *AuthRepo) GetProfileToken(id string) (*oauth2.Token, error) { + profile, err := db.getAuthProfile(id) + if err != nil { + return nil, err + } + tkn, err := profile.Token() + if err != nil { + return nil, err + } + refreshedToken, err := oauth2.ReuseTokenSource(tkn, profile).Token() + if err != nil { + return nil, err + } + return refreshedToken, nil +} diff --git a/internal/image/service.go b/internal/image/service.go index 4ffd513..10f4148 100644 --- a/internal/image/service.go +++ b/internal/image/service.go @@ -8,7 +8,7 @@ import ( "path" "time" - "github.com/matoous/go-nanoid" + gonanoid "github.com/matoous/go-nanoid" "github.com/pkg/errors" "go.ofmax.li/tmpl" diff --git a/internal/mock/mock_auth/mock_auth.go b/internal/mock/mock_auth/mock_auth.go new file mode 100644 index 0000000..be22934 --- /dev/null +++ b/internal/mock/mock_auth/mock_auth.go @@ -0,0 +1,196 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: git.ofmax.li/iserv/internal/auth (interfaces: Servicer,Repo,Handler) + +// Package mock_auth is a generated GoMock package. +package mock_auth + +import ( + auth "git.ofmax.li/iserv/internal/auth" + gomock "github.com/golang/mock/gomock" + oauth2 "golang.org/x/oauth2" + http "net/http" + reflect "reflect" +) + +// MockServicer is a mock of Servicer interface +type MockServicer struct { + ctrl *gomock.Controller + recorder *MockServicerMockRecorder +} + +// MockServicerMockRecorder is the mock recorder for MockServicer +type MockServicerMockRecorder struct { + mock *MockServicer +} + +// NewMockServicer creates a new mock instance +func NewMockServicer(ctrl *gomock.Controller) *MockServicer { + mock := &MockServicer{ctrl: ctrl} + mock.recorder = &MockServicerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockServicer) EXPECT() *MockServicerMockRecorder { + return m.recorder +} + +// GenerateStateToken mocks base method +func (m *MockServicer) GenerateStateToken() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GenerateStateToken") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GenerateStateToken indicates an expected call of GenerateStateToken +func (mr *MockServicerMockRecorder) GenerateStateToken() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateStateToken", reflect.TypeOf((*MockServicer)(nil).GenerateStateToken)) +} + +// LoginOrRegisterSessionID mocks base method +func (m *MockServicer) LoginOrRegisterSessionID(arg0 *oauth2.Token, arg1 *auth.GoogleAuthProfile) (string, bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LoginOrRegisterSessionID", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(bool) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// LoginOrRegisterSessionID indicates an expected call of LoginOrRegisterSessionID +func (mr *MockServicerMockRecorder) LoginOrRegisterSessionID(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoginOrRegisterSessionID", reflect.TypeOf((*MockServicer)(nil).LoginOrRegisterSessionID), arg0, arg1) +} + +// ValidateStateToken mocks base method +func (m *MockServicer) ValidateStateToken(arg0, arg1 string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateStateToken", arg0, arg1) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ValidateStateToken indicates an expected call of ValidateStateToken +func (mr *MockServicerMockRecorder) ValidateStateToken(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateStateToken", reflect.TypeOf((*MockServicer)(nil).ValidateStateToken), arg0, arg1) +} + +// MockRepo is a mock of Repo interface +type MockRepo struct { + ctrl *gomock.Controller + recorder *MockRepoMockRecorder +} + +// MockRepoMockRecorder is the mock recorder for MockRepo +type MockRepoMockRecorder struct { + mock *MockRepo +} + +// NewMockRepo creates a new mock instance +func NewMockRepo(ctrl *gomock.Controller) *MockRepo { + mock := &MockRepo{ctrl: ctrl} + mock.recorder = &MockRepoMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockRepo) EXPECT() *MockRepoMockRecorder { + return m.recorder +} + +// IsAuthorized mocks base method +func (m *MockRepo) IsAuthorized(arg0 *auth.GoogleAuthProfile) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsAuthorized", arg0) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsAuthorized indicates an expected call of IsAuthorized +func (mr *MockRepoMockRecorder) IsAuthorized(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAuthorized", reflect.TypeOf((*MockRepo)(nil).IsAuthorized), arg0) +} + +// LookUpAuthProfileID mocks base method +func (m *MockRepo) LookUpAuthProfileID(arg0 *auth.GoogleAuthProfile) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LookUpAuthProfileID", arg0) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LookUpAuthProfileID indicates an expected call of LookUpAuthProfileID +func (mr *MockRepoMockRecorder) LookUpAuthProfileID(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LookUpAuthProfileID", reflect.TypeOf((*MockRepo)(nil).LookUpAuthProfileID), arg0) +} + +// SaveAuthProfile mocks base method +func (m *MockRepo) SaveAuthProfile(arg0 *auth.Profile) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveAuthProfile", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SaveAuthProfile indicates an expected call of SaveAuthProfile +func (mr *MockRepoMockRecorder) SaveAuthProfile(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAuthProfile", reflect.TypeOf((*MockRepo)(nil).SaveAuthProfile), arg0) +} + +// MockHandler is a mock of Handler interface +type MockHandler struct { + ctrl *gomock.Controller + recorder *MockHandlerMockRecorder +} + +// MockHandlerMockRecorder is the mock recorder for MockHandler +type MockHandlerMockRecorder struct { + mock *MockHandler +} + +// NewMockHandler creates a new mock instance +func NewMockHandler(ctrl *gomock.Controller) *MockHandler { + mock := &MockHandler{ctrl: ctrl} + mock.recorder = &MockHandlerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockHandler) EXPECT() *MockHandlerMockRecorder { + return m.recorder +} + +// Login mocks base method +func (m *MockHandler) Login(arg0 http.ResponseWriter, arg1 *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Login", arg0, arg1) +} + +// Login indicates an expected call of Login +func (mr *MockHandlerMockRecorder) Login(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockHandler)(nil).Login), arg0, arg1) +} + +// OauthCallback mocks base method +func (m *MockHandler) OauthCallback(arg0 http.ResponseWriter, arg1 *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "OauthCallback", arg0, arg1) +} + +// OauthCallback indicates an expected call of OauthCallback +func (mr *MockHandlerMockRecorder) OauthCallback(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OauthCallback", reflect.TypeOf((*MockHandler)(nil).OauthCallback), arg0, arg1) +} diff --git a/internal/test/helpers.go b/internal/test/helpers.go new file mode 100644 index 0000000..91fcbdc --- /dev/null +++ b/internal/test/helpers.go @@ -0,0 +1,14 @@ +package testhelper + +import ( + "golang.org/x/oauth2" +) + +func NewConf(url string) *oauth2.Config { + return &oauth2.Config{ + ClientID: "CLIENT_ID", + ClientSecret: "CLIENT_SECRET", + RedirectURL: "REDIRECT_URL", + Scopes: []string{"scope1", "scope2"}, + } +} diff --git a/templates/pages/image.tmpl b/templates/pages/image.tmpl index fcebdd5..79bf5dc 100644 --- a/templates/pages/image.tmpl +++ b/templates/pages/image.tmpl @@ -6,7 +6,7 @@ <div> <figure> <img src="http://localhost:8080{{ .ImageUrl }}"> - <caption>{{.ImageDesc}}</caption> + <figcaption>{{.ImageDesc }}</figcaption> </figure> </div> </body> diff --git a/templates/pages/login.tmpl b/templates/pages/login.tmpl new file mode 100644 index 0000000..8598cb1 --- /dev/null +++ b/templates/pages/login.tmpl @@ -0,0 +1,17 @@ +{{ define "content" }} +<body> +<div> +<h3>Login</h3> +</div> +<div> +<form action="https://indielogin.com/auth" method="get"> + <label for="url">Email Address:</label> + <input id="url" type="text" name="me" placeholder="yourdomain.com" /> + <p><input type="submit">Sign In</input></p> + <input type="hidden" name="client_id" value="https://img.ofmax.li/" /> + <input type="hidden" name="redirect_uri" value="https://img.ofmax.li/a/g" /> + <input type="hidden" name="state" value="{{ .Token }}" /> +</form> +</div> +</body> +{{ end }} |