UPDATE: Outh2 google

This commit is contained in:
2026-03-30 16:13:00 +07:00
parent d3f128b284
commit 0410ae508e
21 changed files with 714 additions and 68 deletions

View File

@@ -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)

View File

@@ -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{
"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"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"},
AllowHeaders: []string{"Accept", "Authorization", "Content-Type", "Origin"}, AllowHeaders: []string{"Accept", "Authorization", "Content-Type", "Origin"},
AllowCredentials: false, AllowCredentials: true,
MaxAge: 300,
})) }))
// 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)

View File

@@ -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": [

View File

@@ -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": [

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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",
}) })
} }
@@ -235,3 +286,105 @@ func (h *AuthController) ForgotPassword(c fiber.Ctx) error {
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")
}

View File

@@ -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"`
} }

View File

@@ -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"`

View File

@@ -23,7 +23,10 @@ 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(
extractors.FromAuthHeader("Bearer"),
extractors.FromCookie("access_token"),
),
Claims: &response.JWTClaims{}, 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(
extractors.FromAuthHeader("Bearer"),
extractors.FromCookie("refresh_token"),
),
Claims: &response.JWTClaims{}, 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).

View File

@@ -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,

View File

@@ -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)
} }

View File

@@ -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,
)
} }

View File

@@ -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.")
} }
user, err := a.userRepo.GetByEmail(ctx, dto.Email)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Internal Server Error")
}
shouldSend := true
if (dto.TokenType == constants.TokenEmailVerify && user != nil) ||
(dto.TokenType == constants.TokenPasswordReset && user == nil) {
shouldSend = false
}
if shouldSend {
otp, err := a.GenerateOTP() otp, err := a.GenerateOTP()
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error()) return fiber.NewError(fiber.StatusInternalServerError, "Internal Server Error")
} }
hash := sha256.Sum256([]byte(otp))
hashString := hex.EncodeToString(hash[:])
token := &models.TokenEntity{ token := &models.TokenEntity{
Email: dto.Email, Email: dto.Email,
Token: otp, Token: hashString,
TokenType: dto.TokenType, TokenType: dto.TokenType,
} }
err = a.tokenRepo.Create(ctx, token) err = a.tokenRepo.Create(ctx, token)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error()) 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
View 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
View 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)
}

View File

@@ -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
View 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
}