diff --git a/README.md b/README.md
index d42602b7f..215d6bae4 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@
-
+
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"))