Compare commits

..

No commits in common. "2f433472d55665b43dd19fb7961f7c06f75db189" and "a2541b0a5c79d24b6a749c73002fef4851a7a3c4" have entirely different histories.

8 changed files with 202 additions and 263 deletions

View File

@ -57,6 +57,7 @@ func routers(r *gin.Engine, frontendDist fs.FS) {
api := r.Group("api/v1") api := r.Group("api/v1")
api.POST("/login", authMiddleware.LoginHandler) api.POST("/login", authMiddleware.LoginHandler)
api.GET("/oauth2/:provider", commonHandler(oauth2redirect)) api.GET("/oauth2/:provider", commonHandler(oauth2redirect))
api.POST("/oauth2/:provider/callback", commonHandler(oauth2callback(authMiddleware)))
optionalAuth := api.Group("", optionalAuthMiddleware(authMiddleware)) optionalAuth := api.Group("", optionalAuthMiddleware(authMiddleware))
optionalAuth.GET("/ws/server", commonHandler(serverStream)) optionalAuth.GET("/ws/server", commonHandler(serverStream))
@ -66,8 +67,6 @@ func routers(r *gin.Engine, frontendDist fs.FS) {
optionalAuth.GET("/service/:id", commonHandler(listServiceHistory)) optionalAuth.GET("/service/:id", commonHandler(listServiceHistory))
optionalAuth.GET("/service/server", commonHandler(listServerWithServices)) optionalAuth.GET("/service/server", commonHandler(listServerWithServices))
optionalAuth.GET("/oauth2/callback", commonHandler(oauth2callback(authMiddleware)))
optionalAuth.GET("/setting", commonHandler(listConfig)) optionalAuth.GET("/setting", commonHandler(listConfig))
auth := api.Group("", authMiddleware.MiddlewareFunc()) auth := api.Group("", authMiddleware.MiddlewareFunc())
@ -82,6 +81,7 @@ func routers(r *gin.Engine, frontendDist fs.FS) {
auth.GET("/profile", commonHandler(getProfile)) auth.GET("/profile", commonHandler(getProfile))
auth.POST("/profile", commonHandler(updateProfile)) auth.POST("/profile", commonHandler(updateProfile))
auth.POST("/oauth2/:provider/bind", commonHandler(bindOauth2))
auth.POST("/oauth2/:provider/unbind", commonHandler(unbindOauth2)) auth.POST("/oauth2/:provider/unbind", commonHandler(unbindOauth2))
auth.GET("/user", adminHandler(listUser)) auth.GET("/user", adminHandler(listUser))

View File

@ -89,7 +89,6 @@ func authenticator() func(c *gin.Context) (interface{}, error) {
var user model.User var user model.User
realip := c.GetString(model.CtxKeyRealIPStr) realip := c.GetString(model.CtxKeyRealIPStr)
if err := singleton.DB.Select("id", "password").Where("username = ?", loginVals.Username).First(&user).Error; err != nil { if err := singleton.DB.Select("id", "password").Where("username = ?", loginVals.Username).First(&user).Error; err != nil {
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
model.BlockIP(singleton.DB, realip, model.WAFBlockReasonTypeLoginFail, model.BlockIDUnknownUser) model.BlockIP(singleton.DB, realip, model.WAFBlockReasonTypeLoginFail, model.BlockIDUnknownUser)
@ -97,11 +96,6 @@ func authenticator() func(c *gin.Context) (interface{}, error) {
return nil, jwt.ErrFailedAuthentication 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 { if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(loginVals.Password)); err != nil {
model.BlockIP(singleton.DB, realip, model.WAFBlockReasonTypeLoginFail, int64(user.ID)) model.BlockIP(singleton.DB, realip, model.WAFBlockReasonTypeLoginFail, int64(user.ID))
return nil, jwt.ErrFailedAuthentication return nil, jwt.ErrFailedAuthentication

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"net/http"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -21,13 +20,27 @@ import (
"github.com/nezhahq/nezha/service/singleton" "github.com/nezhahq/nezha/service/singleton"
) )
func getRedirectURL(c *gin.Context) string { type Oauth2LoginType uint8
const (
_ Oauth2LoginType = iota
rTypeLogin
rTypeBind
)
func getRedirectURL(c *gin.Context, provider string, rType Oauth2LoginType) string {
scheme := "http://" scheme := "http://"
referer := c.Request.Referer() referer := c.Request.Referer()
if forwardedProto := c.Request.Header.Get("X-Forwarded-Proto"); forwardedProto == "https" || strings.HasPrefix(referer, "https://") { if forwardedProto := c.Request.Header.Get("X-Forwarded-Proto"); forwardedProto == "https" || strings.HasPrefix(referer, "https://") {
scheme = "https://" scheme = "https://"
} }
return scheme + c.Request.Host + "/api/v1/oauth2/callback" 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
} }
// @Summary Get Oauth2 Redirect URL // @Summary Get Oauth2 Redirect URL
@ -43,7 +56,7 @@ func oauth2redirect(c *gin.Context) (*model.Oauth2LoginResponse, error) {
return nil, singleton.Localizer.ErrorT("provider is required") return nil, singleton.Localizer.ErrorT("provider is required")
} }
rTypeInt, err := strconv.ParseUint(c.Query("type"), 10, 8) rTypeInt, err := strconv.Atoi(c.Query("type"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -52,18 +65,14 @@ func oauth2redirect(c *gin.Context) (*model.Oauth2LoginResponse, error) {
if !has { if !has {
return nil, singleton.Localizer.ErrorT("provider not found") return nil, singleton.Localizer.ErrorT("provider not found")
} }
o2conf := o2confRaw.Setup(getRedirectURL(c)) o2conf := o2confRaw.Setup(getRedirectURL(c, provider, Oauth2LoginType(rTypeInt)))
randomString, err := utils.GenerateRandomString(32) randomString, err := utils.GenerateRandomString(32)
if err != nil { if err != nil {
return nil, err return nil, err
} }
state, stateKey := randomString[:16], randomString[16:] state, stateKey := randomString[:16], randomString[16:]
singleton.Cache.Set(fmt.Sprintf("%s%s", model.CacheKeyOauth2State, stateKey), &model.Oauth2State{ singleton.Cache.Set(fmt.Sprintf("%s%s", model.CacheKeyOauth2State, stateKey), state, cache.DefaultExpiration)
Action: model.Oauth2LoginType(rTypeInt),
Provider: provider,
State: state,
}, cache.DefaultExpiration)
url := o2conf.AuthCodeURL(state, oauth2.AccessTypeOnline) url := o2conf.AuthCodeURL(state, oauth2.AccessTypeOnline)
c.SetCookie("nz-o2s", stateKey, 60*5, "", "", false, false) c.SetCookie("nz-o2s", stateKey, 60*5, "", "", false, false)
@ -71,125 +80,19 @@ func oauth2redirect(c *gin.Context) (*model.Oauth2LoginResponse, error) {
return &model.Oauth2LoginResponse{Redirect: url}, nil return &model.Oauth2LoginResponse{Redirect: url}, nil
} }
// @Summary Unbind Oauth2 func exchangeOpenId(c *gin.Context, o2confRaw *model.Oauth2Config, provider string, callbackData model.Oauth2Callback, typ Oauth2LoginType) (string, error) {
// @Description Unbind Oauth2 // 验证登录跳转时的 State
// @Accept json stateKey, err := c.Cookie("nz-o2s")
// @Produce json
// @Param provider path string true "provider"
// @Success 200 {object} any
// @Router /api/v1/oauth2/{provider}/unbind [post]
func unbindOauth2(c *gin.Context) (any, error) {
provider := c.Param("provider")
if provider == "" {
return nil, singleton.Localizer.ErrorT("provider is required")
}
_, has := singleton.Conf.Oauth2[provider]
if !has {
return nil, singleton.Localizer.ErrorT("provider not found")
}
provider = strings.ToLower(provider)
u := c.MustGet(model.CtxKeyAuthorizedUser).(*model.User)
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 { if err != nil {
return nil, err 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")
} }
o2confRaw, has := singleton.Conf.Oauth2[state.Provider] o2conf := o2confRaw.Setup(getRedirectURL(c, provider, typ))
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() ctx := context.Background()
otk, err := o2conf.Exchange(ctx, callbackData.Code) otk, err := o2conf.Exchange(ctx, callbackData.Code)
@ -210,23 +113,127 @@ func exchangeOpenId(c *gin.Context, o2confRaw *model.Oauth2Config, callbackData
return gjson.GetBytes(body, o2confRaw.UserIDPath).String(), nil return gjson.GetBytes(body, o2confRaw.UserIDPath).String(), nil
} }
func verifyState(c *gin.Context, state string) (*model.Oauth2State, error) { // @Summary Oauth2 Callback
// 验证登录跳转时的 State // @Description Oauth2 Callback
stateKey, err := c.Cookie("nz-o2s") // @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")
}
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, rTypeLogin)
if err != nil { if err != nil {
return nil, singleton.Localizer.ErrorT("invalid state key") model.BlockIP(singleton.DB, realip, model.WAFBlockReasonTypeBruteForceOauth2, model.BlockIDToken)
return nil, err
} }
cacheKey := fmt.Sprintf("%s%s", model.CacheKeyOauth2State, stateKey) var bind model.Oauth2Bind
istate, ok := singleton.Cache.Get(cacheKey) if err := singleton.DB.Where("provider = ? AND open_id = ?", strings.ToLower(provider), openId).First(&bind).Error; err != nil {
if !ok { return nil, singleton.Localizer.ErrorT("oauth2 user not binded yet")
return nil, singleton.Localizer.ErrorT("invalid state key")
} }
oauth2State, ok := istate.(*model.Oauth2State) tokenString, expire, err := jwtConfig.TokenGenerator(fmt.Sprintf("%d", bind.UserID))
if !ok || oauth2State.State != state { if err != nil {
return nil, singleton.Localizer.ErrorT("invalid state key") return nil, err
} }
return oauth2State, nil 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")
}
openId, err := exchangeOpenId(c, o2conf, provider, bindData, rTypeBind)
if err != nil {
return nil, err
}
u := c.MustGet(model.CtxKeyAuthorizedUser).(*model.User)
provider = strings.ToLower(provider)
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
// @Produce json
// @Param provider path string true "provider"
// @Success 200 {object} any
// @Router /api/v1/oauth2/{provider}/unbind [post]
func unbindOauth2(c *gin.Context) (any, error) {
provider := c.Param("provider")
if provider == "" {
return nil, singleton.Localizer.ErrorT("provider is required")
}
_, has := singleton.Conf.Oauth2[provider]
if !has {
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 {
return nil, newGormError("%v", err)
}
return nil, nil
} }

View File

@ -73,18 +73,8 @@ func updateProfile(c *gin.Context) (any, error) {
return nil, err 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.Username = pf.NewUsername
user.Password = string(hash) user.Password = string(hash)
user.RejectPassword = pf.RejectPassword
if err := singleton.DB.Save(&user).Error; err != nil { if err := singleton.DB.Save(&user).Error; err != nil {
return nil, newGormError("%v", err) return nil, newGormError("%v", err)
} }

View File

@ -7,17 +7,3 @@ type Oauth2Bind struct {
Provider string `gorm:"uniqueIndex:u_p_o" json:"provider,omitempty"` Provider string `gorm:"uniqueIndex:u_p_o" json:"provider,omitempty"`
OpenID string `gorm:"uniqueIndex:u_p_o" json:"open_id,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
}

View File

@ -19,7 +19,6 @@ type User struct {
Password string `json:"password,omitempty" gorm:"type:char(72)"` Password string `json:"password,omitempty" gorm:"type:char(72)"`
Role uint8 `json:"role,omitempty"` Role uint8 `json:"role,omitempty"`
AgentSecret string `json:"agent_secret,omitempty" gorm:"type:char(32)"` AgentSecret string `json:"agent_secret,omitempty" gorm:"type:char(32)"`
RejectPassword bool `json:"reject_password,omitempty"`
} }
type UserInfo struct { type UserInfo struct {

View File

@ -10,5 +10,4 @@ type ProfileForm struct {
OriginalPassword string `json:"original_password,omitempty"` OriginalPassword string `json:"original_password,omitempty"`
NewUsername string `json:"new_username,omitempty"` NewUsername string `json:"new_username,omitempty"`
NewPassword string `json:"new_password,omitempty"` NewPassword string `json:"new_password,omitempty"`
RejectPassword bool `json:"reject_password,omitempty"`
} }

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-31 21:25+0800\n" "POT-Creation-Date: 2024-12-22 11:55+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -17,90 +17,89 @@ msgstr ""
"Content-Type: text/plain; charset=CHARSET\n" "Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
#: cmd/dashboard/controller/alertrule.go:104 #: cmd/dashboard/controller/alertrule.go:103
#, c-format #, c-format
msgid "alert id %d does not exist" msgid "alert id %d does not exist"
msgstr "" msgstr ""
#: cmd/dashboard/controller/alertrule.go:108 #: cmd/dashboard/controller/alertrule.go:107
#: cmd/dashboard/controller/alertrule.go:156 #: cmd/dashboard/controller/alertrule.go:155
#: cmd/dashboard/controller/alertrule.go:176 #: cmd/dashboard/controller/alertrule.go:175
#: cmd/dashboard/controller/controller.go:216 #: cmd/dashboard/controller/controller.go:211
#: cmd/dashboard/controller/cron.go:58 cmd/dashboard/controller/cron.go:124 #: cmd/dashboard/controller/cron.go:57 cmd/dashboard/controller/cron.go:123
#: cmd/dashboard/controller/cron.go:136 cmd/dashboard/controller/cron.go:195 #: cmd/dashboard/controller/cron.go:135 cmd/dashboard/controller/cron.go:194
#: cmd/dashboard/controller/cron.go:224 cmd/dashboard/controller/ddns.go:131 #: cmd/dashboard/controller/cron.go:223 cmd/dashboard/controller/ddns.go:130
#: cmd/dashboard/controller/ddns.go:192 cmd/dashboard/controller/fm.go:43 #: cmd/dashboard/controller/ddns.go:191 cmd/dashboard/controller/fm.go:43
#: cmd/dashboard/controller/nat.go:59 cmd/dashboard/controller/nat.go:110 #: cmd/dashboard/controller/nat.go:58 cmd/dashboard/controller/nat.go:109
#: cmd/dashboard/controller/nat.go:121 cmd/dashboard/controller/nat.go:160 #: cmd/dashboard/controller/nat.go:120 cmd/dashboard/controller/nat.go:159
#: cmd/dashboard/controller/notification.go:112 #: cmd/dashboard/controller/notification.go:111
#: cmd/dashboard/controller/notification.go:166 #: cmd/dashboard/controller/notification.go:165
#: cmd/dashboard/controller/notification_group.go:76 #: cmd/dashboard/controller/notification_group.go:76
#: cmd/dashboard/controller/notification_group.go:152 #: cmd/dashboard/controller/notification_group.go:152
#: cmd/dashboard/controller/notification_group.go:164 #: cmd/dashboard/controller/notification_group.go:164
#: cmd/dashboard/controller/notification_group.go:233 #: cmd/dashboard/controller/notification_group.go:233
#: cmd/dashboard/controller/server.go:65 cmd/dashboard/controller/server.go:77 #: cmd/dashboard/controller/server.go:64 cmd/dashboard/controller/server.go:76
#: cmd/dashboard/controller/server.go:128 #: cmd/dashboard/controller/server.go:127
#: cmd/dashboard/controller/server.go:192 #: cmd/dashboard/controller/server.go:191
#: cmd/dashboard/controller/server_group.go:75 #: cmd/dashboard/controller/server_group.go:75
#: cmd/dashboard/controller/server_group.go:150 #: cmd/dashboard/controller/server_group.go:150
#: cmd/dashboard/controller/server_group.go:229 #: cmd/dashboard/controller/server_group.go:229
#: cmd/dashboard/controller/service.go:273 #: cmd/dashboard/controller/service.go:272
#: cmd/dashboard/controller/service.go:344 #: cmd/dashboard/controller/service.go:343
#: cmd/dashboard/controller/service.go:371 #: cmd/dashboard/controller/service.go:370
#: cmd/dashboard/controller/terminal.go:41 #: cmd/dashboard/controller/terminal.go:41
msgid "permission denied" msgid "permission denied"
msgstr "" msgstr ""
#: cmd/dashboard/controller/alertrule.go:184 #: cmd/dashboard/controller/alertrule.go:183
msgid "duration need to be at least 3" msgid "duration need to be at least 3"
msgstr "" msgstr ""
#: cmd/dashboard/controller/alertrule.go:188 #: cmd/dashboard/controller/alertrule.go:187
msgid "cycle_interval need to be at least 1" msgid "cycle_interval need to be at least 1"
msgstr "" msgstr ""
#: cmd/dashboard/controller/alertrule.go:191 #: cmd/dashboard/controller/alertrule.go:190
msgid "cycle_start is not set" msgid "cycle_start is not set"
msgstr "" msgstr ""
#: cmd/dashboard/controller/alertrule.go:194 #: cmd/dashboard/controller/alertrule.go:193
msgid "cycle_start is a future value" msgid "cycle_start is a future value"
msgstr "" msgstr ""
#: cmd/dashboard/controller/alertrule.go:199 #: cmd/dashboard/controller/alertrule.go:198
msgid "need to configure at least a single rule" msgid "need to configure at least a single rule"
msgstr "" msgstr ""
#: cmd/dashboard/controller/controller.go:210 #: cmd/dashboard/controller/controller.go:205
#: cmd/dashboard/controller/oauth2.go:152
#: cmd/dashboard/controller/server_group.go:162 #: cmd/dashboard/controller/server_group.go:162
#: cmd/dashboard/controller/service.go:96 cmd/dashboard/controller/user.go:27 #: cmd/dashboard/controller/service.go:95 cmd/dashboard/controller/user.go:26
#: cmd/dashboard/controller/user.go:63 #: cmd/dashboard/controller/user.go:53
msgid "unauthorized" msgid "unauthorized"
msgstr "" msgstr ""
#: cmd/dashboard/controller/controller.go:233 #: cmd/dashboard/controller/controller.go:228
msgid "database error" msgid "database error"
msgstr "" msgstr ""
#: cmd/dashboard/controller/cron.go:75 cmd/dashboard/controller/cron.go:149 #: cmd/dashboard/controller/cron.go:74 cmd/dashboard/controller/cron.go:148
msgid "scheduled tasks cannot be triggered by alarms" msgid "scheduled tasks cannot be triggered by alarms"
msgstr "" msgstr ""
#: cmd/dashboard/controller/cron.go:132 cmd/dashboard/controller/cron.go:190 #: cmd/dashboard/controller/cron.go:131 cmd/dashboard/controller/cron.go:189
#, c-format #, c-format
msgid "task id %d does not exist" msgid "task id %d does not exist"
msgstr "" msgstr ""
#: cmd/dashboard/controller/ddns.go:57 cmd/dashboard/controller/ddns.go:122 #: cmd/dashboard/controller/ddns.go:56 cmd/dashboard/controller/ddns.go:121
msgid "the retry count must be an integer between 1 and 10" msgid "the retry count must be an integer between 1 and 10"
msgstr "" msgstr ""
#: cmd/dashboard/controller/ddns.go:81 cmd/dashboard/controller/ddns.go:154 #: cmd/dashboard/controller/ddns.go:80 cmd/dashboard/controller/ddns.go:153
msgid "error parsing %s: %v" msgid "error parsing %s: %v"
msgstr "" msgstr ""
#: cmd/dashboard/controller/ddns.go:127 cmd/dashboard/controller/nat.go:117 #: cmd/dashboard/controller/ddns.go:126 cmd/dashboard/controller/nat.go:116
#, c-format #, c-format
msgid "profile id %d does not exist" msgid "profile id %d does not exist"
msgstr "" msgstr ""
@ -109,12 +108,12 @@ msgstr ""
msgid "server not found or not connected" msgid "server not found or not connected"
msgstr "" msgstr ""
#: cmd/dashboard/controller/notification.go:69 #: cmd/dashboard/controller/notification.go:68
#: cmd/dashboard/controller/notification.go:131 #: cmd/dashboard/controller/notification.go:130
msgid "a test message" msgid "a test message"
msgstr "" msgstr ""
#: cmd/dashboard/controller/notification.go:108 #: cmd/dashboard/controller/notification.go:107
#, c-format #, c-format
msgid "notification id %d does not exist" msgid "notification id %d does not exist"
msgstr "" msgstr ""
@ -130,34 +129,7 @@ msgstr ""
msgid "group id %d does not exist" msgid "group id %d does not exist"
msgstr "" msgstr ""
#: cmd/dashboard/controller/oauth2.go:42 cmd/dashboard/controller/oauth2.go:83 #: cmd/dashboard/controller/server.go:72
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 #, c-format
msgid "server id %d does not exist" msgid "server id %d does not exist"
msgstr "" msgstr ""
@ -167,37 +139,29 @@ msgstr ""
msgid "have invalid server id" msgid "have invalid server id"
msgstr "" msgstr ""
#: cmd/dashboard/controller/service.go:89 #: cmd/dashboard/controller/service.go:88
#: cmd/dashboard/controller/service.go:165 #: cmd/dashboard/controller/service.go:164
msgid "server not found" msgid "server not found"
msgstr "" msgstr ""
#: cmd/dashboard/controller/service.go:269 #: cmd/dashboard/controller/service.go:268
#, c-format #, c-format
msgid "service id %d does not exist" msgid "service id %d does not exist"
msgstr "" msgstr ""
#: cmd/dashboard/controller/user.go:68 #: cmd/dashboard/controller/user.go:58
msgid "incorrect password" msgid "incorrect password"
msgstr "" msgstr ""
#: cmd/dashboard/controller/user.go:82 #: cmd/dashboard/controller/user.go:110
msgid "you don't have any oauth2 bindings"
msgstr ""
#: cmd/dashboard/controller/user.go:130
msgid "password length must be greater than 6" msgid "password length must be greater than 6"
msgstr "" msgstr ""
#: cmd/dashboard/controller/user.go:133 #: cmd/dashboard/controller/user.go:113
msgid "username can't be empty" msgid "username can't be empty"
msgstr "" msgstr ""
#: cmd/dashboard/controller/user.go:136 #: cmd/dashboard/controller/user.go:152
msgid "invalid role"
msgstr ""
#: cmd/dashboard/controller/user.go:175
msgid "can't delete yourself" msgid "can't delete yourself"
msgstr "" msgstr ""
@ -225,11 +189,11 @@ msgstr ""
msgid "IP Changed" msgid "IP Changed"
msgstr "" msgstr ""
#: service/singleton/alertsentinel.go:170 #: service/singleton/alertsentinel.go:167
msgid "Incident" msgid "Incident"
msgstr "" msgstr ""
#: service/singleton/alertsentinel.go:180 #: service/singleton/alertsentinel.go:177
msgid "Resolved" msgid "Resolved"
msgstr "" msgstr ""
@ -295,6 +259,6 @@ msgstr ""
msgid "Down" msgid "Down"
msgstr "" msgstr ""
#: service/singleton/user.go:60 #: service/singleton/user.go:53
msgid "user id not specified" msgid "user id not specified"
msgstr "" msgstr ""