diff --git a/README.md b/README.md index d42602b7f..215d6bae4 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ test-coverage Go Report Card Apache 2.0 License -discord +discord diff --git a/docs/quick-start/connecting-mysql/page.md b/docs/quick-start/connecting-mysql/page.md index b6043eb5c..c5674db0b 100644 --- a/docs/quick-start/connecting-mysql/page.md +++ b/docs/quick-start/connecting-mysql/page.md @@ -36,6 +36,11 @@ DB_PASSWORD=root123 DB_NAME=test_db DB_PORT=3306 DB_DIALECT=mysql +DB_CHARSET= + +# DB_CHARSET: The character set for database connection (default: utf8). +# The `DB_CHARSET` defaults to utf8, but setting it to utf8mb4 is recommended if you need full Unicode support, +# including emojis and special characters. ``` Now in the following example, we'll store customer data using **POST** `/customer` and then use **GET** `/customer` to retrieve the same. @@ -50,7 +55,7 @@ import ( "errors" "github.com/redis/go-redis/v9" - + "gofr.dev/pkg/gofr" ) @@ -105,7 +110,7 @@ func main() { // return the customer return customers, nil }) - + app.Run() } ``` diff --git a/go.mod b/go.mod index b89e593fb..54701577f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module gofr.dev go 1.22 require ( - cloud.google.com/go/pubsub v1.44.0 + cloud.google.com/go/pubsub v1.45.1 github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/XSAM/otelsql v0.34.0 github.com/alicebob/miniredis/v2 v2.33.0 @@ -39,7 +39,7 @@ require ( golang.org/x/sync v0.8.0 golang.org/x/term v0.25.0 golang.org/x/text v0.19.0 - google.golang.org/api v0.202.0 + google.golang.org/api v0.203.0 google.golang.org/grpc v1.67.1 google.golang.org/protobuf v1.35.1 modernc.org/sqlite v1.33.1 @@ -47,7 +47,7 @@ require ( require ( cloud.google.com/go v0.116.0 // indirect - cloud.google.com/go/auth v0.9.8 // indirect + cloud.google.com/go/auth v0.9.9 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect cloud.google.com/go/compute/metadata v0.5.2 // indirect cloud.google.com/go/iam v1.2.1 // indirect diff --git a/go.sum b/go.sum index 3c5acd1d6..7821ecc6b 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= -cloud.google.com/go/auth v0.9.8 h1:+CSJ0Gw9iVeSENVCKJoLHhdUykDgXSc4Qn+gu2BRtR8= -cloud.google.com/go/auth v0.9.8/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= +cloud.google.com/go/auth v0.9.9 h1:BmtbpNQozo8ZwW2t7QJjnrQtdganSdmqeIBxHxNkEZQ= +cloud.google.com/go/auth v0.9.9/go.mod h1:xxA5AqpDrvS+Gkmo9RqrGGRh6WSNKKOXhY3zNOr38tI= cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY= cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc= cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= @@ -13,8 +13,8 @@ cloud.google.com/go/kms v1.20.0 h1:uKUvjGqbBlI96xGE669hcVnEMw1Px/Mvfa62dhM5UrY= cloud.google.com/go/kms v1.20.0/go.mod h1:/dMbFF1tLLFnQV44AoI2GlotbjowyUfgVwezxW291fM= cloud.google.com/go/longrunning v0.6.1 h1:lOLTFxYpr8hcRtcwWir5ITh1PAKUD/sG2lKrTSYjyMc= cloud.google.com/go/longrunning v0.6.1/go.mod h1:nHISoOZpBcmlwbJmiVk5oDRz0qG/ZxPynEGs1iZ79s0= -cloud.google.com/go/pubsub v1.44.0 h1:pLaMJVDTlnUDIKT5L0k53YyLszfBbGoUBo/IqDK/fEI= -cloud.google.com/go/pubsub v1.44.0/go.mod h1:BD4a/kmE8OePyHoa1qAHEw1rMzXX+Pc8Se54T/8mc3I= +cloud.google.com/go/pubsub v1.45.1 h1:ZC/UzYcrmK12THWn1P72z+Pnp2vu/zCZRXyhAfP1hJY= +cloud.google.com/go/pubsub v1.45.1/go.mod h1:3bn7fTmzZFwaUjllitv1WlsNMkqBgGUb3UdMhI54eCc= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -345,8 +345,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.202.0 h1:y1iuVHMqokQbimW79ZqPZWo4CiyFu6HcCYHwSNyzlfo= -google.golang.org/api v0.202.0/go.mod h1:3Jjeq7M/SFblTNCp7ES2xhq+WvGL0KeXI0joHQBfwTQ= +google.golang.org/api v0.203.0 h1:SrEeuwU3S11Wlscsn+LA1kb/Y5xT8uggJSkIhD08NAU= +google.golang.org/api v0.203.0/go.mod h1:BuOVyCSYEPwJb3npWvDnNmFI92f3GeRnHNkETneT3SI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= diff --git a/pkg/gofr/context.go b/pkg/gofr/context.go index 9858a1673..5788fd597 100644 --- a/pkg/gofr/context.go +++ b/pkg/gofr/context.go @@ -3,12 +3,14 @@ package gofr import ( "context" + "github.com/golang-jwt/jwt/v5" "github.com/gorilla/websocket" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" "gofr.dev/pkg/gofr/container" + "gofr.dev/pkg/gofr/http/middleware" ) type Context struct { @@ -28,6 +30,12 @@ type Context struct { responder Responder } +type AuthInfo interface { + GetClaims() jwt.MapClaims + GetUsername() string + GetAPIKey() string +} + /* Trace returns an open telemetry span. We have to always close the span after corresponding work is done. Usages: @@ -75,6 +83,49 @@ func (c *Context) WriteMessageToSocket(data any) error { return conn.WriteMessage(websocket.TextMessage, message) } +type authInfo struct { + claims jwt.MapClaims + username string + apiKey string +} + +// GetAuthInfo is a method on context, to access different methods to retrieve authentication info. +// +// GetAuthInfo().GetClaims() : retrieves the jwt claims. +// GetAuthInfo().GetUsername() : retrieves the username while basic authentication. +// GetAuthInfo().GetAPIKey() : retrieves the APIKey being used for authentication. +func (c *Context) GetAuthInfo() AuthInfo { + claims, _ := c.Request.Context().Value(middleware.JWTClaim).(jwt.MapClaims) + + APIKey, _ := c.Request.Context().Value(middleware.APIKey).(string) + + username, _ := c.Request.Context().Value(middleware.Username).(string) + + return &authInfo{ + claims: claims, + username: username, + apiKey: APIKey, + } +} + +// GetClaims returns a response of jwt.MapClaims type when OAuth is enabled. +// It returns nil if called, when OAuth is not enabled. +func (a *authInfo) GetClaims() jwt.MapClaims { + return a.claims +} + +// GetUsername returns the username when basic auth is enabled. +// It returns an empty string if called, when basic auth is not enabled. +func (a *authInfo) GetUsername() string { + return a.username +} + +// GetAPIKey returns the APIKey when APIKey auth is enabled. +// It returns an empty strung if called, when APIKey auth is not enabled. +func (a *authInfo) GetAPIKey() string { + return a.apiKey +} + // func (c *Context) reset(w Responder, r Request) { // c.Request = r // c.responder = w diff --git a/pkg/gofr/context_test.go b/pkg/gofr/context_test.go index 599791bf4..9aa23958e 100644 --- a/pkg/gofr/context_test.go +++ b/pkg/gofr/context_test.go @@ -7,6 +7,7 @@ import ( "net/http/httptest" "testing" + "github.com/golang-jwt/jwt/v5" "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,6 +17,7 @@ import ( "gofr.dev/pkg/gofr/config" "gofr.dev/pkg/gofr/container" gofrHTTP "gofr.dev/pkg/gofr/http" + "gofr.dev/pkg/gofr/http/middleware" "gofr.dev/pkg/gofr/logging" "gofr.dev/pkg/gofr/version" ) @@ -109,3 +111,71 @@ func TestContext_WriteMessageToSocket(t *testing.T) { expectedResponse := "Hello! GoFr" assert.Equal(t, expectedResponse, string(message)) } + +func TestGetAuthInfo_BasicAuth(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", http.NoBody) + + ctx := context.WithValue(req.Context(), middleware.Username, "validUser") + *req = *req.Clone(ctx) + + mockContainer, _ := container.NewMockContainer(t) + gofrRq := gofrHTTP.NewRequest(req) + + c := &Context{ + Context: ctx, + Request: gofrRq, + Container: mockContainer, + } + + res := c.GetAuthInfo().GetUsername() + + assert.Equal(t, "validUser", res) +} + +func TestGetAuthInfo_ApiKey(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", http.NoBody) + + ctx := context.WithValue(req.Context(), middleware.APIKey, "9221e451-451f-4cd6-a23d-2b2d3adea9cf") + + *req = *req.Clone(ctx) + gofrRq := gofrHTTP.NewRequest(req) + + mockContainer, _ := container.NewMockContainer(t) + + c := &Context{ + Context: ctx, + Request: gofrRq, + Container: mockContainer, + } + + res := c.GetAuthInfo().GetAPIKey() + + assert.Equal(t, "9221e451-451f-4cd6-a23d-2b2d3adea9cf", res) +} + +func TestGetAuthInfo_JWTClaims(t *testing.T) { + claims := jwt.MapClaims{ + "sub": "1234567890", + "name": "John Doe", + "admin": true, + } + + req := httptest.NewRequest(http.MethodGet, "/", http.NoBody) + + ctx := context.WithValue(req.Context(), middleware.JWTClaim, claims) + + *req = *req.Clone(ctx) + gofrRq := gofrHTTP.NewRequest(req) + + mockContainer, _ := container.NewMockContainer(t) + + c := &Context{ + Context: ctx, + Request: gofrRq, + Container: mockContainer, + } + + res := c.GetAuthInfo().GetClaims() + + assert.Equal(t, claims, res) +} diff --git a/pkg/gofr/datasource/README.md b/pkg/gofr/datasource/README.md index 6bc172eb1..aa34b8602 100644 --- a/pkg/gofr/datasource/README.md +++ b/pkg/gofr/datasource/README.md @@ -74,15 +74,15 @@ Therefore, GoFr utilizes a pluggable approach for new datasources by separating | MySQL | ✅ | ✅ | ✅ | ✅ | | | REDIS | ✅ | ✅ | ✅ | ✅ | | | PostgreSQL | ✅ | ✅ | ✅ | ✅ | | -| MongoDB | ✅ | ✅ | ✅ | | ✅ | +| MongoDB | ✅ | ✅ | ✅ | ✅ | ✅ | | SQLite | ✅ | ✅ | ✅ | ✅ | | -| BadgerDB | ✅ | ✅ | | | ✅ | -| Cassandra | ✅ | ✅ | ✅ | | ✅ | -| Clickhouse | | ✅ | ✅ | | ✅ | +| BadgerDB | ✅ | ✅ | ✅ | ✅ | ✅ | +| Cassandra | ✅ | ✅ | ✅ | ✅ | ✅ | +| Clickhouse | | ✅ | ✅ | ✅ | ✅ | | FTP | | ✅ | | | ✅ | | SFTP | | ✅ | | | ✅ | -| Solr | | ✅ | ✅ | | ✅ | -| DGraph | ✅ | ✅ |✅ | || +| Solr | | ✅ | ✅ | ✅ | ✅ | +| DGraph | ✅ | ✅ |✅ | ✅ || | Azure Eventhub | | ✅ |✅ | |✅| diff --git a/pkg/gofr/datasource/file/ftp/go.mod b/pkg/gofr/datasource/file/ftp/go.mod index 69d4422eb..6919e322a 100644 --- a/pkg/gofr/datasource/file/ftp/go.mod +++ b/pkg/gofr/datasource/file/ftp/go.mod @@ -22,4 +22,4 @@ require ( github.com/rogpeppe/go-internal v1.10.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect -) +) \ No newline at end of file diff --git a/pkg/gofr/datasource/file/ftp/go.sum b/pkg/gofr/datasource/file/ftp/go.sum index 11c7d1680..ad22d4586 100644 --- a/pkg/gofr/datasource/file/ftp/go.sum +++ b/pkg/gofr/datasource/file/ftp/go.sum @@ -36,4 +36,4 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= \ No newline at end of file diff --git a/pkg/gofr/datasource/file/s3/go.mod b/pkg/gofr/datasource/file/s3/go.mod index 4fa4d32ae..323efce2a 100644 --- a/pkg/gofr/datasource/file/s3/go.mod +++ b/pkg/gofr/datasource/file/s3/go.mod @@ -34,4 +34,4 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect -) +) \ No newline at end of file diff --git a/pkg/gofr/datasource/file/s3/go.sum b/pkg/gofr/datasource/file/s3/go.sum index 284ef1896..0e1c602d8 100644 --- a/pkg/gofr/datasource/file/s3/go.sum +++ b/pkg/gofr/datasource/file/s3/go.sum @@ -53,4 +53,4 @@ golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= \ No newline at end of file diff --git a/pkg/gofr/datasource/file/sftp/go.mod b/pkg/gofr/datasource/file/sftp/go.mod index 8c6c99a07..a955395ad 100644 --- a/pkg/gofr/datasource/file/sftp/go.mod +++ b/pkg/gofr/datasource/file/sftp/go.mod @@ -20,4 +20,4 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.26.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect -) +) \ No newline at end of file diff --git a/pkg/gofr/datasource/file/sftp/go.sum b/pkg/gofr/datasource/file/sftp/go.sum index 23a4e010a..1b978f9bf 100644 --- a/pkg/gofr/datasource/file/sftp/go.sum +++ b/pkg/gofr/datasource/file/sftp/go.sum @@ -60,4 +60,4 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= \ No newline at end of file diff --git a/pkg/gofr/datasource/kv-store/badger/badger.go b/pkg/gofr/datasource/kv-store/badger/badger.go index 8342bd33b..e8c4e93b0 100644 --- a/pkg/gofr/datasource/kv-store/badger/badger.go +++ b/pkg/gofr/datasource/kv-store/badger/badger.go @@ -3,14 +3,18 @@ package badger import ( "context" "errors" + "fmt" "strings" "time" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "github.com/dgraph-io/badger/v4" ) +var errStatusDown = errors.New("status down") + type Configs struct { DirPath string } @@ -63,8 +67,10 @@ func (c *client) Connect() { c.db = db } -func (c *client) Get(_ context.Context, key string) (string, error) { - defer c.sendOperationStats(time.Now(), "GET", key, "") +func (c *client) Get(ctx context.Context, key string) (string, error) { + span := c.addTrace(ctx, "get", key) + + defer c.sendOperationStats(time.Now(), "GET", "get", span, key) var value []byte @@ -88,7 +94,7 @@ func (c *client) Get(_ context.Context, key string) (string, error) { err = txn.Commit() if err != nil { - c.logger.Debugf("error while commiting transaction: %v", err) + c.logger.Debugf("error while committing transaction: %v", err) return "", err } @@ -96,16 +102,20 @@ func (c *client) Get(_ context.Context, key string) (string, error) { return string(value), nil } -func (c *client) Set(_ context.Context, key, value string) error { - defer c.sendOperationStats(time.Now(), "SET", key, value) +func (c *client) Set(ctx context.Context, key, value string) error { + span := c.addTrace(ctx, "set", key) + + defer c.sendOperationStats(time.Now(), "SET", "set", span, key, value) return c.useTransaction(func(txn *badger.Txn) error { return txn.Set([]byte(key), []byte(value)) }) } -func (c *client) Delete(_ context.Context, key string) error { - defer c.sendOperationStats(time.Now(), "DELETE", key, "") +func (c *client) Delete(ctx context.Context, key string) error { + span := c.addTrace(ctx, "delete", key) + + defer c.sendOperationStats(time.Now(), "DELETE", "delete", span, key, "") return c.useTransaction(func(txn *badger.Txn) error { return txn.Delete([]byte(key)) @@ -125,7 +135,7 @@ func (c *client) useTransaction(f func(txn *badger.Txn) error) error { err = txn.Commit() if err != nil { - c.logger.Debugf("error while commiting transaction: %v", err) + c.logger.Debugf("error while committing transaction: %v", err) return err } @@ -133,7 +143,8 @@ func (c *client) useTransaction(f func(txn *badger.Txn) error) error { return nil } -func (c *client) sendOperationStats(start time.Time, methodType string, kv ...string) { +func (c *client) sendOperationStats(start time.Time, methodType string, method string, + span trace.Span, kv ...string) { duration := time.Since(start).Milliseconds() c.logger.Debug(&Log{ @@ -142,6 +153,11 @@ func (c *client) sendOperationStats(start time.Time, methodType string, kv ...st Key: strings.Join(kv, " "), }) + if span != nil { + defer span.End() + span.SetAttributes(attribute.Int64(fmt.Sprintf("badger.%v.duration(μs)", method), time.Since(start).Microseconds())) + } + c.metrics.RecordHistogram(context.Background(), "app_badger_stats", float64(duration), "database", c.configs.DirPath, "type", methodType) } @@ -162,10 +178,24 @@ func (c *client) HealthCheck(context.Context) (any, error) { if closed { h.Status = "DOWN" - return &h, errors.New("status down") + return &h, errStatusDown } h.Status = "UP" return &h, nil } + +func (c *client) addTrace(ctx context.Context, method, key string) trace.Span { + if c.tracer != nil { + _, span := c.tracer.Start(ctx, fmt.Sprintf("badger-%v", method)) + + span.SetAttributes( + attribute.String("badger.key", key), + ) + + return span + } + + return nil +} diff --git a/pkg/gofr/datasource/kv-store/badger/badger_test.go b/pkg/gofr/datasource/kv-store/badger/badger_test.go index cf355f83d..827d2b233 100644 --- a/pkg/gofr/datasource/kv-store/badger/badger_test.go +++ b/pkg/gofr/datasource/kv-store/badger/badger_test.go @@ -46,6 +46,7 @@ func Test_ClientGet(t *testing.T) { cl := setupDB(t) err := cl.Set(context.Background(), "lkey", "lvalue") + require.NoError(t, err) val, err := cl.Get(context.Background(), "lkey") @@ -58,7 +59,7 @@ func Test_ClientGetError(t *testing.T) { val, err := cl.Get(context.Background(), "lkey") - assert.EqualError(t, err, "Key not found") + require.EqualError(t, err, "Key not found") assert.Empty(t, val) } diff --git a/pkg/gofr/datasource/kv-store/badger/go.mod b/pkg/gofr/datasource/kv-store/badger/go.mod index 43cf8b6c0..bc706f6df 100644 --- a/pkg/gofr/datasource/kv-store/badger/go.mod +++ b/pkg/gofr/datasource/kv-store/badger/go.mod @@ -5,6 +5,7 @@ go 1.22 require ( github.com/dgraph-io/badger/v4 v4.2.0 github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/otel v1.30.0 go.opentelemetry.io/otel/trace v1.30.0 go.uber.org/mock v0.4.0 ) @@ -25,7 +26,6 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.opencensus.io v0.22.5 // indirect - go.opentelemetry.io/otel v1.30.0 // indirect golang.org/x/net v0.23.0 // indirect golang.org/x/sys v0.18.0 // indirect google.golang.org/protobuf v1.33.0 // indirect diff --git a/pkg/gofr/datasource/kv-store/badger/logger.go b/pkg/gofr/datasource/kv-store/badger/logger.go index 073df9f4c..042295877 100644 --- a/pkg/gofr/datasource/kv-store/badger/logger.go +++ b/pkg/gofr/datasource/kv-store/badger/logger.go @@ -3,7 +3,6 @@ package badger import ( "fmt" "io" - "strings" ) type Logger interface { @@ -24,5 +23,5 @@ type Log struct { func (l *Log) PrettyPrint(writer io.Writer) { fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;162m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s \n", - l.Type, "BADGR", l.Duration, strings.Join([]string{l.Key, l.Value}, " ")) + l.Type, "BADGR", l.Duration, l.Key+" "+l.Value) } diff --git a/pkg/gofr/datasource/sql/sql.go b/pkg/gofr/datasource/sql/sql.go index 8c30e4196..09e4c4b2b 100644 --- a/pkg/gofr/datasource/sql/sql.go +++ b/pkg/gofr/datasource/sql/sql.go @@ -33,6 +33,7 @@ type DBConfig struct { SSLMode string MaxIdleConn int MaxOpenConn int + Charset string } func NewSQL(configs config.Config, logger datasource.Logger, metrics Metrics) *DB { @@ -159,18 +160,24 @@ func getDBConfig(configs config.Config) *DBConfig { MaxIdleConn: maxIdleConn, // only for postgres SSLMode: configs.GetOrDefault("DB_SSL_MODE", "disable"), + Charset: configs.Get("DB_CHARSET"), } } func getDBConnectionString(dbConfig *DBConfig) (string, error) { switch dbConfig.Dialect { case "mysql": - return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local&interpolateParams=true", + if dbConfig.Charset == "" { + dbConfig.Charset = "utf8" + } + + return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local&interpolateParams=true", dbConfig.User, dbConfig.Password, dbConfig.HostName, dbConfig.Port, dbConfig.Database, + dbConfig.Charset, ), nil case "postgres": return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", diff --git a/pkg/gofr/datasource/sql/sql_test.go b/pkg/gofr/datasource/sql/sql_test.go index ed1828446..343526523 100644 --- a/pkg/gofr/datasource/sql/sql_test.go +++ b/pkg/gofr/datasource/sql/sql_test.go @@ -106,6 +106,7 @@ func TestSQL_GetDBConfig(t *testing.T) { "DB_SSL_MODE": "require", "DB_MAX_IDLE_CONNECTION": "25", "DB_MAX_OPEN_CONNECTION": "50", + "DB_CHARSET": "utf8mb4", }) expectedComfigs := &DBConfig{ @@ -118,6 +119,7 @@ func TestSQL_GetDBConfig(t *testing.T) { SSLMode: "require", MaxIdleConn: 25, MaxOpenConn: 50, + Charset: "utf8mb4", } configs := getDBConfig(mockConfig) @@ -187,6 +189,19 @@ func TestSQL_getDBConnectionString(t *testing.T) { }, expOut: "user:password@tcp(host:3201)/test?charset=utf8&parseTime=True&loc=Local&interpolateParams=true", }, + { + desc: "mysql dialect with Configurable charset", + configs: &DBConfig{ + Dialect: "mysql", + HostName: "host", + User: "user", + Password: "password", + Port: "3201", + Database: "test", + Charset: "utf8mb4", + }, + expOut: "user:password@tcp(host:3201)/test?charset=utf8mb4&parseTime=True&loc=Local&interpolateParams=true", + }, { desc: "postgresql dialect", configs: &DBConfig{ diff --git a/pkg/gofr/external_db.go b/pkg/gofr/external_db.go index 0c7d40853..11706715e 100644 --- a/pkg/gofr/external_db.go +++ b/pkg/gofr/external_db.go @@ -92,6 +92,10 @@ func (a *App) AddKVStore(db container.KVStoreProvider) { db.UseLogger(a.Logger()) db.UseMetrics(a.Metrics()) + tracer := otel.GetTracerProvider().Tracer("gofr-badger") + + db.UseTracer(tracer) + db.Connect() a.container.KVStore = db diff --git a/pkg/gofr/external_db_test.go b/pkg/gofr/external_db_test.go index f60f0b66e..7e2d38bc4 100644 --- a/pkg/gofr/external_db_test.go +++ b/pkg/gofr/external_db_test.go @@ -22,6 +22,7 @@ func TestApp_AddKVStore(t *testing.T) { mock.EXPECT().UseLogger(app.Logger()) mock.EXPECT().UseMetrics(app.Metrics()) + mock.EXPECT().UseTracer(otel.GetTracerProvider().Tracer("gofr-badger")) mock.EXPECT().Connect() app.AddKVStore(mock) diff --git a/pkg/gofr/gofr.go b/pkg/gofr/gofr.go index ab4ae49c4..6e7adc5be 100644 --- a/pkg/gofr/gofr.go +++ b/pkg/gofr/gofr.go @@ -88,6 +88,22 @@ func New() *App { app.httpServer.certFile = app.Config.GetOrDefault("CERT_FILE", "") app.httpServer.keyFile = app.Config.GetOrDefault("KEY_FILE", "") + // Add Default routes + app.add(http.MethodGet, "/.well-known/health", healthHandler) + app.add(http.MethodGet, "/.well-known/alive", liveHandler) + app.add(http.MethodGet, "/favicon.ico", faviconHandler) + + // If the openapi.json file exists in the static directory, set up routes for OpenAPI and Swagger documentation. + if _, err = os.Stat("./static/" + gofrHTTP.DefaultSwaggerFileName); err == nil { + // Route to serve the OpenAPI JSON specification file. + app.add(http.MethodGet, "/.well-known/"+gofrHTTP.DefaultSwaggerFileName, OpenAPIHandler) + // Route to serve the Swagger UI, providing a user interface for the API documentation. + app.add(http.MethodGet, "/.well-known/swagger", SwaggerUIHandler) + // Catchall route: any request to /.well-known/{name} (e.g., /.well-known/other) + // will be handled by the SwaggerUIHandler, serving the Swagger UI. + app.add(http.MethodGet, "/.well-known/{name}", SwaggerUIHandler) + } + if app.Config.Get("APP_ENV") == "DEBUG" { app.httpServer.RegisterProfilingRoutes() } @@ -226,17 +242,6 @@ func (a *App) Shutdown(ctx context.Context) error { } func (a *App) httpServerSetup() { - // Add Default routes - a.add(http.MethodGet, "/.well-known/health", healthHandler) - a.add(http.MethodGet, "/.well-known/alive", liveHandler) - a.add(http.MethodGet, "/favicon.ico", faviconHandler) - - if _, err := os.Stat("./static/openapi.json"); err == nil { - a.add(http.MethodGet, "/.well-known/openapi.json", OpenAPIHandler) - a.add(http.MethodGet, "/.well-known/swagger", SwaggerUIHandler) - a.add(http.MethodGet, "/.well-known/{name}", SwaggerUIHandler) - } - // TODO: find a way to read REQUEST_TIMEOUT config only once and log it there. currently doing it twice one for populating // the value and other for logging requestTimeout := a.Config.Get("REQUEST_TIMEOUT") diff --git a/pkg/gofr/http/middleware/apikey_auth.go b/pkg/gofr/http/middleware/apikey_auth.go index cf6961b90..5ed1210fb 100644 --- a/pkg/gofr/http/middleware/apikey_auth.go +++ b/pkg/gofr/http/middleware/apikey_auth.go @@ -3,6 +3,7 @@ package middleware import ( + "context" "net/http" "gofr.dev/pkg/gofr/container" @@ -15,6 +16,8 @@ type APIKeyAuthProvider struct { Container *container.Container } +const APIKey authMethod = 2 + // APIKeyAuthMiddleware creates a middleware function that enforces API key authentication based on the provided API // keys or a validation function. func APIKeyAuthMiddleware(a APIKeyAuthProvider, apiKeys ...string) func(handler http.Handler) http.Handler { @@ -36,6 +39,9 @@ func APIKeyAuthMiddleware(a APIKeyAuthProvider, apiKeys ...string) func(handler return } + ctx := context.WithValue(r.Context(), APIKey, authKey) + *r = *r.Clone(ctx) + handler.ServeHTTP(w, r) }) } diff --git a/pkg/gofr/http/middleware/basic_auth.go b/pkg/gofr/http/middleware/basic_auth.go index 37390acb6..0f389f016 100644 --- a/pkg/gofr/http/middleware/basic_auth.go +++ b/pkg/gofr/http/middleware/basic_auth.go @@ -1,6 +1,7 @@ package middleware import ( + "context" "encoding/base64" "net/http" "strings" @@ -16,6 +17,8 @@ type BasicAuthProvider struct { Container *container.Container } +const Username authMethod = 1 + // BasicAuthMiddleware creates a middleware function that enforces basic authentication using the provided BasicAuthProvider. func BasicAuthMiddleware(basicAuthProvider BasicAuthProvider) func(handler http.Handler) http.Handler { return func(handler http.Handler) http.Handler { @@ -54,6 +57,9 @@ func BasicAuthMiddleware(basicAuthProvider BasicAuthProvider) func(handler http. return } + ctx := context.WithValue(r.Context(), Username, username) + *r = *r.Clone(ctx) + handler.ServeHTTP(w, r) }) } diff --git a/pkg/gofr/http/router.go b/pkg/gofr/http/router.go index 48c3ecee1..137e260f2 100644 --- a/pkg/gofr/http/router.go +++ b/pkg/gofr/http/router.go @@ -2,14 +2,16 @@ package http import ( "net/http" - "os" "path/filepath" "strings" "github.com/gorilla/mux" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) +const DefaultSwaggerFileName = "openapi.json" + // Router is responsible for routing HTTP request. type Router struct { mux.Router @@ -56,6 +58,13 @@ func (rou *Router) AddStaticFiles(endpoint, dirName string) { cfg := staticFileConfig{directoryName: dirName} fileServer := http.FileServer(http.Dir(cfg.directoryName)) + + if endpoint == "/" { + rou.Router.NewRoute().PathPrefix("/").Handler(cfg.staticHandler(fileServer)) + + return + } + rou.Router.NewRoute().PathPrefix(endpoint + "/").Handler(http.StripPrefix(endpoint, cfg.staticHandler(fileServer))) } @@ -67,9 +76,11 @@ func (staticConfig staticFileConfig) staticHandler(fileServer http.Handler) http fileName := filePath[len(filePath)-1] - const defaultSwaggerFileName = "openapi.json" - - if _, err := os.Stat(filepath.Clean(filepath.Join(staticConfig.directoryName, url))); fileName == defaultSwaggerFileName && err == nil { + // Prevent direct access to the openapi.json file via static file routes. + // The file should only be accessible through the explicitly defined /.well-known/swagger or + // /.well-known/openapi.json for controlled access. + absPath, err := filepath.Abs(filepath.Join(staticConfig.directoryName, url)) + if err != nil || !strings.HasPrefix(absPath, staticConfig.directoryName) || (fileName == DefaultSwaggerFileName && err == nil) { w.WriteHeader(http.StatusForbidden) _, _ = w.Write([]byte("403 forbidden"))