aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMax Resnick <max@ofmax.li>2020-08-14 23:13:41 -0700
committerMax Resnick <max@ofmax.li>2020-11-08 07:57:13 -0800
commit689a57ec4a444f8233fe2e5ec7ceb0903218218d (patch)
tree1bcfe6786c38b4ae11997d5d97dc3c5fba747b97
parent77c2e6aca2dc0f851f55e30a0f49c9ee7c2c952e (diff)
downloadiserv-master.tar.gz
feat: working login gauthHEADmaster
-rw-r--r--cmd/web/main.go44
-rw-r--r--go.mod20
-rw-r--r--go.sum29
-rw-r--r--internal/auth/handler.go105
-rw-r--r--internal/auth/handler_test.go60
-rw-r--r--internal/auth/model.go49
-rw-r--r--internal/auth/repo.go8
-rw-r--r--internal/auth/service.go97
-rw-r--r--internal/auth/service_test.go133
-rw-r--r--internal/db/redis/auth.go108
-rw-r--r--internal/image/service.go2
-rw-r--r--internal/mock/mock_auth/mock_auth.go196
-rw-r--r--internal/test/helpers.go14
-rw-r--r--templates/pages/image.tmpl2
-rw-r--r--templates/pages/login.tmpl17
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))
}
diff --git a/go.mod b/go.mod
index 2332a46..7ff0ba8 100644
--- a/go.mod
+++ b/go.mod
@@ -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
)
diff --git a/go.sum b/go.sum
index e8a75e7..f4693d1 100644
--- a/go.sum
+++ b/go.sum
@@ -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 }}