mirror of
https://github.com/nezhahq/nezha.git
synced 2025-01-22 04:38:14 -05:00
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:
parent
953fa153cc
commit
553f8e58d4
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
2
go.mod
2
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
|
||||
|
4
go.sum
4
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=
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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 <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\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 ""
|
||||
|
Loading…
Reference in New Issue
Block a user