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
This commit is contained in:
UUBulb 2024-12-31 22:29:29 +08:00 committed by GitHub
parent 953fa153cc
commit 553f8e58d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 272 additions and 212 deletions

View File

@ -57,7 +57,6 @@ 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))
@ -67,6 +66,8 @@ 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())
@ -81,7 +82,6 @@ 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,6 +89,7 @@ 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)
@ -96,6 +97,11 @@ 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,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"net/http"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -20,27 +21,13 @@ import (
"github.com/nezhahq/nezha/service/singleton" "github.com/nezhahq/nezha/service/singleton"
) )
type Oauth2LoginType uint8 func getRedirectURL(c *gin.Context) string {
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://"
} }
var suffix string return scheme + c.Request.Host + "/api/v1/oauth2/callback"
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
@ -56,7 +43,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.Atoi(c.Query("type")) rTypeInt, err := strconv.ParseUint(c.Query("type"), 10, 8)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -65,14 +52,18 @@ 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, provider, Oauth2LoginType(rTypeInt))) o2conf := o2confRaw.Setup(getRedirectURL(c))
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), 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) url := o2conf.AuthCodeURL(state, oauth2.AccessTypeOnline)
c.SetCookie("nz-o2s", stateKey, 60*5, "", "", false, false) 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 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 // @Summary Unbind Oauth2
// @Description Unbind Oauth2 // @Description Unbind Oauth2
// @Accept json // @Accept json
@ -232,9 +88,145 @@ func unbindOauth2(c *gin.Context) (any, error) {
return nil, singleton.Localizer.ErrorT("provider not found") return nil, singleton.Localizer.ErrorT("provider not found")
} }
provider = strings.ToLower(provider) provider = strings.ToLower(provider)
u := c.MustGet(model.CtxKeyAuthorizedUser).(*model.User) 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) 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 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
}

View File

@ -73,8 +73,18 @@ 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)
} }

2
go.mod
View File

