From 553f8e58d474814dd3258cae663615bccc79cac0 Mon Sep 17 00:00:00 2001 From: UUBulb <35923940+uubulb@users.noreply.github.com> Date: Tue, 31 Dec 2024 22:29:29 +0800 Subject: [PATCH] fix: oauth2 redirect url not consistent (#930) * fix: oauth2 redirect url not consistent * only use one redirect uri * feat: allow to disable password authentication * generate translation template * update error * redirect * query --- cmd/dashboard/controller/controller.go | 4 +- cmd/dashboard/controller/jwt.go | 6 + cmd/dashboard/controller/oauth2.go | 302 ++++++++++++------------- cmd/dashboard/controller/user.go | 10 + go.mod | 2 +- go.sum | 4 +- model/oauth2bind.go | 14 ++ model/user.go | 9 +- model/user_api.go | 1 + pkg/i18n/template.pot | 132 +++++++---- 10 files changed, 272 insertions(+), 212 deletions(-) diff --git a/cmd/dashboard/controller/controller.go b/cmd/dashboard/controller/controller.go index e1c7b7a..1e9c054 100644 --- a/cmd/dashboard/controller/controller.go +++ b/cmd/dashboard/controller/controller.go @@ -57,7 +57,6 @@ func routers(r *gin.Engine, frontendDist fs.FS) { api := r.Group("api/v1") api.POST("/login", authMiddleware.LoginHandler) api.GET("/oauth2/:provider", commonHandler(oauth2redirect)) - api.POST("/oauth2/:provider/callback", commonHandler(oauth2callback(authMiddleware))) optionalAuth := api.Group("", optionalAuthMiddleware(authMiddleware)) optionalAuth.GET("/ws/server", commonHandler(serverStream)) @@ -67,6 +66,8 @@ func routers(r *gin.Engine, frontendDist fs.FS) { optionalAuth.GET("/service/:id", commonHandler(listServiceHistory)) optionalAuth.GET("/service/server", commonHandler(listServerWithServices)) + optionalAuth.GET("/oauth2/callback", commonHandler(oauth2callback(authMiddleware))) + optionalAuth.GET("/setting", commonHandler(listConfig)) auth := api.Group("", authMiddleware.MiddlewareFunc()) @@ -81,7 +82,6 @@ func routers(r *gin.Engine, frontendDist fs.FS) { auth.GET("/profile", commonHandler(getProfile)) auth.POST("/profile", commonHandler(updateProfile)) - auth.POST("/oauth2/:provider/bind", commonHandler(bindOauth2)) auth.POST("/oauth2/:provider/unbind", commonHandler(unbindOauth2)) auth.GET("/user", adminHandler(listUser)) diff --git a/cmd/dashboard/controller/jwt.go b/cmd/dashboard/controller/jwt.go index f1e217b..adbcf1d 100644 --- a/cmd/dashboard/controller/jwt.go +++ b/cmd/dashboard/controller/jwt.go @@ -89,6 +89,7 @@ func authenticator() func(c *gin.Context) (interface{}, error) { var user model.User realip := c.GetString(model.CtxKeyRealIPStr) + if err := singleton.DB.Select("id", "password").Where("username = ?", loginVals.Username).First(&user).Error; err != nil { if err == gorm.ErrRecordNotFound { model.BlockIP(singleton.DB, realip, model.WAFBlockReasonTypeLoginFail, model.BlockIDUnknownUser) @@ -96,6 +97,11 @@ func authenticator() func(c *gin.Context) (interface{}, error) { return nil, jwt.ErrFailedAuthentication } + if user.RejectPassword { + model.BlockIP(singleton.DB, realip, model.WAFBlockReasonTypeLoginFail, int64(user.ID)) + return nil, jwt.ErrFailedAuthentication + } + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(loginVals.Password)); err != nil { model.BlockIP(singleton.DB, realip, model.WAFBlockReasonTypeLoginFail, int64(user.ID)) return nil, jwt.ErrFailedAuthentication diff --git a/cmd/dashboard/controller/oauth2.go b/cmd/dashboard/controller/oauth2.go index 5dd6f16..10eb640 100644 --- a/cmd/dashboard/controller/oauth2.go +++ b/cmd/dashboard/controller/oauth2.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "net/http" "strconv" "strings" "time" @@ -20,27 +21,13 @@ import ( "github.com/nezhahq/nezha/service/singleton" ) -type Oauth2LoginType uint8 - -const ( - _ Oauth2LoginType = iota - rTypeLogin - rTypeBind -) - -func getRedirectURL(c *gin.Context, provider string, rType Oauth2LoginType) string { +func getRedirectURL(c *gin.Context) string { scheme := "http://" referer := c.Request.Referer() if forwardedProto := c.Request.Header.Get("X-Forwarded-Proto"); forwardedProto == "https" || strings.HasPrefix(referer, "https://") { scheme = "https://" } - var suffix string - if rType == rTypeLogin { - suffix = "/dashboard/login?provider=" + provider - } else if rType == rTypeBind { - suffix = "/dashboard/profile?provider=" + provider - } - return scheme + c.Request.Host + suffix + return scheme + c.Request.Host + "/api/v1/oauth2/callback" } // @Summary Get Oauth2 Redirect URL @@ -56,7 +43,7 @@ func oauth2redirect(c *gin.Context) (*model.Oauth2LoginResponse, error) { return nil, singleton.Localizer.ErrorT("provider is required") } - rTypeInt, err := strconv.Atoi(c.Query("type")) + rTypeInt, err := strconv.ParseUint(c.Query("type"), 10, 8) if err != nil { return nil, err } @@ -65,14 +52,18 @@ func oauth2redirect(c *gin.Context) (*model.Oauth2LoginResponse, error) { if !has { return nil, singleton.Localizer.ErrorT("provider not found") } - o2conf := o2confRaw.Setup(getRedirectURL(c, provider, Oauth2LoginType(rTypeInt))) + o2conf := o2confRaw.Setup(getRedirectURL(c)) randomString, err := utils.GenerateRandomString(32) if err != nil { return nil, err } state, stateKey := randomString[:16], randomString[16:] - singleton.Cache.Set(fmt.Sprintf("%s%s", model.CacheKeyOauth2State, stateKey), state, cache.DefaultExpiration) + singleton.Cache.Set(fmt.Sprintf("%s%s", model.CacheKeyOauth2State, stateKey), &model.Oauth2State{ + Action: model.Oauth2LoginType(rTypeInt), + Provider: provider, + State: state, + }, cache.DefaultExpiration) url := o2conf.AuthCodeURL(state, oauth2.AccessTypeOnline) c.SetCookie("nz-o2s", stateKey, 60*5, "", "", false, false) @@ -80,141 +71,6 @@ func oauth2redirect(c *gin.Context) (*model.Oauth2LoginResponse, error) { return &model.Oauth2LoginResponse{Redirect: url}, nil } -func exchangeOpenId(c *gin.Context, o2confRaw *model.Oauth2Config, provider string, callbackData model.Oauth2Callback) (string, error) { - // 验证登录跳转时的 State - stateKey, err := c.Cookie("nz-o2s") - if err != nil { - return "", singleton.Localizer.ErrorT("invalid state key") - } - state, ok := singleton.Cache.Get(fmt.Sprintf("%s%s", model.CacheKeyOauth2State, stateKey)) - if !ok || state.(string) != callbackData.State { - return "", singleton.Localizer.ErrorT("invalid state key") - } - - o2conf := o2confRaw.Setup(getRedirectURL(c, provider, rTypeLogin)) - - ctx := context.Background() - - otk, err := o2conf.Exchange(ctx, callbackData.Code) - if err != nil { - return "", err - } - oauth2client := o2conf.Client(ctx, otk) - resp, err := oauth2client.Get(o2confRaw.UserInfoURL) - if err != nil { - return "", err - } - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - return gjson.Get(string(body), o2confRaw.UserIDPath).String(), nil -} - -// @Summary Oauth2 Callback -// @Description Oauth2 Callback -// @Accept json -// @Produce json -// @Param provider path string true "provider" -// @Param body body model.Oauth2Callback true "body" -// @Success 200 {object} model.LoginResponse -// @Router /api/v1/oauth2/{provider}/callback [post] -func oauth2callback(jwtConfig *jwt.GinJWTMiddleware) func(c *gin.Context) (*model.LoginResponse, error) { - return func(c *gin.Context) (*model.LoginResponse, error) { - provider := c.Param("provider") - if provider == "" { - return nil, singleton.Localizer.ErrorT("provider is required") - } - - o2confRaw, has := singleton.Conf.Oauth2[provider] - if !has { - return nil, singleton.Localizer.ErrorT("provider not found") - } - provider = strings.ToLower(provider) - - var callbackData model.Oauth2Callback - if err := c.ShouldBind(&callbackData); err != nil { - return nil, err - } - - realip := c.GetString(model.CtxKeyRealIPStr) - - if callbackData.Code == "" { - model.BlockIP(singleton.DB, realip, model.WAFBlockReasonTypeBruteForceOauth2, model.BlockIDToken) - return nil, singleton.Localizer.ErrorT("code is required") - } - - openId, err := exchangeOpenId(c, o2confRaw, provider, callbackData) - if err != nil { - model.BlockIP(singleton.DB, realip, model.WAFBlockReasonTypeBruteForceOauth2, model.BlockIDToken) - return nil, err - } - - var bind model.Oauth2Bind - if err := singleton.DB.Where("provider = ? AND open_id = ?", provider, openId).First(&bind).Error; err != nil { - return nil, singleton.Localizer.ErrorT("oauth2 user not binded yet") - } - - tokenString, expire, err := jwtConfig.TokenGenerator(fmt.Sprintf("%d", bind.UserID)) - if err != nil { - return nil, err - } - - jwtConfig.SetCookie(c, tokenString) - - return &model.LoginResponse{Token: tokenString, Expire: expire.Format(time.RFC3339)}, nil - } -} - -// @Summary Bind Oauth2 -// @Description Bind Oauth2 -// @Accept json -// @Produce json -// @Param provider path string true "provider" -// @Param body body model.Oauth2Callback true "body" -// @Success 200 {object} any -// @Router /api/v1/oauth2/{provider}/bind [post] -func bindOauth2(c *gin.Context) (any, error) { - var bindData model.Oauth2Callback - if err := c.ShouldBind(&bindData); err != nil { - return nil, err - } - - provider := c.Param("provider") - o2conf, has := singleton.Conf.Oauth2[provider] - if !has { - return nil, singleton.Localizer.ErrorT("provider not found") - } - provider = strings.ToLower(provider) - - openId, err := exchangeOpenId(c, o2conf, provider, bindData) - if err != nil { - return nil, err - } - - u := c.MustGet(model.CtxKeyAuthorizedUser).(*model.User) - - var bind model.Oauth2Bind - result := singleton.DB.Where("provider = ? AND open_id = ?", provider, openId).Limit(1).Find(&bind) - if result.Error != nil && result.Error != gorm.ErrRecordNotFound { - return nil, newGormError("%v", result.Error) - } - bind.UserID = u.ID - bind.Provider = provider - bind.OpenID = openId - if result.Error == gorm.ErrRecordNotFound { - result = singleton.DB.Create(&bind) - } else { - result = singleton.DB.Save(&bind) - } - if result.Error != nil { - return nil, newGormError("%v", result.Error) - } - return nil, nil -} - // @Summary Unbind Oauth2 // @Description Unbind Oauth2 // @Accept json @@ -232,9 +88,145 @@ func unbindOauth2(c *gin.Context) (any, error) { return nil, singleton.Localizer.ErrorT("provider not found") } provider = strings.ToLower(provider) + u := c.MustGet(model.CtxKeyAuthorizedUser).(*model.User) - if err := singleton.DB.Where("provider = ? AND user_id = ?", provider, u.ID).Delete(&model.Oauth2Bind{}).Error; err != nil { + query := singleton.DB.Where("provider = ? AND user_id = ?", provider, u.ID) + + var bindCount int64 + if err := query.Model(&model.Oauth2Bind{}).Count(&bindCount).Error; err != nil { return nil, newGormError("%v", err) } + + if bindCount < 2 && u.RejectPassword { + return nil, singleton.Localizer.ErrorT("operation not permitted") + } + + if err := query.Delete(&model.Oauth2Bind{}).Error; err != nil { + return nil, newGormError("%v", err) + } + return nil, nil } + +// @Summary Oauth2 Callback +// @Description Oauth2 Callback +// @Accept json +// @Produce json +// @Param state query string true "state" +// @Param code query string true "code" +// @Success 200 {object} model.LoginResponse +// @Router /api/v1/oauth2/callback [get] +func oauth2callback(jwtConfig *jwt.GinJWTMiddleware) func(c *gin.Context) (*model.LoginResponse, error) { + return func(c *gin.Context) (*model.LoginResponse, error) { + callbackData := &model.Oauth2Callback{ + State: c.Query("state"), + Code: c.Query("code"), + } + + state, err := verifyState(c, callbackData.State) + if err != nil { + return nil, err + } + + o2confRaw, has := singleton.Conf.Oauth2[state.Provider] + if !has { + return nil, singleton.Localizer.ErrorT("provider not found") + } + + realip := c.GetString(model.CtxKeyRealIPStr) + if callbackData.Code == "" { + model.BlockIP(singleton.DB, realip, model.WAFBlockReasonTypeBruteForceOauth2, model.BlockIDToken) + return nil, singleton.Localizer.ErrorT("code is required") + } + + openId, err := exchangeOpenId(c, o2confRaw, callbackData) + if err != nil { + model.BlockIP(singleton.DB, realip, model.WAFBlockReasonTypeBruteForceOauth2, model.BlockIDToken) + return nil, err + } + + var bind model.Oauth2Bind + switch state.Action { + case model.RTypeBind: + u, authorized := c.Get(model.CtxKeyAuthorizedUser) + if !authorized { + return nil, singleton.Localizer.ErrorT("unauthorized") + } + user := u.(*model.User) + + result := singleton.DB.Where("provider = ? AND open_id = ?", strings.ToLower(state.Provider), openId).Limit(1).Find(&bind) + if result.Error != nil && result.Error != gorm.ErrRecordNotFound { + return nil, newGormError("%v", result.Error) + } + bind.UserID = user.ID + bind.Provider = state.Provider + bind.OpenID = openId + + if result.Error == gorm.ErrRecordNotFound { + result = singleton.DB.Create(&bind) + } else { + result = singleton.DB.Save(&bind) + } + if result.Error != nil { + return nil, newGormError("%v", result.Error) + } + default: + if err := singleton.DB.Where("provider = ? AND open_id = ?", strings.ToLower(state.Provider), openId).First(&bind).Error; err != nil { + return nil, singleton.Localizer.ErrorT("oauth2 user not binded yet") + } + } + + tokenString, expire, err := jwtConfig.TokenGenerator(fmt.Sprintf("%d", bind.UserID)) + if err != nil { + return nil, err + } + + jwtConfig.SetCookie(c, tokenString) + c.Redirect(http.StatusFound, utils.IfOr(state.Action == model.RTypeBind, "/dashboard/profile?oauth2=true", "/dashboard/login?oauth2=true")) + + return &model.LoginResponse{Token: tokenString, Expire: expire.Format(time.RFC3339)}, nil + } +} + +func exchangeOpenId(c *gin.Context, o2confRaw *model.Oauth2Config, callbackData *model.Oauth2Callback) (string, error) { + o2conf := o2confRaw.Setup(getRedirectURL(c)) + ctx := context.Background() + + otk, err := o2conf.Exchange(ctx, callbackData.Code) + if err != nil { + return "", err + } + oauth2client := o2conf.Client(ctx, otk) + resp, err := oauth2client.Get(o2confRaw.UserInfoURL) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + return gjson.GetBytes(body, o2confRaw.UserIDPath).String(), nil +} + +func verifyState(c *gin.Context, state string) (*model.Oauth2State, error) { + // 验证登录跳转时的 State + stateKey, err := c.Cookie("nz-o2s") + if err != nil { + return nil, singleton.Localizer.ErrorT("invalid state key") + } + + cacheKey := fmt.Sprintf("%s%s", model.CacheKeyOauth2State, stateKey) + istate, ok := singleton.Cache.Get(cacheKey) + if !ok { + return nil, singleton.Localizer.ErrorT("invalid state key") + } + + oauth2State, ok := istate.(*model.Oauth2State) + if !ok || oauth2State.State != state { + return nil, singleton.Localizer.ErrorT("invalid state key") + } + + return oauth2State, nil +} diff --git a/cmd/dashboard/controller/user.go b/cmd/dashboard/controller/user.go index 1a0f7ae..44f7577 100644 --- a/cmd/dashboard/controller/user.go +++ b/cmd/dashboard/controller/user.go @@ -73,8 +73,18 @@ func updateProfile(c *gin.Context) (any, error) { return nil, err } + var bindCount int64 + if err := singleton.DB.Where("user_id = ?", auth.(*model.User).ID).Count(&bindCount).Error; err != nil { + return nil, newGormError("%v", err) + } + + if pf.RejectPassword && bindCount < 1 { + return nil, singleton.Localizer.ErrorT("you don't have any oauth2 bindings") + } + user.Username = pf.NewUsername user.Password = string(hash) + user.RejectPassword = pf.RejectPassword if err := singleton.DB.Save(&user).Error; err != nil { return nil, newGormError("%v", err) } diff --git a/go.mod b/go.mod index 1df547b..9973d7d 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( golang.org/x/crypto v0.31.0 golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 golang.org/x/net v0.33.0 - golang.org/x/oauth2 v0.23.0 + golang.org/x/oauth2 v0.24.0 golang.org/x/sync v0.10.0 google.golang.org/grpc v1.69.2 google.golang.org/protobuf v1.36.0 diff --git a/go.sum b/go.sum index a103bcc..d5f9460 100644 --- a/go.sum +++ b/go.sum @@ -198,8 +198,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= diff --git a/model/oauth2bind.go b/model/oauth2bind.go index 3631510..75873ef 100644 --- a/model/oauth2bind.go +++ b/model/oauth2bind.go @@ -7,3 +7,17 @@ type Oauth2Bind struct { Provider string `gorm:"uniqueIndex:u_p_o" json:"provider,omitempty"` OpenID string `gorm:"uniqueIndex:u_p_o" json:"open_id,omitempty"` } + +type Oauth2LoginType uint8 + +const ( + _ Oauth2LoginType = iota + RTypeLogin + RTypeBind +) + +type Oauth2State struct { + Action Oauth2LoginType + Provider string + State string +} diff --git a/model/user.go b/model/user.go index 24c1613..9e214a5 100644 --- a/model/user.go +++ b/model/user.go @@ -15,10 +15,11 @@ const ( type User struct { Common - Username string `json:"username,omitempty" gorm:"uniqueIndex"` - Password string `json:"password,omitempty" gorm:"type:char(72)"` - Role uint8 `json:"role,omitempty"` - AgentSecret string `json:"agent_secret,omitempty" gorm:"type:char(32)"` + Username string `json:"username,omitempty" gorm:"uniqueIndex"` + Password string `json:"password,omitempty" gorm:"type:char(72)"` + Role uint8 `json:"role,omitempty"` + AgentSecret string `json:"agent_secret,omitempty" gorm:"type:char(32)"` + RejectPassword bool `json:"reject_password,omitempty"` } type UserInfo struct { diff --git a/model/user_api.go b/model/user_api.go index 62219b5..315fb50 100644 --- a/model/user_api.go +++ b/model/user_api.go @@ -10,4 +10,5 @@ type ProfileForm struct { OriginalPassword string `json:"original_password,omitempty"` NewUsername string `json:"new_username,omitempty"` NewPassword string `json:"new_password,omitempty"` + RejectPassword bool `json:"reject_password,omitempty"` } diff --git a/pkg/i18n/template.pot b/pkg/i18n/template.pot index f8edd9b..596ffd8 100644 --- a/pkg/i18n/template.pot +++ b/pkg/i18n/template.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-12-22 11:55+0800\n" +"POT-Creation-Date: 2024-12-31 21:25+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,89 +17,90 @@ msgstr "" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" -#: cmd/dashboard/controller/alertrule.go:103 +#: cmd/dashboard/controller/alertrule.go:104 #, c-format msgid "alert id %d does not exist" msgstr "" -#: cmd/dashboard/controller/alertrule.go:107 -#: cmd/dashboard/controller/alertrule.go:155 -#: cmd/dashboard/controller/alertrule.go:175 -#: cmd/dashboard/controller/controller.go:211 -#: cmd/dashboard/controller/cron.go:57 cmd/dashboard/controller/cron.go:123 -#: cmd/dashboard/controller/cron.go:135 cmd/dashboard/controller/cron.go:194 -#: cmd/dashboard/controller/cron.go:223 cmd/dashboard/controller/ddns.go:130 -#: cmd/dashboard/controller/ddns.go:191 cmd/dashboard/controller/fm.go:43 -#: cmd/dashboard/controller/nat.go:58 cmd/dashboard/controller/nat.go:109 -#: cmd/dashboard/controller/nat.go:120 cmd/dashboard/controller/nat.go:159 -#: cmd/dashboard/controller/notification.go:111 -#: cmd/dashboard/controller/notification.go:165 +#: cmd/dashboard/controller/alertrule.go:108 +#: cmd/dashboard/controller/alertrule.go:156 +#: cmd/dashboard/controller/alertrule.go:176 +#: cmd/dashboard/controller/controller.go:216 +#: cmd/dashboard/controller/cron.go:58 cmd/dashboard/controller/cron.go:124 +#: cmd/dashboard/controller/cron.go:136 cmd/dashboard/controller/cron.go:195 +#: cmd/dashboard/controller/cron.go:224 cmd/dashboard/controller/ddns.go:131 +#: cmd/dashboard/controller/ddns.go:192 cmd/dashboard/controller/fm.go:43 +#: cmd/dashboard/controller/nat.go:59 cmd/dashboard/controller/nat.go:110 +#: cmd/dashboard/controller/nat.go:121 cmd/dashboard/controller/nat.go:160 +#: cmd/dashboard/controller/notification.go:112 +#: cmd/dashboard/controller/notification.go:166 #: cmd/dashboard/controller/notification_group.go:76 #: cmd/dashboard/controller/notification_group.go:152 #: cmd/dashboard/controller/notification_group.go:164 #: cmd/dashboard/controller/notification_group.go:233 -#: cmd/dashboard/controller/server.go:64 cmd/dashboard/controller/server.go:76 -#: cmd/dashboard/controller/server.go:127 -#: cmd/dashboard/controller/server.go:191 +#: cmd/dashboard/controller/server.go:65 cmd/dashboard/controller/server.go:77 +#: cmd/dashboard/controller/server.go:128 +#: cmd/dashboard/controller/server.go:192 #: cmd/dashboard/controller/server_group.go:75 #: cmd/dashboard/controller/server_group.go:150 #: cmd/dashboard/controller/server_group.go:229 -#: cmd/dashboard/controller/service.go:272 -#: cmd/dashboard/controller/service.go:343 -#: cmd/dashboard/controller/service.go:370 +#: cmd/dashboard/controller/service.go:273 +#: cmd/dashboard/controller/service.go:344 +#: cmd/dashboard/controller/service.go:371 #: cmd/dashboard/controller/terminal.go:41 msgid "permission denied" msgstr "" -#: cmd/dashboard/controller/alertrule.go:183 +#: cmd/dashboard/controller/alertrule.go:184 msgid "duration need to be at least 3" msgstr "" -#: cmd/dashboard/controller/alertrule.go:187 +#: cmd/dashboard/controller/alertrule.go:188 msgid "cycle_interval need to be at least 1" msgstr "" -#: cmd/dashboard/controller/alertrule.go:190 +#: cmd/dashboard/controller/alertrule.go:191 msgid "cycle_start is not set" msgstr "" -#: cmd/dashboard/controller/alertrule.go:193 +#: cmd/dashboard/controller/alertrule.go:194 msgid "cycle_start is a future value" msgstr "" -#: cmd/dashboard/controller/alertrule.go:198 +#: cmd/dashboard/controller/alertrule.go:199 msgid "need to configure at least a single rule" msgstr "" -#: cmd/dashboard/controller/controller.go:205 +#: cmd/dashboard/controller/controller.go:210 +#: cmd/dashboard/controller/oauth2.go:152 #: cmd/dashboard/controller/server_group.go:162 -#: cmd/dashboard/controller/service.go:95 cmd/dashboard/controller/user.go:26 -#: cmd/dashboard/controller/user.go:53 +#: cmd/dashboard/controller/service.go:96 cmd/dashboard/controller/user.go:27 +#: cmd/dashboard/controller/user.go:63 msgid "unauthorized" msgstr "" -#: cmd/dashboard/controller/controller.go:228 +#: cmd/dashboard/controller/controller.go:233 msgid "database error" msgstr "" -#: cmd/dashboard/controller/cron.go:74 cmd/dashboard/controller/cron.go:148 +#: cmd/dashboard/controller/cron.go:75 cmd/dashboard/controller/cron.go:149 msgid "scheduled tasks cannot be triggered by alarms" msgstr "" -#: cmd/dashboard/controller/cron.go:131 cmd/dashboard/controller/cron.go:189 +#: cmd/dashboard/controller/cron.go:132 cmd/dashboard/controller/cron.go:190 #, c-format msgid "task id %d does not exist" msgstr "" -#: cmd/dashboard/controller/ddns.go:56 cmd/dashboard/controller/ddns.go:121 +#: cmd/dashboard/controller/ddns.go:57 cmd/dashboard/controller/ddns.go:122 msgid "the retry count must be an integer between 1 and 10" msgstr "" -#: cmd/dashboard/controller/ddns.go:80 cmd/dashboard/controller/ddns.go:153 +#: cmd/dashboard/controller/ddns.go:81 cmd/dashboard/controller/ddns.go:154 msgid "error parsing %s: %v" msgstr "" -#: cmd/dashboard/controller/ddns.go:126 cmd/dashboard/controller/nat.go:116 +#: cmd/dashboard/controller/ddns.go:127 cmd/dashboard/controller/nat.go:117 #, c-format msgid "profile id %d does not exist" msgstr "" @@ -108,12 +109,12 @@ msgstr "" msgid "server not found or not connected" msgstr "" -#: cmd/dashboard/controller/notification.go:68 -#: cmd/dashboard/controller/notification.go:130 +#: cmd/dashboard/controller/notification.go:69 +#: cmd/dashboard/controller/notification.go:131 msgid "a test message" msgstr "" -#: cmd/dashboard/controller/notification.go:107 +#: cmd/dashboard/controller/notification.go:108 #, c-format msgid "notification id %d does not exist" msgstr "" @@ -129,7 +130,34 @@ msgstr "" msgid "group id %d does not exist" msgstr "" -#: cmd/dashboard/controller/server.go:72 +#: cmd/dashboard/controller/oauth2.go:42 cmd/dashboard/controller/oauth2.go:83 +msgid "provider is required" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:52 cmd/dashboard/controller/oauth2.go:87 +#: cmd/dashboard/controller/oauth2.go:132 +msgid "provider not found" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:100 +msgid "operation not permitted" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:138 +msgid "code is required" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:174 +msgid "oauth2 user not binded yet" +msgstr "" + +#: cmd/dashboard/controller/oauth2.go:215 +#: cmd/dashboard/controller/oauth2.go:221 +#: cmd/dashboard/controller/oauth2.go:226 +msgid "invalid state key" +msgstr "" + +#: cmd/dashboard/controller/server.go:73 #, c-format msgid "server id %d does not exist" msgstr "" @@ -139,29 +167,37 @@ msgstr "" msgid "have invalid server id" msgstr "" -#: cmd/dashboard/controller/service.go:88 -#: cmd/dashboard/controller/service.go:164 +#: cmd/dashboard/controller/service.go:89 +#: cmd/dashboard/controller/service.go:165 msgid "server not found" msgstr "" -#: cmd/dashboard/controller/service.go:268 +#: cmd/dashboard/controller/service.go:269 #, c-format msgid "service id %d does not exist" msgstr "" -#: cmd/dashboard/controller/user.go:58 +#: cmd/dashboard/controller/user.go:68 msgid "incorrect password" msgstr "" -#: cmd/dashboard/controller/user.go:110 +#: cmd/dashboard/controller/user.go:82 +msgid "you don't have any oauth2 bindings" +msgstr "" + +#: cmd/dashboard/controller/user.go:130 msgid "password length must be greater than 6" msgstr "" -#: cmd/dashboard/controller/user.go:113 +#: cmd/dashboard/controller/user.go:133 msgid "username can't be empty" msgstr "" -#: cmd/dashboard/controller/user.go:152 +#: cmd/dashboard/controller/user.go:136 +msgid "invalid role" +msgstr "" + +#: cmd/dashboard/controller/user.go:175 msgid "can't delete yourself" msgstr "" @@ -189,11 +225,11 @@ msgstr "" msgid "IP Changed" msgstr "" -#: service/singleton/alertsentinel.go:167 +#: service/singleton/alertsentinel.go:170 msgid "Incident" msgstr "" -#: service/singleton/alertsentinel.go:177 +#: service/singleton/alertsentinel.go:180 msgid "Resolved" msgstr "" @@ -259,6 +295,6 @@ msgstr "" msgid "Down" msgstr "" -#: service/singleton/user.go:53 +#: service/singleton/user.go:60 msgid "user id not specified" msgstr ""