diff --git a/docker-compose.yml b/docker-compose.yml index 74e85d77..942e5c7a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,10 @@ services: environment: REEARTH_ENV: docker REEARTH_DB_URL: mongodb://reearth-mongo + REEARTH_MAILER: smtp + REEARTH_SMTP_URL: #add later + REEARTH_SMTP_USER: #add later + REEARTH_SMTP_PASSWORD: #add later depends_on: - reearth-mongo reearth-mongo: diff --git a/go.mod b/go.mod index 7804b96f..deed69fd 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,13 @@ require ( cloud.google.com/go/storage v1.21.0 github.com/99designs/gqlgen v0.17.1 github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.3.0 - github.com/auth0/go-jwt-middleware v1.0.1 + github.com/auth0/go-jwt-middleware/v2 v2.0.0 github.com/blang/semver v3.5.1+incompatible - github.com/form3tech-oss/jwt-go v3.2.5+incompatible + github.com/caos/oidc v1.0.0 github.com/goccy/go-yaml v1.9.5 + github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f github.com/google/uuid v1.3.0 + github.com/gorilla/mux v1.8.0 github.com/iancoleman/strcase v0.2.0 github.com/idubinskiy/schematyper v0.0.0-20190118213059-f71b40dac30d github.com/jarcoal/httpmock v1.1.0 @@ -24,6 +26,7 @@ require ( github.com/paulmach/go.geojson v1.4.0 github.com/pkg/errors v0.9.1 github.com/ravilushqa/otelgqlgen v0.5.1 + github.com/sendgrid/sendgrid-go v3.11.1+incompatible github.com/sirupsen/logrus v1.8.1 github.com/spf13/afero v1.8.1 github.com/square/mongo-lock v0.0.0-20201208161834-4db518ed7fb2 @@ -38,11 +41,13 @@ require ( go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo v0.29.0 go.opentelemetry.io/otel v1.4.1 go.opentelemetry.io/otel/sdk v1.4.1 + golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce golang.org/x/text v0.3.7 golang.org/x/tools v0.1.9 google.golang.org/api v0.70.0 gopkg.in/go-playground/colors.v1 v1.2.0 gopkg.in/h2non/gock.v1 v1.1.2 + gopkg.in/square/go-jose.v2 v2.6.0 ) require ( @@ -54,10 +59,12 @@ require ( github.com/agnivade/levenshtein v1.1.1 // indirect github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a // indirect + github.com/caos/logging v0.0.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/trifles v0.0.0-20200705224438-cafc02a1ee2b // indirect github.com/fatih/color v1.12.0 // indirect + github.com/felixge/httpsnoop v1.0.2 // indirect github.com/gedex/inflector v0.0.0-20170307190818-16278e9db813 // indirect github.com/go-logr/logr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -69,6 +76,10 @@ require ( github.com/google/go-cmp v0.5.7 // indirect github.com/google/pprof v0.0.0-20220113144219-d25a53d42d00 // indirect github.com/googleapis/gax-go/v2 v2.1.1 // indirect + github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect + github.com/gorilla/handlers v1.5.1 // indirect + github.com/gorilla/schema v1.2.0 // indirect + github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect @@ -76,11 +87,12 @@ require ( github.com/matryer/moq v0.2.3 // indirect github.com/mattn/go-colorable v0.1.11 // indirect github.com/mattn/go-isatty v0.0.14 // indirect - github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sendgrid/rest v2.6.6+incompatible // indirect github.com/smartystreets/assertions v1.1.1 // indirect + github.com/smartystreets/goconvey v1.6.4 // indirect github.com/stretchr/objx v0.2.0 // indirect github.com/tidwall/pretty v1.0.1 // indirect github.com/urfave/cli/v2 v2.3.0 // indirect @@ -94,7 +106,6 @@ require ( go.opentelemetry.io/contrib v1.4.0 // indirect go.opentelemetry.io/otel/trace v1.4.1 // indirect go.uber.org/atomic v1.7.0 // indirect - golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa // indirect golang.org/x/mod v0.5.1 // indirect golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect @@ -107,7 +118,6 @@ require ( google.golang.org/grpc v1.44.0 // indirect google.golang.org/protobuf v1.27.1 // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect - gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index 80bd7c53..d66fe4c6 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -89,12 +90,17 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= -github.com/auth0/go-jwt-middleware v1.0.1 h1:/fsQ4vRr4zod1wKReUH+0A3ySRjGiT9G34kypO/EKwI= -github.com/auth0/go-jwt-middleware v1.0.1/go.mod h1:YSeUX3z6+TF2H+7padiEqNJ73Zy9vXW72U//IgN0BIM= +github.com/auth0/go-jwt-middleware/v2 v2.0.0 h1:jft2yYteA6wpwTj1uxSLwE0TlHCjodMQvX7+eyqJiOQ= +github.com/auth0/go-jwt-middleware/v2 v2.0.0/go.mod h1:/y7nPmfWDnJhCbFq22haCAU7vufwsOUzTthLVleE6/8= github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= github.com/aws/aws-sdk-go v1.35.5/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= +github.com/caos/logging v0.0.2 h1:ebg5C/HN0ludYR+WkvnFjwSExF4wvyiWPyWGcKMYsoo= +github.com/caos/logging v0.0.2/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0= +github.com/caos/oidc v1.0.0 h1:3sHkYf8zsuARR89qO9CyvfYhHGdliWPcou4glzGMXmQ= +github.com/caos/oidc v1.0.0/go.mod h1:4l0PPwdc6BbrdCFhNrRTUddsG292uHGa7gE2DSEIqoU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -132,11 +138,11 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/form3tech-oss/jwt-go v3.2.5+incompatible h1:/l4kBbb4/vGSsdtB5nUe8L7B9mImVMaBPw9L/0TBHU8= -github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/gedex/inflector v0.0.0-20170307190818-16278e9db813 h1:Uc+IZ7gYqAf/rSGFplbWBSHaGolEQlNLgMgSE3ccnIQ= github.com/gedex/inflector v0.0.0-20170307190818-16278e9db813/go.mod h1:P+oSoE9yhSRvsmYyZsshflcR6ePWYLql6UU1amW13IM= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -158,6 +164,7 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+ 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= +github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg= @@ -190,12 +197,15 @@ github.com/goccy/go-yaml v1.9.5 h1:Eh/+3uk9kLxG4koCX6lRMAPS1OaMSAi+FJcya0INdB0= github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f h1:16RtHeWGkJMc80Etb8RPCcKevXGldr57+LOyZt8zOlg= +github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f/go.mod h1:ijRvpgDJDI262hYq/IQVYgf8hd8IHUs93Ol0kvMBAx4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -224,12 +234,14 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.1.1-0.20171103154506-982329095285/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -244,6 +256,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -272,6 +286,8 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= @@ -283,10 +299,17 @@ github.com/googleinterns/cloud-operations-api-mock v0.0.0-20200709193332-a1e58c2 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= -github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= +github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= @@ -294,6 +317,7 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v0.0.0-20170914154624-68e816d1c783/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= @@ -303,6 +327,7 @@ github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1: github.com/idubinskiy/schematyper v0.0.0-20190118213059-f71b40dac30d h1:sQbbvtUoen3Tfl9G/079tXeqniwPH6TgM/lU4y7lQN8= github.com/idubinskiy/schematyper v0.0.0-20190118213059-f71b40dac30d/go.mod h1:xVHEhsiSJJnT0jlcQpQUg+GyoLf0i0xciM1kqWTGT58= github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jarcoal/httpmock v1.1.0 h1:F47ChZj1Y2zFsCXxNkBPwNNKnAyOATcdQibk0qEdVCE= github.com/jarcoal/httpmock v1.1.0/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= @@ -333,6 +358,7 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxv github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -345,19 +371,23 @@ github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3 github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= +github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/matryer/moq v0.2.3 h1:Q06vEqnBYjjfx5KKgHfYRKE/lvlRu+Nj+xodG4YdHnU= github.com/matryer/moq v0.2.3/go.mod h1:9RtPYjTnH1bSBIkpvtHkFN7nbWAnO7oRpdJkEIn6UtE= +github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.2.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -373,6 +403,7 @@ github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+ github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/paulmach/go.geojson v1.4.0 h1:5x5moCkCtDo5x8af62P9IOAYGQcYHtxz2QJ3x1DoCgY= github.com/paulmach/go.geojson v1.4.0/go.mod h1:YaKx1hKpWF+T2oj2lFJPsW/t1Q5e1jQI61eoQSTwpIs= +github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -391,6 +422,10 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sendgrid/rest v2.6.6+incompatible h1:3rO5UTPhLQo6fjytWwdwRWclP101CqErg2klf8LneB4= +github.com/sendgrid/rest v2.6.6+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= +github.com/sendgrid/sendgrid-go v3.11.1+incompatible h1:ai0+woZ3r/+tKLQExznak5XerOFoD6S7ePO0lMV8WXo= +github.com/sendgrid/sendgrid-go v3.11.1+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= @@ -400,16 +435,20 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/assertions v1.1.1 h1:T/YLemO5Yp7KPzS+lVtu+WsHn8yoSwTfItdAd1r3cck= github.com/smartystreets/assertions v1.1.1/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.8.1 h1:izYHOT71f9iZ7iq37Uqjael60/vYC6vMtzedudZ0zEk= github.com/spf13/afero v1.8.1/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/jwalterweatherman v0.0.0-20170901151539-12bd96e66386/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.1-0.20170901120850-7aff26db30c1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= github.com/square/mongo-lock v0.0.0-20201208161834-4db518ed7fb2 h1:Fod/tm/5c19889+T6j7mXxg/tEJrcLuDJxR/98raj80= github.com/square/mongo-lock v0.0.0-20201208161834-4db518ed7fb2/go.mod h1:h98Zzl76KWv7bG0FHBMA9MAcDhwcIyE7q570tDP7CmY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -437,8 +476,6 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= -github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= @@ -524,8 +561,9 @@ golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce h1:Roh6XWxHFKrPgC/EQhVubSAGQ6Ozk6IdxHSzt1mR0EI= +golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -602,8 +640,10 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 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= @@ -622,6 +662,7 @@ golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -654,6 +695,7 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -719,6 +761,7 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -793,6 +836,7 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T 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.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -841,6 +885,7 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20170918111702-1e559d0a00ee/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -920,6 +965,7 @@ google.golang.org/genproto v0.0.0-20220211171837-173942840c17/go.mod h1:kGP+zUP2 google.golang.org/genproto v0.0.0-20220216160803-4663080d8bc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c h1:TU4rFa5APdKTq0s6B7WTsH6Xmx0Knj86s6Biz56mErE= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -968,16 +1014,20 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/go-playground/colors.v1 v1.2.0 h1:SPweMUve+ywPrfwao+UvfD5Ah78aOLUkT5RlJiZn52c= gopkg.in/go-playground/colors.v1 v1.2.0/go.mod h1:AvbqcMpNXVl5gBrM20jBm3VjjKBbH/kI5UnqjU7lxFI= gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/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= diff --git a/internal/adapter/gql/generated.go b/internal/adapter/gql/generated.go index f7e13ce8..91f53d42 100644 --- a/internal/adapter/gql/generated.go +++ b/internal/adapter/gql/generated.go @@ -7522,13 +7522,13 @@ type RemoveAssetPayload { assetId: ID! } -type SignupPayload { +type UpdateMePayload { user: User! - team: Team! } -type UpdateMePayload { +type SignupPayload { user: User! + team: Team! } type DeleteMePayload { diff --git a/internal/adapter/gql/resolver_mutation_user.go b/internal/adapter/gql/resolver_mutation_user.go index e6236235..b42475f1 100644 --- a/internal/adapter/gql/resolver_mutation_user.go +++ b/internal/adapter/gql/resolver_mutation_user.go @@ -14,13 +14,14 @@ func (r *mutationResolver) Signup(ctx context.Context, input gqlmodel.SignupInpu secret = *input.Secret } + sub := getSub(ctx) u, team, err := usecases(ctx).User.Signup(ctx, interfaces.SignupParam{ - Sub: getSub(ctx), + Sub: &sub, Lang: input.Lang, Theme: gqlmodel.ToTheme(input.Theme), UserID: id.UserIDFromRefID(input.UserID), TeamID: id.TeamIDFromRefID(input.TeamID), - Secret: secret, + Secret: &secret, }) if err != nil { return nil, err diff --git a/internal/adapter/http/user.go b/internal/adapter/http/user.go index 414ca04e..b2d7a0a3 100644 --- a/internal/adapter/http/user.go +++ b/internal/adapter/http/user.go @@ -17,6 +17,31 @@ func NewUserController(usecase interfaces.User) *UserController { } } +type PasswordResetInput struct { + Email string `json:"email"` + Token string `json:"token"` + Password string `json:"password"` +} + +type SignupInput struct { + Sub *string `json:"sub"` + Secret *string `json:"secret"` + UserID *id.UserID `json:"userId"` + TeamID *id.TeamID `json:"teamId"` + Name *string `json:"username"` + Email *string `json:"email"` + Password *string `json:"password"` +} + +type CreateVerificationInput struct { + Email string `json:"email"` +} + +type VerifyUserOutput struct { + UserID string `json:"userId"` + Verified bool `json:"verified"` +} + type CreateUserInput struct { Sub string `json:"sub"` Secret string `json:"secret"` @@ -24,26 +49,63 @@ type CreateUserInput struct { TeamID *id.TeamID `json:"teamId"` } -type CreateUserOutput struct { +type SignupOutput struct { ID string `json:"id"` Name string `json:"name"` Email string `json:"email"` } -func (c *UserController) CreateUser(ctx context.Context, input CreateUserInput) (interface{}, error) { +func (c *UserController) Signup(ctx context.Context, input SignupInput) (interface{}, error) { u, _, err := c.usecase.Signup(ctx, interfaces.SignupParam{ - Sub: input.Sub, - Secret: input.Secret, - UserID: input.UserID, - TeamID: input.TeamID, + Sub: input.Sub, + Secret: input.Secret, + UserID: input.UserID, + TeamID: input.TeamID, + Name: input.Name, + Email: input.Email, + Password: input.Password, }) if err != nil { return nil, err } + if err := c.usecase.CreateVerification(ctx, *input.Email); err != nil { + return nil, err + } - return CreateUserOutput{ + return SignupOutput{ ID: u.ID().String(), Name: u.Name(), Email: u.Email(), }, nil } + +func (c *UserController) CreateVerification(ctx context.Context, input CreateVerificationInput) error { + if err := c.usecase.CreateVerification(ctx, input.Email); err != nil { + return err + } + return nil +} + +func (c *UserController) VerifyUser(ctx context.Context, code string) (interface{}, error) { + u, err := c.usecase.VerifyUser(ctx, code) + if err != nil { + return nil, err + } + return VerifyUserOutput{ + UserID: u.ID().String(), + Verified: u.Verification().IsVerified(), + }, nil +} + +func (c *UserController) StartPasswordReset(ctx context.Context, input PasswordResetInput) error { + err := c.usecase.StartPasswordReset(ctx, input.Email) + if err != nil { + return err + } + + return nil +} + +func (c *UserController) PasswordReset(ctx context.Context, input PasswordResetInput) error { + return c.usecase.PasswordReset(ctx, input.Password, input.Token) +} diff --git a/internal/app/app.go b/internal/app/app.go index 864c017a..58afca7f 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1,6 +1,7 @@ package app import ( + "context" "errors" "io/fs" "net/http" @@ -16,7 +17,7 @@ import ( "go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho" ) -func initEcho(cfg *ServerConfig) *echo.Echo { +func initEcho(ctx context.Context, cfg *ServerConfig) *echo.Echo { if cfg.Config == nil { log.Fatalln("ServerConfig.Config is nil") } @@ -68,27 +69,34 @@ func initEcho(cfg *ServerConfig) *echo.Echo { SignupSecret: cfg.Config.SignupSecret, PublishedIndexHTML: publishedIndexHTML, PublishedIndexURL: cfg.Config.Published.IndexURL, + AuthSrvUIDomain: cfg.Config.AuthSrv.UIDomain, }) e.Use(UsecaseMiddleware(&usecases)) + // auth srv + auth := e.Group("") + authEndPoints(ctx, e, auth, cfg) + // apis api := e.Group("/api") api.GET("/ping", Ping()) api.POST("/signup", Signup()) + api.POST("/signup/verify", StartSignupVerify()) + api.POST("/signup/verify/:code", SignupVerify()) + api.POST("/password-reset", PasswordReset()) api.GET("/published/:name", PublishedMetadata()) api.GET("/published_data/:name", PublishedData()) privateApi := api.Group("") - jwks := &JwksSyncOnce{} - authRequired(privateApi, jwks, cfg) + authRequired(privateApi, cfg) graphqlAPI(e, privateApi, cfg) privateAPI(e, privateApi, cfg.Repos) published := e.Group("/p") - auth := PublishedAuthMiddleware() - published.GET("/:name/data.json", PublishedData(), auth) - published.GET("/:name/", PublishedIndex(), auth) + publishedAuth := PublishedAuthMiddleware() + published.GET("/:name/data.json", PublishedData(), publishedAuth) + published.GET("/:name/", PublishedIndex(), publishedAuth) serveFiles(e, cfg.Gateways.File) web(e, cfg.Config.Web, cfg.Config.Auth0) @@ -113,9 +121,9 @@ func errorHandler(next func(error, echo.Context)) func(error, echo.Context) { } } -func authRequired(g *echo.Group, jwks Jwks, cfg *ServerConfig) { - g.Use(jwtEchoMiddleware(jwks, cfg)) - g.Use(parseJwtMiddleware(cfg)) +func authRequired(g *echo.Group, cfg *ServerConfig) { + g.Use(jwtEchoMiddleware(cfg)) + g.Use(parseJwtMiddleware()) g.Use(authMiddleware(cfg)) } diff --git a/internal/app/auth.go b/internal/app/auth_client.go similarity index 100% rename from internal/app/auth.go rename to internal/app/auth_client.go diff --git a/internal/app/auth_server.go b/internal/app/auth_server.go new file mode 100644 index 00000000..bdb895da --- /dev/null +++ b/internal/app/auth_server.go @@ -0,0 +1,249 @@ +package app + +import ( + "context" + "crypto/sha256" + "encoding/json" + "net/http" + "net/url" + "strings" + + "github.com/caos/oidc/pkg/op" + "github.com/golang/gddo/httputil/header" + "github.com/gorilla/mux" + "github.com/labstack/echo/v4" + "github.com/reearth/reearth-backend/internal/usecase/interactor" + "github.com/reearth/reearth-backend/internal/usecase/interfaces" +) + +var ( + loginEndpoint = "api/login" + logoutEndpoint = "api/logout" + jwksEndpoint = ".well-known/jwks.json" +) + +func authEndPoints(ctx context.Context, e *echo.Echo, r *echo.Group, cfg *ServerConfig) { + + userUsecase := interactor.NewUser(cfg.Repos, cfg.Gateways, cfg.Config.SignupSecret, cfg.Config.AuthSrv.UIDomain) + + domain, err := url.Parse(cfg.Config.AuthSrv.Domain) + if err != nil { + panic("not valid auth domain") + } + domain.Path = "/" + + config := &op.Config{ + Issuer: domain.String(), + CryptoKey: sha256.Sum256([]byte(cfg.Config.AuthSrv.Key)), + GrantTypeRefreshToken: true, + } + + var dn *interactor.AuthDNConfig = nil + if cfg.Config.AuthSrv.DN != nil { + dn = &interactor.AuthDNConfig{ + CommonName: cfg.Config.AuthSrv.DN.CN, + Organization: cfg.Config.AuthSrv.DN.O, + OrganizationalUnit: cfg.Config.AuthSrv.DN.OU, + Country: cfg.Config.AuthSrv.DN.C, + Locality: cfg.Config.AuthSrv.DN.L, + Province: cfg.Config.AuthSrv.DN.ST, + StreetAddress: cfg.Config.AuthSrv.DN.Street, + PostalCode: cfg.Config.AuthSrv.DN.PostalCode, + } + } + + storage, err := interactor.NewAuthStorage( + ctx, + &interactor.StorageConfig{ + Domain: domain.String(), + Debug: cfg.Debug, + DN: dn, + }, + cfg.Repos.AuthRequest, + cfg.Repos.Config, + userUsecase.GetUserBySubject, + ) + if err != nil { + e.Logger.Fatal(err) + } + + handler, err := op.NewOpenIDProvider( + ctx, + config, + storage, + op.WithHttpInterceptors(jsonToFormHandler()), + op.WithHttpInterceptors(setURLVarsHandler()), + op.WithCustomEndSessionEndpoint(op.NewEndpoint(logoutEndpoint)), + op.WithCustomKeysEndpoint(op.NewEndpoint(jwksEndpoint)), + ) + if err != nil { + e.Logger.Fatal(err) + } + + router := handler.HttpHandler().(*mux.Router) + + if err := router.Walk(muxToEchoMapper(r)); err != nil { + e.Logger.Fatal(err) + } + + // Actual login endpoint + r.POST(loginEndpoint, login(ctx, cfg, storage, userUsecase)) + + r.GET(logoutEndpoint, logout()) + + // used for auth0/auth0-react; the logout endpoint URL is hard-coded + // can be removed when the mentioned issue is solved + // https://github.com/auth0/auth0-spa-js/issues/845 + r.GET("v2/logout", logout()) + +} + +func setURLVarsHandler() func(handler http.Handler) http.Handler { + return func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/authorize/callback" { + handler.ServeHTTP(w, r) + return + } + + r2 := mux.SetURLVars(r, map[string]string{"id": r.URL.Query().Get("id")}) + handler.ServeHTTP(w, r2) + }) + } +} + +func jsonToFormHandler() func(handler http.Handler) http.Handler { + return func(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/oauth/token" { + handler.ServeHTTP(w, r) + return + } + + if r.Header.Get("Content-Type") != "" { + value, _ := header.ParseValueAndParams(r.Header, "Content-Type") + if value != "application/json" { + // Content-Type header is not application/json + handler.ServeHTTP(w, r) + return + } + } + + if err := r.ParseForm(); err != nil { + return + } + + var result map[string]string + + if err := json.NewDecoder(r.Body).Decode(&result); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + for key, value := range result { + r.Form.Set(key, value) + } + + handler.ServeHTTP(w, r) + }) + } +} + +func muxToEchoMapper(r *echo.Group) func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { + return func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { + path, err := route.GetPathTemplate() + if err != nil { + return err + } + + methods, err := route.GetMethods() + if err != nil { + r.Any(path, echo.WrapHandler(route.GetHandler())) + return nil + } + + for _, method := range methods { + r.Add(method, path, echo.WrapHandler(route.GetHandler())) + } + + return nil + } +} + +type loginForm struct { + Email string `json:"username" form:"username"` + Password string `json:"password" form:"password"` + AuthRequestID string `json:"id" form:"id"` +} + +func login(ctx context.Context, cfg *ServerConfig, storage op.Storage, userUsecase interfaces.User) func(ctx echo.Context) error { + return func(ec echo.Context) error { + + request := new(loginForm) + err := ec.Bind(request) + if err != nil { + ec.Logger().Error("filed to parse login request") + return err + } + + authRequest, err := storage.AuthRequestByID(ctx, request.AuthRequestID) + if err != nil { + ec.Logger().Error("filed to parse login request") + return err + } + + if len(request.Email) == 0 || len(request.Password) == 0 { + ec.Logger().Error("credentials are not provided") + return ec.Redirect(http.StatusFound, redirectURL(authRequest.GetRedirectURI(), !cfg.Debug, request.AuthRequestID, "invalid login")) + } + + // check user credentials from db + user, err := userUsecase.GetUserByCredentials(ctx, interfaces.GetUserByCredentials{ + Email: request.Email, + Password: request.Password, + }) + if err != nil { + ec.Logger().Error("wrong credentials!") + return ec.Redirect(http.StatusFound, redirectURL(authRequest.GetRedirectURI(), !cfg.Debug, request.AuthRequestID, "invalid login")) + } + + // Complete the auth request && set the subject + err = storage.(*interactor.AuthStorage).CompleteAuthRequest(ctx, request.AuthRequestID, user.GetAuthByProvider("reearth").Sub) + if err != nil { + ec.Logger().Error("failed to complete the auth request !") + return ec.Redirect(http.StatusFound, redirectURL(authRequest.GetRedirectURI(), !cfg.Debug, request.AuthRequestID, "invalid login")) + } + + return ec.Redirect(http.StatusFound, "/authorize/callback?id="+request.AuthRequestID) + } +} + +func logout() func(ec echo.Context) error { + return func(ec echo.Context) error { + u := ec.QueryParam("returnTo") + return ec.Redirect(http.StatusTemporaryRedirect, u) + } +} + +func redirectURL(domain string, secure bool, requestID string, error string) string { + domain = strings.TrimPrefix(domain, "http://") + domain = strings.TrimPrefix(domain, "https://") + + schema := "http" + if secure { + schema = "https" + } + + u := url.URL{ + Scheme: schema, + Host: domain, + Path: "login", + } + + queryValues := u.Query() + queryValues.Set("id", requestID) + queryValues.Set("error", error) + u.RawQuery = queryValues.Encode() + + return u.String() +} diff --git a/internal/app/config.go b/internal/app/config.go index d6c7e1a5..8ae478a8 100644 --- a/internal/app/config.go +++ b/internal/app/config.go @@ -1,6 +1,7 @@ package app import ( + "encoding/json" "fmt" "net/url" "os" @@ -18,6 +19,11 @@ type Config struct { Dev bool DB string `default:"mongodb://localhost"` Auth0 Auth0Config + AuthSrv AuthSrvConfig + Auth AuthConfigs + Mailer string + SMTP SMTPConfig + SendGrid SendGridConfig GraphQL GraphQLConfig Published PublishedConfig GCPProject string `envconfig:"GOOGLE_CLOUD_PROJECT"` @@ -39,6 +45,24 @@ type Auth0Config struct { WebClientID string } +type AuthSrvConfig struct { + Domain string `default:"http://localhost:8080"` + UIDomain string `default:"http://localhost:3000"` + Key string + DN *AuthDNConfig +} + +type AuthDNConfig struct { + CN string + O []string + OU []string + C []string + L []string + ST []string + Street []string + PostalCode []string +} + type GraphQLConfig struct { ComplexityLimit int `default:"6000"` } @@ -52,6 +76,20 @@ type GCSConfig struct { PublicationCacheControl string } +type SendGridConfig struct { + Email string + Name string + API string +} + +type SMTPConfig struct { + Host string + Port string + SMTPUsername string + Email string + Password string +} + func ReadConfig(debug bool) (*Config, error) { // load .env if err := godotenv.Load(".env"); err != nil && !os.IsNotExist(err) { @@ -80,3 +118,36 @@ func (c Config) Print() string { } return s } + +type AuthConfig struct { + ISS string + AUD []string + ALG *string + TTL *int +} + +type AuthConfigs []AuthConfig + +// Decode is a custom decoder for AuthConfigs +func (ipd *AuthConfigs) Decode(value string) error { + var providers []AuthConfig + + err := json.Unmarshal([]byte(value), &providers) + if err != nil { + return fmt.Errorf("invalid identity providers json: %w", err) + } + + for i := range providers { + if providers[i].TTL == nil { + providers[i].TTL = new(int) + *providers[i].TTL = 5 + } + if providers[i].ALG == nil { + providers[i].ALG = new(string) + *providers[i].ALG = "RS256" + } + } + + *ipd = providers + return nil +} diff --git a/internal/app/jwt.go b/internal/app/jwt.go index 1cececd8..cd51e5e6 100644 --- a/internal/app/jwt.go +++ b/internal/app/jwt.go @@ -2,17 +2,13 @@ package app import ( "context" - "encoding/json" - "errors" - "net/http" - "strings" - "sync" - - jwtmiddleware "github.com/auth0/go-jwt-middleware" - // TODO: github.com/form3tech-oss/jwt-go is decrepated. - // Alternative is https://github.com/golang-jwt/jwt, but go-jwt-middleware still uses github.comform3tech-oss/jwt-go - // See also https://github.com/auth0/go-jwt-middleware/issues/73 - "github.com/form3tech-oss/jwt-go" + "fmt" + "net/url" + "time" + + jwtmiddleware "github.com/auth0/go-jwt-middleware/v2" + "github.com/auth0/go-jwt-middleware/v2/jwks" + "github.com/auth0/go-jwt-middleware/v2/validator" "github.com/labstack/echo/v4" "github.com/reearth/reearth-backend/pkg/log" ) @@ -20,120 +16,77 @@ import ( type contextKey string const ( - userProfileKey = "auth0_user" - debugUserHeader = "X-Reearth-Debug-User" - contextAuth0AccessToken contextKey = "auth0AccessToken" - contextAuth0Sub contextKey = "auth0Sub" - contextUser contextKey = "reearth_user" + debugUserHeader = "X-Reearth-Debug-User" + contextAuth0Sub contextKey = "auth0Sub" + contextUser contextKey = "reearth_user" ) -type JSONWebKeys struct { - Kty string `json:"kty"` - Kid string `json:"kid"` - Use string `json:"use"` - N string `json:"n"` - E string `json:"e"` - X5c []string `json:"x5c"` -} +type MultiValidator []*validator.Validator -type Jwks interface { - GetJwks(string) ([]JSONWebKeys, error) -} +func NewMultiValidator(providers []AuthConfig) (MultiValidator, error) { + validators := make([]*validator.Validator, 0, len(providers)) + for _, p := range providers { -type JwksSyncOnce struct { - jwks []JSONWebKeys - once sync.Once -} - -func (jso *JwksSyncOnce) GetJwks(publicKeyURL string) ([]JSONWebKeys, error) { - var err error - jso.once.Do(func() { - jso.jwks, err = fetchJwks(publicKeyURL) - }) - - if err != nil { - return nil, err - } + issuerURL, err := url.Parse(p.ISS) + if err != nil { + return nil, fmt.Errorf("failed to parse the issuer url: %w", err) + } - return jso.jwks, nil -} + provider := jwks.NewCachingProvider(issuerURL, time.Duration(*p.TTL)*time.Minute) -func fetchJwks(publicKeyURL string) ([]JSONWebKeys, error) { - resp, err := http.Get(publicKeyURL) - var res struct { - Jwks []JSONWebKeys `json:"keys"` - } + algorithm := validator.SignatureAlgorithm(*p.ALG) - if err != nil { - return nil, err + v, err := validator.New( + provider.KeyFunc, + algorithm, + p.ISS, + p.AUD, + ) + if err != nil { + return nil, err + } + validators = append(validators, v) } - defer func() { - _ = resp.Body.Close() - }() - - err = json.NewDecoder(resp.Body).Decode(&res) + return validators, nil +} - if err != nil { - return nil, err +// ValidateToken Trys to validate the token with each validator +// NOTE: the last validation error only is returned +func (mv MultiValidator) ValidateToken(ctx context.Context, tokenString string) (res interface{}, err error) { + for _, v := range mv { + res, err = v.ValidateToken(ctx, tokenString) + if err == nil { + return + } } - - return res.Jwks, nil + return } -func getPemCert(token *jwt.Token, publicKeyURL string, jwks Jwks) (string, error) { - cert := "" - keys, err := jwks.GetJwks(publicKeyURL) +// Validate the access token and inject the user clams into ctx +func jwtEchoMiddleware(cfg *ServerConfig) echo.MiddlewareFunc { + jwtValidator, err := NewMultiValidator(cfg.Config.Auth) if err != nil { - return cert, err + log.Fatalf("failed to set up the validator: %v", err) } - for k := range keys { - if token.Header["kid"] == keys[k].Kid { - cert = "-----BEGIN CERTIFICATE-----\n" + keys[k].X5c[0] + "\n-----END CERTIFICATE-----" - } - } + middleware := jwtmiddleware.New(jwtValidator.ValidateToken) - if cert == "" { - err := errors.New("unable to find appropriate key") - return cert, err - } - - return cert, nil + return echo.WrapMiddleware(middleware.CheckJWT) } -func parseJwtMiddleware(cfg *ServerConfig) echo.MiddlewareFunc { - iss := urlFromDomain(cfg.Config.Auth0.Domain) - aud := cfg.Config.Auth0.Audience - +// load claim from ctx and inject the user sub into ctx +func parseJwtMiddleware() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { req := c.Request() ctx := req.Context() - token := ctx.Value(userProfileKey) - if userProfile, ok := token.(*jwt.Token); ok { - claims := userProfile.Claims.(jwt.MapClaims) - - // Verify 'iss' claim - checkIss := claims.VerifyIssuer(iss, false) - if !checkIss { - return errorResponse(c, "invalid issuer") - } - - // Verify 'aud' claim - if !verifyAudience(claims, aud) { - return errorResponse(c, "invalid audience") - } + rawClaims := ctx.Value(jwtmiddleware.ContextKey{}) + if claims, ok := rawClaims.(*validator.ValidatedClaims); ok { // attach sub and access token to context - if sub, ok := claims["sub"].(string); ok { - ctx = context.WithValue(ctx, contextAuth0Sub, sub) - } - if user, ok := claims["https://reearth.io/user_id"].(string); ok { - ctx = context.WithValue(ctx, contextUser, user) - } - ctx = context.WithValue(ctx, contextAuth0AccessToken, userProfile.Raw) + ctx = context.WithValue(ctx, contextAuth0Sub, claims.RegisteredClaims.Subject) } c.SetRequest(req.WithContext(ctx)) @@ -141,84 +94,3 @@ func parseJwtMiddleware(cfg *ServerConfig) echo.MiddlewareFunc { } } } - -func jwtEchoMiddleware(jwks Jwks, cfg *ServerConfig) echo.MiddlewareFunc { - jwksURL := urlFromDomain(cfg.Config.Auth0.Domain) + ".well-known/jwks.json" - - jwtMiddleware := jwtmiddleware.New(jwtmiddleware.Options{ - CredentialsOptional: cfg.Debug, - UserProperty: userProfileKey, - SigningMethod: jwt.SigningMethodRS256, - // Make jwtmiddleware return an error object by not writing ErrorHandler to ResponseWriter - ErrorHandler: func(w http.ResponseWriter, req *http.Request, err string) {}, - ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) { - cert, err := getPemCert(token, jwksURL, jwks) - if err != nil { - log.Errorf("jwt: %s", err) - return nil, err - } - result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert)) - return result, nil - }, - }) - - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - err := jwtMiddleware.CheckJWT(c.Response(), c.Request()) - if err != nil { - return errorResponse(c, err.Error()) - } - return next(c) - } - } -} - -func urlFromDomain(path string) string { - if path == "" { - return path - } - if !strings.HasPrefix(path, "http://") && !strings.HasPrefix(path, "https://") { - path = "https://" + path - } - if path[len(path)-1] != '/' { - path += "/" - } - return path -} - -// WORKAROUND: golang-jwt/jwt-go supports multiple audiences, but go-jwt-middleware still uses github.comform3tech-oss/jwt-go -func verifyAudience(claims jwt.MapClaims, aud string) bool { - if aud == "" { - return true - } - - auds, ok := claims["aud"].([]string) - if !ok { - auds2, ok := claims["aud"].([]interface{}) - if ok { - for _, a := range auds2 { - if aa, ok := a.(string); ok { - auds = append(auds, aa) - } - } - } else { - a, ok := claims["aud"].(string) - if !ok || a == "" { - return false - } - auds = append(auds, a) - } - } - - for _, a := range auds { - if jwt.MapClaims(map[string]interface{}{"aud": a}).VerifyAudience(aud, true) { - return true - } - } - return false -} - -func errorResponse(c echo.Context, err string) error { - res := map[string]string{"error": err} - return c.JSON(http.StatusUnauthorized, res) -} diff --git a/internal/app/main.go b/internal/app/main.go index bdce1744..4750ab59 100644 --- a/internal/app/main.go +++ b/internal/app/main.go @@ -40,7 +40,7 @@ func Start(debug bool, version string) { repos, gateways := initReposAndGateways(ctx, conf, debug) // Start web server - NewServer(&ServerConfig{ + NewServer(ctx, &ServerConfig{ Config: conf, Debug: debug, Repos: repos, @@ -60,7 +60,7 @@ type ServerConfig struct { Gateways *gateway.Container } -func NewServer(cfg *ServerConfig) *WebServer { +func NewServer(ctx context.Context, cfg *ServerConfig) *WebServer { port := cfg.Config.Port if port == "" { port = "8080" @@ -75,7 +75,7 @@ func NewServer(cfg *ServerConfig) *WebServer { address: address, } - w.appServer = initEcho(cfg) + w.appServer = initEcho(ctx, cfg) return w } diff --git a/internal/app/public.go b/internal/app/public.go index 174c3862..e1624f0d 100644 --- a/internal/app/public.go +++ b/internal/app/public.go @@ -23,7 +23,7 @@ func Ping() echo.HandlerFunc { func Signup() echo.HandlerFunc { return func(c echo.Context) error { - var inp http1.CreateUserInput + var inp http1.SignupInput if err := c.Bind(&inp); err != nil { return &echo.HTTPError{Code: http.StatusBadRequest, Message: fmt.Errorf("failed to parse request body: %w", err)} } @@ -31,7 +31,72 @@ func Signup() echo.HandlerFunc { uc := adapter.Usecases(c.Request().Context()) controller := http1.NewUserController(uc.User) - output, err := controller.CreateUser(c.Request().Context(), inp) + output, err := controller.Signup(c.Request().Context(), inp) + if err != nil { + return err + } + + return c.JSON(http.StatusOK, output) + } +} + +func PasswordReset() echo.HandlerFunc { + return func(c echo.Context) error { + var inp http1.PasswordResetInput + if err := c.Bind(&inp); err != nil { + return err + } + + uc := adapter.Usecases(c.Request().Context()) + controller := http1.NewUserController(uc.User) + + if len(inp.Email) > 0 { + if err := controller.StartPasswordReset(c.Request().Context(), inp); err != nil { + return err + } + return c.JSON(http.StatusOK, true) + } + + if len(inp.Token) > 0 && len(inp.Password) > 0 { + if err := controller.PasswordReset(c.Request().Context(), inp); err != nil { + return err + } + return c.JSON(http.StatusOK, true) + } + + return &echo.HTTPError{Code: http.StatusBadRequest, Message: "Bad reset password request"} + } +} + +func StartSignupVerify() echo.HandlerFunc { + return func(c echo.Context) error { + var inp http1.CreateVerificationInput + if err := c.Bind(&inp); err != nil { + return &echo.HTTPError{Code: http.StatusBadRequest, Message: fmt.Errorf("failed to parse request body: %w", err)} + } + + uc := adapter.Usecases(c.Request().Context()) + controller := http1.NewUserController(uc.User) + + if err := controller.CreateVerification(c.Request().Context(), inp); err != nil { + return err + } + + return c.NoContent(http.StatusOK) + } +} + +func SignupVerify() echo.HandlerFunc { + return func(c echo.Context) error { + code := c.Param("code") + if len(code) == 0 { + return echo.ErrBadRequest + } + + uc := adapter.Usecases(c.Request().Context()) + controller := http1.NewUserController(uc.User) + + output, err := controller.VerifyUser(c.Request().Context(), code) if err != nil { return err } diff --git a/internal/app/repo.go b/internal/app/repo.go index 53e5e311..fdff03a0 100644 --- a/internal/app/repo.go +++ b/internal/app/repo.go @@ -5,6 +5,8 @@ import ( "fmt" "time" + "github.com/reearth/reearth-backend/internal/infrastructure/mailer" + "github.com/reearth/reearth-backend/internal/infrastructure/github" "github.com/reearth/reearth-backend/internal/infrastructure/google" "github.com/spf13/afero" @@ -70,6 +72,9 @@ func initReposAndGateways(ctx context.Context, conf *Config, debug bool) (*repo. // google gateways.Google = google.NewGoogle() + // SMTP Mailer + gateways.Mailer = initMailer(conf) + // release lock of all scenes if err := repos.SceneLock.ReleaseAllLock(context.Background()); err != nil { log.Fatalln(fmt.Sprintf("repo initialization error: %+v", err)) @@ -77,3 +82,12 @@ func initReposAndGateways(ctx context.Context, conf *Config, debug bool) (*repo. return repos, gateways } + +func initMailer(conf *Config) gateway.Mailer { + if conf.Mailer == "sendgrid" { + return mailer.NewWithSendGrid(conf.SendGrid.Name, conf.SendGrid.Email, conf.SendGrid.API) + } else if conf.Mailer == "smtp" { + return mailer.NewWithSMTP(conf.SMTP.Host, conf.SMTP.Port, conf.SMTP.SMTPUsername, conf.SMTP.Email, conf.SMTP.Password) + } + return nil +} diff --git a/internal/infrastructure/mailer/sendgrid.go b/internal/infrastructure/mailer/sendgrid.go new file mode 100644 index 00000000..cd078d0f --- /dev/null +++ b/internal/infrastructure/mailer/sendgrid.go @@ -0,0 +1,32 @@ +package mailer + +import ( + "github.com/reearth/reearth-backend/internal/usecase/gateway" + "github.com/sendgrid/sendgrid-go" + "github.com/sendgrid/sendgrid-go/helpers/mail" +) + +type sendgridMailer struct { + api string + // sender data + name string + email string +} + +func NewWithSendGrid(senderName, senderEmail, api string) gateway.Mailer { + return &sendgridMailer{ + name: senderName, + email: senderEmail, + api: api, + } +} + +func (m *sendgridMailer) SendMail(to []gateway.Contact, subject, plainContent, htmlContent string) error { + contact := to[0] + sender := mail.NewEmail(m.name, m.email) + receiver := mail.NewEmail(contact.Name, contact.Email) + message := mail.NewSingleEmail(sender, subject, receiver, plainContent, htmlContent) + client := sendgrid.NewSendClient(m.api) + _, err := client.Send(message) + return err +} diff --git a/internal/infrastructure/mailer/sendgrid_test.go b/internal/infrastructure/mailer/sendgrid_test.go new file mode 100644 index 00000000..2a7251ed --- /dev/null +++ b/internal/infrastructure/mailer/sendgrid_test.go @@ -0,0 +1,41 @@ +package mailer + +import ( + "testing" + + "github.com/reearth/reearth-backend/internal/usecase/gateway" + "github.com/stretchr/testify/assert" +) + +func TestNewWithSendGrid(t *testing.T) { + type args struct { + senderName string + senderEmail string + api string + } + tests := []struct { + name string + args args + want gateway.Mailer + }{ + { + name: "should create a sendGrid mailer", + args: args{ + senderName: "test sender", + senderEmail: "sender@test.com", + api: "TEST_API", + }, + want: &sendgridMailer{ + api: "TEST_API", + name: "test sender", + email: "sender@test.com", + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(tt *testing.T) { + got := NewWithSendGrid(tc.args.senderName, tc.args.senderEmail, tc.args.api) + assert.Equal(tt, tc.want, got) + }) + } +} diff --git a/internal/infrastructure/mailer/smtp.go b/internal/infrastructure/mailer/smtp.go new file mode 100644 index 00000000..e54127d7 --- /dev/null +++ b/internal/infrastructure/mailer/smtp.go @@ -0,0 +1,121 @@ +package mailer + +import ( + "bytes" + "errors" + "fmt" + "io" + "mime/multipart" + "net/mail" + "net/smtp" + "net/textproto" + "strings" + + "github.com/reearth/reearth-backend/internal/usecase/gateway" +) + +type smtpMailer struct { + host string + port string + email string + username string + password string +} + +type message struct { + to []string + from string + subject string + plainContent string + htmlContent string +} + +func (m *message) encodeContent() (string, error) { + buf := bytes.NewBuffer(nil) + writer := multipart.NewWriter(buf) + boundary := writer.Boundary() + + altBuffer, err := writer.CreatePart(textproto.MIMEHeader{"Content-Type": {"multipart/alternative; boundary=" + boundary}}) + if err != nil { + return "", err + } + altWriter := multipart.NewWriter(altBuffer) + err = altWriter.SetBoundary(boundary) + if err != nil { + return "", err + } + var content io.Writer + content, err = altWriter.CreatePart(textproto.MIMEHeader{"Content-Type": {"text/plain"}}) + if err != nil { + return "", err + } + + _, err = content.Write([]byte(m.plainContent + "\r\n\r\n")) + if err != nil { + return "", err + } + content, err = altWriter.CreatePart(textproto.MIMEHeader{"Content-Type": {"text/html"}}) + if err != nil { + return "", err + } + _, err = content.Write([]byte(m.htmlContent + "\r\n")) + if err != nil { + return "", err + } + _ = altWriter.Close() + return buf.String(), nil +} + +func (m *message) encodeMessage() ([]byte, error) { + buf := bytes.NewBuffer(nil) + buf.WriteString(fmt.Sprintf("Subject: %s\n", m.subject)) + buf.WriteString(fmt.Sprintf("From: %s\n", m.from)) + buf.WriteString(fmt.Sprintf("To: %s\n", strings.Join(m.to, ","))) + content, err := m.encodeContent() + if err != nil { + return nil, err + } + buf.WriteString(content) + + return buf.Bytes(), nil +} + +func NewWithSMTP(host, port, username, email, password string) gateway.Mailer { + return &smtpMailer{ + host: host, + port: port, + username: username, + email: email, + password: password, + } +} + +func (m *smtpMailer) SendMail(to []gateway.Contact, subject, plainContent, htmlContent string) error { + emails := make([]string, 0, len(to)) + for _, c := range to { + _, err := mail.ParseAddress(c.Email) + if err != nil { + return fmt.Errorf("invalid email %s", c.Email) + } + emails = append(emails, c.Email) + } + + msg := &message{ + to: emails, + from: m.email, + subject: subject, + plainContent: plainContent, + htmlContent: htmlContent, + } + + encodedMsg, err := msg.encodeMessage() + if err != nil { + return err + } + + auth := smtp.PlainAuth("", m.username, m.password, m.host) + if len(m.host) == 0 { + return errors.New("invalid smtp url") + } + return smtp.SendMail(m.host+":"+m.port, auth, m.email, emails, encodedMsg) +} diff --git a/internal/infrastructure/mailer/smtp_test.go b/internal/infrastructure/mailer/smtp_test.go new file mode 100644 index 00000000..ed217d9c --- /dev/null +++ b/internal/infrastructure/mailer/smtp_test.go @@ -0,0 +1,144 @@ +package mailer + +import ( + "strings" + "testing" + + "github.com/reearth/reearth-backend/internal/usecase/gateway" + + "github.com/stretchr/testify/assert" +) + +func TestNewWithSMTP(t *testing.T) { + type args struct { + host string + port string + email string + username string + password string + } + tests := []struct { + name string + args args + want gateway.Mailer + }{ + { + name: "should create mailer with given args", + args: args{ + host: "x.x.x", + port: "8080", + username: "foo", + email: "xxx@test.com", + password: "foo.pass", + }, + want: &smtpMailer{ + host: "x.x.x", + port: "8080", + username: "foo", + email: "xxx@test.com", + password: "foo.pass", + }, + }, + } + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + got := NewWithSMTP(tc.args.host, tc.args.port, tc.args.username, tc.args.email, tc.args.password) + assert.Equal(tt, tc.want, got) + }) + } +} + +func Test_message_encodeContent(t *testing.T) { + // subject and receiver email are not needed for encoding the content + tests := []struct { + name string + plainContent string + htmlContent string + wantContentTypes []string + wantPlain bool + wantHtml bool + wantErr bool + }{ + { + name: "should return encoded message content", + plainContent: "plain content", + htmlContent: `

