UPDATE: Outh2 google
This commit is contained in:
@@ -9,9 +9,11 @@ import (
|
|||||||
"history-api/pkg/database"
|
"history-api/pkg/database"
|
||||||
_ "history-api/pkg/log"
|
_ "history-api/pkg/log"
|
||||||
"history-api/pkg/mbtiles"
|
"history-api/pkg/mbtiles"
|
||||||
|
"history-api/pkg/oauth"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -64,6 +66,12 @@ func StartServer() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
googleOAuthConfig, err := oauth.NewGoogleProvider()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msg(err.Error())
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
serverIp, _ := config.GetConfig("SERVER_IP")
|
serverIp, _ := config.GetConfig("SERVER_IP")
|
||||||
if serverIp == "" {
|
if serverIp == "" {
|
||||||
serverIp = "127.0.0.1"
|
serverIp = "127.0.0.1"
|
||||||
@@ -75,7 +83,7 @@ func StartServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
serverHttp := NewHttpServer()
|
serverHttp := NewHttpServer()
|
||||||
serverHttp.SetupServer(poolPg, sqlTile, redisClient)
|
serverHttp.SetupServer(poolPg, sqlTile, redisClient, googleOAuthConfig)
|
||||||
Singleton = serverHttp
|
Singleton = serverHttp
|
||||||
|
|
||||||
done := make(chan bool, 1)
|
done := make(chan bool, 1)
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ import (
|
|||||||
"history-api/internal/services"
|
"history-api/internal/services"
|
||||||
"history-api/pkg/cache"
|
"history-api/pkg/cache"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
swagger "github.com/gofiber/contrib/v3/swaggerui"
|
swagger "github.com/gofiber/contrib/v3/swaggerui"
|
||||||
middleware "github.com/gofiber/contrib/v3/zerolog"
|
middleware "github.com/gofiber/contrib/v3/zerolog"
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
"github.com/gofiber/fiber/v3/middleware/cors"
|
"github.com/gofiber/fiber/v3/middleware/cors"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -43,21 +45,30 @@ func NewHttpServer() *FiberServer {
|
|||||||
|
|
||||||
server.App.Use(swagger.New(cfg))
|
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{
|
server.App.Use(middleware.New(middleware.Config{
|
||||||
Logger: &logger,
|
Logger: &logger,
|
||||||
}))
|
}))
|
||||||
return server
|
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
|
// Apply CORS middleware
|
||||||
s.App.Use(cors.New(cors.Config{
|
s.App.Use(cors.New(cors.Config{
|
||||||
AllowOrigins: []string{"*"},
|
AllowOrigins: []string{
|
||||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"},
|
"http://localhost:3000",
|
||||||
AllowHeaders: []string{"Accept", "Authorization", "Content-Type", "Origin"},
|
"http://localhost:3001",
|
||||||
AllowCredentials: false,
|
"http://localhost:3002",
|
||||||
MaxAge: 300,
|
"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
|
// repo setup
|
||||||
@@ -72,7 +83,7 @@ func (s *FiberServer) SetupServer(sqlPg sqlc.DBTX, sqlTile *sql.DB, redis cache.
|
|||||||
tileService := services.NewTileService(tileRepo)
|
tileService := services.NewTileService(tileRepo)
|
||||||
|
|
||||||
// controller setup
|
// controller setup
|
||||||
authController := controllers.NewAuthController(authService)
|
authController := controllers.NewAuthController(authService, oauth)
|
||||||
userController := controllers.NewUserController(userService)
|
userController := controllers.NewUserController(userService)
|
||||||
tileController := controllers.NewTileController(tileService)
|
tileController := controllers.NewTileController(tileService)
|
||||||
|
|
||||||
|
|||||||
62
docs/docs.go
62
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": {
|
"/auth/refresh": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|||||||
@@ -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": {
|
"/auth/refresh": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|||||||
@@ -207,6 +207,49 @@ paths:
|
|||||||
summary: Handle forgotten password
|
summary: Handle forgotten password
|
||||||
tags:
|
tags:
|
||||||
- Auth
|
- 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:
|
/auth/refresh:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
19
go.mod
19
go.mod
@@ -21,13 +21,19 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
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/KyleBanks/depth v1.2.1 // indirect
|
||||||
github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect
|
github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // 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/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/analysis v0.24.2 // indirect
|
||||||
github.com/go-openapi/errors v0.22.6 // indirect
|
github.com/go-openapi/errors v0.22.6 // indirect
|
||||||
github.com/go-openapi/jsonpointer v0.22.4 // 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/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/gofiber/schema v1.7.0 // indirect
|
github.com/gofiber/schema v1.7.0 // indirect
|
||||||
github.com/gofiber/utils/v2 v2.0.2 // 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/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // 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/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||||
go.mongodb.org/mongo-driver v1.17.9 // 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.uber.org/atomic v1.11.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/mod v0.33.0 // indirect
|
golang.org/x/mod v0.33.0 // indirect
|
||||||
golang.org/x/net v0.52.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/sync v0.20.0 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
golang.org/x/tools v0.42.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
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
modernc.org/libc v1.37.6 // indirect
|
modernc.org/libc v1.37.6 // indirect
|
||||||
modernc.org/mathutil v1.6.0 // indirect
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
|
|||||||
41
go.sum
41
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 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
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=
|
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/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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
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 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
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 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
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 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
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 h1:6p7WXEuKy1llDgOH8FooVeO+Uq2za9qoAOq4ZN08B50=
|
||||||
github.com/go-openapi/analysis v0.24.2/go.mod h1:x27OOHKANE0lutg2ml4kzYLoHGMKgRm1Cj2ijVOjJuE=
|
github.com/go-openapi/analysis v0.24.2/go.mod h1:x27OOHKANE0lutg2ml4kzYLoHGMKgRm1Cj2ijVOjJuE=
|
||||||
github.com/go-openapi/errors v0.22.6 h1:eDxcf89O8odEnohIXwEjY1IB4ph5vmbUsBMsFNwXWPo=
|
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/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 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
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=
|
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 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
|
||||||
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
|
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 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
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/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 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
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 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
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=
|
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/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 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
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 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|||||||
@@ -6,17 +6,22 @@ import (
|
|||||||
"history-api/internal/dtos/response"
|
"history-api/internal/dtos/response"
|
||||||
"history-api/internal/services"
|
"history-api/internal/services"
|
||||||
"history-api/pkg/validator"
|
"history-api/pkg/validator"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"google.golang.org/api/idtoken"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthController struct {
|
type AuthController struct {
|
||||||
service services.AuthService
|
service services.AuthService
|
||||||
|
oauth *oauth2.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthController(svc services.AuthService) *AuthController {
|
func NewAuthController(svc services.AuthService, oauth *oauth2.Config) *AuthController {
|
||||||
return &AuthController{service: svc}
|
return &AuthController{service: svc, oauth: oauth}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signin godoc
|
// Signin godoc
|
||||||
@@ -50,6 +55,21 @@ func (h *AuthController) Signin(c fiber.Ctx) error {
|
|||||||
Message: err.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{
|
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||||
Status: true,
|
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{
|
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||||
Status: true,
|
Status: true,
|
||||||
Data: res,
|
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{
|
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||||
Status: true,
|
Status: true,
|
||||||
Data: res,
|
Data: res,
|
||||||
@@ -193,8 +245,7 @@ func (h *AuthController) CreateToken(c fiber.Ctx) error {
|
|||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||||
Status: true,
|
Status: true,
|
||||||
Data: nil,
|
Message: "If this email exists, an OTP has been sent",
|
||||||
Message: "Token created successfully",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,4 +285,106 @@ func (h *AuthController) ForgotPassword(c fiber.Ctx) error {
|
|||||||
Data: nil,
|
Data: nil,
|
||||||
Message: "Password reset successfully",
|
Message: "Password reset successfully",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -209,8 +209,8 @@ func (h *UserController) ChangeRoleUser(c fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||||
Status: true,
|
Status: true,
|
||||||
Data: user,
|
Data: user,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,4 +273,4 @@ func (h *UserController) Search(c fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
return c.Status(fiber.StatusOK).JSON(res)
|
return c.Status(fiber.StatusOK).JSON(res)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ type ForgotPasswordDto struct {
|
|||||||
NewPassword string `json:"new_password" validate:"required,min=8,max=64"`
|
NewPassword string `json:"new_password" validate:"required,min=8,max=64"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SigninWith3rdDto struct {
|
type SigninWithGoogleDto struct {
|
||||||
Provider string `json:"provider" validate:"required,oneof=google github facebook"`
|
Sub string `json:"sub"` // GoogleID
|
||||||
AccessToken string `json:"access_token" validate:"required"`
|
Email string `json:"email"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Picture string `json:"picture"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ type CommonResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type JWTClaims struct {
|
type JWTClaims struct {
|
||||||
UId string `json:"uid"`
|
UId string `json:"uid"`
|
||||||
Roles []constants.Role `json:"roles"`
|
Roles []constants.Role `json:"roles"`
|
||||||
TokenVersion int32 `json:"token_version"`
|
TokenVersion int32 `json:"token_version"`
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ type UserResponse struct {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Profile *UserProfileSimpleResponse `json:"profile"`
|
Profile *UserProfileSimpleResponse `json:"profile"`
|
||||||
IsVerified bool `json:"is_verified"`
|
|
||||||
TokenVersion int32 `json:"token_version"`
|
TokenVersion int32 `json:"token_version"`
|
||||||
IsDeleted bool `json:"is_deleted"`
|
IsDeleted bool `json:"is_deleted"`
|
||||||
CreatedAt *time.Time `json:"created_at"`
|
CreatedAt *time.Time `json:"created_at"`
|
||||||
|
|||||||
@@ -23,8 +23,11 @@ func JwtAccess(userRepo repositories.UserRepository) fiber.Handler {
|
|||||||
SigningKey: jwtware.SigningKey{Key: []byte(jwtSecret)},
|
SigningKey: jwtware.SigningKey{Key: []byte(jwtSecret)},
|
||||||
ErrorHandler: jwtError,
|
ErrorHandler: jwtError,
|
||||||
SuccessHandler: jwtSuccess(userRepo),
|
SuccessHandler: jwtSuccess(userRepo),
|
||||||
Extractor: extractors.FromAuthHeader("Bearer"),
|
Extractor: extractors.Chain(
|
||||||
Claims: &response.JWTClaims{},
|
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)},
|
SigningKey: jwtware.SigningKey{Key: []byte(jwtRefreshSecret)},
|
||||||
ErrorHandler: jwtError,
|
ErrorHandler: jwtError,
|
||||||
SuccessHandler: jwtSuccess(userRepo),
|
SuccessHandler: jwtSuccess(userRepo),
|
||||||
Extractor: extractors.FromAuthHeader("Bearer"),
|
Extractor: extractors.Chain(
|
||||||
Claims: &response.JWTClaims{},
|
extractors.FromAuthHeader("Bearer"),
|
||||||
|
extractors.FromCookie("refresh_token"),
|
||||||
|
),
|
||||||
|
Claims: &response.JWTClaims{},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func jwtSuccess(userRepo repositories.UserRepository) fiber.Handler {
|
func jwtSuccess(userRepo repositories.UserRepository) fiber.Handler {
|
||||||
return func(c fiber.Ctx) error {
|
return func(c fiber.Ctx) error {
|
||||||
user := jwtware.FromContext(c)
|
|
||||||
|
|
||||||
unauthorized := func() error {
|
unauthorized := func() error {
|
||||||
return c.Status(fiber.StatusUnauthorized).JSON(response.CommonResponse{
|
return c.Status(fiber.StatusUnauthorized).JSON(response.CommonResponse{
|
||||||
Status: false,
|
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()
|
return unauthorized()
|
||||||
}
|
}
|
||||||
|
|
||||||
claims, ok := user.Claims.(*response.JWTClaims)
|
claims, ok := jwtToken.Claims.(*response.JWTClaims)
|
||||||
if !ok {
|
if !ok {
|
||||||
return unauthorized()
|
return unauthorized()
|
||||||
}
|
}
|
||||||
@@ -71,10 +76,12 @@ func jwtSuccess(userRepo repositories.UserRepository) fiber.Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var pgID pgtype.UUID
|
var pgID pgtype.UUID
|
||||||
|
|
||||||
err := pgID.Scan(claims.UId)
|
err := pgID.Scan(claims.UId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return unauthorized()
|
return unauthorized()
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenVersion, err := userRepo.GetTokenVersion(c.Context(), pgID)
|
tokenVersion, err := userRepo.GetTokenVersion(c.Context(), pgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return unauthorized()
|
return unauthorized()
|
||||||
@@ -89,10 +96,10 @@ func jwtSuccess(userRepo repositories.UserRepository) fiber.Handler {
|
|||||||
|
|
||||||
c.Locals("uid", claims.UId)
|
c.Locals("uid", claims.UId)
|
||||||
c.Locals("user_claims", claims)
|
c.Locals("user_claims", claims)
|
||||||
|
|
||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func jwtError(c fiber.Ctx, err error) error {
|
func jwtError(c fiber.Ctx, err error) error {
|
||||||
if err.Error() == "Missing or malformed JWT" {
|
if err.Error() == "Missing or malformed JWT" {
|
||||||
return c.Status(fiber.StatusBadRequest).
|
return c.Status(fiber.StatusBadRequest).
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ type UserEntity struct {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
PasswordHash string `json:"password_hash"`
|
PasswordHash string `json:"password_hash"`
|
||||||
Profile *UserProfileSimple `json:"profile"`
|
Profile *UserProfileSimple `json:"profile"`
|
||||||
IsVerified bool `json:"is_verified"`
|
|
||||||
TokenVersion int32 `json:"token_version"`
|
TokenVersion int32 `json:"token_version"`
|
||||||
GoogleID string `json:"google_id"`
|
GoogleID string `json:"google_id"`
|
||||||
AuthProvider string `json:"auth_provider"`
|
AuthProvider string `json:"auth_provider"`
|
||||||
@@ -42,7 +41,6 @@ func (u *UserEntity) ToResponse() *response.UserResponse {
|
|||||||
return &response.UserResponse{
|
return &response.UserResponse{
|
||||||
ID: u.ID,
|
ID: u.ID,
|
||||||
Email: u.Email,
|
Email: u.Email,
|
||||||
IsVerified: u.IsVerified,
|
|
||||||
TokenVersion: u.TokenVersion,
|
TokenVersion: u.TokenVersion,
|
||||||
IsDeleted: u.IsDeleted,
|
IsDeleted: u.IsDeleted,
|
||||||
CreatedAt: u.CreatedAt,
|
CreatedAt: u.CreatedAt,
|
||||||
|
|||||||
@@ -16,4 +16,6 @@ func AuthRoutes(app *fiber.App, controller *controllers.AuthController, userRepo
|
|||||||
route.Post("/token/create", controller.CreateToken)
|
route.Post("/token/create", controller.CreateToken)
|
||||||
route.Post("/token/verify", controller.VerifyToken)
|
route.Post("/token/verify", controller.VerifyToken)
|
||||||
route.Post("/forgot-password", controller.ForgotPassword)
|
route.Post("/forgot-password", controller.ForgotPassword)
|
||||||
|
route.Get("/google/login", controller.GoogleLogin)
|
||||||
|
route.Get("/google/callback", controller.GoogleCallback)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ func UserRoutes(app *fiber.App, controller *controllers.UserController, userRepo
|
|||||||
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||||
controller.Search,
|
controller.Search,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
route.Get(
|
||||||
|
"/current",
|
||||||
|
middlewares.JwtAccess(userRepo),
|
||||||
|
controller.GetUserCurrent,
|
||||||
|
)
|
||||||
route.Get(
|
route.Get(
|
||||||
"/:id",
|
"/:id",
|
||||||
middlewares.JwtAccess(userRepo),
|
middlewares.JwtAccess(userRepo),
|
||||||
@@ -25,6 +31,12 @@ func UserRoutes(app *fiber.App, controller *controllers.UserController, userRepo
|
|||||||
controller.Search,
|
controller.Search,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
route.Put(
|
||||||
|
"/:id",
|
||||||
|
middlewares.JwtAccess(userRepo),
|
||||||
|
controller.UpdateProfile,
|
||||||
|
)
|
||||||
|
|
||||||
route.Delete(
|
route.Delete(
|
||||||
"/:id",
|
"/:id",
|
||||||
middlewares.JwtAccess(userRepo),
|
middlewares.JwtAccess(userRepo),
|
||||||
@@ -50,16 +62,4 @@ func UserRoutes(app *fiber.App, controller *controllers.UserController, userRepo
|
|||||||
middlewares.JwtAccess(userRepo),
|
middlewares.JwtAccess(userRepo),
|
||||||
controller.ChangePassword,
|
controller.ChangePassword,
|
||||||
)
|
)
|
||||||
|
|
||||||
route.Get(
|
|
||||||
"/current",
|
|
||||||
middlewares.JwtAccess(userRepo),
|
|
||||||
controller.GetUserCurrent,
|
|
||||||
)
|
|
||||||
|
|
||||||
route.Put(
|
|
||||||
"/:id",
|
|
||||||
middlewares.JwtAccess(userRepo),
|
|
||||||
controller.UpdateProfile,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ package services
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/subtle"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"history-api/internal/dtos/request"
|
"history-api/internal/dtos/request"
|
||||||
"history-api/internal/dtos/response"
|
"history-api/internal/dtos/response"
|
||||||
@@ -31,7 +36,7 @@ type AuthService interface {
|
|||||||
ForgotPassword(ctx context.Context, dto *request.ForgotPasswordDto) error
|
ForgotPassword(ctx context.Context, dto *request.ForgotPasswordDto) error
|
||||||
VerifyToken(ctx context.Context, dto *request.VerifyTokenDto) (*response.VerifyTokenResponse, error)
|
VerifyToken(ctx context.Context, dto *request.VerifyTokenDto) (*response.VerifyTokenResponse, error)
|
||||||
CreateToken(ctx context.Context, dto *request.CreateTokenDto) 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)
|
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())
|
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 {
|
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(dto.Password)); err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusUnauthorized, "Invalid identity or password!")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SigninWith3rd implements [AuthService].
|
func (a *authService) SigninWithGoogle(ctx context.Context, dto *request.SigninWithGoogleDto) (*response.AuthResponse, error) {
|
||||||
func (a *authService) SigninWith3rd(ctx context.Context, dto *request.SigninWith3rdDto) error {
|
user, err := a.userRepo.GetByEmail(ctx, dto.Email)
|
||||||
panic("unimplemented")
|
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) {
|
func (a *authService) GenerateOTP() (string, error) {
|
||||||
max := big.NewInt(900000)
|
max := big.NewInt(900000)
|
||||||
n, err := rand.Int(rand.Reader, max)
|
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 {
|
func (a *authService) CreateToken(ctx context.Context, dto *request.CreateTokenDto) error {
|
||||||
ok, err := a.tokenRepo.CheckCooldown(ctx, dto.Email, dto.TokenType)
|
ok, err := a.tokenRepo.CheckCooldown(ctx, dto.Email, dto.TokenType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
return fiber.NewError(fiber.StatusInternalServerError, "Internal Server Error")
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok {
|
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 {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
return fiber.NewError(fiber.StatusInternalServerError, "Internal Server Error")
|
||||||
}
|
}
|
||||||
|
|
||||||
token := &models.TokenEntity{
|
shouldSend := true
|
||||||
Email: dto.Email,
|
if (dto.TokenType == constants.TokenEmailVerify && user != nil) ||
|
||||||
Token: otp,
|
(dto.TokenType == constants.TokenPasswordReset && user == nil) {
|
||||||
TokenType: dto.TokenType,
|
shouldSend = false
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.tokenRepo.Create(ctx, token)
|
if shouldSend {
|
||||||
if err != nil {
|
otp, err := a.GenerateOTP()
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *authService) VerifyToken(ctx context.Context, dto *request.VerifyTokenDto) (*response.VerifyTokenResponse, error) {
|
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)
|
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 {
|
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()
|
tokenId := uuid.New().String()
|
||||||
err = a.tokenRepo.CreateVerified(ctx, dto.Email, dto.TokenType, tokenId)
|
err = a.tokenRepo.CreateVerified(ctx, dto.Email, dto.TokenType, tokenId)
|
||||||
if err != nil {
|
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{
|
return &response.VerifyTokenResponse{
|
||||||
TokenID: tokenId,
|
TokenID: tokenId,
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
33
outh2Test.html
Normal file
33
outh2Test.html
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Test Google OAuth</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Google OAuth Test</h2>
|
||||||
|
|
||||||
|
<button onclick="login()">Login with Google</button>
|
||||||
|
<button onclick="getProfile()">Call API (check login)</button>
|
||||||
|
|
||||||
|
<pre id="output"></pre>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API = "http://localhost:3344";
|
||||||
|
|
||||||
|
function login() {
|
||||||
|
window.location.href = API + "/auth/google/login";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getProfile() {
|
||||||
|
const res = await fetch(API + "/users/current", {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include"
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
document.getElementById("output").innerText = JSON.stringify(data, null, 2);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
14
pkg/constants/provider.go
Normal file
14
pkg/constants/provider.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package constants
|
||||||
|
|
||||||
|
type ProviderType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
GoogleProvider ProviderType = "google"
|
||||||
|
GithubProvider ProviderType = "github"
|
||||||
|
FacebookProvider ProviderType = "facebook"
|
||||||
|
LocalProvider ProviderType = "local"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p ProviderType) String() string {
|
||||||
|
return string(p)
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package log
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
@@ -11,7 +12,8 @@ func init() {
|
|||||||
output := zerolog.ConsoleWriter{
|
output := zerolog.ConsoleWriter{
|
||||||
Out: os.Stdout,
|
Out: os.Stdout,
|
||||||
PartsOrder: []string{"level", "message"},
|
PartsOrder: []string{"level", "message"},
|
||||||
|
TimeFormat: time.RFC3339,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Logger = zerolog.New(output).With().Logger()
|
log.Logger = zerolog.New(output).With().Timestamp().Logger()
|
||||||
}
|
}
|
||||||
|
|||||||
38
pkg/oauth/google.go
Normal file
38
pkg/oauth/google.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package oauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"history-api/pkg/config"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"golang.org/x/oauth2/google"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewGoogleProvider() (*oauth2.Config, error) {
|
||||||
|
userGoogle, err := config.GetConfig("GOOGLE_CLIENT_ID")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
passGoogle, err := config.GetConfig("GOOGLE_CLIENT_SECRET")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectURL, err := config.GetConfig("GOOGLE_REDIRECT_URL")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &oauth2.Config{
|
||||||
|
RedirectURL: redirectURL,
|
||||||
|
ClientID: fmt.Sprintf("%s.apps.googleusercontent.com", userGoogle),
|
||||||
|
ClientSecret: passGoogle,
|
||||||
|
Scopes: []string{
|
||||||
|
"openid",
|
||||||
|
"email",
|
||||||
|
"profile",
|
||||||
|
},
|
||||||
|
Endpoint: google.Endpoint,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user