@ -33,7 +33,7 @@ require (
golang.org/x/crypto v0.31.0 golang.org/x/crypto v0.31.0
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67
golang.org/x/net v0.33.0 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 golang.org/x/sync v0.10.0
google.golang.org/grpc v1.69.2 google.golang.org/grpc v1.69.2
google.golang.org/protobuf v1.36.0 google.golang.org/protobuf v1.36.0

4
go.sum
View File

@ -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.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 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 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.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 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-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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=

View File

@ -7,3 +7,17 @@ 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

@ -15,10 +15,11 @@ const (
type User struct { type User struct {
Common Common
Username string `json:"username,omitempty" gorm:"uniqueIndex"` Username string `json:"username,omitempty" gorm:"uniqueIndex"`
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,4 +10,5 @@ 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-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" "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,89 +17,90 @@ 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:103 #: cmd/dashboard/controller/alertrule.go:104
#, 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:107 #: cmd/dashboard/controller/alertrule.go:108
#: cmd/dashboard/controller/alertrule.go:155 #: cmd/dashboard/controller/alertrule.go:156
#: cmd/dashboard/controller/alertrule.go:175 #: cmd/dashboard/controller/alertrule.go:176
#: cmd/dashboard/controller/controller.go:211 #: cmd/dashboard/controller/controller.go:216
#: cmd/dashboard/controller/cron.go:57 cmd/dashboard/controller/cron.go:123 #: cmd/dashboard/controller/cron.go:58 cmd/dashboard/controller/cron.go:124
#: cmd/dashboard/controller/cron.go:135 cmd/dashboard/controller/cron.go:194 #: cmd/dashboard/controller/cron.go:136 cmd/dashboard/controller/cron.go:195
#: cmd/dashboard/controller/cron.go:223 cmd/dashboard/controller/ddns.go:130 #: cmd/dashboard/controller/cron.go:224 cmd/dashboard/controller/ddns.go:131
#: cmd/dashboard/controller/ddns.go:191 cmd/dashboard/controller/fm.go:43 #: cmd/dashboard/controller/ddns.go:192 cmd/dashboard/controller/fm.go:43
#: cmd/dashboard/controller/nat.go:58 cmd/dashboard/controller/nat.go:109 #: cmd/dashboard/controller/nat.go:59 cmd/dashboard/controller/nat.go:110
#: cmd/dashboard/controller/nat.go:120 cmd/dashboard/controller/nat.go:159 #: cmd/dashboard/controller/nat.go:121 cmd/dashboard/controller/nat.go:160
#: cmd/dashboard/controller/notification.go:111 #: cmd/dashboard/controller/notification.go:112
#: cmd/dashboard/controller/notification.go:165 #: cmd/dashboard/controller/notification.go:166
#: 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:64 cmd/dashboard/controller/server.go:76 #: cmd/dashboard/controller/server.go:65 cmd/dashboard/controller/server.go:77
#: cmd/dashboard/controller/server.go:127 #: cmd/dashboard/controller/server.go:128
#: cmd/dashboard/controller/server.go:191 #: cmd/dashboard/controller/server.go:192
#: 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:272 #: cmd/dashboard/controller/service.go:273
#: cmd/dashboard/controller/service.go:343 #: cmd/dashboard/controller/service.go:344
#: cmd/dashboard/controller/service.go:370 #: cmd/dashboard/controller/service.go:371
#: 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:183 #: cmd/dashboard/controller/alertrule.go:184
msgid "duration need to be at least 3" msgid "duration need to be at least 3"
msgstr "" msgstr ""
#: cmd/dashboard/controller/alertrule.go:187 #: cmd/dashboard/controller/alertrule.go:188
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:190 #: cmd/dashboard/controller/alertrule.go:191
msgid "cycle_start is not set" msgid "cycle_start is not set"
msgstr "" msgstr ""
#: cmd/dashboard/controller/alertrule.go:193 #: cmd/dashboard/controller/alertrule.go:194
msgid "cycle_start is a future value" msgid "cycle_start is a future value"
msgstr "" msgstr ""
#: cmd/dashboard/controller/alertrule.go:198 #: cmd/dashboard/controller/alertrule.go:199
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:205 #: cmd/dashboard/controller/controller.go:210
#: 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:95 cmd/dashboard/controller/user.go:26 #: cmd/dashboard/controller/service.go:96 cmd/dashboard/controller/user.go:27
#: cmd/dashboard/controller/user.go:53 #: cmd/dashboard/controller/user.go:63
msgid "unauthorized" msgid "unauthorized"
msgstr "" msgstr ""
#: cmd/dashboard/controller/controller.go:228 #: cmd/dashboard/controller/controller.go:233
msgid "database error" msgid "database error"
msgstr "" 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" msgid "scheduled tasks cannot be triggered by alarms"
msgstr "" 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 #, c-format
msgid "task id %d does not exist" msgid "task id %d does not exist"
msgstr "" 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" msgid "the retry count must be an integer between 1 and 10"
msgstr "" 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" msgid "error parsing %s: %v"
msgstr "" 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 #, c-format
msgid "profile id %d does not exist" msgid "profile id %d does not exist"
msgstr "" msgstr ""
@ -108,12 +109,12 @@ msgstr ""
msgid "server not found or not connected" msgid "server not found or not connected"
msgstr "" msgstr ""
#: cmd/dashboard/controller/notification.go:68 #: cmd/dashboard/controller/notification.go:69
#: cmd/dashboard/controller/notification.go:130 #: cmd/dashboard/controller/notification.go:131
msgid "a test message" msgid "a test message"
msgstr "" msgstr ""
#: cmd/dashboard/controller/notification.go:107 #: cmd/dashboard/controller/notification.go:108
#, c-format #, c-format
msgid "notification id %d does not exist" msgid "notification id %d does not exist"
msgstr "" msgstr ""
@ -129,7 +130,34 @@ msgstr ""
msgid "group id %d does not exist" msgid "group id %d does not exist"
msgstr "" 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 #, c-format
msgid "server id %d does not exist" msgid "server id %d does not exist"
msgstr "" msgstr ""
@ -139,29 +167,37 @@ msgstr ""
msgid "have invalid server id" msgid "have invalid server id"
msgstr "" msgstr ""
#: cmd/dashboard/controller/service.go:88 #: cmd/dashboard/controller/service.go:89
#: cmd/dashboard/controller/service.go:164 #: cmd/dashboard/controller/service.go:165
msgid "server not found" msgid "server not found"
msgstr "" msgstr ""
#: cmd/dashboard/controller/service.go:268 #: cmd/dashboard/controller/service.go:269
#, 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:58 #: cmd/dashboard/controller/user.go:68
msgid "incorrect password" msgid "incorrect password"
msgstr "" 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" msgid "password length must be greater than 6"
msgstr "" msgstr ""
#: cmd/dashboard/controller/user.go:113 #: cmd/dashboard/controller/user.go:133
msgid "username can't be empty" msgid "username can't be empty"
msgstr "" 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" msgid "can't delete yourself"
msgstr "" msgstr ""
@ -189,11 +225,11 @@ msgstr ""
msgid "IP Changed" msgid "IP Changed"
msgstr "" msgstr ""
#: service/singleton/alertsentinel.go:167 #: service/singleton/alertsentinel.go:170
msgid "Incident" msgid "Incident"
msgstr "" msgstr ""
#: service/singleton/alertsentinel.go:177 #: service/singleton/alertsentinel.go:180
msgid "Resolved" msgid "Resolved"
msgstr "" msgstr ""
@ -259,6 +295,6 @@ msgstr ""
msgid "Down" msgid "Down"
msgstr "" msgstr ""
#: service/singleton/user.go:53 #: service/singleton/user.go:60
msgid "user id not specified" msgid "user id not specified"
msgstr "" msgstr ""