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
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+ Hi {{ .UserName }}:
+ |
+
+
+
+ {{ .Message }}
+ |
+
+
+ {{ .ActionLabel }} |
+
+
+
+
+ {{ .Suffix }}
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+ If any problems, please contact the Re:Earth team: info@reearth.io
+ You can also find us on discord! Feel free to join and ask any questions.
+
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+ |
+ |
+
+
+
+
\ 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 {