html content

`, + wantContentTypes: []string{ + "Content-Type: multipart/alternative", + "Content-Type: text/plain", + "Content-Type: text/html", + }, + wantPlain: true, + wantHtml: true, + wantErr: false, + }, + } + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + m := &message{ + plainContent: tc.plainContent, + htmlContent: tc.htmlContent, + } + got, err := m.encodeContent() + gotTypes := true + for _, ct := range tc.wantContentTypes { + gotTypes = strings.Contains(got, ct) && gotTypes + } + assert.Equal(tt, tc.wantErr, err != nil) + assert.True(tt, gotTypes) + assert.Equal(tt, tc.wantPlain, strings.Contains(got, tc.plainContent)) + assert.Equal(tt, tc.wantHtml, strings.Contains(got, tc.htmlContent)) + }) + } +} + +func Test_message_encodeMessage(t *testing.T) { + tests := []struct { + name string + to []string + subject string + plainContent string + htmlContent string + wantTo bool + wantSubject bool + wantPlain bool + wantHtml bool + wantErr bool + }{ + { + name: "should return encoded message", + to: []string{"someone@email.com"}, + subject: "test", + plainContent: "plain content", + htmlContent: `

html content

`, + wantTo: true, + wantSubject: true, + wantPlain: true, + wantHtml: true, + wantErr: false, + }, + } + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + m := &message{ + to: []string{"someone@email.com"}, + subject: "test", + plainContent: tc.plainContent, + htmlContent: tc.htmlContent, + } + got, err := m.encodeMessage() + str := string(got) + assert.Equal(tt, tc.wantErr, err != nil) + assert.Equal(tt, tc.wantSubject, strings.Contains(str, tc.subject)) + assert.Equal(tt, tc.wantTo, strings.Contains(str, tc.to[0])) + assert.Equal(tt, tc.wantPlain, strings.Contains(str, tc.plainContent)) + assert.Equal(tt, tc.wantHtml, strings.Contains(str, tc.htmlContent)) + }) + } +} diff --git a/internal/infrastructure/memory/auth_request.go b/internal/infrastructure/memory/auth_request.go new file mode 100644 index 00000000..daabf8c6 --- /dev/null +++ b/internal/infrastructure/memory/auth_request.go @@ -0,0 +1,75 @@ +package memory + +import ( + "context" + "sync" + + "github.com/reearth/reearth-backend/internal/usecase/repo" + "github.com/reearth/reearth-backend/pkg/auth" + "github.com/reearth/reearth-backend/pkg/id" + "github.com/reearth/reearth-backend/pkg/rerror" +) + +type AuthRequest struct { + lock sync.Mutex + data map[id.AuthRequestID]auth.Request +} + +func NewAuthRequest() repo.AuthRequest { + return &AuthRequest{ + data: map[id.AuthRequestID]auth.Request{}, + } +} + +func (r *AuthRequest) FindByID(_ context.Context, id id.AuthRequestID) (*auth.Request, error) { + r.lock.Lock() + defer r.lock.Unlock() + + d, ok := r.data[id] + if ok { + return &d, nil + } + return &auth.Request{}, rerror.ErrNotFound +} + +func (r *AuthRequest) FindByCode(_ context.Context, s string) (*auth.Request, error) { + r.lock.Lock() + defer r.lock.Unlock() + + for _, ar := range r.data { + if ar.GetCode() == s { + return &ar, nil + } + } + + return &auth.Request{}, rerror.ErrNotFound +} + +func (r *AuthRequest) FindBySubject(_ context.Context, s string) (*auth.Request, error) { + r.lock.Lock() + defer r.lock.Unlock() + + for _, ar := range r.data { + if ar.GetSubject() == s { + return &ar, nil + } + } + + return &auth.Request{}, rerror.ErrNotFound +} + +func (r *AuthRequest) Save(_ context.Context, request *auth.Request) error { + r.lock.Lock() + defer r.lock.Unlock() + + r.data[request.ID()] = *request + return nil +} + +func (r *AuthRequest) Remove(_ context.Context, requestID id.AuthRequestID) error { + r.lock.Lock() + defer r.lock.Unlock() + + delete(r.data, requestID) + return nil +} diff --git a/internal/infrastructure/memory/user.go b/internal/infrastructure/memory/user.go index 76d4383f..4024b1ad 100644 --- a/internal/infrastructure/memory/user.go +++ b/internal/infrastructure/memory/user.go @@ -72,6 +72,24 @@ func (r *User) FindByAuth0Sub(ctx context.Context, auth0sub string) (*user.User, return nil, rerror.ErrNotFound } +func (r *User) FindByPasswordResetRequest(ctx context.Context, token string) (*user.User, error) { + r.lock.Lock() + defer r.lock.Unlock() + + if token == "" { + return nil, rerror.ErrInvalidParams + } + + for _, u := range r.data { + pwdReq := u.PasswordReset() + if pwdReq != nil && pwdReq.Token == token { + return &u, nil + } + } + + return nil, rerror.ErrNotFound +} + func (r *User) FindByEmail(ctx context.Context, email string) (*user.User, error) { r.lock.Lock() defer r.lock.Unlock() @@ -113,3 +131,20 @@ func (r *User) Remove(ctx context.Context, user id.UserID) error { delete(r.data, user) return nil } + +func (r *User) FindByVerification(ctx context.Context, code string) (*user.User, error) { + r.lock.Lock() + defer r.lock.Unlock() + + if code == "" { + return nil, rerror.ErrInvalidParams + } + + for _, u := range r.data { + if u.Verification() != nil && u.Verification().Code() == code { + return &u, nil + } + } + + return nil, rerror.ErrNotFound +} diff --git a/internal/infrastructure/mongo/auth_request.go b/internal/infrastructure/mongo/auth_request.go new file mode 100644 index 00000000..247e1f6e --- /dev/null +++ b/internal/infrastructure/mongo/auth_request.go @@ -0,0 +1,64 @@ +package mongo + +import ( + "context" + + "github.com/reearth/reearth-backend/internal/infrastructure/mongo/mongodoc" + "github.com/reearth/reearth-backend/internal/usecase/repo" + "github.com/reearth/reearth-backend/pkg/auth" + "github.com/reearth/reearth-backend/pkg/id" + "github.com/reearth/reearth-backend/pkg/log" + "go.mongodb.org/mongo-driver/bson" +) + +type authRequestRepo struct { + client *mongodoc.ClientCollection +} + +func NewAuthRequest(client *mongodoc.Client) repo.AuthRequest { + r := &authRequestRepo{client: client.WithCollection("authRequest")} + r.init() + return r +} + +func (r *authRequestRepo) init() { + i := r.client.CreateIndex(context.Background(), []string{"code", "subject"}) + if len(i) > 0 { + log.Infof("mongo: %s: index created: %s", "authRequest", i) + } +} + +func (r *authRequestRepo) FindByID(ctx context.Context, id2 id.AuthRequestID) (*auth.Request, error) { + filter := bson.D{{Key: "id", Value: id2.String()}} + return r.findOne(ctx, filter) +} + +func (r *authRequestRepo) FindByCode(ctx context.Context, s string) (*auth.Request, error) { + filter := bson.D{{Key: "code", Value: s}} + return r.findOne(ctx, filter) +} + +func (r *authRequestRepo) FindBySubject(ctx context.Context, s string) (*auth.Request, error) { + filter := bson.D{{Key: "subject", Value: s}} + return r.findOne(ctx, filter) +} + +func (r *authRequestRepo) Save(ctx context.Context, request *auth.Request) error { + doc, id1 := mongodoc.NewAuthRequest(request) + return r.client.SaveOne(ctx, id1, doc) +} + +func (r *authRequestRepo) Remove(ctx context.Context, requestID id.AuthRequestID) error { + return r.client.RemoveOne(ctx, requestID.String()) +} + +func (r *authRequestRepo) findOne(ctx context.Context, filter bson.D) (*auth.Request, error) { + dst := make([]*auth.Request, 0, 1) + c := mongodoc.AuthRequestConsumer{ + Rows: dst, + } + if err := r.client.FindOne(ctx, filter, &c); err != nil { + return nil, err + } + return c.Rows[0], nil +} diff --git a/internal/infrastructure/mongo/container.go b/internal/infrastructure/mongo/container.go index 95b2251c..2dcd699b 100644 --- a/internal/infrastructure/mongo/container.go +++ b/internal/infrastructure/mongo/container.go @@ -21,6 +21,7 @@ func InitRepos(ctx context.Context, c *repo.Container, mc *mongo.Client, databas client := mongodoc.NewClient(databaseName, mc) c.Asset = NewAsset(client) + c.AuthRequest = NewAuthRequest(client) c.Config = NewConfig(client, lock) c.DatasetSchema = NewDatasetSchema(client) c.Dataset = NewDataset(client) diff --git a/internal/infrastructure/mongo/mongodoc/auth_request.go b/internal/infrastructure/mongo/mongodoc/auth_request.go new file mode 100644 index 00000000..245e94c2 --- /dev/null +++ b/internal/infrastructure/mongo/mongodoc/auth_request.go @@ -0,0 +1,116 @@ +package mongodoc + +import ( + "time" + + "github.com/caos/oidc/pkg/oidc" + "github.com/reearth/reearth-backend/pkg/auth" + "github.com/reearth/reearth-backend/pkg/id" + "go.mongodb.org/mongo-driver/bson" +) + +type AuthRequestDocument struct { + ID string + ClientID string + Subject string + Code string + State string + ResponseType string + Scopes []string + Audiences []string + RedirectURI string + Nonce string + CodeChallenge *CodeChallengeDocument + CreatedAt time.Time + AuthorizedAt *time.Time +} + +type CodeChallengeDocument struct { + Challenge string + Method string +} + +type AuthRequestConsumer struct { + Rows []*auth.Request +} + +func (a *AuthRequestConsumer) Consume(raw bson.Raw) error { + if raw == nil { + return nil + } + + var doc AuthRequestDocument + if err := bson.Unmarshal(raw, &doc); err != nil { + return err + } + request, err := doc.Model() + if err != nil { + return err + } + a.Rows = append(a.Rows, request) + return nil +} + +func NewAuthRequest(req *auth.Request) (*AuthRequestDocument, string) { + if req == nil { + return nil, "" + } + reqID := req.GetID() + var cc *CodeChallengeDocument + if req.GetCodeChallenge() != nil { + cc = &CodeChallengeDocument{ + Challenge: req.GetCodeChallenge().Challenge, + Method: string(req.GetCodeChallenge().Method), + } + } + return &AuthRequestDocument{ + ID: reqID, + ClientID: req.GetClientID(), + Subject: req.GetSubject(), + Code: req.GetCode(), + State: req.GetState(), + ResponseType: string(req.GetResponseType()), + Scopes: req.GetScopes(), + Audiences: req.GetAudience(), + RedirectURI: req.GetRedirectURI(), + Nonce: req.GetNonce(), + CodeChallenge: cc, + CreatedAt: req.CreatedAt(), + AuthorizedAt: req.AuthorizedAt(), + }, reqID +} + +func (d *AuthRequestDocument) Model() (*auth.Request, error) { + if d == nil { + return nil, nil + } + + ulid, err := id.AuthRequestIDFrom(d.ID) + if err != nil { + return nil, err + } + + var cc *oidc.CodeChallenge + if d.CodeChallenge != nil { + cc = &oidc.CodeChallenge{ + Challenge: d.CodeChallenge.Challenge, + Method: oidc.CodeChallengeMethod(d.CodeChallenge.Method), + } + } + var req = auth.NewRequest(). + ID(ulid). + ClientID(d.ClientID). + Subject(d.Subject). + Code(d.Code). + State(d.State). + ResponseType(oidc.ResponseType(d.ResponseType)). + Scopes(d.Scopes). + Audiences(d.Audiences). + RedirectURI(d.RedirectURI). + Nonce(d.Nonce). + CodeChallenge(cc). + CreatedAt(d.CreatedAt). + AuthorizedAt(d.AuthorizedAt). + MustBuild() + return req, nil +} diff --git a/internal/infrastructure/mongo/mongodoc/config.go b/internal/infrastructure/mongo/mongodoc/config.go index b5464901..d20ca128 100644 --- a/internal/infrastructure/mongo/mongodoc/config.go +++ b/internal/infrastructure/mongo/mongodoc/config.go @@ -4,19 +4,39 @@ import "github.com/reearth/reearth-backend/pkg/config" type ConfigDocument struct { Migration int64 + Auth *Auth +} + +type Auth struct { + Cert string + Key string } func NewConfig(c config.Config) ConfigDocument { - return ConfigDocument{ + d := ConfigDocument{ Migration: c.Migration, } + if c.Auth != nil { + d.Auth = &Auth{ + Cert: c.Auth.Cert, + Key: c.Auth.Key, + } + } + return d } func (c *ConfigDocument) Model() *config.Config { if c == nil { return &config.Config{} } - return &config.Config{ + m := &config.Config{ Migration: c.Migration, } + if c.Auth != nil { + m.Auth = &config.Auth{ + Cert: c.Auth.Cert, + Key: c.Auth.Key, + } + } + return m } diff --git a/internal/infrastructure/mongo/mongodoc/user.go b/internal/infrastructure/mongo/mongodoc/user.go index de3030c2..2f52da3e 100644 --- a/internal/infrastructure/mongo/mongodoc/user.go +++ b/internal/infrastructure/mongo/mongodoc/user.go @@ -1,6 +1,8 @@ package mongodoc import ( + "time" + "go.mongodb.org/mongo-driver/bson" "github.com/reearth/reearth-backend/pkg/id" @@ -8,15 +10,29 @@ import ( user1 "github.com/reearth/reearth-backend/pkg/user" ) +type PasswordResetDocument struct { + Token string + CreatedAt time.Time +} + type UserDocument struct { - ID string - Name string - Email string - Auth0Sub string - Auth0SubList []string - Team string - Lang string - Theme string + ID string + Name string + Email string + Auth0Sub string + Auth0SubList []string + Team string + Lang string + Theme string + Password []byte + PasswordReset *PasswordResetDocument + Verification *UserVerificationDoc +} + +type UserVerificationDoc struct { + Code string + Expiration time.Time + Verified bool } type UserConsumer struct { @@ -47,15 +63,35 @@ func NewUser(user *user1.User) (*UserDocument, string) { for _, a := range auths { authsdoc = append(authsdoc, a.Sub) } + var v *UserVerificationDoc + if user.Verification() != nil { + v = &UserVerificationDoc{ + Code: user.Verification().Code(), + Expiration: user.Verification().Expiration(), + Verified: user.Verification().IsVerified(), + } + } + pwdReset := user.PasswordReset() + + var pwdResetDoc *PasswordResetDocument + if pwdReset != nil { + pwdResetDoc = &PasswordResetDocument{ + Token: pwdReset.Token, + CreatedAt: pwdReset.CreatedAt, + } + } return &UserDocument{ - ID: id, - Name: user.Name(), - Email: user.Email(), - Auth0SubList: authsdoc, - Team: user.Team().String(), - Lang: user.Lang().String(), - Theme: string(user.Theme()), + ID: id, + Name: user.Name(), + Email: user.Email(), + Auth0SubList: authsdoc, + Team: user.Team().String(), + Lang: user.Lang().String(), + Theme: string(user.Theme()), + Verification: v, + Password: user.Password(), + PasswordReset: pwdResetDoc, }, id } @@ -75,17 +111,36 @@ func (d *UserDocument) Model() (*user1.User, error) { if d.Auth0Sub != "" { auths = append(auths, user.AuthFromAuth0Sub(d.Auth0Sub)) } - user, err := user1.New(). + var v *user.Verification + if d.Verification != nil { + v = user.VerificationFrom(d.Verification.Code, d.Verification.Expiration, d.Verification.Verified) + } + + u, err := user1.New(). ID(uid). Name(d.Name). Email(d.Email). Auths(auths). Team(tid). LangFrom(d.Lang). + Verification(v). + Password(d.Password). + PasswordReset(d.PasswordReset.Model()). Theme(user.Theme(d.Theme)). Build() + if err != nil { return nil, err } - return user, nil + return u, nil +} + +func (d *PasswordResetDocument) Model() *user1.PasswordReset { + if d == nil { + return nil + } + return &user1.PasswordReset{ + Token: d.Token, + CreatedAt: d.CreatedAt, + } } diff --git a/internal/infrastructure/mongo/user.go b/internal/infrastructure/mongo/user.go index d553cb86..b5b542e6 100644 --- a/internal/infrastructure/mongo/user.go +++ b/internal/infrastructure/mongo/user.go @@ -73,6 +73,18 @@ func (r *userRepo) FindByNameOrEmail(ctx context.Context, nameOrEmail string) (* return r.findOne(ctx, filter) } +func (r *userRepo) FindByVerification(ctx context.Context, code string) (*user.User, error) { + filter := bson.D{{Key: "verification.code", Value: code}} + return r.findOne(ctx, filter) +} + +func (r *userRepo) FindByPasswordResetRequest(ctx context.Context, pwdResetToken string) (*user.User, error) { + filter := bson.D{ + {Key: "passwordreset.token", Value: pwdResetToken}, + } + return r.findOne(ctx, filter) +} + func (r *userRepo) Save(ctx context.Context, user *user.User) error { doc, id := mongodoc.NewUser(user) return r.client.SaveOne(ctx, id, doc) diff --git a/internal/usecase/gateway/mailer.go b/internal/usecase/gateway/mailer.go index 27f53085..3784d29f 100644 --- a/internal/usecase/gateway/mailer.go +++ b/internal/usecase/gateway/mailer.go @@ -1,5 +1,10 @@ package gateway +type Contact struct { + Email string + Name string +} + type Mailer interface { - SendMail(to, content string) error + SendMail(toContacts []Contact, subject, plainContent, htmlContent string) error } diff --git a/internal/usecase/interactor/auth.go b/internal/usecase/interactor/auth.go new file mode 100644 index 00000000..5cc6d830 --- /dev/null +++ b/internal/usecase/interactor/auth.go @@ -0,0 +1,403 @@ +package interactor + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "fmt" + "math/big" + "time" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/op" + "github.com/reearth/reearth-backend/internal/usecase/repo" + "github.com/reearth/reearth-backend/pkg/auth" + config2 "github.com/reearth/reearth-backend/pkg/config" + "github.com/reearth/reearth-backend/pkg/id" + "github.com/reearth/reearth-backend/pkg/log" + "github.com/reearth/reearth-backend/pkg/user" + "gopkg.in/square/go-jose.v2" +) + +type AuthStorage struct { + appConfig *StorageConfig + getUserBySubject func(context.Context, string) (*user.User, error) + clients map[string]op.Client + requests repo.AuthRequest + keySet jose.JSONWebKeySet + key *rsa.PrivateKey + sigKey jose.SigningKey +} + +type StorageConfig struct { + Domain string `default:"http://localhost:8080"` + Debug bool + DN *AuthDNConfig +} + +type AuthDNConfig struct { + CommonName string + Organization []string + OrganizationalUnit []string + Country []string + Province []string + Locality []string + StreetAddress []string + PostalCode []string +} + +var dummyName = pkix.Name{ + CommonName: "Dummy company, INC.", + Organization: []string{"Dummy company, INC."}, + OrganizationalUnit: []string{"Dummy OU"}, + Country: []string{"US"}, + Province: []string{"Dummy"}, + Locality: []string{"Dummy locality"}, + StreetAddress: []string{"Dummy street"}, + PostalCode: []string{"1"}, +} + +func NewAuthStorage(ctx context.Context, cfg *StorageConfig, request repo.AuthRequest, config repo.Config, getUserBySubject func(context.Context, string) (*user.User, error)) (op.Storage, error) { + + client := auth.NewLocalClient(cfg.Debug) + + name := dummyName + if cfg.DN != nil { + name = pkix.Name{ + CommonName: cfg.DN.CommonName, + Organization: cfg.DN.Organization, + OrganizationalUnit: cfg.DN.OrganizationalUnit, + Country: cfg.DN.Country, + Province: cfg.DN.Province, + Locality: cfg.DN.Locality, + StreetAddress: cfg.DN.StreetAddress, + PostalCode: cfg.DN.PostalCode, + } + } + c, err := config.LockAndLoad(ctx) + if err != nil { + return nil, fmt.Errorf("Could not load auth config: %w\n", err) + } + defer func() { + if err := config.Unlock(ctx); err != nil { + log.Errorf("auth: Could not release config lock: %s\n", err) + } + }() + + var keyBytes, certBytes []byte + if c.Auth != nil { + keyBytes = []byte(c.Auth.Key) + certBytes = []byte(c.Auth.Cert) + } else { + keyBytes, certBytes, err = generateCert(name) + if err != nil { + return nil, fmt.Errorf("Could not generate raw cert: %w\n", err) + } + c.Auth = &config2.Auth{ + Key: string(keyBytes), + Cert: string(certBytes), + } + + if err := config.Save(ctx, c); err != nil { + return nil, fmt.Errorf("Could not save raw cert: %w\n", err) + } + } + + key, sigKey, keySet, err := initKeys(keyBytes, certBytes) + if err != nil { + return nil, fmt.Errorf("Fail to init keys: %w\n", err) + } + + return &AuthStorage{ + appConfig: cfg, + getUserBySubject: getUserBySubject, + requests: request, + key: key, + sigKey: *sigKey, + keySet: *keySet, + clients: map[string]op.Client{ + client.GetID(): client, + }, + }, nil +} + +func initKeys(keyBytes, certBytes []byte) (*rsa.PrivateKey, *jose.SigningKey, *jose.JSONWebKeySet, error) { + + block, _ := pem.Decode(keyBytes) + if block == nil { + return nil, nil, nil, fmt.Errorf("failed to decode the key bytes") + } + + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to parse the private key bytes: %w\n", err) + } + + cert, err := x509.ParseCertificate(certBytes) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to parse the cert bytes: %w\n", err) + } + + keyID := "RE01" + sk := jose.SigningKey{ + Algorithm: jose.RS256, + Key: jose.JSONWebKey{Key: key, Use: "sig", Algorithm: string(jose.RS256), KeyID: keyID, Certificates: []*x509.Certificate{cert}}, + } + + return key, &sk, &jose.JSONWebKeySet{ + Keys: []jose.JSONWebKey{ + {Key: key.Public(), Use: "sig", Algorithm: string(jose.RS256), KeyID: keyID, Certificates: []*x509.Certificate{cert}}, + }, + }, nil +} + +func generateCert(name pkix.Name) (keyPem, certPem []byte, err error) { + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + err = fmt.Errorf("failed to generate key: %w\n", err) + return + } + + keyPem = pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + }) + + cert := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: name, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(100, 0, 0), + IsCA: true, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + } + + certPem, err = x509.CreateCertificate(rand.Reader, cert, cert, key.Public(), key) + if err != nil { + err = fmt.Errorf("failed to create the cert: %w\n", err) + } + + return +} + +func (s *AuthStorage) Health(_ context.Context) error { + return nil +} + +func (s *AuthStorage) CreateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, _ string) (op.AuthRequest, error) { + audiences := []string{ + s.appConfig.Domain, + } + if s.appConfig.Debug { + audiences = append(audiences, "http://localhost:8080") + } + + var cc *oidc.CodeChallenge + if authReq.CodeChallenge != "" { + cc = &oidc.CodeChallenge{ + Challenge: authReq.CodeChallenge, + Method: authReq.CodeChallengeMethod, + } + } + var request = auth.NewRequest(). + NewID(). + ClientID(authReq.ClientID). + State(authReq.State). + ResponseType(authReq.ResponseType). + Scopes(authReq.Scopes). + Audiences(audiences). + RedirectURI(authReq.RedirectURI). + Nonce(authReq.Nonce). + CodeChallenge(cc). + CreatedAt(time.Now().UTC()). + AuthorizedAt(nil). + MustBuild() + + if err := s.requests.Save(ctx, request); err != nil { + return nil, err + } + return request, nil +} + +func (s *AuthStorage) AuthRequestByID(ctx context.Context, requestID string) (op.AuthRequest, error) { + if requestID == "" { + return nil, errors.New("invalid id") + } + reqId, err := id.AuthRequestIDFrom(requestID) + if err != nil { + return nil, err + } + request, err := s.requests.FindByID(ctx, reqId) + if err != nil { + return nil, err + } + return request, nil +} + +func (s *AuthStorage) AuthRequestByCode(ctx context.Context, code string) (op.AuthRequest, error) { + if code == "" { + return nil, errors.New("invalid code") + } + return s.requests.FindByCode(ctx, code) +} + +func (s *AuthStorage) AuthRequestBySubject(ctx context.Context, subject string) (op.AuthRequest, error) { + if subject == "" { + return nil, errors.New("invalid subject") + } + + return s.requests.FindBySubject(ctx, subject) +} + +func (s *AuthStorage) SaveAuthCode(ctx context.Context, requestID, code string) error { + + request, err := s.AuthRequestByID(ctx, requestID) + if err != nil { + return err + } + request2 := request.(*auth.Request) + request2.SetCode(code) + err = s.updateRequest(ctx, requestID, *request2) + return err +} + +func (s *AuthStorage) DeleteAuthRequest(_ context.Context, requestID string) error { + delete(s.clients, requestID) + return nil +} + +func (s *AuthStorage) CreateAccessToken(_ context.Context, _ op.TokenRequest) (string, time.Time, error) { + return "id", time.Now().UTC().Add(5 * time.Hour), nil +} + +func (s *AuthStorage) CreateAccessAndRefreshTokens(_ context.Context, request op.TokenRequest, _ string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) { + authReq := request.(*auth.Request) + return "id", authReq.GetID(), time.Now().UTC().Add(5 * time.Minute), nil +} + +func (s *AuthStorage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (op.RefreshTokenRequest, error) { + r, err := s.AuthRequestByID(ctx, refreshToken) + if err != nil { + return nil, err + } + return r.(op.RefreshTokenRequest), err +} + +func (s *AuthStorage) TerminateSession(_ context.Context, _, _ string) error { + return errors.New("not implemented") +} + +func (s *AuthStorage) GetSigningKey(_ context.Context, keyCh chan<- jose.SigningKey) { + keyCh <- s.sigKey +} + +func (s *AuthStorage) GetKeySet(_ context.Context) (*jose.JSONWebKeySet, error) { + return &s.keySet, nil +} + +func (s *AuthStorage) GetKeyByIDAndUserID(_ context.Context, kid, _ string) (*jose.JSONWebKey, error) { + return &s.keySet.Key(kid)[0], nil +} + +func (s *AuthStorage) GetClientByClientID(_ context.Context, clientID string) (op.Client, error) { + + if clientID == "" { + return nil, errors.New("invalid client id") + } + + client, exists := s.clients[clientID] + if !exists { + return nil, errors.New("not found") + } + + return client, nil +} + +func (s *AuthStorage) AuthorizeClientIDSecret(_ context.Context, _ string, _ string) error { + return nil +} + +func (s *AuthStorage) SetUserinfoFromToken(ctx context.Context, userinfo oidc.UserInfoSetter, _, _, _ string) error { + return s.SetUserinfoFromScopes(ctx, userinfo, "", "", []string{}) +} + +func (s *AuthStorage) SetUserinfoFromScopes(ctx context.Context, userinfo oidc.UserInfoSetter, subject, _ string, _ []string) error { + + request, err := s.AuthRequestBySubject(ctx, subject) + if err != nil { + return err + } + + u, err := s.getUserBySubject(ctx, subject) + if err != nil { + return err + } + + userinfo.SetSubject(request.GetSubject()) + userinfo.SetEmail(u.Email(), true) + userinfo.SetName(u.Name()) + userinfo.AppendClaims("lang", u.Lang()) + userinfo.AppendClaims("theme", u.Theme()) + + return nil +} + +func (s *AuthStorage) GetPrivateClaimsFromScopes(_ context.Context, _, _ string, _ []string) (map[string]interface{}, error) { + return map[string]interface{}{"private_claim": "test"}, nil +} + +func (s *AuthStorage) SetIntrospectionFromToken(ctx context.Context, introspect oidc.IntrospectionResponse, _, subject, clientID string) error { + if err := s.SetUserinfoFromScopes(ctx, introspect, subject, clientID, []string{}); err != nil { + return err + } + request, err := s.AuthRequestBySubject(ctx, subject) + if err != nil { + return err + } + introspect.SetClientID(request.GetClientID()) + return nil +} + +func (s *AuthStorage) ValidateJWTProfileScopes(_ context.Context, _ string, scope []string) ([]string, error) { + return scope, nil +} + +func (s *AuthStorage) RevokeToken(_ context.Context, _ string, _ string, _ string) *oidc.Error { + // TODO implement me + panic("implement me") +} + +func (s *AuthStorage) CompleteAuthRequest(ctx context.Context, requestId, sub string) error { + request, err := s.AuthRequestByID(ctx, requestId) + if err != nil { + return err + } + req := request.(*auth.Request) + req.Complete(sub) + err = s.updateRequest(ctx, requestId, *req) + return err +} + +func (s *AuthStorage) updateRequest(ctx context.Context, requestID string, req auth.Request) error { + if requestID == "" { + return errors.New("invalid id") + } + reqId, err := id.AuthRequestIDFrom(requestID) + if err != nil { + return err + } + + if _, err := s.requests.FindByID(ctx, reqId); err != nil { + return err + } + + if err := s.requests.Save(ctx, &req); err != nil { + return err + } + + return nil +} diff --git a/internal/usecase/interactor/common.go b/internal/usecase/interactor/common.go index b3d0bb11..dcd3039e 100644 --- a/internal/usecase/interactor/common.go +++ b/internal/usecase/interactor/common.go @@ -18,6 +18,7 @@ import ( type ContainerConfig struct { SignupSecret string + AuthSrvUIDomain string PublishedIndexHTML string PublishedIndexURL *url.URL } @@ -41,7 +42,7 @@ func NewContainer(r *repo.Container, g *gateway.Container, config ContainerConfi Scene: NewScene(r, g), Tag: NewTag(r), Team: NewTeam(r), - User: NewUser(r, g, config.SignupSecret), + User: NewUser(r, g, config.SignupSecret, config.AuthSrvUIDomain), } } diff --git a/internal/usecase/interactor/emails/auth_html.tmpl b/internal/usecase/interactor/emails/auth_html.tmpl new file mode 100644 index 00000000..9d1d1e3a --- /dev/null +++ b/internal/usecase/interactor/emails/auth_html.tmpl @@ -0,0 +1,435 @@ + + + + + + + Re:Earth reset password + + + + + + + + + + + + + \ No newline at end of file diff --git a/internal/usecase/interactor/emails/auth_text.tmpl b/internal/usecase/interactor/emails/auth_text.tmpl new file mode 100644 index 00000000..0ed590d5 --- /dev/null +++ b/internal/usecase/interactor/emails/auth_text.tmpl @@ -0,0 +1,7 @@ +Hi {{ .UserName }}: +{{ .Message }} + +To {{ .ActionLabel }}: +{{ .ActionURL }} + +{{ .Suffix }} \ No newline at end of file diff --git a/internal/usecase/interactor/user.go b/internal/usecase/interactor/user.go index 08d6ef6b..13875798 100644 --- a/internal/usecase/interactor/user.go +++ b/internal/usecase/interactor/user.go @@ -1,14 +1,20 @@ package interactor import ( + "bytes" "context" + _ "embed" "errors" + htmlTmpl "html/template" + "net/mail" + textTmpl "text/template" "github.com/reearth/reearth-backend/internal/usecase" "github.com/reearth/reearth-backend/internal/usecase/gateway" "github.com/reearth/reearth-backend/internal/usecase/interfaces" "github.com/reearth/reearth-backend/internal/usecase/repo" "github.com/reearth/reearth-backend/pkg/id" + "github.com/reearth/reearth-backend/pkg/log" "github.com/reearth/reearth-backend/pkg/project" "github.com/reearth/reearth-backend/pkg/rerror" "github.com/reearth/reearth-backend/pkg/user" @@ -28,10 +34,57 @@ type User struct { transaction repo.Transaction file gateway.File authenticator gateway.Authenticator + mailer gateway.Mailer signupSecret string + authSrvUIDomain string } -func NewUser(r *repo.Container, g *gateway.Container, signupSecret string) interfaces.User { +type mailContent struct { + UserName string + Message string + Suffix string + ActionLabel string + ActionURL htmlTmpl.URL +} + +var ( + //go:embed emails/auth_html.tmpl + autHTMLTMPLStr string + //go:embed emails/auth_text.tmpl + authTextTMPLStr string + + authTextTMPL *textTmpl.Template + authHTMLTMPL *htmlTmpl.Template + + signupMailContent mailContent + passwordResetMailContent mailContent +) + +func init() { + var err error + authTextTMPL, err = textTmpl.New("passwordReset").Parse(authTextTMPLStr) + if err != nil { + log.Panicf("password reset email template parse error: %s\n", err) + } + authHTMLTMPL, err = htmlTmpl.New("passwordReset").Parse(autHTMLTMPLStr) + if err != nil { + log.Panicf("password reset email template parse error: %s\n", err) + } + + signupMailContent = mailContent{ + Message: "Thank you for signing up to Re:Earth. Please verify your email address by clicking the button below.", + Suffix: "You can use this email address to log in to Re:Earth account anytime.", + ActionLabel: "Activate your account and log in", + } + + passwordResetMailContent = mailContent{ + Message: "Thank you for using Re:Earth. We’ve received a request to reset your password. If this was you, please click the link below to confirm and change your password.", + Suffix: "If you did not mean to reset your password, then you can ignore this email.", + ActionLabel: "Confirm to reset your password", + } +} + +func NewUser(r *repo.Container, g *gateway.Container, signupSecret, authSrcUIDomain string) interfaces.User { return &User{ userRepo: r.User, teamRepo: r.Team, @@ -46,6 +99,8 @@ func NewUser(r *repo.Container, g *gateway.Container, signupSecret string) inter file: g.File, authenticator: g.Authenticator, signupSecret: signupSecret, + authSrvUIDomain: authSrcUIDomain, + mailer: g.Mailer, } } @@ -77,17 +132,19 @@ func (i *User) Fetch(ctx context.Context, ids []id.UserID, operator *usecase.Ope } func (i *User) Signup(ctx context.Context, inp interfaces.SignupParam) (u *user.User, _ *user.Team, err error) { - if i.signupSecret != "" && inp.Secret != i.signupSecret { - return nil, nil, interfaces.ErrSignupInvalidSecret - } - - if len(inp.Sub) == 0 { - return nil, nil, errors.New("sub is required") + var team *user.Team + var email, name string + var auth *user.Auth + var tx repo.Tx + isOidc := inp.Secret != nil && inp.Sub != nil + isAuth := inp.Name != nil && inp.Email != nil && inp.Password != nil + if !isAuth && !isOidc { + return } - tx, err := i.transaction.Begin() + tx, err = i.transaction.Begin() if err != nil { - return + return nil, nil, err } defer func() { if err2 := tx.End(ctx); err == nil && err2 != nil { @@ -95,25 +152,43 @@ func (i *User) Signup(ctx context.Context, inp interfaces.SignupParam) (u *user. } }() - // Check if user and team already exists - existed, err := i.userRepo.FindByAuth0Sub(ctx, inp.Sub) - if err != nil && !errors.Is(err, rerror.ErrNotFound) { - return nil, nil, err - } - if existed != nil { - return nil, nil, errors.New("existed user") - } + if isOidc { + // Auth0 + if i.signupSecret != "" && *inp.Secret != i.signupSecret { + return nil, nil, interfaces.ErrSignupInvalidSecret + } - if inp.UserID != nil { - existed, err := i.userRepo.FindByID(ctx, *inp.UserID) - if err != nil && !errors.Is(err, rerror.ErrNotFound) { - return nil, nil, err + if len(*inp.Sub) == 0 { + return nil, nil, errors.New("sub is required") } - if existed != nil { - return nil, nil, errors.New("existed user") + name, email, auth, err = i.oidcSignup(ctx, inp) + if err != nil { + return + } + + } else if isAuth { + if *inp.Name == "" { + return nil, nil, interfaces.ErrSignupInvalidName + } + if _, err := mail.ParseAddress(*inp.Email); err != nil { + return nil, nil, interfaces.ErrInvalidUserEmail + } + if *inp.Password == "" { + return nil, nil, interfaces.ErrSignupInvalidPassword + } + + var unverifiedUser *user.User + var unverifiedTeam *user.Team + name, email, unverifiedUser, unverifiedTeam, err = i.reearthSignup(ctx, inp) + if err != nil { + return + } + if unverifiedUser != nil && unverifiedTeam != nil { + return unverifiedUser, unverifiedTeam, nil } } + // Check if team already exists if inp.TeamID != nil { existed, err := i.teamRepo.FindByID(ctx, *inp.TeamID) if err != nil && !errors.Is(err, rerror.ErrNotFound) { @@ -124,27 +199,12 @@ func (i *User) Signup(ctx context.Context, inp interfaces.SignupParam) (u *user. } } - // Fetch user info - ui, err := i.authenticator.FetchUser(inp.Sub) - if err != nil { - return nil, nil, err - } - - // Check if user and team already exists - var team *user.Team - existed, err = i.userRepo.FindByEmail(ctx, ui.Email) - if err != nil && !errors.Is(err, rerror.ErrNotFound) { - return nil, nil, err - } - if existed != nil { - return nil, nil, errors.New("existed user") - } - // Initialize user and team u, team, err = user.Init(user.InitParams{ - Email: ui.Email, - Name: ui.Name, - Auth0Sub: inp.Sub, + Email: email, + Name: name, + Sub: auth, + Password: *inp.Password, Lang: inp.Lang, Theme: inp.Theme, UserID: inp.UserID, @@ -159,11 +219,189 @@ func (i *User) Signup(ctx context.Context, inp interfaces.SignupParam) (u *user. if err := i.teamRepo.Save(ctx, team); err != nil { return nil, nil, err } + if tx != nil { + tx.Commit() + } - tx.Commit() return u, team, nil } +func (i *User) reearthSignup(ctx context.Context, inp interfaces.SignupParam) (string, string, *user.User, *user.Team, error) { + // Check if user email already exists + existed, err := i.userRepo.FindByEmail(ctx, *inp.Email) + if err != nil && !errors.Is(err, rerror.ErrNotFound) { + return "", "", nil, nil, err + } + + if existed != nil { + if existed.Verification().IsVerified() { + return "", "", nil, nil, errors.New("existed user email") + } else { + // if user exists but not verified -> create a new verification + if err := i.CreateVerification(ctx, *inp.Email); err != nil { + return "", "", nil, nil, err + } else { + team, err := i.teamRepo.FindByID(ctx, existed.Team()) + if err != nil && !errors.Is(err, rerror.ErrNotFound) { + return "", "", nil, nil, err + } + return "", "", existed, team, nil + } + } + } + + return *inp.Name, *inp.Email, nil, nil, nil +} + +func (i *User) oidcSignup(ctx context.Context, inp interfaces.SignupParam) (string, string, *user.Auth, error) { + // Check if user already exists + existed, err := i.userRepo.FindByAuth0Sub(ctx, *inp.Sub) + if err != nil && !errors.Is(err, rerror.ErrNotFound) { + return "", "", nil, err + } + if existed != nil { + return "", "", nil, errors.New("existed user") + } + + if inp.UserID != nil { + existed, err := i.userRepo.FindByID(ctx, *inp.UserID) + if err != nil && !errors.Is(err, rerror.ErrNotFound) { + return "", "", nil, err + } + if existed != nil { + return "", "", nil, errors.New("existed user") + } + } + + // Fetch user info + ui, err := i.authenticator.FetchUser(*inp.Sub) + if err != nil { + return "", "", nil, err + } + + // Check if user and team already exists + existed, err = i.userRepo.FindByEmail(ctx, ui.Email) + if err != nil && !errors.Is(err, rerror.ErrNotFound) { + return "", "", nil, err + } + if existed != nil { + return "", "", nil, errors.New("existed user") + } + + return ui.Name, ui.Email, user.AuthFromAuth0Sub(*inp.Sub).Ref(), nil +} + +func (i *User) GetUserByCredentials(ctx context.Context, inp interfaces.GetUserByCredentials) (u *user.User, err error) { + u, err = i.userRepo.FindByNameOrEmail(ctx, inp.Email) + if err != nil && !errors.Is(rerror.ErrNotFound, err) { + return nil, err + } else if u == nil { + return nil, interfaces.ErrInvalidUserEmail + } + matched, err := u.MatchPassword(inp.Password) + if err != nil { + return nil, err + } + if !matched { + return nil, interfaces.ErrSignupInvalidPassword + } + return u, nil +} + +func (i *User) GetUserBySubject(ctx context.Context, sub string) (u *user.User, err error) { + u, err = i.userRepo.FindByAuth0Sub(ctx, sub) + if err != nil { + return nil, err + } + return u, nil +} + +func (i *User) StartPasswordReset(ctx context.Context, email string) error { + tx, err := i.transaction.Begin() + if err != nil { + return err + } + defer func() { + if err2 := tx.End(ctx); err == nil && err2 != nil { + err = err2 + } + }() + + u, err := i.userRepo.FindByEmail(ctx, email) + if err != nil { + return err + } + + pr := user.NewPasswordReset() + u.SetPasswordReset(pr) + + if err := i.userRepo.Save(ctx, u); err != nil { + return err + } + + var TextOut, HTMLOut bytes.Buffer + link := i.authSrvUIDomain + "/?pwd-reset-token=" + pr.Token + passwordResetMailContent.UserName = u.Name() + passwordResetMailContent.ActionURL = htmlTmpl.URL(link) + + if err := authTextTMPL.Execute(&TextOut, passwordResetMailContent); err != nil { + return err + } + if err := authHTMLTMPL.Execute(&HTMLOut, passwordResetMailContent); err != nil { + return err + } + + err = i.mailer.SendMail([]gateway.Contact{ + { + Email: u.Email(), + Name: u.Name(), + }, + }, "Password reset", TextOut.String(), HTMLOut.String()) + if err != nil { + return err + } + + tx.Commit() + return nil +} + +func (i *User) PasswordReset(ctx context.Context, password, token string) error { + tx, err := i.transaction.Begin() + if err != nil { + return err + } + defer func() { + if err2 := tx.End(ctx); err == nil && err2 != nil { + err = err2 + } + }() + + u, err := i.userRepo.FindByPasswordResetRequest(ctx, token) + if err != nil { + return err + } + + passwordReset := u.PasswordReset() + ok := passwordReset.Validate(token) + + if !ok { + return interfaces.ErrUserInvalidPasswordReset + } + + u.SetPasswordReset(nil) + + if err := u.SetPassword(password); err != nil { + return err + } + + if err := i.userRepo.Save(ctx, u); err != nil { + return err + } + + tx.Commit() + return nil +} + func (i *User) UpdateMe(ctx context.Context, p interfaces.UpdateMeParam, operator *usecase.Operator) (u *user.User, err error) { if err := i.OnlyOperator(operator); err != nil { return nil, err @@ -375,3 +613,67 @@ func (i *User) DeleteMe(ctx context.Context, userID id.UserID, operator *usecase tx.Commit() return nil } + +func (i *User) CreateVerification(ctx context.Context, email string) error { + tx, err := i.transaction.Begin() + if err != nil { + return err + } + u, err := i.userRepo.FindByEmail(ctx, email) + if err != nil { + return err + } + + vr := user.NewVerification() + u.SetVerification(vr) + err = i.userRepo.Save(ctx, u) + if err != nil { + return err + } + + var TextOut, HTMLOut bytes.Buffer + link := i.authSrvUIDomain + "/?user-verification-token=" + vr.Code() + signupMailContent.UserName = email + signupMailContent.ActionURL = htmlTmpl.URL(link) + + if err := authTextTMPL.Execute(&TextOut, signupMailContent); err != nil { + return err + } + if err := authHTMLTMPL.Execute(&HTMLOut, signupMailContent); err != nil { + return err + } + + err = i.mailer.SendMail([]gateway.Contact{ + { + Email: u.Email(), + Name: u.Name(), + }, + }, "email verification", TextOut.String(), HTMLOut.String()) + if err != nil { + return err + } + tx.Commit() + return nil +} + +func (i *User) VerifyUser(ctx context.Context, code string) (*user.User, error) { + tx, err := i.transaction.Begin() + if err != nil { + return nil, err + } + u, err := i.userRepo.FindByVerification(ctx, code) + if err != nil { + return nil, err + } + if u.Verification().IsExpired() { + return nil, errors.New("verification expired") + } + u.Verification().SetVerified(true) + err = i.userRepo.Save(ctx, u) + if err != nil { + return nil, err + } + + tx.Commit() + return u, nil +} diff --git a/internal/usecase/interfaces/user.go b/internal/usecase/interfaces/user.go index c933d896..acca0bb5 100644 --- a/internal/usecase/interfaces/user.go +++ b/internal/usecase/interfaces/user.go @@ -13,17 +13,29 @@ import ( var ( ErrUserInvalidPasswordConfirmation = errors.New("invalid password confirmation") + ErrUserInvalidPasswordReset = errors.New("invalid password reset request") ErrUserInvalidLang = errors.New("invalid lang") ErrSignupInvalidSecret = errors.New("invalid secret") + ErrSignupInvalidName = errors.New("invalid name") + ErrInvalidUserEmail = errors.New("invalid email") + ErrSignupInvalidPassword = errors.New("invalid password") ) type SignupParam struct { - Sub string - Lang *language.Tag - Theme *user.Theme - UserID *id.UserID - TeamID *id.TeamID - Secret string + Sub *string + UserID *id.UserID + Secret *string + Name *string + Email *string + Password *string + Lang *language.Tag + Theme *user.Theme + TeamID *id.TeamID +} + +type GetUserByCredentials struct { + Email string + Password string } type UpdateMeParam struct { @@ -38,6 +50,12 @@ type UpdateMeParam struct { type User interface { Fetch(context.Context, []id.UserID, *usecase.Operator) ([]*user.User, error) Signup(context.Context, SignupParam) (*user.User, *user.Team, error) + CreateVerification(context.Context, string) error + VerifyUser(context.Context, string) (*user.User, error) + GetUserByCredentials(context.Context, GetUserByCredentials) (*user.User, error) + GetUserBySubject(context.Context, string) (*user.User, error) + StartPasswordReset(context.Context, string) error + PasswordReset(context.Context, string, string) error UpdateMe(context.Context, UpdateMeParam, *usecase.Operator) (*user.User, error) RemoveMyAuth(context.Context, string, *usecase.Operator) (*user.User, error) SearchUser(context.Context, string, *usecase.Operator) (*user.User, error) diff --git a/internal/usecase/repo/auth_request.go b/internal/usecase/repo/auth_request.go new file mode 100644 index 00000000..378926bb --- /dev/null +++ b/internal/usecase/repo/auth_request.go @@ -0,0 +1,16 @@ +package repo + +import ( + "context" + + "github.com/reearth/reearth-backend/pkg/auth" + "github.com/reearth/reearth-backend/pkg/id" +) + +type AuthRequest interface { + FindByID(context.Context, id.AuthRequestID) (*auth.Request, error) + FindByCode(context.Context, string) (*auth.Request, error) + FindBySubject(context.Context, string) (*auth.Request, error) + Save(context.Context, *auth.Request) error + Remove(context.Context, id.AuthRequestID) error +} diff --git a/internal/usecase/repo/container.go b/internal/usecase/repo/container.go index 39ea3717..e29c96ba 100644 --- a/internal/usecase/repo/container.go +++ b/internal/usecase/repo/container.go @@ -2,6 +2,7 @@ package repo type Container struct { Asset Asset + AuthRequest AuthRequest Config Config DatasetSchema DatasetSchema Dataset Dataset diff --git a/internal/usecase/repo/user.go b/internal/usecase/repo/user.go index 2b415278..fdc3f276 100644 --- a/internal/usecase/repo/user.go +++ b/internal/usecase/repo/user.go @@ -13,6 +13,8 @@ type User interface { FindByAuth0Sub(context.Context, string) (*user.User, error) FindByEmail(context.Context, string) (*user.User, error) FindByNameOrEmail(context.Context, string) (*user.User, error) + FindByVerification(context.Context, string) (*user.User, error) + FindByPasswordResetRequest(context.Context, string) (*user.User, error) Save(context.Context, *user.User) error Remove(context.Context, id.UserID) error } diff --git a/pkg/auth/builder.go b/pkg/auth/builder.go new file mode 100644 index 00000000..9eed6e64 --- /dev/null +++ b/pkg/auth/builder.go @@ -0,0 +1,102 @@ +package auth + +import ( + "time" + + "github.com/caos/oidc/pkg/oidc" + "github.com/reearth/reearth-backend/pkg/id" +) + +type RequestBuilder struct { + r *Request +} + +func NewRequest() *RequestBuilder { + return &RequestBuilder{r: &Request{}} +} + +func (b *RequestBuilder) Build() (*Request, error) { + if id.ID(b.r.id).IsNil() { + return nil, id.ErrInvalidID + } + b.r.createdAt = time.Now() + return b.r, nil +} + +func (b *RequestBuilder) MustBuild() *Request { + r, err := b.Build() + if err != nil { + panic(err) + } + return r +} + +func (b *RequestBuilder) ID(id id.AuthRequestID) *RequestBuilder { + b.r.id = id + return b +} + +func (b *RequestBuilder) NewID() *RequestBuilder { + b.r.id = id.AuthRequestID(id.New()) + return b +} + +func (b *RequestBuilder) ClientID(id string) *RequestBuilder { + b.r.clientID = id + return b +} + +func (b *RequestBuilder) Subject(subject string) *RequestBuilder { + b.r.subject = subject + return b +} + +func (b *RequestBuilder) Code(code string) *RequestBuilder { + b.r.code = code + return b +} + +func (b *RequestBuilder) State(state string) *RequestBuilder { + b.r.state = state + return b +} + +func (b *RequestBuilder) ResponseType(rt oidc.ResponseType) *RequestBuilder { + b.r.responseType = rt + return b +} + +func (b *RequestBuilder) Scopes(scopes []string) *RequestBuilder { + b.r.scopes = scopes + return b +} + +func (b *RequestBuilder) Audiences(audiences []string) *RequestBuilder { + b.r.audiences = audiences + return b +} + +func (b *RequestBuilder) RedirectURI(redirectURI string) *RequestBuilder { + b.r.redirectURI = redirectURI + return b +} + +func (b *RequestBuilder) Nonce(nonce string) *RequestBuilder { + b.r.nonce = nonce + return b +} + +func (b *RequestBuilder) CodeChallenge(CodeChallenge *oidc.CodeChallenge) *RequestBuilder { + b.r.codeChallenge = CodeChallenge + return b +} + +func (b *RequestBuilder) CreatedAt(createdAt time.Time) *RequestBuilder { + b.r.createdAt = createdAt + return b +} + +func (b *RequestBuilder) AuthorizedAt(authorizedAt *time.Time) *RequestBuilder { + b.r.authorizedAt = authorizedAt + return b +} diff --git a/pkg/auth/client.go b/pkg/auth/client.go new file mode 100644 index 00000000..93075710 --- /dev/null +++ b/pkg/auth/client.go @@ -0,0 +1,115 @@ +package auth + +import ( + "fmt" + "time" + + "github.com/caos/oidc/pkg/oidc" + "github.com/caos/oidc/pkg/op" +) + +type Client struct { + id string + applicationType op.ApplicationType + authMethod oidc.AuthMethod + accessTokenType op.AccessTokenType + responseTypes []oidc.ResponseType + grantTypes []oidc.GrantType + allowedScopes []string + redirectURIs []string + logoutRedirectURIs []string + loginURI string + idTokenLifetime time.Duration + clockSkew time.Duration + devMode bool +} + +func NewLocalClient(devMode bool) op.Client { + return &Client{ + id: "01FH69GFQ4DFCXS5XD91JK4HZ1", + applicationType: op.ApplicationTypeWeb, + authMethod: oidc.AuthMethodNone, + accessTokenType: op.AccessTokenTypeJWT, + responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode}, + grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken}, + redirectURIs: []string{"http://localhost:3000"}, + allowedScopes: []string{"openid", "profile", "email"}, + loginURI: "http://localhost:3000/login?id=%s", + idTokenLifetime: 5 * time.Minute, + clockSkew: 0, + devMode: devMode, + } +} + +func (c *Client) GetID() string { + return c.id +} + +func (c *Client) RedirectURIs() []string { + return c.redirectURIs +} + +func (c *Client) PostLogoutRedirectURIs() []string { + return c.logoutRedirectURIs +} + +func (c *Client) LoginURL(id string) string { + return fmt.Sprintf(c.loginURI, id) +} + +func (c *Client) ApplicationType() op.ApplicationType { + return c.applicationType +} + +func (c *Client) AuthMethod() oidc.AuthMethod { + return c.authMethod +} + +func (c *Client) IDTokenLifetime() time.Duration { + return c.idTokenLifetime +} + +func (c *Client) AccessTokenType() op.AccessTokenType { + return c.accessTokenType +} + +func (c *Client) ResponseTypes() []oidc.ResponseType { + return c.responseTypes +} + +func (c *Client) GrantTypes() []oidc.GrantType { + return c.grantTypes +} + +func (c *Client) DevMode() bool { + return c.devMode +} + +func (c *Client) RestrictAdditionalIdTokenScopes() func(scopes []string) []string { + return func(scopes []string) []string { + return scopes + } +} + +func (c *Client) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { + return func(scopes []string) []string { + return scopes + } +} + +func (c *Client) IsScopeAllowed(scope string) bool { + for _, clientScope := range c.allowedScopes { + if clientScope == scope { + return true + } + } + return false +} + +func (c *Client) IDTokenUserinfoClaimsAssertion() bool { + return false +} + +func (c *Client) ClockSkew() time.Duration { + return c.clockSkew +} diff --git a/pkg/auth/request.go b/pkg/auth/request.go new file mode 100644 index 00000000..c2645b3b --- /dev/null +++ b/pkg/auth/request.go @@ -0,0 +1,143 @@ +package auth + +import ( + "time" + + "github.com/caos/oidc/pkg/oidc" + "github.com/reearth/reearth-backend/pkg/id" +) + +var essentialScopes = []string{"openid", "profile", "email"} + +type Request struct { + id id.AuthRequestID + clientID string + subject string + code string + state string + responseType oidc.ResponseType + scopes []string + audiences []string + redirectURI string + nonce string + codeChallenge *oidc.CodeChallenge + createdAt time.Time + authorizedAt *time.Time +} + +func (a *Request) ID() id.AuthRequestID { + return a.id +} + +func (a *Request) GetID() string { + return a.id.String() +} + +func (a *Request) GetACR() string { + return "" +} + +func (a *Request) GetAMR() []string { + return []string{ + "password", + } +} + +func (a *Request) GetAudience() []string { + if a.audiences == nil { + return make([]string, 0) + } + + return a.audiences +} + +func (a *Request) GetAuthTime() time.Time { + return a.createdAt +} + +func (a *Request) GetClientID() string { + return a.clientID +} + +func (a *Request) GetResponseMode() oidc.ResponseMode { + // TODO make sure about this + return oidc.ResponseModeQuery +} + +func (a *Request) GetCode() string { + return a.code +} + +func (a *Request) GetState() string { + return a.state +} + +func (a *Request) GetCodeChallenge() *oidc.CodeChallenge { + return a.codeChallenge +} + +func (a *Request) GetNonce() string { + return a.nonce +} + +func (a *Request) GetRedirectURI() string { + return a.redirectURI +} + +func (a *Request) GetResponseType() oidc.ResponseType { + return a.responseType +} + +func (a *Request) GetScopes() []string { + return unique(append(a.scopes, essentialScopes...)) +} + +func (a *Request) SetCurrentScopes(scopes []string) { + a.scopes = unique(append(scopes, essentialScopes...)) +} + +func (a *Request) GetSubject() string { + return a.subject +} + +func (a *Request) CreatedAt() time.Time { + return a.createdAt +} + +func (a *Request) SetCreatedAt(createdAt time.Time) { + a.createdAt = createdAt +} + +func (a *Request) AuthorizedAt() *time.Time { + return a.authorizedAt +} + +func (a *Request) SetAuthorizedAt(authorizedAt *time.Time) { + a.authorizedAt = authorizedAt +} + +func (a *Request) Done() bool { + return a.authorizedAt != nil +} + +func (a *Request) Complete(sub string) { + a.subject = sub + now := time.Now() + a.authorizedAt = &now +} + +func (a *Request) SetCode(code string) { + a.code = code +} + +func unique(list []string) []string { + allKeys := make(map[string]struct{}) + var uniqueList []string + for _, item := range list { + if _, ok := allKeys[item]; !ok { + allKeys[item] = struct{}{} + uniqueList = append(uniqueList, item) + } + } + return uniqueList +} diff --git a/pkg/config/config.go b/pkg/config/config.go index fd069478..a0f48115 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -4,6 +4,12 @@ import "sort" type Config struct { Migration int64 + Auth *Auth +} + +type Auth struct { + Cert string + Key string } func (c *Config) NextMigrations(migrations []int64) []int64 { diff --git a/pkg/id/auth_request_gen.go b/pkg/id/auth_request_gen.go new file mode 100644 index 00000000..76a36140 --- /dev/null +++ b/pkg/id/auth_request_gen.go @@ -0,0 +1,297 @@ +// Code generated by gen, DO NOT EDIT. + +package id + +import "encoding/json" + +// AuthRequestID is an ID for AuthRequest. +type AuthRequestID ID + +// NewAuthRequestID generates a new AuthRequestId. +func NewAuthRequestID() AuthRequestID { + return AuthRequestID(New()) +} + +// AuthRequestIDFrom generates a new AuthRequestID from a string. +func AuthRequestIDFrom(i string) (nid AuthRequestID, err error) { + var did ID + did, err = FromID(i) + if err != nil { + return + } + nid = AuthRequestID(did) + return +} + +// MustAuthRequestID generates a new AuthRequestID from a string, but panics if the string cannot be parsed. +func MustAuthRequestID(i string) AuthRequestID { + did, err := FromID(i) + if err != nil { + panic(err) + } + return AuthRequestID(did) +} + +// AuthRequestIDFromRef generates a new AuthRequestID from a string ref. +func AuthRequestIDFromRef(i *string) *AuthRequestID { + did := FromIDRef(i) + if did == nil { + return nil + } + nid := AuthRequestID(*did) + return &nid +} + +// AuthRequestIDFromRefID generates a new AuthRequestID from a ref of a generic ID. +func AuthRequestIDFromRefID(i *ID) *AuthRequestID { + if i == nil { + return nil + } + nid := AuthRequestID(*i) + return &nid +} + +// ID returns a domain ID. +func (d AuthRequestID) ID() ID { + return ID(d) +} + +// String returns a string representation. +func (d AuthRequestID) String() string { + return ID(d).String() +} + +// GoString implements fmt.GoStringer interface. +func (d AuthRequestID) GoString() string { + return "id.AuthRequestID(" + d.String() + ")" +} + +// RefString returns a reference of string representation. +func (d AuthRequestID) RefString() *string { + id := ID(d).String() + return &id +} + +// Ref returns a reference. +func (d AuthRequestID) Ref() *AuthRequestID { + d2 := d + return &d2 +} + +// Contains returns whether the id is contained in the slice. +func (d AuthRequestID) Contains(ids []AuthRequestID) bool { + for _, i := range ids { + if d.ID().Equal(i.ID()) { + return true + } + } + return false +} + +// CopyRef returns a copy of a reference. +func (d *AuthRequestID) CopyRef() *AuthRequestID { + if d == nil { + return nil + } + d2 := *d + return &d2 +} + +// IDRef returns a reference of a domain id. +func (d *AuthRequestID) IDRef() *ID { + if d == nil { + return nil + } + id := ID(*d) + return &id +} + +// StringRef returns a reference of a string representation. +func (d *AuthRequestID) StringRef() *string { + if d == nil { + return nil + } + id := ID(*d).String() + return &id +} + +// MarhsalJSON implements json.Marhsaler interface +func (d *AuthRequestID) MarhsalJSON() ([]byte, error) { + return json.Marshal(d.String()) +} + +// UnmarhsalJSON implements json.Unmarshaler interface +func (d *AuthRequestID) UnmarhsalJSON(bs []byte) (err error) { + var idstr string + if err = json.Unmarshal(bs, &idstr); err != nil { + return + } + *d, err = AuthRequestIDFrom(idstr) + return +} + +// MarshalText implements encoding.TextMarshaler interface +func (d *AuthRequestID) MarshalText() ([]byte, error) { + if d == nil { + return nil, nil + } + return []byte(d.String()), nil +} + +// UnmarshalText implements encoding.TextUnmarshaler interface +func (d *AuthRequestID) UnmarshalText(text []byte) (err error) { + *d, err = AuthRequestIDFrom(string(text)) + return +} + +// Ref returns true if a ID is nil or zero-value +func (d AuthRequestID) IsNil() bool { + return ID(d).IsNil() +} + +// AuthRequestIDToKeys converts IDs into a string slice. +func AuthRequestIDToKeys(ids []AuthRequestID) []string { + keys := make([]string, 0, len(ids)) + for _, i := range ids { + keys = append(keys, i.String()) + } + return keys +} + +// AuthRequestIDsFrom converts a string slice into a ID slice. +func AuthRequestIDsFrom(ids []string) ([]AuthRequestID, error) { + dids := make([]AuthRequestID, 0, len(ids)) + for _, i := range ids { + did, err := AuthRequestIDFrom(i) + if err != nil { + return nil, err + } + dids = append(dids, did) + } + return dids, nil +} + +// AuthRequestIDsFromID converts a generic ID slice into a ID slice. +func AuthRequestIDsFromID(ids []ID) []AuthRequestID { + dids := make([]AuthRequestID, 0, len(ids)) + for _, i := range ids { + dids = append(dids, AuthRequestID(i)) + } + return dids +} + +// AuthRequestIDsFromIDRef converts a ref of a generic ID slice into a ID slice. +func AuthRequestIDsFromIDRef(ids []*ID) []AuthRequestID { + dids := make([]AuthRequestID, 0, len(ids)) + for _, i := range ids { + if i != nil { + dids = append(dids, AuthRequestID(*i)) + } + } + return dids +} + +// AuthRequestIDsToID converts a ID slice into a generic ID slice. +func AuthRequestIDsToID(ids []AuthRequestID) []ID { + dids := make([]ID, 0, len(ids)) + for _, i := range ids { + dids = append(dids, i.ID()) + } + return dids +} + +// AuthRequestIDsToIDRef converts a ID ref slice into a generic ID ref slice. +func AuthRequestIDsToIDRef(ids []*AuthRequestID) []*ID { + dids := make([]*ID, 0, len(ids)) + for _, i := range ids { + dids = append(dids, i.IDRef()) + } + return dids +} + +// AuthRequestIDSet represents a set of AuthRequestIDs +type AuthRequestIDSet struct { + m map[AuthRequestID]struct{} + s []AuthRequestID +} + +// NewAuthRequestIDSet creates a new AuthRequestIDSet +func NewAuthRequestIDSet() *AuthRequestIDSet { + return &AuthRequestIDSet{} +} + +// Add adds a new ID if it does not exists in the set +func (s *AuthRequestIDSet) Add(p ...AuthRequestID) { + if s == nil || p == nil { + return + } + if s.m == nil { + s.m = map[AuthRequestID]struct{}{} + } + for _, i := range p { + if _, ok := s.m[i]; !ok { + if s.s == nil { + s.s = []AuthRequestID{} + } + s.m[i] = struct{}{} + s.s = append(s.s, i) + } + } +} + +// AddRef adds a new ID ref if it does not exists in the set +func (s *AuthRequestIDSet) AddRef(p *AuthRequestID) { + if s == nil || p == nil { + return + } + s.Add(*p) +} + +// Has checks if the ID exists in the set +func (s *AuthRequestIDSet) Has(p AuthRequestID) bool { + if s == nil || s.m == nil { + return false + } + _, ok := s.m[p] + return ok +} + +// Clear clears all stored IDs +func (s *AuthRequestIDSet) Clear() { + if s == nil { + return + } + s.m = nil + s.s = nil +} + +// All returns stored all IDs as a slice +func (s *AuthRequestIDSet) All() []AuthRequestID { + if s == nil { + return nil + } + return append([]AuthRequestID{}, s.s...) +} + +// Clone returns a cloned set +func (s *AuthRequestIDSet) Clone() *AuthRequestIDSet { + if s == nil { + return NewAuthRequestIDSet() + } + s2 := NewAuthRequestIDSet() + s2.Add(s.s...) + return s2 +} + +// Merge returns a merged set +func (s *AuthRequestIDSet) Merge(s2 *AuthRequestIDSet) *AuthRequestIDSet { + if s == nil { + return nil + } + s3 := s.Clone() + if s2 == nil { + return s3 + } + s3.Add(s2.s...) + return s3 +} diff --git a/pkg/id/auth_request_gen_test.go b/pkg/id/auth_request_gen_test.go new file mode 100644 index 00000000..5f84e759 --- /dev/null +++ b/pkg/id/auth_request_gen_test.go @@ -0,0 +1,1011 @@ +// Code generated by gen, DO NOT EDIT. + +package id + +import ( + "encoding/json" + "errors" + "testing" + + "github.com/oklog/ulid" + "github.com/stretchr/testify/assert" +) + +func TestNewAuthRequestID(t *testing.T) { + id := NewAuthRequestID() + assert.NotNil(t, id) + ulID, err := ulid.Parse(id.String()) + + assert.NotNil(t, ulID) + assert.Nil(t, err) +} + +func TestAuthRequestIDFrom(t *testing.T) { + t.Parallel() + testCases := []struct { + name string + input string + expected struct { + result AuthRequestID + err error + } + }{ + { + name: "Fail:Not valid string", + input: "testMustFail", + expected: struct { + result AuthRequestID + err error + }{ + AuthRequestID{}, + ErrInvalidID, + }, + }, + { + name: "Fail:Not valid string", + input: "", + expected: struct { + result AuthRequestID + err error + }{ + AuthRequestID{}, + ErrInvalidID, + }, + }, + { + name: "success:valid string", + input: "01f2r7kg1fvvffp0gmexgy5hxy", + expected: struct { + result AuthRequestID + err error + }{ + AuthRequestID{ulid.MustParse("01f2r7kg1fvvffp0gmexgy5hxy")}, + nil, + }, + }, + } + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + result, err := AuthRequestIDFrom(tc.input) + assert.Equal(tt, tc.expected.result, result) + if err != nil { + assert.True(tt, errors.As(tc.expected.err, &err)) + } + }) + } +} + +func TestMustAuthRequestID(t *testing.T) { + t.Parallel() + testCases := []struct { + name string + input string + shouldPanic bool + expected AuthRequestID + }{ + { + name: "Fail:Not valid string", + input: "testMustFail", + shouldPanic: true, + }, + { + name: "Fail:Not valid string", + input: "", + shouldPanic: true, + }, + { + name: "success:valid string", + input: "01f2r7kg1fvvffp0gmexgy5hxy", + shouldPanic: false, + expected: AuthRequestID{ulid.MustParse("01f2r7kg1fvvffp0gmexgy5hxy")}, + }, + } + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + + if tc.shouldPanic { + assert.Panics(tt, func() { MustBeID(tc.input) }) + return + } + result := MustAuthRequestID(tc.input) + assert.Equal(tt, tc.expected, result) + }) + } +} + +func TestAuthRequestIDFromRef(t *testing.T) { + testCases := []struct { + name string + input string + expected *AuthRequestID + }{ + { + name: "Fail:Not valid string", + input: "testMustFail", + expected: nil, + }, + { + name: "Fail:Not valid string", + input: "", + expected: nil, + }, + { + name: "success:valid string", + input: "01f2r7kg1fvvffp0gmexgy5hxy", + expected: &AuthRequestID{ulid.MustParse("01f2r7kg1fvvffp0gmexgy5hxy")}, + }, + } + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + result := AuthRequestIDFromRef(&tc.input) + assert.Equal(tt, tc.expected, result) + if tc.expected != nil { + assert.Equal(tt, *tc.expected, *result) + } + }) + } +} + +func TestAuthRequestIDFromRefID(t *testing.T) { + id := New() + + subId := AuthRequestIDFromRefID(&id) + + assert.NotNil(t, subId) + assert.Equal(t, subId.id, id.id) +} + +func TestAuthRequestID_ID(t *testing.T) { + id := New() + subId := AuthRequestIDFromRefID(&id) + + idOrg := subId.ID() + + assert.Equal(t, id, idOrg) +} + +func TestAuthRequestID_String(t *testing.T) { + id := New() + subId := AuthRequestIDFromRefID(&id) + + assert.Equal(t, subId.String(), id.String()) +} + +func TestAuthRequestID_GoString(t *testing.T) { + id := New() + subId := AuthRequestIDFromRefID(&id) + + assert.Equal(t, subId.GoString(), "id.AuthRequestID("+id.String()+")") +} + +func TestAuthRequestID_RefString(t *testing.T) { + id := New() + subId := AuthRequestIDFromRefID(&id) + + refString := subId.StringRef() + + assert.NotNil(t, refString) + assert.Equal(t, *refString, id.String()) +} + +func TestAuthRequestID_Ref(t *testing.T) { + id := New() + subId := AuthRequestIDFromRefID(&id) + + subIdRef := subId.Ref() + + assert.Equal(t, *subId, *subIdRef) +} + +func TestAuthRequestID_Contains(t *testing.T) { + id := NewAuthRequestID() + id2 := NewAuthRequestID() + assert.True(t, id.Contains([]AuthRequestID{id, id2})) + assert.False(t, id.Contains([]AuthRequestID{id2})) +} + +func TestAuthRequestID_CopyRef(t *testing.T) { + id := New() + subId := AuthRequestIDFromRefID(&id) + + subIdCopyRef := subId.CopyRef() + + assert.Equal(t, *subId, *subIdCopyRef) + assert.NotSame(t, subId, subIdCopyRef) +} + +func TestAuthRequestID_IDRef(t *testing.T) { + id := New() + subId := AuthRequestIDFromRefID(&id) + + assert.Equal(t, id, *subId.IDRef()) +} + +func TestAuthRequestID_StringRef(t *testing.T) { + id := New() + subId := AuthRequestIDFromRefID(&id) + + assert.Equal(t, *subId.StringRef(), id.String()) +} + +func TestAuthRequestID_MarhsalJSON(t *testing.T) { + id := New() + subId := AuthRequestIDFromRefID(&id) + + res, err := subId.MarhsalJSON() + exp, _ := json.Marshal(subId.String()) + + assert.Nil(t, err) + assert.Equal(t, exp, res) +} + +func TestAuthRequestID_UnmarhsalJSON(t *testing.T) { + jsonString := "\"01f3zhkysvcxsnzepyyqtq21fb\"" + + subId := &AuthRequestID{} + + err := subId.UnmarhsalJSON([]byte(jsonString)) + + assert.Nil(t, err) + assert.Equal(t, "01f3zhkysvcxsnzepyyqtq21fb", subId.String()) +} + +func TestAuthRequestID_MarshalText(t *testing.T) { + id := New() + subId := AuthRequestIDFromRefID(&id) + + res, err := subId.MarshalText() + + assert.Nil(t, err) + assert.Equal(t, []byte(id.String()), res) +} + +func TestAuthRequestID_UnmarshalText(t *testing.T) { + text := []byte("01f3zhcaq35403zdjnd6dcm0t2") + + subId := &AuthRequestID{} + + err := subId.UnmarshalText(text) + + assert.Nil(t, err) + assert.Equal(t, "01f3zhcaq35403zdjnd6dcm0t2", subId.String()) + +} + +func TestAuthRequestID_IsNil(t *testing.T) { + subId := AuthRequestID{} + + assert.True(t, subId.IsNil()) + + id := New() + subId = *AuthRequestIDFromRefID(&id) + + assert.False(t, subId.IsNil()) +} + +func TestAuthRequestIDToKeys(t *testing.T) { + t.Parallel() + testCases := []struct { + name string + input []AuthRequestID + expected []string + }{ + { + name: "Empty slice", + input: make([]AuthRequestID, 0), + expected: make([]string, 0), + }, + { + name: "1 element", + input: []AuthRequestID{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2")}, + expected: []string{"01f3zhcaq35403zdjnd6dcm0t2"}, + }, + { + name: "multiple elements", + input: []AuthRequestID{ + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t3"), + }, + expected: []string{ + "01f3zhcaq35403zdjnd6dcm0t1", + "01f3zhcaq35403zdjnd6dcm0t2", + "01f3zhcaq35403zdjnd6dcm0t3", + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + assert.Equal(tt, tc.expected, AuthRequestIDToKeys(tc.input)) + }) + } + +} + +func TestAuthRequestIDsFrom(t *testing.T) { + t.Parallel() + testCases := []struct { + name string + input []string + expected struct { + res []AuthRequestID + err error + } + }{ + { + name: "Empty slice", + input: make([]string, 0), + expected: struct { + res []AuthRequestID + err error + }{ + res: make([]AuthRequestID, 0), + err: nil, + }, + }, + { + name: "1 element", + input: []string{"01f3zhcaq35403zdjnd6dcm0t2"}, + expected: struct { + res []AuthRequestID + err error + }{ + res: []AuthRequestID{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2")}, + err: nil, + }, + }, + { + name: "multiple elements", + input: []string{ + "01f3zhcaq35403zdjnd6dcm0t1", + "01f3zhcaq35403zdjnd6dcm0t2", + "01f3zhcaq35403zdjnd6dcm0t3", + }, + expected: struct { + res []AuthRequestID + err error + }{ + res: []AuthRequestID{ + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t3"), + }, + err: nil, + }, + }, + { + name: "multiple elements", + input: []string{ + "01f3zhcaq35403zdjnd6dcm0t1", + "01f3zhcaq35403zdjnd6dcm0t2", + "01f3zhcaq35403zdjnd6dcm0t3", + }, + expected: struct { + res []AuthRequestID + err error + }{ + res: nil, + err: ErrInvalidID, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + + if tc.expected.err != nil { + _, err := AuthRequestIDsFrom(tc.input) + assert.True(tt, errors.As(ErrInvalidID, &err)) + } else { + res, err := AuthRequestIDsFrom(tc.input) + assert.Equal(tt, tc.expected.res, res) + assert.Nil(tt, err) + } + + }) + } +} + +func TestAuthRequestIDsFromID(t *testing.T) { + t.Parallel() + testCases := []struct { + name string + input []ID + expected []AuthRequestID + }{ + { + name: "Empty slice", + input: make([]ID, 0), + expected: make([]AuthRequestID, 0), + }, + { + name: "1 element", + input: []ID{MustBeID("01f3zhcaq35403zdjnd6dcm0t2")}, + expected: []AuthRequestID{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2")}, + }, + { + name: "multiple elements", + input: []ID{ + MustBeID("01f3zhcaq35403zdjnd6dcm0t1"), + MustBeID("01f3zhcaq35403zdjnd6dcm0t2"), + MustBeID("01f3zhcaq35403zdjnd6dcm0t3"), + }, + expected: []AuthRequestID{ + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t3"), + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + + res := AuthRequestIDsFromID(tc.input) + assert.Equal(tt, tc.expected, res) + }) + } +} + +func TestAuthRequestIDsFromIDRef(t *testing.T) { + t.Parallel() + + id1 := MustBeID("01f3zhcaq35403zdjnd6dcm0t1") + id2 := MustBeID("01f3zhcaq35403zdjnd6dcm0t2") + id3 := MustBeID("01f3zhcaq35403zdjnd6dcm0t3") + + testCases := []struct { + name string + input []*ID + expected []AuthRequestID + }{ + { + name: "Empty slice", + input: make([]*ID, 0), + expected: make([]AuthRequestID, 0), + }, + { + name: "1 element", + input: []*ID{&id1}, + expected: []AuthRequestID{MustAuthRequestID(id1.String())}, + }, + { + name: "multiple elements", + input: []*ID{&id1, &id2, &id3}, + expected: []AuthRequestID{ + MustAuthRequestID(id1.String()), + MustAuthRequestID(id2.String()), + MustAuthRequestID(id3.String()), + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + + res := AuthRequestIDsFromIDRef(tc.input) + assert.Equal(tt, tc.expected, res) + }) + } +} + +func TestAuthRequestIDsToID(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + input []AuthRequestID + expected []ID + }{ + { + name: "Empty slice", + input: make([]AuthRequestID, 0), + expected: make([]ID, 0), + }, + { + name: "1 element", + input: []AuthRequestID{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2")}, + expected: []ID{MustBeID("01f3zhcaq35403zdjnd6dcm0t2")}, + }, + { + name: "multiple elements", + input: []AuthRequestID{ + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t3"), + }, + expected: []ID{ + MustBeID("01f3zhcaq35403zdjnd6dcm0t1"), + MustBeID("01f3zhcaq35403zdjnd6dcm0t2"), + MustBeID("01f3zhcaq35403zdjnd6dcm0t3"), + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + + res := AuthRequestIDsToID(tc.input) + assert.Equal(tt, tc.expected, res) + }) + } +} + +func TestAuthRequestIDsToIDRef(t *testing.T) { + t.Parallel() + + id1 := MustBeID("01f3zhcaq35403zdjnd6dcm0t1") + subId1 := MustAuthRequestID(id1.String()) + id2 := MustBeID("01f3zhcaq35403zdjnd6dcm0t2") + subId2 := MustAuthRequestID(id2.String()) + id3 := MustBeID("01f3zhcaq35403zdjnd6dcm0t3") + subId3 := MustAuthRequestID(id3.String()) + + testCases := []struct { + name string + input []*AuthRequestID + expected []*ID + }{ + { + name: "Empty slice", + input: make([]*AuthRequestID, 0), + expected: make([]*ID, 0), + }, + { + name: "1 element", + input: []*AuthRequestID{&subId1}, + expected: []*ID{&id1}, + }, + { + name: "multiple elements", + input: []*AuthRequestID{&subId1, &subId2, &subId3}, + expected: []*ID{&id1, &id2, &id3}, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + + res := AuthRequestIDsToIDRef(tc.input) + assert.Equal(tt, tc.expected, res) + }) + } +} + +func TestNewAuthRequestIDSet(t *testing.T) { + AuthRequestIdSet := NewAuthRequestIDSet() + + assert.NotNil(t, AuthRequestIdSet) + assert.Empty(t, AuthRequestIdSet.m) + assert.Empty(t, AuthRequestIdSet.s) +} + +func TestAuthRequestIDSet_Add(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + input []AuthRequestID + expected *AuthRequestIDSet + }{ + { + name: "Empty slice", + input: make([]AuthRequestID, 0), + expected: &AuthRequestIDSet{ + m: map[AuthRequestID]struct{}{}, + s: nil, + }, + }, + { + name: "1 element", + input: []AuthRequestID{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1")}, + expected: &AuthRequestIDSet{ + m: map[AuthRequestID]struct{}{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"): {}}, + s: []AuthRequestID{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1")}, + }, + }, + { + name: "multiple elements", + input: []AuthRequestID{ + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t3"), + }, + expected: &AuthRequestIDSet{ + m: map[AuthRequestID]struct{}{ + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"): {}, + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2"): {}, + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t3"): {}, + }, + s: []AuthRequestID{ + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t3"), + }, + }, + }, + { + name: "multiple elements with duplication", + input: []AuthRequestID{ + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t3"), + }, + expected: &AuthRequestIDSet{ + m: map[AuthRequestID]struct{}{ + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"): {}, + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t3"): {}, + }, + s: []AuthRequestID{ + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t3"), + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + + set := NewAuthRequestIDSet() + set.Add(tc.input...) + assert.Equal(tt, tc.expected, set) + }) + } +} + +func TestAuthRequestIDSet_AddRef(t *testing.T) { + t.Parallel() + + AuthRequestId := MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1") + + testCases := []struct { + name string + input *AuthRequestID + expected *AuthRequestIDSet + }{ + { + name: "Empty slice", + input: nil, + expected: &AuthRequestIDSet{ + m: nil, + s: nil, + }, + }, + { + name: "1 element", + input: &AuthRequestId, + expected: &AuthRequestIDSet{ + m: map[AuthRequestID]struct{}{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"): {}}, + s: []AuthRequestID{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1")}, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + + set := NewAuthRequestIDSet() + set.AddRef(tc.input) + assert.Equal(tt, tc.expected, set) + }) + } +} + +func TestAuthRequestIDSet_Has(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + input struct { + AuthRequestIDSet + AuthRequestID + } + expected bool + }{ + { + name: "Empty Set", + input: struct { + AuthRequestIDSet + AuthRequestID + }{AuthRequestIDSet: AuthRequestIDSet{}, AuthRequestID: MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1")}, + expected: false, + }, + { + name: "Set Contains the element", + input: struct { + AuthRequestIDSet + AuthRequestID + }{AuthRequestIDSet: AuthRequestIDSet{ + m: map[AuthRequestID]struct{}{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"): {}}, + s: []AuthRequestID{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1")}, + }, AuthRequestID: MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1")}, + expected: true, + }, + { + name: "Set does not Contains the element", + input: struct { + AuthRequestIDSet + AuthRequestID + }{AuthRequestIDSet: AuthRequestIDSet{ + m: map[AuthRequestID]struct{}{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"): {}}, + s: []AuthRequestID{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1")}, + }, AuthRequestID: MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2")}, + expected: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + assert.Equal(tt, tc.expected, tc.input.AuthRequestIDSet.Has(tc.input.AuthRequestID)) + }) + } +} + +func TestAuthRequestIDSet_Clear(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + input AuthRequestIDSet + expected AuthRequestIDSet + }{ + { + name: "Empty Set", + input: AuthRequestIDSet{}, + expected: AuthRequestIDSet{ + m: nil, + s: nil, + }, + }, + { + name: "Set Contains the element", + input: AuthRequestIDSet{ + m: map[AuthRequestID]struct{}{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"): {}}, + s: []AuthRequestID{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1")}, + }, + expected: AuthRequestIDSet{ + m: nil, + s: nil, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + set := tc.input + p := &set + p.Clear() + assert.Equal(tt, tc.expected, *p) + }) + } +} + +func TestAuthRequestIDSet_All(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + input *AuthRequestIDSet + expected []AuthRequestID + }{ + { + name: "Empty slice", + input: &AuthRequestIDSet{ + m: map[AuthRequestID]struct{}{}, + s: nil, + }, + expected: make([]AuthRequestID, 0), + }, + { + name: "1 element", + input: &AuthRequestIDSet{ + m: map[AuthRequestID]struct{}{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"): {}}, + s: []AuthRequestID{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1")}, + }, + expected: []AuthRequestID{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1")}, + }, + { + name: "multiple elements", + input: &AuthRequestIDSet{ + m: map[AuthRequestID]struct{}{ + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"): {}, + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2"): {}, + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t3"): {}, + }, + s: []AuthRequestID{ + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t3"), + }, + }, + expected: []AuthRequestID{ + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t3"), + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + + assert.Equal(tt, tc.expected, tc.input.All()) + }) + } +} + +func TestAuthRequestIDSet_Clone(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + input *AuthRequestIDSet + expected *AuthRequestIDSet + }{ + { + name: "nil set", + input: nil, + expected: NewAuthRequestIDSet(), + }, + { + name: "Empty set", + input: NewAuthRequestIDSet(), + expected: NewAuthRequestIDSet(), + }, + { + name: "1 element", + input: &AuthRequestIDSet{ + m: map[AuthRequestID]struct{}{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"): {}}, + s: []AuthRequestID{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1")}, + }, + expected: &AuthRequestIDSet{ + m: map[AuthRequestID]struct{}{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"): {}}, + s: []AuthRequestID{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1")}, + }, + }, + { + name: "multiple elements", + input: &AuthRequestIDSet{ + m: map[AuthRequestID]struct{}{ + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"): {}, + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2"): {}, + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t3"): {}, + }, + s: []AuthRequestID{ + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t3"), + }, + }, + expected: &AuthRequestIDSet{ + m: map[AuthRequestID]struct{}{ + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"): {}, + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2"): {}, + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t3"): {}, + }, + s: []AuthRequestID{ + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t3"), + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + clone := tc.input.Clone() + assert.Equal(tt, tc.expected, clone) + assert.False(tt, tc.input == clone) + }) + } +} + +func TestAuthRequestIDSet_Merge(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + input struct { + a *AuthRequestIDSet + b *AuthRequestIDSet + } + expected *AuthRequestIDSet + }{ + { + name: "Empty Set", + input: struct { + a *AuthRequestIDSet + b *AuthRequestIDSet + }{ + a: &AuthRequestIDSet{}, + b: &AuthRequestIDSet{}, + }, + expected: &AuthRequestIDSet{}, + }, + { + name: "1 Empty Set", + input: struct { + a *AuthRequestIDSet + b *AuthRequestIDSet + }{ + a: &AuthRequestIDSet{ + m: map[AuthRequestID]struct{}{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"): {}}, + s: []AuthRequestID{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1")}, + }, + b: &AuthRequestIDSet{}, + }, + expected: &AuthRequestIDSet{ + m: map[AuthRequestID]struct{}{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"): {}}, + s: []AuthRequestID{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1")}, + }, + }, + { + name: "2 non Empty Set", + input: struct { + a *AuthRequestIDSet + b *AuthRequestIDSet + }{ + a: &AuthRequestIDSet{ + m: map[AuthRequestID]struct{}{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"): {}}, + s: []AuthRequestID{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1")}, + }, + b: &AuthRequestIDSet{ + m: map[AuthRequestID]struct{}{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2"): {}}, + s: []AuthRequestID{MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2")}, + }, + }, + expected: &AuthRequestIDSet{ + m: map[AuthRequestID]struct{}{ + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"): {}, + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2"): {}, + }, + s: []AuthRequestID{ + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t1"), + MustAuthRequestID("01f3zhcaq35403zdjnd6dcm0t2"), + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + + assert.Equal(tt, tc.expected, tc.input.a.Merge(tc.input.b)) + }) + } +} diff --git a/pkg/id/gen.go b/pkg/id/gen.go index 7c4519f8..0fb71558 100644 --- a/pkg/id/gen.go +++ b/pkg/id/gen.go @@ -11,6 +11,7 @@ //go:generate go run github.com/reearth/reearth-backend/tools/cmd/gen --template=id.tmpl --output=user_gen.go --name=User //go:generate go run github.com/reearth/reearth-backend/tools/cmd/gen --template=id.tmpl --output=dataset_schema_field_gen.go --name=DatasetSchemaField //go:generate go run github.com/reearth/reearth-backend/tools/cmd/gen --template=id.tmpl --output=infobox_field_gen.go --name=InfoboxField +//go:generate go run github.com/reearth/reearth-backend/tools/cmd/gen --template=id.tmpl --output=auth_request_gen.go --name=AuthRequest //go:generate go run github.com/reearth/reearth-backend/tools/cmd/gen --template=id.tmpl --output=tag_gen.go --name=Tag //go:generate go run github.com/reearth/reearth-backend/tools/cmd/gen --template=id.tmpl --output=cluster_gen.go --name=Cluster @@ -29,6 +30,7 @@ //go:generate go run github.com/reearth/reearth-backend/tools/cmd/gen --template=id_test.tmpl --output=user_gen_test.go --name=User //go:generate go run github.com/reearth/reearth-backend/tools/cmd/gen --template=id_test.tmpl --output=dataset_schema_field_gen_test.go --name=DatasetSchemaField //go:generate go run github.com/reearth/reearth-backend/tools/cmd/gen --template=id_test.tmpl --output=infobox_field_gen_test.go --name=InfoboxField +//go:generate go run github.com/reearth/reearth-backend/tools/cmd/gen --template=id_test.tmpl --output=auth_request_gen_test.go --name=AuthRequest //go:generate go run github.com/reearth/reearth-backend/tools/cmd/gen --template=id_test.tmpl --output=cluster_field_gen_test.go --name=Cluster package id diff --git a/pkg/log/log.go b/pkg/log/log.go index 23ed1c84..a42a6f00 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -102,3 +102,7 @@ func Errorln(args ...interface{}) { func Fatalln(args ...interface{}) { logrus.Fatalln(args...) } + +func Panicf(format string, args ...interface{}) { + logrus.Panicf(format, args...) +} diff --git a/pkg/tag/list_test.go b/pkg/tag/list_test.go index 59208d91..bf4775bb 100644 --- a/pkg/tag/list_test.go +++ b/pkg/tag/list_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TesIDtList_Add(t *testing.T) { +func TestIDtList_Add(t *testing.T) { tid := NewID() var tl *IDList tl.Add(tid) diff --git a/pkg/user/auth.go b/pkg/user/auth.go index 45ac7fd0..f68ab615 100644 --- a/pkg/user/auth.go +++ b/pkg/user/auth.go @@ -1,6 +1,8 @@ package user -import "strings" +import ( + "strings" +) type Auth struct { Provider string @@ -18,3 +20,15 @@ func AuthFromAuth0Sub(sub string) Auth { func (a Auth) IsAuth0() bool { return a.Provider == "auth0" } + +func (a Auth) Ref() *Auth { + a2 := a + return &a2 +} + +func GenReearthSub(userID string) *Auth { + return &Auth{ + Provider: "reearth", + Sub: "reearth|" + userID, + } +} diff --git a/pkg/user/auth_test.go b/pkg/user/auth_test.go index 8719ede1..ff9d7b97 100644 --- a/pkg/user/auth_test.go +++ b/pkg/user/auth_test.go @@ -3,6 +3,8 @@ package user import ( "testing" + "github.com/reearth/reearth-backend/pkg/id" + "github.com/stretchr/testify/assert" ) @@ -67,3 +69,28 @@ func TestAuth_IsAuth0(t *testing.T) { }) } } + +func TestGenReearthSub(t *testing.T) { + uid := id.NewUserID() + + tests := []struct { + name string + input string + want *Auth + }{ + { + name: "should return reearth sub", + input: uid.String(), + want: &Auth{ + Provider: "reearth", + Sub: "reearth|" + uid.String(), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GenReearthSub(tt.input) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/user/builder.go b/pkg/user/builder.go index e822fb35..264eba11 100644 --- a/pkg/user/builder.go +++ b/pkg/user/builder.go @@ -5,7 +5,8 @@ import ( ) type Builder struct { - u *User + u *User + passwordText string } func New() *Builder { @@ -16,6 +17,11 @@ func (b *Builder) Build() (*User, error) { if b.u.id.IsNil() { return nil, ErrInvalidID } + if b.passwordText != "" { + if err := b.u.SetPassword(b.passwordText); err != nil { + return nil, ErrEncodingPassword + } + } return b.u, nil } @@ -47,6 +53,20 @@ func (b *Builder) Email(email string) *Builder { return b } +func (b *Builder) Password(p []byte) *Builder { + if p == nil { + b.u.password = nil + } else { + b.u.password = append(p[:0:0], p...) + } + return b +} + +func (b *Builder) PasswordPlainText(p string) *Builder { + b.passwordText = p + return b +} + func (b *Builder) Team(team TeamID) *Builder { b.u.team = team return b @@ -75,3 +95,13 @@ func (b *Builder) Auths(auths []Auth) *Builder { b.u.auths = append([]Auth{}, auths...) return b } + +func (b *Builder) PasswordReset(pr *PasswordReset) *Builder { + b.u.passwordReset = pr + return b +} + +func (b *Builder) Verification(v *Verification) *Builder { + b.u.verification = v + return b +} diff --git a/pkg/user/builder_test.go b/pkg/user/builder_test.go index 8699ce53..dc02a227 100644 --- a/pkg/user/builder_test.go +++ b/pkg/user/builder_test.go @@ -2,6 +2,7 @@ package user import ( "testing" + "time" "github.com/stretchr/testify/assert" "golang.org/x/text/language" @@ -11,6 +12,7 @@ func TestBuilder_ID(t *testing.T) { uid := NewID() b := New().ID(uid).MustBuild() assert.Equal(t, uid, b.ID()) + assert.Nil(t, b.passwordReset) } func TestBuilder_Name(t *testing.T) { @@ -87,6 +89,31 @@ func TestBuilder_LangFrom(t *testing.T) { } } +func TestBuilder_PasswordReset(t *testing.T) { + testCases := []struct { + Name, Token string + CreatedAt time.Time + Expected PasswordReset + }{ + { + Name: "Test1", + Token: "xyz", + CreatedAt: time.Unix(0, 0), + Expected: PasswordReset{ + Token: "xyz", + CreatedAt: time.Unix(0, 0), + }, + }, + } + for _, tc := range testCases { + t.Run(tc.Name, func(tt *testing.T) { + tt.Parallel() + // u := New().NewID().PasswordReset(tc.Token, tc.CreatedAt).MustBuild() + // assert.Equal(t, tc.Expected, *u.passwordReset) + }) + } +} + func TestNew(t *testing.T) { b := New() assert.NotNil(t, b) @@ -97,12 +124,14 @@ func TestBuilder_Build(t *testing.T) { uid := NewID() tid := NewTeamID() en, _ := language.Parse("en") + pass, _ := encodePassword("pass") type args struct { Name, Lang, Email string ID ID Team TeamID Auths []Auth + PasswordBin []byte } tests := []struct { @@ -114,11 +143,12 @@ func TestBuilder_Build(t *testing.T) { { Name: "Success build user", Args: args{ - Name: "xxx", - Email: "xx@yy.zz", - Lang: "en", - ID: uid, - Team: tid, + Name: "xxx", + Email: "xx@yy.zz", + Lang: "en", + ID: uid, + Team: tid, + PasswordBin: pass, Auths: []Auth{ { Provider: "ppp", @@ -127,16 +157,18 @@ func TestBuilder_Build(t *testing.T) { }, }, Expected: &User{ - id: uid, - team: tid, - email: "xx@yy.zz", - name: "xxx", - auths: []Auth{{Provider: "ppp", Sub: "sss"}}, - lang: en, + id: uid, + team: tid, + email: "xx@yy.zz", + name: "xxx", + password: pass, + auths: []Auth{{Provider: "ppp", Sub: "sss"}}, + lang: en, }, }, { - Name: "failed invalid id", - Err: ErrInvalidID, + Name: "failed invalid id", + Expected: nil, + Err: ErrInvalidID, }, } @@ -146,6 +178,7 @@ func TestBuilder_Build(t *testing.T) { t.Parallel() res, err := New(). ID(tt.Args.ID). + Password(pass). Name(tt.Args.Name). Auths(tt.Args.Auths). LangFrom(tt.Args.Lang). @@ -165,11 +198,13 @@ func TestBuilder_MustBuild(t *testing.T) { uid := NewID() tid := NewTeamID() en, _ := language.Parse("en") + pass, _ := encodePassword("pass") type args struct { Name, Lang, Email string ID ID Team TeamID + PasswordBin []byte Auths []Auth } @@ -182,11 +217,12 @@ func TestBuilder_MustBuild(t *testing.T) { { Name: "Success build user", Args: args{ - Name: "xxx", - Email: "xx@yy.zz", - Lang: "en", - ID: uid, - Team: tid, + Name: "xxx", + Email: "xx@yy.zz", + Lang: "en", + ID: uid, + Team: tid, + PasswordBin: pass, Auths: []Auth{ { Provider: "ppp", @@ -195,12 +231,13 @@ func TestBuilder_MustBuild(t *testing.T) { }, }, Expected: &User{ - id: uid, - team: tid, - email: "xx@yy.zz", - name: "xxx", - auths: []Auth{{Provider: "ppp", Sub: "sss"}}, - lang: en, + id: uid, + team: tid, + email: "xx@yy.zz", + name: "xxx", + password: pass, + auths: []Auth{{Provider: "ppp", Sub: "sss"}}, + lang: en, }, }, { Name: "failed invalid id", @@ -217,6 +254,7 @@ func TestBuilder_MustBuild(t *testing.T) { t.Helper() return New(). ID(tt.Args.ID). + Password(pass). Name(tt.Args.Name). Auths(tt.Args.Auths). LangFrom(tt.Args.Lang). @@ -233,3 +271,37 @@ func TestBuilder_MustBuild(t *testing.T) { }) } } + +func TestBuilder_Verification(t *testing.T) { + tests := []struct { + name string + input *Verification + want *Builder + }{ + { + name: "should return verification", + input: &Verification{ + verified: true, + code: "xxx", + expiration: time.Time{}, + }, + + want: &Builder{ + u: &User{ + verification: &Verification{ + verified: true, + code: "xxx", + expiration: time.Time{}, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := New() + b.Verification(tt.input) + assert.Equal(t, tt.want, b) + }) + } +} diff --git a/pkg/user/initializer.go b/pkg/user/initializer.go index a50d7229..eadd1756 100644 --- a/pkg/user/initializer.go +++ b/pkg/user/initializer.go @@ -7,7 +7,8 @@ import ( type InitParams struct { Email string Name string - Auth0Sub string + Sub *Auth + Password string Lang *language.Tag Theme *Theme UserID *ID @@ -28,13 +29,17 @@ func Init(p InitParams) (*User, *Team, error) { t := ThemeDefault p.Theme = &t } + if p.Sub == nil { + p.Sub = GenReearthSub(p.UserID.String()) + } u, err := New(). ID(*p.UserID). Name(p.Name). Email(p.Email). - Auths([]Auth{AuthFromAuth0Sub(p.Auth0Sub)}). + Auths([]Auth{*p.Sub}). Lang(*p.Lang). + PasswordPlainText(p.Password). Theme(*p.Theme). Build() if err != nil { diff --git a/pkg/user/initializer_test.go b/pkg/user/initializer_test.go index 17b24260..f99c5e30 100644 --- a/pkg/user/initializer_test.go +++ b/pkg/user/initializer_test.go @@ -9,28 +9,35 @@ import ( func TestInit(t *testing.T) { uid := NewID() tid := NewTeamID() - + expectedSub := Auth{ + Provider: "###", + Sub: "###", + } tests := []struct { - Name, Email, Username, Sub string - UID *ID - TID *TeamID - ExpectedUser *User - ExpectedTeam *Team - Err error + Name, Email, Username string + Sub Auth + UID *ID + TID *TeamID + ExpectedUser *User + ExpectedTeam *Team + Err error }{ { Name: "Success create user", Email: "xx@yy.zz", Username: "nnn", - Sub: "###", - UID: &uid, - TID: &tid, + Sub: Auth{ + Provider: "###", + Sub: "###", + }, + UID: &uid, + TID: &tid, ExpectedUser: New(). ID(uid). Email("xx@yy.zz"). Name("nnn"). Team(tid). - Auths([]Auth{AuthFromAuth0Sub("###")}). + Auths([]Auth{expectedSub}). MustBuild(), ExpectedTeam: NewTeam(). ID(tid). @@ -44,15 +51,18 @@ func TestInit(t *testing.T) { Name: "Success nil team id", Email: "xx@yy.zz", Username: "nnn", - Sub: "###", - UID: &uid, - TID: nil, + Sub: Auth{ + Provider: "###", + Sub: "###", + }, + UID: &uid, + TID: nil, ExpectedUser: New(). ID(uid). Email("xx@yy.zz"). Name("nnn"). Team(tid). - Auths([]Auth{AuthFromAuth0Sub("###")}). + Auths([]Auth{expectedSub}). MustBuild(), ExpectedTeam: NewTeam(). NewID(). @@ -66,15 +76,18 @@ func TestInit(t *testing.T) { Name: "Success nil id", Email: "xx@yy.zz", Username: "nnn", - Sub: "###", - UID: nil, - TID: &tid, + Sub: Auth{ + Provider: "###", + Sub: "###", + }, + UID: nil, + TID: &tid, ExpectedUser: New(). NewID(). Email("xx@yy.zz"). Name("nnn"). Team(tid). - Auths([]Auth{AuthFromAuth0Sub("###")}). + Auths([]Auth{expectedSub}). MustBuild(), ExpectedTeam: NewTeam(). ID(tid). @@ -85,17 +98,16 @@ func TestInit(t *testing.T) { Err: nil, }, } - for _, tt := range tests { tt := tt t.Run(tt.Name, func(t *testing.T) { t.Parallel() user, team, err := Init(InitParams{ - Email: tt.Email, - Name: tt.Username, - Auth0Sub: tt.Sub, - UserID: tt.UID, - TeamID: tt.TID, + Email: tt.Email, + Name: tt.Username, + Sub: &tt.Sub, + UserID: tt.UID, + TeamID: tt.TID, }) if tt.Err == nil { assert.Equal(t, tt.ExpectedUser.Email(), user.Email()) diff --git a/pkg/user/password_reset.go b/pkg/user/password_reset.go new file mode 100644 index 00000000..6ec20872 --- /dev/null +++ b/pkg/user/password_reset.go @@ -0,0 +1,44 @@ +package user + +import ( + "time" + + "github.com/google/uuid" +) + +var timeNow = time.Now + +type PasswordReset struct { + Token string + CreatedAt time.Time +} + +func NewPasswordReset() *PasswordReset { + return &PasswordReset{ + Token: generateToken(), + CreatedAt: timeNow(), + } +} + +func PasswordResetFrom(token string, createdAt time.Time) *PasswordReset { + return &PasswordReset{ + Token: token, + CreatedAt: createdAt, + } +} + +func generateToken() string { + return uuid.New().String() +} + +func (pr *PasswordReset) Validate(token string) bool { + return pr != nil && pr.Token == token && pr.CreatedAt.Add(24*time.Hour).After(time.Now()) +} + +func (pr *PasswordReset) Clone() *PasswordReset { + if pr == nil { + return nil + } + pr2 := PasswordResetFrom(pr.Token, pr.CreatedAt) + return pr2 +} diff --git a/pkg/user/password_reset_test.go b/pkg/user/password_reset_test.go new file mode 100644 index 00000000..253a7b92 --- /dev/null +++ b/pkg/user/password_reset_test.go @@ -0,0 +1,103 @@ +package user + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestNewPasswordReset(t *testing.T) { + mockTime := time.Now() + timeNow = func() time.Time { + return mockTime + } + pr := NewPasswordReset() + assert.NotNil(t, pr) + assert.NotEmpty(t, pr.Token) + assert.Equal(t, mockTime, pr.CreatedAt) +} + +func TestPasswordReset_Validate(t *testing.T) { + tests := []struct { + name string + pr *PasswordReset + token string + want bool + }{ + { + name: "valid", + pr: &PasswordReset{ + Token: "xyz", + CreatedAt: time.Now(), + }, + token: "xyz", + want: true, + }, + { + name: "wrong token", + pr: &PasswordReset{ + Token: "xyz", + CreatedAt: time.Now(), + }, + token: "xxx", + want: false, + }, + { + name: "old request", + pr: &PasswordReset{ + Token: "xyz", + CreatedAt: time.Now().Add(-24 * time.Hour), + }, + token: "xyz", + want: false, + }, + { + name: "nil request", + pr: nil, + token: "xyz", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.pr.Validate(tt.token)) + }) + } +} + +func Test_generateToken(t *testing.T) { + t1 := generateToken() + t2 := generateToken() + + assert.NotNil(t, t1) + assert.NotNil(t, t2) + assert.NotEmpty(t, t1) + assert.NotEmpty(t, t2) + assert.NotEqual(t, t1, t2) + +} + +func TestPasswordResetFrom(t *testing.T) { + tests := []struct { + name string + token string + createdAt time.Time + want *PasswordReset + }{ + { + name: "prFrom", + token: "xyz", + createdAt: time.Unix(1, 1), + want: &PasswordReset{ + Token: "xyz", + CreatedAt: time.Unix(1, 1), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, PasswordResetFrom(tt.token, tt.createdAt)) + }) + } +} diff --git a/pkg/user/user.go b/pkg/user/user.go index 5277999d..c3dc7fd9 100644 --- a/pkg/user/user.go +++ b/pkg/user/user.go @@ -1,17 +1,34 @@ package user import ( + "errors" + "unicode" + + "golang.org/x/crypto/bcrypt" + "golang.org/x/text/language" ) +var ( + ErrEncodingPassword = errors.New("error encoding password") + ErrInvalidPassword = errors.New("error invalid password") + ErrPasswordLength = errors.New("password at least 8 characters") + ErrPasswordUpper = errors.New("password should have upper case letters") + ErrPasswordLower = errors.New("password should have lower case letters") + ErrPasswordNumber = errors.New("password should have numbers") +) + type User struct { - id ID - name string - email string - team TeamID - auths []Auth - lang language.Tag - theme Theme + id ID + name string + email string + password []byte + team TeamID + auths []Auth + lang language.Tag + theme Theme + verification *Verification + passwordReset *PasswordReset } func (u *User) ID() ID { @@ -38,6 +55,10 @@ func (u *User) Theme() Theme { return u.theme } +func (u *User) Password() []byte { + return u.password +} + func (u *User) UpdateName(name string) { u.name = name } @@ -58,6 +79,10 @@ func (u *User) UpdateTheme(t Theme) { u.theme = t } +func (u *User) Verification() *Verification { + return u.verification +} + func (u *User) Auths() []Auth { if u == nil { return nil @@ -101,6 +126,18 @@ func (u *User) RemoveAuth(a Auth) bool { return false } +func (u *User) GetAuthByProvider(provider string) *Auth { + if u == nil || u.auths == nil { + return nil + } + for _, b := range u.auths { + if provider == b.Provider { + return &b + } + } + return nil +} + func (u *User) RemoveAuthByProvider(provider string) bool { if u == nil || provider == "auth0" { return false @@ -117,3 +154,78 @@ func (u *User) RemoveAuthByProvider(provider string) bool { func (u *User) ClearAuths() { u.auths = []Auth{} } + +func (u *User) SetPassword(pass string) error { + if err := validatePassword(pass); err != nil { + return err + } + p, err := encodePassword(pass) + if err != nil { + return err + } + u.password = p + return nil +} + +func (u *User) MatchPassword(pass string) (bool, error) { + if u == nil || len(u.password) == 0 { + return false, nil + } + return verifyPassword(pass, u.password) +} + +func encodePassword(pass string) ([]byte, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(pass), 14) + return bytes, err +} + +func verifyPassword(toVerify string, encoded []byte) (bool, error) { + err := bcrypt.CompareHashAndPassword(encoded, []byte(toVerify)) + if err != nil { + if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { + return false, nil + } + return false, err + } + return true, nil +} + +func (u *User) PasswordReset() *PasswordReset { + return u.passwordReset +} + +func (u *User) SetPasswordReset(pr *PasswordReset) { + u.passwordReset = pr.Clone() +} + +func (u *User) SetVerification(v *Verification) { + u.verification = v +} + +func validatePassword(pass string) error { + var hasNum, hasUpper, hasLower bool + for _, c := range pass { + switch { + case unicode.IsNumber(c): + hasNum = true + case unicode.IsUpper(c): + hasUpper = true + case unicode.IsLower(c) || c == ' ': + hasLower = true + } + } + if len(pass) < 8 { + return ErrPasswordLength + } + if !hasLower { + return ErrPasswordLower + } + if !hasUpper { + return ErrPasswordUpper + } + if !hasNum { + return ErrPasswordNumber + } + + return nil +} diff --git a/pkg/user/user_test.go b/pkg/user/user_test.go index 7e2e80bc..008e2f8f 100644 --- a/pkg/user/user_test.go +++ b/pkg/user/user_test.go @@ -2,6 +2,7 @@ package user import ( "testing" + "time" "github.com/stretchr/testify/assert" "golang.org/x/text/language" @@ -281,3 +282,298 @@ func TestUser_UpdateName(t *testing.T) { u.UpdateName("xxx") assert.Equal(t, "xxx", u.Name()) } + +func TestUser_GetAuthByProvider(t *testing.T) { + testCases := []struct { + Name string + User *User + Provider string + Expected *Auth + }{ + { + Name: "existing auth", + User: New().NewID().Auths([]Auth{{ + Provider: "xxx", + Sub: "zzz", + }}).MustBuild(), + Provider: "xxx", + Expected: &Auth{ + Provider: "xxx", + Sub: "zzz", + }, + }, + { + Name: "not existing auth", + User: New().NewID().Auths([]Auth{{ + Provider: "xxx", + Sub: "zzz", + }}).MustBuild(), + Provider: "yyy", + Expected: nil, + }, + } + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(tt *testing.T) { + tt.Parallel() + res := tc.User.GetAuthByProvider(tc.Provider) + assert.Equal(tt, tc.Expected, res) + }) + } +} + +func TestUser_MatchPassword(t *testing.T) { + encodedPass, _ := encodePassword("test") + type args struct { + pass string + } + tests := []struct { + name string + password []byte + args args + want bool + wantErr bool + }{ + { + name: "passwords should match", + password: encodedPass, + args: args{ + pass: "test", + }, + want: true, + wantErr: false, + }, + { + name: "passwords shouldn't match", + password: encodedPass, + args: args{ + pass: "xxx", + }, + want: false, + wantErr: false, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(tt *testing.T) { + u := &User{ + password: tc.password, + } + got, err := u.MatchPassword(tc.args.pass) + assert.Equal(tt, tc.want, got) + if tc.wantErr { + assert.Error(tt, err) + } else { + assert.NoError(tt, err) + } + }) + } +} + +func TestUser_SetPassword(t *testing.T) { + type args struct { + pass string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "should set non-latin characters password", + args: args{ + pass: "Àêîôûtest1", + }, + want: "Àêîôûtest1", + }, + { + name: "should set latin characters password", + args: args{ + pass: "Testabc1", + }, + want: "Testabc1", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(tt *testing.T) { + u := &User{} + _ = u.SetPassword(tc.args.pass) + got, err := verifyPassword(tc.want, u.password) + assert.NoError(tt, err) + assert.True(tt, got) + }) + } +} + +func TestUser_PasswordReset(t *testing.T) { + testCases := []struct { + Name string + User *User + Expected *PasswordReset + }{ + { + Name: "not password request", + User: New().NewID().MustBuild(), + Expected: nil, + }, + { + Name: "create new password request over existing one", + User: New().NewID().PasswordReset(&PasswordReset{"xzy", time.Unix(0, 0)}).MustBuild(), + Expected: &PasswordReset{ + Token: "xzy", + CreatedAt: time.Unix(0, 0), + }, + }, + } + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(tt *testing.T) { + tt.Parallel() + assert.Equal(tt, tc.Expected, tc.User.PasswordReset()) + }) + } +} + +func TestUser_SetPasswordReset(t *testing.T) { + tests := []struct { + Name string + User *User + Pr *PasswordReset + Expected *PasswordReset + }{ + { + Name: "nil", + User: New().NewID().MustBuild(), + Pr: nil, + Expected: nil, + }, + { + Name: "nil", + User: New().NewID().MustBuild(), + Pr: &PasswordReset{ + Token: "xyz", + CreatedAt: time.Unix(1, 1), + }, + Expected: &PasswordReset{ + Token: "xyz", + CreatedAt: time.Unix(1, 1), + }, + }, + { + Name: "create new password request", + User: New().NewID().MustBuild(), + Pr: &PasswordReset{ + Token: "xyz", + CreatedAt: time.Unix(1, 1), + }, + Expected: &PasswordReset{ + Token: "xyz", + CreatedAt: time.Unix(1, 1), + }, + }, + { + Name: "create new password request over existing one", + User: New().NewID().PasswordReset(&PasswordReset{"xzy", time.Now()}).MustBuild(), + Pr: &PasswordReset{ + Token: "xyz", + CreatedAt: time.Unix(1, 1), + }, + Expected: &PasswordReset{ + Token: "xyz", + CreatedAt: time.Unix(1, 1), + }, + }, + { + Name: "remove none existing password request", + User: New().NewID().MustBuild(), + Pr: nil, + Expected: nil, + }, + { + Name: "remove existing password request", + User: New().NewID().PasswordReset(&PasswordReset{"xzy", time.Now()}).MustBuild(), + Pr: nil, + Expected: nil, + }, + } + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + tt.User.SetPasswordReset(tt.Pr) + assert.Equal(t, tt.Expected, tt.User.PasswordReset()) + }) + } +} + +func TestUser_SetVerification(t *testing.T) { + input := &User{} + v := &Verification{ + verified: false, + code: "xxx", + expiration: time.Time{}, + } + input.SetVerification(v) + assert.Equal(t, v, input.verification) +} + +func TestUser_Verification(t *testing.T) { + v := NewVerification() + tests := []struct { + name string + verification *Verification + want *Verification + }{ + { + name: "should return the same verification", + verification: v, + want: v, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u := &User{ + verification: tt.verification, + } + assert.Equal(t, tt.want, u.Verification()) + }) + } +} + +func Test_ValidatePassword(t *testing.T) { + + tests := []struct { + name string + pass string + wantErr bool + }{ + { + name: "should pass", + pass: "Abcdafgh1", + wantErr: false, + }, + { + name: "shouldn't pass: length<8", + pass: "Aafgh1", + wantErr: true, + }, + { + name: "shouldn't pass: don't have numbers", + pass: "Abcdefghi", + wantErr: true, + }, + { + name: "shouldn't pass: don't have upper", + pass: "abcdefghi1", + wantErr: true, + }, + { + name: "shouldn't pass: don't have lower", + pass: "ABCDEFGHI1", + wantErr: true, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(tt *testing.T) { + out := validatePassword(tc.pass) + assert.Equal(tt, out != nil, tc.wantErr) + }) + } +} diff --git a/pkg/user/verification.go b/pkg/user/verification.go new file mode 100644 index 00000000..2b7215f0 --- /dev/null +++ b/pkg/user/verification.go @@ -0,0 +1,71 @@ +package user + +import ( + "time" + + uuid "github.com/google/uuid" +) + +type Verification struct { + verified bool + code string + expiration time.Time +} + +func (v *Verification) IsVerified() bool { + if v == nil { + return false + } + return v.verified +} + +func (v *Verification) Code() string { + if v == nil { + return "" + } + return v.code +} + +func (v *Verification) Expiration() time.Time { + if v == nil { + return time.Time{} + } + return v.expiration +} + +func generateCode() string { + return uuid.NewString() +} + +func (v *Verification) IsExpired() bool { + if v == nil { + return true + } + now := time.Now() + return now.After(v.expiration) +} + +func (v *Verification) SetVerified(b bool) { + if v == nil { + return + } + v.verified = b +} + +func NewVerification() *Verification { + v := &Verification{ + verified: false, + code: generateCode(), + expiration: time.Now().Add(time.Hour * 24), + } + return v +} + +func VerificationFrom(c string, e time.Time, b bool) *Verification { + v := &Verification{ + verified: b, + code: c, + expiration: e, + } + return v +} diff --git a/pkg/user/verification_test.go b/pkg/user/verification_test.go new file mode 100644 index 00000000..342c5937 --- /dev/null +++ b/pkg/user/verification_test.go @@ -0,0 +1,215 @@ +package user + +import ( + "testing" + "time" + + "github.com/google/uuid" + + "github.com/stretchr/testify/assert" +) + +func TestNewVerification(t *testing.T) { + type fields struct { + verified bool + code bool + expiration bool + } + + tests := []struct { + name string + want fields + }{ + { + name: "init verification struct", + + want: fields{ + verified: false, + code: true, + expiration: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NewVerification() + assert.Equal(t, tt.want.verified, got.IsVerified()) + assert.Equal(t, tt.want.code, len(got.Code()) > 0) + assert.Equal(t, tt.want.expiration, !got.Expiration().IsZero()) + }) + } +} + +func TestVerification_Code(t *testing.T) { + tests := []struct { + name string + verification *Verification + want string + }{ + { + name: "should return a code string", + verification: &Verification{ + verified: false, + code: "xxx", + expiration: time.Time{}, + }, + want: "xxx", + }, + { + name: "should return a empty string", + want: "", + }, + } + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + + assert.Equal(tt, tc.want, tc.verification.Code()) + }) + } +} + +func TestVerification_Expiration(t *testing.T) { + e := time.Now() + + tests := []struct { + name string + verification *Verification + want time.Time + }{ + { + name: "should return now date", + verification: &Verification{ + verified: false, + code: "", + expiration: e, + }, + want: e, + }, + { + name: "should return zero time", + verification: nil, + want: time.Time{}, + }, + } + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + assert.Equal(tt, tc.want, tc.verification.Expiration()) + }) + } +} + +func TestVerification_IsExpired(t *testing.T) { + tim, _ := time.Parse(time.RFC3339, "2021-03-16T04:19:57.592Z") + tim2 := time.Now().Add(time.Hour * 24) + + type fields struct { + verified bool + code string + expiration time.Time + } + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "should be expired", + fields: fields{ + verified: false, + code: "xxx", + expiration: tim, + }, + want: true, + }, + { + name: "shouldn't be expired", + fields: fields{ + verified: false, + code: "xxx", + expiration: tim2, + }, + want: false, + }, + } + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + v := &Verification{ + verified: tc.fields.verified, + code: tc.fields.code, + expiration: tc.fields.expiration, + } + assert.Equal(tt, tc.want, v.IsExpired()) + }) + } +} + +func TestVerification_IsVerified(t *testing.T) { + tests := []struct { + name string + verification *Verification + want bool + }{ + { + name: "should return true", + verification: &Verification{ + verified: true, + }, + want: true, + }, + { + name: "should return false", + verification: nil, + want: false, + }, + } + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + assert.Equal(tt, tc.want, tc.verification.IsVerified()) + }) + } +} + +func TestVerification_SetVerified(t *testing.T) { + tests := []struct { + name string + verification *Verification + input bool + want bool + }{ + { + name: "should set true", + verification: &Verification{ + verified: false, + }, + input: true, + want: true, + }, + { + name: "should return false", + verification: nil, + want: false, + }, + } + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(tt *testing.T) { + tt.Parallel() + tc.verification.SetVerified(tc.input) + assert.Equal(tt, tc.want, tc.verification.IsVerified()) + }) + } +} + +func Test_generateCode(t *testing.T) { + str := generateCode() + _, err := uuid.Parse(str) + assert.NoError(t, err) +} diff --git a/schema.graphql b/schema.graphql index d45c51cb..76c81921 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1261,13 +1261,13 @@ type RemoveAssetPayload { assetId: ID! } -type SignupPayload { +type UpdateMePayload { user: User! - team: Team! } -type UpdateMePayload { +type SignupPayload { user: User! + team: Team! } type DeleteMePayload {