From b1d6d8d05d84de5caa8505a2b04a857d54c7f46e Mon Sep 17 00:00:00 2001 From: HUAHUAI23 Date: Mon, 23 Sep 2024 11:31:19 +0000 Subject: [PATCH 1/2] fix giftcode concurrency issue and add realname info api --- .../pkg/database/cockroach/accountv2.go | 29 +++++++++++ controllers/pkg/types/global.go | 21 +++++++- service/account/api/api.go | 52 +++++++++++++++++++ service/account/dao/interface.go | 12 +++++ service/account/dao/interface_test.go | 26 +++++++++- service/account/helper/common.go | 1 + service/account/helper/request.go | 26 ++++++++++ service/account/router/router.go | 3 +- 8 files changed, 165 insertions(+), 5 deletions(-) diff --git a/controllers/pkg/database/cockroach/accountv2.go b/controllers/pkg/database/cockroach/accountv2.go index 19233ebd91a..3fcf8974f9d 100644 --- a/controllers/pkg/database/cockroach/accountv2.go +++ b/controllers/pkg/database/cockroach/accountv2.go @@ -17,6 +17,7 @@ package cockroach import ( "errors" "fmt" + "gorm.io/gorm/clause" "log" "os" "path/filepath" @@ -1069,6 +1070,17 @@ func (c *Cockroach) GetGiftCodeWithCode(code string) (*types.GiftCode, error) { func (c *Cockroach) UseGiftCode(giftCode *types.GiftCode, userID string) error { return c.DB.Transaction(func(tx *gorm.DB) error { + var lockedGiftCode types.GiftCode + // Lock the gift code record for update + if err := tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "NOWAIT"}). + Where(&types.GiftCode{ID: giftCode.ID}).First(&lockedGiftCode).Error; err != nil { + return fmt.Errorf("failed to lock gift code: %w", err) + } + + if lockedGiftCode.Used { + return fmt.Errorf("gift code has already been used") + } + ops := &types.UserQueryOpts{ID: userID} // Update the user's balance if err := c.updateBalance(tx, ops, giftCode.CreditAmount, false, true); err != nil { @@ -1103,3 +1115,20 @@ func (c *Cockroach) UseGiftCode(giftCode *types.GiftCode, userID string) error { return nil }) } + +func (c *Cockroach) GetUserRealNameInfoByUserID(userID string) (*types.UserRealNameInfo, error) { + // get user info + ops := &types.UserQueryOpts{ID: userID} + user, err := c.GetUserCr(ops) + + if err != nil { + return nil, fmt.Errorf("failed to get user: %v", err) + } + + // get user realname info + var userRealNameInfo types.UserRealNameInfo + if err := c.DB.Where(&types.UserRealNameInfo{UserUID: user.UserUID}).First(&userRealNameInfo).Error; err != nil { + return nil, fmt.Errorf("failed to get user real name info: %w", err) + } + return &userRealNameInfo, nil +} diff --git a/controllers/pkg/types/global.go b/controllers/pkg/types/global.go index 87097db16a5..dd48444ae44 100644 --- a/controllers/pkg/types/global.go +++ b/controllers/pkg/types/global.go @@ -15,9 +15,9 @@ package types import ( - "time" - + "encoding/json" "github.com/google/uuid" + "time" ) type Account struct { @@ -320,3 +320,20 @@ type AccountTransaction struct { func (AccountTransaction) TableName() string { return "AccountTransaction" } + +type UserRealNameInfo struct { + ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key"` + UserUID uuid.UUID `gorm:"column:userUid;type:uuid;unique"` + RealName *string `gorm:"column:realName;type:text"` + IDCard *string `gorm:"column:idCard;type:text"` + Phone *string `gorm:"column:phone;type:text"` + IsVerified bool `gorm:"column:isVerified;type:boolean;default:false"` + IDVerifyFailedTimes int `gorm:"column:idVerifyFailedTimes;type:integer;default:0"` + CreatedAt time.Time `gorm:"column:createdAt;type:timestamp(3) with time zone;default:current_timestamp()"` + UpdatedAt time.Time `gorm:"column:updatedAt;type:timestamp(3) with time zone;autoUpdateTime"` + AdditionalInfo json.RawMessage `gorm:"column:additionalInfo;type:jsonb"` +} + +func (UserRealNameInfo) TableName() string { + return "UserRealNameInfo" +} diff --git a/service/account/api/api.go b/service/account/api/api.go index 665b1d3d05e..734b82e6913 100644 --- a/service/account/api/api.go +++ b/service/account/api/api.go @@ -993,6 +993,58 @@ func UserUsage(c *gin.Context) { }) } +// GetUserRealNameInfo +// @Summary Get user real name information +// @Description Retrieve the real name information for a user +// @Tags GetUserRealNameInfo +// @Accept json +// @Produce json +// @Param request body helper.GetRealNameInfoReq true "Get real name info request" +// @Success 200 {object} helper.GetRealNameInfoResp "Successfully retrieved user real name info" +// @Failure 400 {object} helper.ErrorMessage "Failed to parse get real name info request" +// @Failure 401 {object} helper.ErrorMessage "Authentication error" +// @Failure 500 {object} helper.ErrorMessage "Failed to get user real name info or info not found/verified" +// @Router /account/v1alpha1/real-name-info [post] +func GetUserRealNameInfo(c *gin.Context) { + // Parse the get real name info request + req, err := helper.ParseGetRealNameInfoReq(c) + if err != nil { + c.JSON(http.StatusBadRequest, helper.ErrorMessage{Error: fmt.Sprintf("failed to parse get real name info request: %v", err)}) + return + } + + if err := authenticateRequest(c, req); err != nil { + c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error : %v", err)}) + return + } + + userRealNameInfo, err := dao.DBClient.GetUserRealNameInfo(req) + + if err != nil { + c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("failed to get user real name info: %v", err)}) + return + } + + if userRealNameInfo == nil { + c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: "user real name info not found"}) + return + } + + if !userRealNameInfo.IsVerified { + c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: "user real name info is not verified"}) + return + } + + // Return success response + c.JSON(http.StatusOK, helper.GetRealNameInfoResp{ + Data: helper.GetRealNameInfoRespData{ + UserID: req.Auth.UserID, + IsRealName: userRealNameInfo.IsVerified, + }, + Message: "successfully get user real name info", + }) +} + func CheckAuthAndCalibrate(auth *helper.Auth) (err error) { if auth == nil { return helper.ErrNullAuth diff --git a/service/account/dao/interface.go b/service/account/dao/interface.go index 2f0d7a17e99..32c510e5629 100644 --- a/service/account/dao/interface.go +++ b/service/account/dao/interface.go @@ -57,6 +57,7 @@ type Interface interface { GetRegions() ([]types.Region, error) GetLocalRegion() types.Region UseGiftCode(req *helper.UseGiftCodeReq) (*types.GiftCode, error) + GetUserRealNameInfo(req *helper.GetRealNameInfoReq) (*types.UserRealNameInfo, error) } type Account struct { @@ -1434,3 +1435,14 @@ func (m *Account) UseGiftCode(req *helper.UseGiftCodeReq) (*types.GiftCode, erro return giftCode, nil } + +func (m *Account) GetUserRealNameInfo(req *helper.GetRealNameInfoReq) (*types.UserRealNameInfo, error) { + // get user info + userRealNameInfo, err := m.ck.GetUserRealNameInfoByUserID(req.UserID) + + if err != nil { + return nil, fmt.Errorf("failed to get user real name info: %v", err) + } + + return userRealNameInfo, nil +} diff --git a/service/account/dao/interface_test.go b/service/account/dao/interface_test.go index 96c80dac85c..2069d10201d 100644 --- a/service/account/dao/interface_test.go +++ b/service/account/dao/interface_test.go @@ -608,10 +608,10 @@ func TestAccount_UseGiftCode(t *testing.T) { } giftcode, err := db.UseGiftCode(&helper.UseGiftCodeReq{ - Code: "DfxAffaeEf", + Code: "d-intl-TXERVADC3ASAGQJSx", AuthBase: helper.AuthBase{ Auth: &helper.Auth{ - Owner: "E1xAJ0fy4k", + UserID: "E1xAJ0fy4k", }, }, }) @@ -623,6 +623,28 @@ func TestAccount_UseGiftCode(t *testing.T) { t.Logf("giftcode = %+v", giftcode) } +func TestAccount_GetUserRealNameInfo(t *testing.T) { + db, err := newAccountForTest("", os.Getenv("GLOBAL_COCKROACH_URI"), os.Getenv("LOCAL_COCKROACH_URI")) + if err != nil { + t.Fatalf("NewAccountInterface() error = %v", err) + return + } + + userRealNameInfo, err := db.GetUserRealNameInfo(&helper.GetRealNameInfoReq{ + AuthBase: helper.AuthBase{ + Auth: &helper.Auth{ + UserID: "E1xAJ0fy4k", + }, + }, + }) + + if err != nil { + t.Fatalf("GetUserRealNameInfo() error = %v", err) + return + } + t.Logf("userRealNameInfo = %+v", userRealNameInfo) +} + func init() { // set env os.Setenv("MONGO_URI", "") diff --git a/service/account/helper/common.go b/service/account/helper/common.go index dc3a796c6ec..7120ea26265 100644 --- a/service/account/helper/common.go +++ b/service/account/helper/common.go @@ -28,6 +28,7 @@ const ( GetInvoicePayment = "/invoice/get-payment" UseGiftCode = "/gift-code/use" UserUsage = "/user-usage" + GetUserRealNameInfo = "/real-name-info" ) // env diff --git a/service/account/helper/request.go b/service/account/helper/request.go index ffda39c369c..60e4f922d51 100644 --- a/service/account/helper/request.go +++ b/service/account/helper/request.go @@ -218,6 +218,13 @@ type UseGiftCodeReq struct { AuthBase `json:",inline" bson:",inline"` } +type GetRealNameInfoReq struct { + // @Summary Authentication information + // @Description Authentication information + // @JSONSchema required + AuthBase `json:",inline" bson:",inline"` +} + type UserUsageReq struct { // @Summary Start and end time for the request // @Description Start and end time for the request @@ -551,6 +558,25 @@ func ParseUseGiftCodeReq(c *gin.Context) (*UseGiftCodeReq, error) { return useGiftCode, nil } +type GetRealNameInfoRespData struct { + UserID string `json:"userID" bson:"userID" example:"user-123"` + IsRealName bool `json:"isRealName" bson:"isRealName" example:"true"` +} + +type GetRealNameInfoResp struct { + Data GetRealNameInfoRespData `json:"data,omitempty" bson:"data,omitempty"` + Message string `json:"message,omitempty" bson:"message" example:"Successfully retrieved real name information"` +} + +func ParseGetRealNameInfoReq(c *gin.Context) (*GetRealNameInfoReq, error) { + getRealNameInfoReq := &GetRealNameInfoReq{} + if err := c.ShouldBindJSON(getRealNameInfoReq); err != nil { + return nil, fmt.Errorf("bind json error: %v", err) + } + + return getRealNameInfoReq, nil +} + func ParseUserUsageReq(c *gin.Context) (*UserUsageReq, error) { userUsage := &UserUsageReq{} if err := c.ShouldBindJSON(userUsage); err != nil { diff --git a/service/account/router/router.go b/service/account/router/router.go index 653199e8b11..b6f6c65163b 100644 --- a/service/account/router/router.go +++ b/service/account/router/router.go @@ -56,7 +56,8 @@ func RegisterPayRouter() { POST(helper.SetStatusInvoice, api.SetStatusInvoice). POST(helper.GetInvoicePayment, api.GetInvoicePayment). POST(helper.UseGiftCode, api.UseGiftCode). - POST(helper.UserUsage, api.UserUsage) + POST(helper.UserUsage, api.UserUsage). + POST(helper.GetUserRealNameInfo, api.GetUserRealNameInfo) docs.SwaggerInfo.Host = env.GetEnvWithDefault("SWAGGER_HOST", "localhost:2333") router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) From d832a38b863647bf539806803580a453c68fed3f Mon Sep 17 00:00:00 2001 From: HUAHUAI23 Date: Mon, 23 Sep 2024 12:02:10 +0000 Subject: [PATCH 2/2] test --- service/account/api/api.go | 163 ++++++------------------------------- 1 file changed, 26 insertions(+), 137 deletions(-) diff --git a/service/account/api/api.go b/service/account/api/api.go index 734b82e6913..8bdba255df6 100644 --- a/service/account/api/api.go +++ b/service/account/api/api.go @@ -9,9 +9,8 @@ import ( "io" "net/http" "os" - "strings" - auth2 "github.com/labring/sealos/service/pkg/auth" + "gorm.io/gorm" "github.com/labring/sealos/controllers/pkg/resources" @@ -242,7 +241,7 @@ func GetPayment(c *gin.Context) { c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error : %v", err)}) return } - payment, limitResp, err := dao.DBClient.GetPayment(&types.UserQueryOpts{Owner: req.Auth.Owner}, req) + payment, limitResp, err := dao.DBClient.GetPayment(&types.UserQueryOpts{ID: req.Auth.UserID}, req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get payment : %v", err)}) return @@ -265,14 +264,14 @@ func GetPayment(c *gin.Context) { // @Tags RechargeAmount // @Accept json // @Produce json -// @Param request body helper.UserBaseReq true "User recharge amount request" +// @Param request body helper.UserTimeRangeReq true "User recharge amount request" // @Success 200 {object} map[string]interface{} "successfully retrieved user recharge amount" // @Failure 400 {object} map[string]interface{} "failed to parse user recharge amount request" // @Failure 401 {object} map[string]interface{} "authenticate error" // @Failure 500 {object} map[string]interface{} "failed to get user recharge amount" // @Router /account/v1alpha1/costs/recharge [post] func GetRechargeAmount(c *gin.Context) { - req, err := helper.ParseUserBaseReq(c) + req, err := helper.ParseUserTimeRangeReq(c) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to parse user recharge amount request: %v", err)}) return @@ -281,7 +280,7 @@ func GetRechargeAmount(c *gin.Context) { c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error : %v", err)}) return } - amount, err := dao.DBClient.GetRechargeAmount(types.UserQueryOpts{Owner: req.Auth.Owner}, req.TimeRange.StartTime, req.TimeRange.EndTime) + amount, err := dao.DBClient.GetRechargeAmount(types.UserQueryOpts{ID: req.Auth.UserID}, req.TimeRange.StartTime, req.TimeRange.EndTime) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get recharge amount : %v", err)}) return @@ -297,14 +296,14 @@ func GetRechargeAmount(c *gin.Context) { // @Tags PropertiesUsedAmount // @Accept json // @Produce json -// @Param request body helper.UserBaseReq true "User properties used amount request" +// @Param request body helper.UserTimeRangeReq true "User properties used amount request" // @Success 200 {object} map[string]interface{} "successfully retrieved user properties used amount" // @Failure 400 {object} map[string]interface{} "failed to parse user properties used amount request" // @Failure 401 {object} map[string]interface{} "authenticate error" // @Failure 500 {object} map[string]interface{} "failed to get user properties used amount" // @Router /account/v1alpha1/costs/properties [post] func GetPropertiesUsedAmount(c *gin.Context) { - req, err := helper.ParseUserBaseReq(c) + req, err := helper.ParseUserTimeRangeReq(c) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to parse user properties used amount request: %v", err)}) return @@ -332,6 +331,7 @@ type CostsResultData struct { Costs common.TimeCostsMap `json:"costs" bson:"costs"` } +// GetCosts // @Summary Get user costs // @Description Get user costs within a specified time range // @Tags Costs @@ -370,22 +370,17 @@ func GetCosts(c *gin.Context) { // @Tags Account // @Accept json // @Produce json -// @Param request body helper.Auth true "auth request" // @Success 200 {object} map[string]interface{} "successfully retrieved user account" // @Failure 401 {object} map[string]interface{} "authenticate error" // @Failure 500 {object} map[string]interface{} "failed to get user account" // @Router /account/v1alpha1/account [post] func GetAccount(c *gin.Context) { - req, err := helper.ParseUserBaseReq(c) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to parse user hour costs amount request: %v", err)}) - return - } + req := &helper.AuthBase{} if err := authenticateRequest(c, req); err != nil { c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error : %v", err)}) return } - account, err := dao.DBClient.GetAccount(types.UserQueryOpts{Owner: req.Auth.Owner}) + account, err := dao.DBClient.GetAccount(types.UserQueryOpts{ID: req.Auth.UserID}) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get account : %v", err)}) return @@ -486,7 +481,7 @@ func GetTransfer(c *gin.Context) { return } ops := types.GetTransfersReq{ - UserQueryOpts: &types.UserQueryOpts{Owner: req.Auth.Owner}, + UserQueryOpts: &types.UserQueryOpts{ID: req.Auth.UserID}, Type: types.TransferType(req.Type), LimitReq: types.LimitReq{ Page: req.Page, @@ -546,18 +541,13 @@ func GetAPPCosts(c *gin.Context) { // @Tags Permission // @Accept json // @Produce json -// @Param request body helper.UserBaseReq true "Check permission request" // @Success 200 {object} map[string]interface{} "successfully check permission" // @Failure 400 {object} map[string]interface{} "failed to parse check permission request" // @Failure 401 {object} map[string]interface{} "authenticate error" // @Failure 500 {object} map[string]interface{} "failed to check permission" // @Router /account/v1alpha1/check-permission [post] func CheckPermission(c *gin.Context) { - req, err := helper.ParseUserBaseReq(c) - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": fmt.Sprintf("failed to parse check permission request: %v", err)}) - return - } + req := &helper.AuthBase{} if err := authenticateRequest(c, req); err != nil { c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error : %v", err)}) return @@ -674,7 +664,7 @@ func GetAppTypeList(c *gin.Context) { // @Failure 400 {object} map[string]interface{} "failed to parse basic cost distribution request" // @Failure 401 {object} map[string]interface{} "authenticate error" // @Failure 500 {object} map[string]interface{} "failed to get basic cost distribution" -// @Router /account/v1alpha1/basic-cost-distribution [post] +// @Router /account/v1alpha1/cost-basic-distribution [post] func GetBasicCostDistribution(c *gin.Context) { req, err := helper.ParseGetCostAppListReq(c) if err != nil { @@ -727,12 +717,15 @@ func GetAppCostTimeRange(c *gin.Context) { }) } -func ParseAuthTokenUser(c *gin.Context) (*helper.Auth, error) { +func ParseAuthTokenUser(c *gin.Context) (auth *helper.Auth, err error) { user, err := dao.JwtMgr.ParseUser(c) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to parse user: %v", err) } - auth := &helper.Auth{ + if user.UserID == "" { + return nil, fmt.Errorf("invalid user: %v", user) + } + auth = &helper.Auth{ Owner: user.UserCrName, UserID: user.UserID, } @@ -740,13 +733,13 @@ func ParseAuthTokenUser(c *gin.Context) (*helper.Auth, error) { if dao.DBClient.GetLocalRegion().UID.String() != user.RegionUID { auth.Owner, err = dao.DBClient.GetUserCrName(types.UserQueryOpts{ID: user.UserID}) if err != nil { - return nil, fmt.Errorf("get user cr name error: %v", err) + if errors.Is(err, gorm.ErrRecordNotFound) { + fmt.Printf("failed to get user cr name: %v\n", err) + return auth, nil + } } } - if auth.Owner == "" || auth.UserID == "" { - return nil, fmt.Errorf("invalid user: %v", user) - } - return auth, nil + return } func checkInvoiceToken(token string) error { @@ -837,18 +830,9 @@ func authenticateRequest(c *gin.Context, req helper.AuthReq) error { if req.GetAuth() != nil && req.GetAuth().Token != "" { return checkInvoiceToken(req.GetAuth().Token) } - auth, err := ParseAuthTokenUser(c) - if err == nil { - req.SetAuth(auth) - return nil - } - - if !errors.Is(err, helper.ErrNullAuth) { - return err - } - - return CheckAuthAndCalibrate(req.GetAuth()) + req.SetAuth(auth) + return err } // SetStatusInvoice @@ -1044,98 +1028,3 @@ func GetUserRealNameInfo(c *gin.Context) { Message: "successfully get user real name info", }) } - -func CheckAuthAndCalibrate(auth *helper.Auth) (err error) { - if auth == nil { - return helper.ErrNullAuth - } - if !dao.Debug || auth.KubeConfig != "" { - if err = checkAuth(auth); err != nil { - return fmt.Errorf("check auth error: %v", err) - } - } - auth.Owner, err = dao.DBClient.GetUserCrName(types.UserQueryOpts{ID: auth.UserID}) - if err != nil { - return fmt.Errorf("get user cr name error: %v", err) - } - return nil -} - -func checkAuth(auth *helper.Auth) error { - if err := helper.AuthenticateKC(*auth); err != nil { - return fmt.Errorf("authenticate error : %v", err) - } - host, err := auth2.GetKcHost(auth.KubeConfig) - if err != nil { - return fmt.Errorf("failed to get kc host: %v", err) - } - host = strings.TrimPrefix(strings.TrimPrefix(host, "https://"), "http://") - if !strings.Contains(host, dao.Cfg.LocalRegionDomain) { - if err := CalibrateRegionAuth(auth, host); err != nil { - return fmt.Errorf("calibrate region auth error: %v", err) - } - } else { - user, err := auth2.GetKcUser(auth.KubeConfig) - if err != nil { - return fmt.Errorf("failed to get kc user: %v", err) - } - userID, err := dao.DBClient.GetUserID(types.UserQueryOpts{Owner: user}) - if err != nil { - return fmt.Errorf("get user id error: %v", err) - } - auth.UserID = userID - } - auth.Owner, err = dao.DBClient.GetUserCrName(types.UserQueryOpts{ID: auth.UserID}) - if err != nil { - return fmt.Errorf("get user cr name error: %v", err) - } - return nil -} - -func CalibrateRegionAuth(auth *helper.Auth, kcHost string) error { - for i := range dao.Cfg.Regions { - reg := dao.Cfg.Regions[i] - if !strings.Contains(kcHost, reg.Domain) { - continue - } - svcURL := fmt.Sprintf("https://%s%s%s", reg.AccountSvc, helper.GROUP, helper.CheckPermission) - - authBody, err := json.Marshal(auth) - if err != nil { - return fmt.Errorf("failed to marshal auth: %v", err) - } - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: os.Getenv("INSECURE_VERIFY") != "true", MinVersion: tls.VersionTLS13}, - } - client := &http.Client{Transport: tr} - resp, err := client.Post(svcURL, "application/json", bytes.NewBuffer(authBody)) - if err != nil { - return fmt.Errorf("failed to post request: %v", err) - } - defer resp.Body.Close() - - responseBody := new(bytes.Buffer) - _, err = responseBody.ReadFrom(resp.Body) - if err != nil { - return fmt.Errorf("failed to read response body: %v", err) - } - var respMap map[string]interface{} - if err = json.Unmarshal(responseBody.Bytes(), &respMap); err != nil { - return fmt.Errorf("failed to unmarshal response body: %v", err) - } - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("failed to check permission: %v, error: %s", resp, respMap["error"]) - } - _userID, ok := respMap["userID"] - if !ok { - return fmt.Errorf("failed to get userID from response: %v", respMap) - } - userID, ok := _userID.(string) - if !ok { - return fmt.Errorf("failed to convert userID to string: %v", _userID) - } - auth.UserID = userID - return nil - } - return fmt.Errorf("failed to calibrate region auth") -}