diff --git a/cmd/api/main.go b/cmd/api/main.go index d743a0a..38ee65e 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -9,9 +9,11 @@ import ( "history-api/pkg/database" _ "history-api/pkg/log" "history-api/pkg/mbtiles" + "history-api/pkg/oauth" "os/signal" "syscall" "time" + "github.com/rs/zerolog/log" ) @@ -64,6 +66,12 @@ func StartServer() { panic(err) } + googleOAuthConfig, err := oauth.NewGoogleProvider() + if err != nil { + log.Error().Msg(err.Error()) + panic(err) + } + serverIp, _ := config.GetConfig("SERVER_IP") if serverIp == "" { serverIp = "127.0.0.1" @@ -75,7 +83,7 @@ func StartServer() { } serverHttp := NewHttpServer() - serverHttp.SetupServer(poolPg, sqlTile, redisClient) + serverHttp.SetupServer(poolPg, sqlTile, redisClient, googleOAuthConfig) Singleton = serverHttp done := make(chan bool, 1) diff --git a/cmd/api/server.go b/cmd/api/server.go index 9af0d35..4496d94 100644 --- a/cmd/api/server.go +++ b/cmd/api/server.go @@ -11,12 +11,14 @@ import ( "history-api/internal/services" "history-api/pkg/cache" "os" + "time" swagger "github.com/gofiber/contrib/v3/swaggerui" middleware "github.com/gofiber/contrib/v3/zerolog" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/cors" "github.com/rs/zerolog" + "golang.org/x/oauth2" ) var ( @@ -43,21 +45,30 @@ func NewHttpServer() *FiberServer { server.App.Use(swagger.New(cfg)) - logger := zerolog.New(os.Stderr).With().Timestamp().Logger() + logger := zerolog.New(zerolog.ConsoleWriter{ + Out: os.Stderr, + TimeFormat: time.RFC3339, + }).With().Timestamp().Logger() server.App.Use(middleware.New(middleware.Config{ Logger: &logger, })) return server } -func (s *FiberServer) SetupServer(sqlPg sqlc.DBTX, sqlTile *sql.DB, redis cache.Cache) { +func (s *FiberServer) SetupServer(sqlPg sqlc.DBTX, sqlTile *sql.DB, redis cache.Cache, oauth *oauth2.Config) { // Apply CORS middleware s.App.Use(cors.New(cors.Config{ - AllowOrigins: []string{"*"}, - AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, - AllowHeaders: []string{"Accept", "Authorization", "Content-Type", "Origin"}, - AllowCredentials: false, - MaxAge: 300, + AllowOrigins: []string{ + "http://localhost:3000", + "http://localhost:3001", + "http://localhost:3002", + "http://localhost:3344", + "http://localhost:5173", + "http://localhost:5500", + }, + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, + AllowHeaders: []string{"Accept", "Authorization", "Content-Type", "Origin"}, + AllowCredentials: true, })) // repo setup @@ -72,7 +83,7 @@ func (s *FiberServer) SetupServer(sqlPg sqlc.DBTX, sqlTile *sql.DB, redis cache. tileService := services.NewTileService(tileRepo) // controller setup - authController := controllers.NewAuthController(authService) + authController := controllers.NewAuthController(authService, oauth) userController := controllers.NewUserController(userService) tileController := controllers.NewTileController(tileService) diff --git a/docs/docs.go b/docs/docs.go index 5ceb3c9..d52fba0 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -70,6 +70,68 @@ const docTemplate = `{ } } }, + "/auth/google/callback": { + "get": { + "description": "Receives the auth code from Google, exchanges it for tokens, creates/logs in the user, and redirects back to the frontend with application tokens.", + "tags": [ + "Auth" + ], + "summary": "Handle Google OAuth2 callback", + "parameters": [ + { + "type": "string", + "description": "Security state string", + "name": "state", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Authorization code from Google", + "name": "code", + "in": "query", + "required": true + } + ], + "responses": { + "302": { + "description": "Redirect to Frontend with JWTs", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Invalid state", + "schema": { + "$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse" + } + } + } + } + }, + "/auth/google/login": { + "get": { + "description": "Generates a state string, sets it in a cookie, and redirects the user to Google's consent page.", + "tags": [ + "Auth" + ], + "summary": "Initiate Google OAuth2 login", + "responses": { + "302": { + "description": "Redirect to Google", + "schema": { + "type": "string" + } + } + } + } + }, "/auth/refresh": { "post": { "security": [ diff --git a/docs/swagger.json b/docs/swagger.json index fd7d4db..cd21826 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -68,6 +68,68 @@ } } }, + "/auth/google/callback": { + "get": { + "description": "Receives the auth code from Google, exchanges it for tokens, creates/logs in the user, and redirects back to the frontend with application tokens.", + "tags": [ + "Auth" + ], + "summary": "Handle Google OAuth2 callback", + "parameters": [ + { + "type": "string", + "description": "Security state string", + "name": "state", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Authorization code from Google", + "name": "code", + "in": "query", + "required": true + } + ], + "responses": { + "302": { + "description": "Redirect to Frontend with JWTs", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Invalid state", + "schema": { + "$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse" + } + } + } + } + }, + "/auth/google/login": { + "get": { + "description": "Generates a state string, sets it in a cookie, and redirects the user to Google's consent page.", + "tags": [ + "Auth" + ], + "summary": "Initiate Google OAuth2 login", + "responses": { + "302": { + "description": "Redirect to Google", + "schema": { + "type": "string" + } + } + } + } + }, "/auth/refresh": { "post": { "security": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1bb1c25..4bb82e3 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -207,6 +207,49 @@ paths: summary: Handle forgotten password tags: - Auth + /auth/google/callback: + get: + description: Receives the auth code from Google, exchanges it for tokens, creates/logs + in the user, and redirects back to the frontend with application tokens. + parameters: + - description: Security state string + in: query + name: state + required: true + type: string + - description: Authorization code from Google + in: query + name: code + required: true + type: string + responses: + "302": + description: Redirect to Frontend with JWTs + schema: + type: string + "401": + description: Invalid state + schema: + $ref: '#/definitions/history-api_internal_dtos_response.CommonResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/history-api_internal_dtos_response.CommonResponse' + summary: Handle Google OAuth2 callback + tags: + - Auth + /auth/google/login: + get: + description: Generates a state string, sets it in a cookie, and redirects the + user to Google's consent page. + responses: + "302": + description: Redirect to Google + schema: + type: string + summary: Initiate Google OAuth2 login + tags: + - Auth /auth/refresh: post: consumes: diff --git a/go.mod b/go.mod index f31df9b..77732d8 100644 --- a/go.mod +++ b/go.mod @@ -21,13 +21,19 @@ require ( ) require ( + cloud.google.com/go/auth v0.18.2 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.24.2 // indirect github.com/go-openapi/errors v0.22.6 // indirect github.com/go-openapi/jsonpointer v0.22.4 // indirect @@ -51,6 +57,9 @@ require ( github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gofiber/schema v1.7.0 // indirect github.com/gofiber/utils/v2 v2.0.2 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect + github.com/googleapis/gax-go/v2 v2.19.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect @@ -66,14 +75,24 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.69.0 // indirect go.mongodb.org/mongo-driver v1.17.9 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/mod v0.33.0 // indirect golang.org/x/net v0.52.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.35.0 // indirect golang.org/x/tools v0.42.0 // indirect + google.golang.org/api v0.273.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect modernc.org/libc v1.37.6 // indirect modernc.org/mathutil v1.6.0 // indirect diff --git a/go.sum b/go.sum index ec8fe3c..1d53e0c 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,11 @@ +cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= +cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k= @@ -19,12 +27,19 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/analysis v0.24.2 h1:6p7WXEuKy1llDgOH8FooVeO+Uq2za9qoAOq4ZN08B50= github.com/go-openapi/analysis v0.24.2/go.mod h1:x27OOHKANE0lutg2ml4kzYLoHGMKgRm1Cj2ijVOjJuE= github.com/go-openapi/errors v0.22.6 h1:eDxcf89O8odEnohIXwEjY1IB4ph5vmbUsBMsFNwXWPo= @@ -97,8 +112,14 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE= +github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -168,6 +189,16 @@ github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -178,6 +209,8 @@ golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -189,6 +222,14 @@ golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +google.golang.org/api v0.273.0 h1:r/Bcv36Xa/te1ugaN1kdJ5LoA5Wj/cL+a4gj6FiPBjQ= +google.golang.org/api v0.273.0/go.mod h1:JbAt7mF+XVmWu6xNP8/+CTiGH30ofmCmk9nM8d8fHew= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/controllers/authController.go b/internal/controllers/authController.go index cdbf25f..65f4ffa 100644 --- a/internal/controllers/authController.go +++ b/internal/controllers/authController.go @@ -6,17 +6,22 @@ import ( "history-api/internal/dtos/response" "history-api/internal/services" "history-api/pkg/validator" + "strings" "time" "github.com/gofiber/fiber/v3" + "github.com/google/uuid" + "golang.org/x/oauth2" + "google.golang.org/api/idtoken" ) type AuthController struct { service services.AuthService + oauth *oauth2.Config } -func NewAuthController(svc services.AuthService) *AuthController { - return &AuthController{service: svc} +func NewAuthController(svc services.AuthService, oauth *oauth2.Config) *AuthController { + return &AuthController{service: svc, oauth: oauth} } // Signin godoc @@ -50,6 +55,21 @@ func (h *AuthController) Signin(c fiber.Ctx) error { Message: err.Error(), }) } + c.Cookie(&fiber.Cookie{ + Name: "access_token", + Value: res.AccessToken, + HTTPOnly: true, + Secure: c.Protocol() == "https", + SameSite: "Lax", + }) + + c.Cookie(&fiber.Cookie{ + Name: "refresh_token", + Value: res.RefreshToken, + HTTPOnly: true, + Secure: c.Protocol() == "https", + SameSite: "Lax", + }) return c.Status(fiber.StatusOK).JSON(response.CommonResponse{ Status: true, @@ -88,6 +108,22 @@ func (h *AuthController) Signup(c fiber.Ctx) error { }) } + c.Cookie(&fiber.Cookie{ + Name: "access_token", + Value: res.AccessToken, + HTTPOnly: true, + Secure: c.Protocol() == "https", + SameSite: "Lax", + }) + + c.Cookie(&fiber.Cookie{ + Name: "refresh_token", + Value: res.RefreshToken, + HTTPOnly: true, + Secure: c.Protocol() == "https", + SameSite: "Lax", + }) + return c.Status(fiber.StatusOK).JSON(response.CommonResponse{ Status: true, Data: res, @@ -117,6 +153,22 @@ func (h *AuthController) RefreshToken(c fiber.Ctx) error { }) } + c.Cookie(&fiber.Cookie{ + Name: "access_token", + Value: res.AccessToken, + HTTPOnly: true, + Secure: c.Protocol() == "https", + SameSite: "Lax", + }) + + c.Cookie(&fiber.Cookie{ + Name: "refresh_token", + Value: res.RefreshToken, + HTTPOnly: true, + Secure: c.Protocol() == "https", + SameSite: "Lax", + }) + return c.Status(fiber.StatusOK).JSON(response.CommonResponse{ Status: true, Data: res, @@ -193,8 +245,7 @@ func (h *AuthController) CreateToken(c fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(response.CommonResponse{ Status: true, - Data: nil, - Message: "Token created successfully", + Message: "If this email exists, an OTP has been sent", }) } @@ -234,4 +285,106 @@ func (h *AuthController) ForgotPassword(c fiber.Ctx) error { Data: nil, Message: "Password reset successfully", }) -} \ No newline at end of file +} + +// GoogleLogin godoc +// @Summary Initiate Google OAuth2 login +// @Description Generates a state string, sets it in a cookie, and redirects the user to Google's consent page. +// @Tags Auth +// @Success 302 {string} string "Redirect to Google" +// @Router /auth/google/login [get] +func (h *AuthController) GoogleLogin(c fiber.Ctx) error { + _, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + state := uuid.New().String() + + secure := c.Protocol() == "https" + + c.Cookie(&fiber.Cookie{ + Name: "oauth_state", + Value: state, + Expires: time.Now().Add(15 * time.Minute), + HTTPOnly: true, + Secure: secure, + SameSite: "Lax", + }) + + url := h.oauth.AuthCodeURL(state) + return c.Redirect().To(url) +} + +// GoogleCallback godoc +// @Summary Handle Google OAuth2 callback +// @Description Receives the auth code from Google, exchanges it for tokens, creates/logs in the user, and redirects back to the frontend with application tokens. +// @Tags Auth +// @Param state query string true "Security state string" +// @Param code query string true "Authorization code from Google" +// @Success 302 {string} string "Redirect to Frontend with JWTs" +// @Failure 401 {object} response.CommonResponse "Invalid state" +// @Failure 500 {object} response.CommonResponse "Internal Server Error" +// @Router /auth/google/callback [get] +func (h *AuthController) GoogleCallback(c fiber.Ctx) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + stateFromGoogle := c.Query("state") + stateFromCookie := c.Cookies("oauth_state") + + if stateFromGoogle == "" || stateFromGoogle != stateFromCookie { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid state"}) + } + c.ClearCookie("oauth_state") + + code := c.Query("code") + token, err := h.oauth.Exchange(context.Background(), code) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Token exchange failed"}) + } + + idToken, ok := token.Extra("id_token").(string) + if !ok { + return c.Status(500).JSON(fiber.Map{"error": "No id_token"}) + } + + parts := strings.Split(idToken, ".") + if len(parts) < 2 { + return c.Status(500).JSON(fiber.Map{"error": "Invalid id_token"}) + } + + payload, err := idtoken.Validate(ctx, idToken, h.oauth.ClientID) + if err != nil { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Token verification failed"}) + } + + googleUser := request.SigninWithGoogleDto{ + Sub: payload.Subject, + Email: payload.Claims["email"].(string), + Name: payload.Claims["name"].(string), + Picture: payload.Claims["picture"].(string), + } + + res, err := h.service.SigninWithGoogle(ctx, &googleUser) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{ + Status: false, + Message: err.Error(), + }) + } + + c.Cookie(&fiber.Cookie{ + Name: "access_token", + Value: res.AccessToken, + HTTPOnly: true, + Secure: c.Protocol() == "https", + SameSite: "Lax", + }) + + c.Cookie(&fiber.Cookie{ + Name: "refresh_token", + Value: res.RefreshToken, + HTTPOnly: true, + Secure: c.Protocol() == "https", + SameSite: "Lax", + }) + + return c.Redirect().To("http://localhost:5500") +} diff --git a/internal/controllers/userController.go b/internal/controllers/userController.go index fc887f6..414ce98 100644 --- a/internal/controllers/userController.go +++ b/internal/controllers/userController.go @@ -209,8 +209,8 @@ func (h *UserController) ChangeRoleUser(c fiber.Ctx) error { }) } return c.Status(fiber.StatusOK).JSON(response.CommonResponse{ - Status: true, - Data: user, + Status: true, + Data: user, }) } @@ -273,4 +273,4 @@ func (h *UserController) Search(c fiber.Ctx) error { }) } return c.Status(fiber.StatusOK).JSON(res) -} \ No newline at end of file +} diff --git a/internal/dtos/request/auth.go b/internal/dtos/request/auth.go index c1a2858..10c92f3 100644 --- a/internal/dtos/request/auth.go +++ b/internal/dtos/request/auth.go @@ -30,7 +30,9 @@ type ForgotPasswordDto struct { NewPassword string `json:"new_password" validate:"required,min=8,max=64"` } -type SigninWith3rdDto struct { - Provider string `json:"provider" validate:"required,oneof=google github facebook"` - AccessToken string `json:"access_token" validate:"required"` +type SigninWithGoogleDto struct { + Sub string `json:"sub"` // GoogleID + Email string `json:"email"` + Name string `json:"name"` + Picture string `json:"picture"` } diff --git a/internal/dtos/response/common.go b/internal/dtos/response/common.go index 18d733b..9ab5abe 100644 --- a/internal/dtos/response/common.go +++ b/internal/dtos/response/common.go @@ -13,9 +13,9 @@ type CommonResponse struct { } type JWTClaims struct { - UId string `json:"uid"` - Roles []constants.Role `json:"roles"` - TokenVersion int32 `json:"token_version"` + UId string `json:"uid"` + Roles []constants.Role `json:"roles"` + TokenVersion int32 `json:"token_version"` jwt.RegisteredClaims } diff --git a/internal/dtos/response/user.go b/internal/dtos/response/user.go index 2431c15..ff0f6f5 100644 --- a/internal/dtos/response/user.go +++ b/internal/dtos/response/user.go @@ -6,7 +6,6 @@ type UserResponse struct { ID string `json:"id"` Email string `json:"email"` Profile *UserProfileSimpleResponse `json:"profile"` - IsVerified bool `json:"is_verified"` TokenVersion int32 `json:"token_version"` IsDeleted bool `json:"is_deleted"` CreatedAt *time.Time `json:"created_at"` diff --git a/internal/middlewares/jwtMiddleware.go b/internal/middlewares/jwtMiddleware.go index 89204cc..4b4ade7 100644 --- a/internal/middlewares/jwtMiddleware.go +++ b/internal/middlewares/jwtMiddleware.go @@ -23,8 +23,11 @@ func JwtAccess(userRepo repositories.UserRepository) fiber.Handler { SigningKey: jwtware.SigningKey{Key: []byte(jwtSecret)}, ErrorHandler: jwtError, SuccessHandler: jwtSuccess(userRepo), - Extractor: extractors.FromAuthHeader("Bearer"), - Claims: &response.JWTClaims{}, + Extractor: extractors.Chain( + extractors.FromAuthHeader("Bearer"), + extractors.FromCookie("access_token"), + ), + Claims: &response.JWTClaims{}, }) } @@ -38,15 +41,16 @@ func JwtRefresh(userRepo repositories.UserRepository) fiber.Handler { SigningKey: jwtware.SigningKey{Key: []byte(jwtRefreshSecret)}, ErrorHandler: jwtError, SuccessHandler: jwtSuccess(userRepo), - Extractor: extractors.FromAuthHeader("Bearer"), - Claims: &response.JWTClaims{}, + Extractor: extractors.Chain( + extractors.FromAuthHeader("Bearer"), + extractors.FromCookie("refresh_token"), + ), + Claims: &response.JWTClaims{}, }) } func jwtSuccess(userRepo repositories.UserRepository) fiber.Handler { return func(c fiber.Ctx) error { - user := jwtware.FromContext(c) - unauthorized := func() error { return c.Status(fiber.StatusUnauthorized).JSON(response.CommonResponse{ Status: false, @@ -54,11 +58,12 @@ func jwtSuccess(userRepo repositories.UserRepository) fiber.Handler { }) } - if user == nil { + jwtToken := jwtware.FromContext(c) + if jwtToken == nil { return unauthorized() } - claims, ok := user.Claims.(*response.JWTClaims) + claims, ok := jwtToken.Claims.(*response.JWTClaims) if !ok { return unauthorized() } @@ -71,10 +76,12 @@ func jwtSuccess(userRepo repositories.UserRepository) fiber.Handler { } var pgID pgtype.UUID + err := pgID.Scan(claims.UId) if err != nil { return unauthorized() } + tokenVersion, err := userRepo.GetTokenVersion(c.Context(), pgID) if err != nil { return unauthorized() @@ -89,10 +96,10 @@ func jwtSuccess(userRepo repositories.UserRepository) fiber.Handler { c.Locals("uid", claims.UId) c.Locals("user_claims", claims) - return c.Next() } } + func jwtError(c fiber.Ctx, err error) error { if err.Error() == "Missing or malformed JWT" { return c.Status(fiber.StatusBadRequest). diff --git a/internal/models/user.go b/internal/models/user.go index 422dea0..0ec84cf 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -11,7 +11,6 @@ type UserEntity struct { Email string `json:"email"` PasswordHash string `json:"password_hash"` Profile *UserProfileSimple `json:"profile"` - IsVerified bool `json:"is_verified"` TokenVersion int32 `json:"token_version"` GoogleID string `json:"google_id"` AuthProvider string `json:"auth_provider"` @@ -42,7 +41,6 @@ func (u *UserEntity) ToResponse() *response.UserResponse { return &response.UserResponse{ ID: u.ID, Email: u.Email, - IsVerified: u.IsVerified, TokenVersion: u.TokenVersion, IsDeleted: u.IsDeleted, CreatedAt: u.CreatedAt, diff --git a/internal/routes/authRoute.go b/internal/routes/authRoute.go index a34f6c6..1e9b518 100644 --- a/internal/routes/authRoute.go +++ b/internal/routes/authRoute.go @@ -16,4 +16,6 @@ func AuthRoutes(app *fiber.App, controller *controllers.AuthController, userRepo route.Post("/token/create", controller.CreateToken) route.Post("/token/verify", controller.VerifyToken) route.Post("/forgot-password", controller.ForgotPassword) + route.Get("/google/login", controller.GoogleLogin) + route.Get("/google/callback", controller.GoogleCallback) } diff --git a/internal/routes/userRoute.go b/internal/routes/userRoute.go index 9d9e2cd..26dd54b 100644 --- a/internal/routes/userRoute.go +++ b/internal/routes/userRoute.go @@ -18,6 +18,12 @@ func UserRoutes(app *fiber.App, controller *controllers.UserController, userRepo middlewares.RequireAnyRole(constants.ADMIN, constants.MOD), controller.Search, ) + + route.Get( + "/current", + middlewares.JwtAccess(userRepo), + controller.GetUserCurrent, + ) route.Get( "/:id", middlewares.JwtAccess(userRepo), @@ -25,6 +31,12 @@ func UserRoutes(app *fiber.App, controller *controllers.UserController, userRepo controller.Search, ) + route.Put( + "/:id", + middlewares.JwtAccess(userRepo), + controller.UpdateProfile, + ) + route.Delete( "/:id", middlewares.JwtAccess(userRepo), @@ -50,16 +62,4 @@ func UserRoutes(app *fiber.App, controller *controllers.UserController, userRepo middlewares.JwtAccess(userRepo), controller.ChangePassword, ) - - route.Get( - "/current", - middlewares.JwtAccess(userRepo), - controller.GetUserCurrent, - ) - - route.Put( - "/:id", - middlewares.JwtAccess(userRepo), - controller.UpdateProfile, - ) } diff --git a/internal/services/authService.go b/internal/services/authService.go index 663e948..18d110a 100644 --- a/internal/services/authService.go +++ b/internal/services/authService.go @@ -3,6 +3,11 @@ package services import ( "context" "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "database/sql" + "encoding/hex" + "errors" "fmt" "history-api/internal/dtos/request" "history-api/internal/dtos/response" @@ -31,7 +36,7 @@ type AuthService interface { ForgotPassword(ctx context.Context, dto *request.ForgotPasswordDto) error VerifyToken(ctx context.Context, dto *request.VerifyTokenDto) (*response.VerifyTokenResponse, error) CreateToken(ctx context.Context, dto *request.CreateTokenDto) error - SigninWith3rd(ctx context.Context, dto *request.SigninWith3rdDto) error + SigninWithGoogle(ctx context.Context, dto *request.SigninWithGoogleDto) (*response.AuthResponse, error) RefreshToken(ctx context.Context, id string) (*response.AuthResponse, error) } @@ -130,6 +135,10 @@ func (a *authService) Signin(ctx context.Context, dto *request.SignInDto) (*resp return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) } + if user.AuthProvider != constants.LocalProvider.String() && user.PasswordHash == "" { + return nil, fiber.NewError(fiber.StatusUnauthorized, "Please sign in with "+user.AuthProvider) + } + if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(dto.Password)); err != nil { return nil, fiber.NewError(fiber.StatusUnauthorized, "Invalid identity or password!") } @@ -341,10 +350,113 @@ func (a *authService) ForgotPassword(ctx context.Context, dto *request.ForgotPas return nil } -// SigninWith3rd implements [AuthService]. -func (a *authService) SigninWith3rd(ctx context.Context, dto *request.SigninWith3rdDto) error { - panic("unimplemented") +func (a *authService) SigninWithGoogle(ctx context.Context, dto *request.SigninWithGoogleDto) (*response.AuthResponse, error) { + user, err := a.userRepo.GetByEmail(ctx, dto.Email) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + if user != nil { + userId, err := convert.StringToUUID(user.ID) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + data, err := a.genToken(user) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + err = a.saveNewRefreshToken( + ctx, + sqlc.UpdateUserRefreshTokenParams{ + ID: userId, + RefreshToken: pgtype.Text{ + String: data.RefreshToken, + Valid: data.RefreshToken != "", + }, + }, + ) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + return data, nil + } + + user, err = a.userRepo.UpsertUser( + ctx, + sqlc.UpsertUserParams{ + Email: dto.Email, + AuthProvider: constants.GoogleProvider.String(), + GoogleID: pgtype.Text{ + String: dto.Sub, + Valid: dto.Sub != "", + }, + }, + ) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + userId, err := convert.StringToUUID(user.ID) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + _, err = a.userRepo.CreateProfile( + ctx, + sqlc.CreateUserProfileParams{ + UserID: userId, + DisplayName: pgtype.Text{ + String: dto.Name, + Valid: dto.Name != "", + }, + AvatarUrl: pgtype.Text{ + String: dto.Picture, + Valid: dto.Picture != "", + }, + }, + ) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + role, err := a.roleRepo.GetByname(ctx, constants.USER.String()) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + roleId, err := convert.StringToUUID(role.ID) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + err = a.roleRepo.AddUserRole( + ctx, + sqlc.AddUserRoleParams{ + UserID: userId, + Column2: []pgtype.UUID{roleId}, + }, + ) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + data, err := a.genToken(user) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + err = a.saveNewRefreshToken( + ctx, + sqlc.UpdateUserRefreshTokenParams{ + ID: userId, + RefreshToken: pgtype.Text{ + String: data.RefreshToken, + Valid: data.RefreshToken != "", + }, + }, + ) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + return data, nil } + func (a *authService) GenerateOTP() (string, error) { max := big.NewInt(900000) n, err := rand.Int(rand.Reader, max) @@ -358,46 +470,86 @@ func (a *authService) GenerateOTP() (string, error) { func (a *authService) CreateToken(ctx context.Context, dto *request.CreateTokenDto) error { ok, err := a.tokenRepo.CheckCooldown(ctx, dto.Email, dto.TokenType) if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + return fiber.NewError(fiber.StatusInternalServerError, "Internal Server Error") } if ok { - return fiber.NewError(fiber.StatusBadRequest, "Please wait before requesting another token") + return fiber.NewError(fiber.StatusBadRequest, "Too many requests. Please try again later.") } - otp, err := a.GenerateOTP() + user, err := a.userRepo.GetByEmail(ctx, dto.Email) if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + return fiber.NewError(fiber.StatusInternalServerError, "Internal Server Error") } - token := &models.TokenEntity{ - Email: dto.Email, - Token: otp, - TokenType: dto.TokenType, + shouldSend := true + if (dto.TokenType == constants.TokenEmailVerify && user != nil) || + (dto.TokenType == constants.TokenPasswordReset && user == nil) { + shouldSend = false } - err = a.tokenRepo.Create(ctx, token) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + if shouldSend { + otp, err := a.GenerateOTP() + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Internal Server Error") + } + hash := sha256.Sum256([]byte(otp)) + hashString := hex.EncodeToString(hash[:]) + token := &models.TokenEntity{ + Email: dto.Email, + Token: hashString, + TokenType: dto.TokenType, + } + err = a.tokenRepo.Create(ctx, token) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Internal Server Error") + } + + token.Token = otp + a.c.PublishTask(ctx, constants.StreamEmailName, constants.TaskTypeSendEmailOTP, token) } - a.c.PublishTask(ctx, constants.StreamEmailName, constants.TaskTypeSendEmailOTP, token) return nil } func (a *authService) VerifyToken(ctx context.Context, dto *request.VerifyTokenDto) (*response.VerifyTokenResponse, error) { + genericError := fiber.NewError(fiber.StatusBadRequest, "Invalid or expired token") token, err := a.tokenRepo.Get(ctx, dto.Email, dto.TokenType) + if err != nil || token == nil { + return nil, genericError + } + + userOtpHash := sha256.Sum256([]byte(dto.Token)) + userOtpHashString := hex.EncodeToString(userOtpHash[:]) + actualHash := []byte(token.Token) + expectedHash := []byte(userOtpHashString) + + if len(actualHash) != len(expectedHash) { + return nil, genericError + } + + if subtle.ConstantTimeCompare(actualHash, expectedHash) != 1 { + return nil, genericError + } + + user, err := a.userRepo.GetByEmail(ctx, dto.Email) if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Internal Server Error") } - if token == nil || token.Token != dto.Token { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid token") + + if (dto.TokenType == constants.TokenEmailVerify && user != nil) || + (dto.TokenType == constants.TokenPasswordReset && user == nil) { + return nil, genericError } + tokenId := uuid.New().String() err = a.tokenRepo.CreateVerified(ctx, dto.Email, dto.TokenType, tokenId) if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error()) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Internal Server Error") } + + _ = a.tokenRepo.Delete(ctx, dto.Email, dto.TokenType) + return &response.VerifyTokenResponse{ TokenID: tokenId, }, nil diff --git a/outh2Test.html b/outh2Test.html new file mode 100644 index 0000000..90b23e2 --- /dev/null +++ b/outh2Test.html @@ -0,0 +1,33 @@ + + +
+ +