diff --git a/app/package-lock.json b/app/package-lock.json
index 4a9978e..26598e3 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -9,7 +9,7 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
- "@authorizerdev/authorizer-react": "^0.25.0",
+ "@authorizerdev/authorizer-react": "^0.26.0-beta.0",
"@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9",
"esbuild": "^0.12.17",
@@ -26,9 +26,9 @@
}
},
"node_modules/@authorizerdev/authorizer-js": {
- "version": "0.14.0",
- "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.14.0.tgz",
- "integrity": "sha512-cpeeFrmG623QPLn+nf+ACHayZYqW8xokIidGikeboBDJtuAAQB50a54/7HwLHriG2FB7WvPuHQ/9LFFX//N1lg==",
+ "version": "0.17.0-beta.1",
+ "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.17.0-beta.1.tgz",
+ "integrity": "sha512-jUlFUrs4Ys6LZ5hclPeRt84teygi+bA57d/IpV9GAqOrfifv70jkFeDln4+Bs0mZk74el23Xn+DR9380mqE4Cg==",
"dependencies": {
"node-fetch": "^2.6.1"
},
@@ -37,11 +37,11 @@
}
},
"node_modules/@authorizerdev/authorizer-react": {
- "version": "0.25.0",
- "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.25.0.tgz",
- "integrity": "sha512-Dt2rZf+cGCVb8dqcJ/9l8Trx+QeXnTdfhER6r/cq0iOnFC9MqWzQPB3RgrlUoMLHtZvKNDXIk1HvfD5hSX9lhw==",
+ "version": "0.26.0-beta.0",
+ "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.26.0-beta.0.tgz",
+ "integrity": "sha512-YfyiGYBmbsp3tLWIxOrOZ/hUTCmdMXVE9SLE8m1xsFsxzJJlUhepp0AMahSbH5EyLj5bchOhOw/rzgpnDZDvMw==",
"dependencies": {
- "@authorizerdev/authorizer-js": "^0.14.0",
+ "@authorizerdev/authorizer-js": "^0.17.0-beta.1",
"final-form": "^4.20.2",
"react-final-form": "^6.5.3",
"styled-components": "^5.3.0"
@@ -852,19 +852,19 @@
},
"dependencies": {
"@authorizerdev/authorizer-js": {
- "version": "0.14.0",
- "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.14.0.tgz",
- "integrity": "sha512-cpeeFrmG623QPLn+nf+ACHayZYqW8xokIidGikeboBDJtuAAQB50a54/7HwLHriG2FB7WvPuHQ/9LFFX//N1lg==",
+ "version": "0.17.0-beta.1",
+ "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.17.0-beta.1.tgz",
+ "integrity": "sha512-jUlFUrs4Ys6LZ5hclPeRt84teygi+bA57d/IpV9GAqOrfifv70jkFeDln4+Bs0mZk74el23Xn+DR9380mqE4Cg==",
"requires": {
"node-fetch": "^2.6.1"
}
},
"@authorizerdev/authorizer-react": {
- "version": "0.25.0",
- "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.25.0.tgz",
- "integrity": "sha512-Dt2rZf+cGCVb8dqcJ/9l8Trx+QeXnTdfhER6r/cq0iOnFC9MqWzQPB3RgrlUoMLHtZvKNDXIk1HvfD5hSX9lhw==",
+ "version": "0.26.0-beta.0",
+ "resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.26.0-beta.0.tgz",
+ "integrity": "sha512-YfyiGYBmbsp3tLWIxOrOZ/hUTCmdMXVE9SLE8m1xsFsxzJJlUhepp0AMahSbH5EyLj5bchOhOw/rzgpnDZDvMw==",
"requires": {
- "@authorizerdev/authorizer-js": "^0.14.0",
+ "@authorizerdev/authorizer-js": "^0.17.0-beta.1",
"final-form": "^4.20.2",
"react-final-form": "^6.5.3",
"styled-components": "^5.3.0"
diff --git a/app/package.json b/app/package.json
index 8c5b77e..c3234c9 100644
--- a/app/package.json
+++ b/app/package.json
@@ -11,7 +11,7 @@
"author": "Lakhan Samani",
"license": "ISC",
"dependencies": {
- "@authorizerdev/authorizer-react": "^0.25.0",
+ "@authorizerdev/authorizer-react": "^0.26.0-beta.0",
"@types/react": "^17.0.15",
"@types/react-dom": "^17.0.9",
"esbuild": "^0.12.17",
diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json
index 41d31f9..c04cac0 100644
--- a/dashboard/package-lock.json
+++ b/dashboard/package-lock.json
@@ -2529,7 +2529,8 @@
"@chakra-ui/css-reset": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@chakra-ui/css-reset/-/css-reset-1.1.1.tgz",
- "integrity": "sha512-+KNNHL4OWqeKia5SL858K3Qbd8WxMij9mWIilBzLD4j2KFrl/+aWFw8syMKth3NmgIibrjsljo+PU3fy2o50dg=="
+ "integrity": "sha512-+KNNHL4OWqeKia5SL858K3Qbd8WxMij9mWIilBzLD4j2KFrl/+aWFw8syMKth3NmgIibrjsljo+PU3fy2o50dg==",
+ "requires": {}
},
"@chakra-ui/descendant": {
"version": "2.1.1",
@@ -3133,7 +3134,8 @@
"@graphql-typed-document-node/core": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz",
- "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg=="
+ "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==",
+ "requires": {}
},
"@popperjs/core": {
"version": "2.11.0",
@@ -3843,7 +3845,8 @@
"react-icons": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz",
- "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ=="
+ "integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==",
+ "requires": {}
},
"react-is": {
"version": "16.13.1",
@@ -4029,7 +4032,8 @@
"use-callback-ref": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.2.5.tgz",
- "integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg=="
+ "integrity": "sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==",
+ "requires": {}
},
"use-sidecar": {
"version": "1.0.5",
diff --git a/server/email/otp.go b/server/email/otp.go
new file mode 100644
index 0000000..181a1e0
--- /dev/null
+++ b/server/email/otp.go
@@ -0,0 +1,118 @@
+package email
+
+import (
+ log "github.com/sirupsen/logrus"
+
+ "github.com/authorizerdev/authorizer/server/constants"
+ "github.com/authorizerdev/authorizer/server/memorystore"
+)
+
+// SendOtpMail to send otp email
+func SendOtpMail(toEmail, otp string) error {
+ // The receiver needs to be in slice as the receive supports multiple receiver
+ Receiver := []string{toEmail}
+
+ Subject := "OTP for your multi factor authentication"
+ message := `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+ data := make(map[string]interface{}, 3)
+ var err error
+ data["org_logo"], err = memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo)
+ if err != nil {
+ return err
+ }
+ data["org_name"], err = memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName)
+ if err != nil {
+ return err
+ }
+ data["otp"] = otp
+ message = addEmailTemplate(message, data, "otp.tmpl")
+ // bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message)
+
+ err = SendMail(Receiver, Subject, message)
+ if err != nil {
+ log.Warn("error sending email: ", err)
+ }
+ return err
+}
diff --git a/server/env/persist_env.go b/server/env/persist_env.go
index 355ef7d..d783b93 100644
--- a/server/env/persist_env.go
+++ b/server/env/persist_env.go
@@ -221,9 +221,10 @@ func PersistEnv() error {
// handle derivative cases like disabling email verification & magic login
// in case SMTP is off but env is set to true
if storeData[constants.EnvKeySmtpHost] == "" || storeData[constants.EnvKeySmtpUsername] == "" || storeData[constants.EnvKeySmtpPassword] == "" || storeData[constants.EnvKeySenderEmail] == "" && storeData[constants.EnvKeySmtpPort] == "" {
+ storeData[constants.EnvKeyIsEmailServiceEnabled] = false
+
if !storeData[constants.EnvKeyDisableEmailVerification].(bool) {
storeData[constants.EnvKeyDisableEmailVerification] = true
- storeData[constants.EnvKeyIsEmailServiceEnabled] = false
hasChanged = true
}
diff --git a/server/memorystore/memory_store.go b/server/memorystore/memory_store.go
index ad6a418..9cbbbb4 100644
--- a/server/memorystore/memory_store.go
+++ b/server/memorystore/memory_store.go
@@ -31,6 +31,7 @@ func InitMemStore() error {
constants.EnvKeyDisableLoginPage: false,
constants.EnvKeyDisableSignUp: false,
constants.EnvKeyDisableStrongPassword: false,
+ constants.EnvKeyIsEmailServiceEnabled: false,
}
requiredEnvs := RequiredEnvStoreObj.GetRequiredEnv()
diff --git a/server/memorystore/providers/redis/store.go b/server/memorystore/providers/redis/store.go
index 36b4b0c..d6ee1df 100644
--- a/server/memorystore/providers/redis/store.go
+++ b/server/memorystore/providers/redis/store.go
@@ -160,7 +160,7 @@ func (c *provider) GetEnvStore() (map[string]interface{}, error) {
return nil, err
}
for key, value := range data {
- if key == constants.EnvKeyDisableBasicAuthentication || key == constants.EnvKeyDisableEmailVerification || key == constants.EnvKeyDisableLoginPage || key == constants.EnvKeyDisableMagicLinkLogin || key == constants.EnvKeyDisableRedisForEnv || key == constants.EnvKeyDisableSignUp || key == constants.EnvKeyDisableStrongPassword {
+ if key == constants.EnvKeyDisableBasicAuthentication || key == constants.EnvKeyDisableEmailVerification || key == constants.EnvKeyDisableLoginPage || key == constants.EnvKeyDisableMagicLinkLogin || key == constants.EnvKeyDisableRedisForEnv || key == constants.EnvKeyDisableSignUp || key == constants.EnvKeyDisableStrongPassword || key == constants.EnvKeyIsEmailServiceEnabled {
boolValue, err := strconv.ParseBool(value)
if err != nil {
return res, err
diff --git a/server/resolvers/invite_members.go b/server/resolvers/invite_members.go
index 2316456..914e7bb 100644
--- a/server/resolvers/invite_members.go
+++ b/server/resolvers/invite_members.go
@@ -35,13 +35,13 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput)
}
// this feature is only allowed if email server is configured
- isEmailVerificationDisabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification)
+ EnvKeyIsEmailServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
if err != nil {
log.Debug("Error getting email verification disabled: ", err)
- isEmailVerificationDisabled = true
+ EnvKeyIsEmailServiceEnabled = false
}
- if isEmailVerificationDisabled {
+ if !EnvKeyIsEmailServiceEnabled {
log.Debug("Email server is not configured")
return nil, errors.New("email sending is disabled")
}
diff --git a/server/resolvers/login.go b/server/resolvers/login.go
index c7fafe3..7d1f28e 100644
--- a/server/resolvers/login.go
+++ b/server/resolvers/login.go
@@ -2,6 +2,7 @@ package resolvers
import (
"context"
+ "errors"
"fmt"
"strings"
"time"
@@ -13,6 +14,7 @@ import (
"github.com/authorizerdev/authorizer/server/cookie"
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/db/models"
+ "github.com/authorizerdev/authorizer/server/email"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/memorystore"
"github.com/authorizerdev/authorizer/server/refs"
@@ -99,12 +101,29 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
}
if refs.BoolValue(user.IsMultiFactorAuthEnabled) {
- //TODO - send email based on email config
- db.Provider.UpsertOTP(ctx, &models.OTP{
+ isEnvServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
+ if err != nil || !isEnvServiceEnabled {
+ log.Debug("Email service not enabled:")
+ return nil, errors.New("email service not enabled")
+ }
+ otp := utils.GenerateOTP()
+ otpData, err := db.Provider.UpsertOTP(ctx, &models.OTP{
Email: user.Email,
- Otp: utils.GenerateOTP(),
+ Otp: otp,
ExpiresAt: time.Now().Add(1 * time.Minute).Unix(),
})
+ if err != nil {
+ log.Debug("Failed to add otp: ", err)
+ return nil, err
+ }
+
+ go func() {
+ err := email.SendOtpMail(user.Email, otpData.Otp)
+ if err != nil {
+ log.Debug("Failed to send otp email: ", err)
+ }
+ }()
+
return &model.AuthResponse{
Message: "Please check the OTP in your inbox",
ShouldShowOtpScreen: refs.NewBoolRef(true),
diff --git a/server/resolvers/resend_otp.go b/server/resolvers/resend_otp.go
index 1eb1333..60367c1 100644
--- a/server/resolvers/resend_otp.go
+++ b/server/resolvers/resend_otp.go
@@ -2,6 +2,7 @@ package resolvers
import (
"context"
+ "errors"
"fmt"
"strings"
"time"
@@ -10,6 +11,7 @@ import (
"github.com/authorizerdev/authorizer/server/db"
"github.com/authorizerdev/authorizer/server/db/models"
+ "github.com/authorizerdev/authorizer/server/email"
"github.com/authorizerdev/authorizer/server/graph/model"
"github.com/authorizerdev/authorizer/server/refs"
"github.com/authorizerdev/authorizer/server/utils"
@@ -17,8 +19,6 @@ import (
// ResendOTPResolver is a resolver for resend otp mutation
func ResendOTPResolver(ctx context.Context, params model.ResendOTPRequest) (*model.Response, error) {
- var res *model.Response
-
log := log.WithFields(log.Fields{
"email": params.Email,
})
@@ -26,34 +26,57 @@ func ResendOTPResolver(ctx context.Context, params model.ResendOTPRequest) (*mod
user, err := db.Provider.GetUserByEmail(ctx, params.Email)
if err != nil {
log.Debug("Failed to get user by email: ", err)
- return res, fmt.Errorf(`user with this email not found`)
+ return nil, fmt.Errorf(`user with this email not found`)
}
if user.RevokedTimestamp != nil {
log.Debug("User access is revoked")
- return res, fmt.Errorf(`user access has been revoked`)
+ return nil, fmt.Errorf(`user access has been revoked`)
}
if user.EmailVerifiedAt == nil {
log.Debug("User email is not verified")
- return res, fmt.Errorf(`email not verified`)
+ return nil, fmt.Errorf(`email not verified`)
}
if !refs.BoolValue(user.IsMultiFactorAuthEnabled) {
log.Debug("User multi factor authentication is not enabled")
- return res, fmt.Errorf(`multi factor authentication not enabled`)
+ return nil, fmt.Errorf(`multi factor authentication not enabled`)
}
- //TODO - send email based on email config
- db.Provider.UpsertOTP(ctx, &models.OTP{
+ // get otp by email
+ otpData, err := db.Provider.GetOTPByEmail(ctx, params.Email)
+ if err != nil {
+ log.Debug("Failed to get otp for given email: ", err)
+ return nil, err
+ }
+
+ if otpData == nil {
+ log.Debug("No otp found for given email: ", params.Email)
+ return &model.Response{
+ Message: "Failed to get for given email",
+ }, errors.New("failed to get otp for given email")
+ }
+
+ otp := utils.GenerateOTP()
+ otpData, err = db.Provider.UpsertOTP(ctx, &models.OTP{
Email: user.Email,
- Otp: utils.GenerateOTP(),
+ Otp: otp,
ExpiresAt: time.Now().Add(1 * time.Minute).Unix(),
})
-
- res = &model.Response{
- Message: `OTP has been sent. Please check your inbox`,
+ if err != nil {
+ log.Debug("Error generating new otp: ", err)
+ return nil, err
}
- return res, nil
+ go func() {
+ err := email.SendOtpMail(params.Email, otp)
+ if err != nil {
+ log.Debug("Error sending otp email: ", otp)
+ }
+ }()
+
+ return &model.Response{
+ Message: `OTP has been sent. Please check your inbox`,
+ }, nil
}
diff --git a/server/resolvers/update_env.go b/server/resolvers/update_env.go
index 508d47e..30abe9e 100644
--- a/server/resolvers/update_env.go
+++ b/server/resolvers/update_env.go
@@ -234,6 +234,7 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
// handle derivative cases like disabling email verification & magic login
// in case SMTP is off but env is set to true
if updatedData[constants.EnvKeySmtpHost] == "" || updatedData[constants.EnvKeySmtpUsername] == "" || updatedData[constants.EnvKeySmtpPassword] == "" || updatedData[constants.EnvKeySenderEmail] == "" && updatedData[constants.EnvKeySmtpPort] == "" {
+ updatedData[constants.EnvKeyIsEmailServiceEnabled] = false
if !updatedData[constants.EnvKeyDisableEmailVerification].(bool) {
updatedData[constants.EnvKeyDisableEmailVerification] = true
}
@@ -243,6 +244,10 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
}
}
+ if updatedData[constants.EnvKeySmtpHost] != "" || updatedData[constants.EnvKeySmtpUsername] != "" || updatedData[constants.EnvKeySmtpPassword] != "" || updatedData[constants.EnvKeySenderEmail] != "" && updatedData[constants.EnvKeySmtpPort] != "" {
+ updatedData[constants.EnvKeyIsEmailServiceEnabled] = true
+ }
+
// check the roles change
if len(params.Roles) > 0 {
if len(params.DefaultRoles) > 0 {
diff --git a/server/resolvers/update_profile.go b/server/resolvers/update_profile.go
index ac2947f..0a47376 100644
--- a/server/resolvers/update_profile.go
+++ b/server/resolvers/update_profile.go
@@ -2,6 +2,7 @@ package resolvers
import (
"context"
+ "errors"
"fmt"
"strings"
"time"
@@ -96,6 +97,13 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
if params.IsMultiFactorAuthEnabled != nil && refs.BoolValue(user.IsMultiFactorAuthEnabled) != refs.BoolValue(params.IsMultiFactorAuthEnabled) {
user.IsMultiFactorAuthEnabled = params.IsMultiFactorAuthEnabled
+ if refs.BoolValue(params.IsMultiFactorAuthEnabled) {
+ isEnvServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
+ if err != nil || !isEnvServiceEnabled {
+ log.Debug("Email service not enabled:")
+ return nil, errors.New("email service not enabled, so cannot enable multi factor authentication")
+ }
+ }
}
isPasswordChanging := false
diff --git a/server/resolvers/update_user.go b/server/resolvers/update_user.go
index da9c58d..d20e4a9 100644
--- a/server/resolvers/update_user.go
+++ b/server/resolvers/update_user.go
@@ -2,6 +2,7 @@ package resolvers
import (
"context"
+ "errors"
"fmt"
"strings"
"time"
@@ -91,6 +92,13 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
if params.IsMultiFactorAuthEnabled != nil && refs.BoolValue(user.IsMultiFactorAuthEnabled) != refs.BoolValue(params.IsMultiFactorAuthEnabled) {
user.IsMultiFactorAuthEnabled = params.IsMultiFactorAuthEnabled
+ if refs.BoolValue(params.IsMultiFactorAuthEnabled) {
+ isEnvServiceEnabled, err := memorystore.Provider.GetBoolStoreEnvVariable(constants.EnvKeyIsEmailServiceEnabled)
+ if err != nil || !isEnvServiceEnabled {
+ log.Debug("Email service not enabled:")
+ return nil, errors.New("email service not enabled, so cannot enable multi factor authentication")
+ }
+ }
}
if params.EmailVerified != nil {
diff --git a/server/resolvers/verify_otp.go b/server/resolvers/verify_otp.go
index 95bb78d..b792adb 100644
--- a/server/resolvers/verify_otp.go
+++ b/server/resolvers/verify_otp.go
@@ -52,8 +52,7 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod
isSignUp := user.EmailVerifiedAt == nil
- // TODO - Add Login method in DB
-
+ // TODO - Add Login method in DB when we introduce OTP for social media login
loginMethod := constants.AuthRecipeMethodBasicAuth
roles := strings.Split(user.Roles, ",")
@@ -65,11 +64,7 @@ func VerifyOtpResolver(ctx context.Context, params model.VerifyOTPRequest) (*mod
}
go func() {
- err = db.Provider.DeleteOTP(gc, otp)
-
- if err != nil {
- log.Debug("Failed to delete otp: ", err)
- }
+ db.Provider.DeleteOTP(gc, otp)
if isSignUp {
utils.RegisterEvent(ctx, constants.UserSignUpWebhookEvent, loginMethod, user)
} else {
diff --git a/server/utils/gin_context.go b/server/utils/gin_context.go
index 72fd480..7e3ced6 100644
--- a/server/utils/gin_context.go
+++ b/server/utils/gin_context.go
@@ -7,7 +7,7 @@ import (
"github.com/gin-gonic/gin"
)
-// TODO renamae GinContextKey -> GinContext
+// TODO re-name GinContextKey -> GinContext
// GinContext to get gin context from context
func GinContextFromContext(ctx context.Context) (*gin.Context, error) {