From 5abd9b75bbae19df48566581ef51cade9c80993d Mon Sep 17 00:00:00 2001 From: Jacky Date: Tue, 23 Jul 2024 17:28:13 +0800 Subject: [PATCH 1/8] feat: login 2fa --- .idea/vcs.xml | 1 - api/api.go | 5 + api/user/auth.go | 41 +++-- api/user/casdoor.go | 2 +- api/user/otp.go | 163 +++++++++++++++++ api/user/router.go | 27 +-- app.example.ini | 8 + app/components.d.ts | 2 + app/package.json | 1 + app/pnpm-lock.yaml | 12 ++ app/src/api/auth.ts | 7 +- app/src/api/otp.ts | 23 +++ app/src/components/OTPInput/OTPInput.vue | 72 ++++++++ app/src/views/other/Login.vue | 109 +++++++++--- app/src/views/preference/AuthSettings.vue | 3 + app/src/views/preference/components/TOTP.vue | 176 +++++++++++++++++++ go.mod | 29 +-- go.sum | 52 ++++-- internal/cron/cron.go | 16 +- internal/crypto/aes.go | 59 +++++++ internal/crypto/aes_test.go | 76 ++++++++ internal/kernal/boot.go | 46 ++--- internal/user/otp.go | 39 ++++ internal/user/user.go | 110 ++++++++++-- model/auth.go | 13 +- query/auth_tokens.gen.go | 14 +- query/auths.gen.go | 6 +- router/middleware.go | 5 +- router/routers.go | 1 + settings/cluster_test.go | 1 + settings/crypto.go | 14 ++ settings/crypto_test.go | 48 +++++ settings/settings.go | 2 + 33 files changed, 1062 insertions(+), 121 deletions(-) create mode 100644 api/user/otp.go create mode 100644 app/src/api/otp.ts create mode 100644 app/src/components/OTPInput/OTPInput.vue create mode 100644 app/src/views/preference/components/TOTP.vue create mode 100644 internal/crypto/aes.go create mode 100644 internal/crypto/aes_test.go create mode 100644 internal/user/otp.go create mode 100644 settings/crypto.go create mode 100644 settings/crypto_test.go diff --git a/.idea/vcs.xml b/.idea/vcs.xml index c8e2b47f9..35eb1ddfb 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,6 +2,5 @@ - \ No newline at end of file diff --git a/api/api.go b/api/api.go index c27460bf8..89c59005b 100644 --- a/api/api.go +++ b/api/api.go @@ -3,6 +3,7 @@ package api import ( "errors" "github.com/0xJacky/Nginx-UI/internal/logger" + "github.com/0xJacky/Nginx-UI/model" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" "net/http" @@ -11,6 +12,10 @@ import ( "strings" ) +func CurrentUser(c *gin.Context) *model.Auth { + return c.MustGet("user").(*model.Auth) +} + func ErrHandler(c *gin.Context, err error) { logger.GetLogger().Errorln(err) c.JSON(http.StatusInternalServerError, gin.H{ diff --git a/api/user/auth.go b/api/user/auth.go index 82feef408..26ff86cf9 100644 --- a/api/user/auth.go +++ b/api/user/auth.go @@ -16,14 +16,19 @@ import ( var mutex = &sync.Mutex{} type LoginUser struct { - Name string `json:"name" binding:"required,max=255"` - Password string `json:"password" binding:"required,max=255"` + Name string `json:"name" binding:"required,max=255"` + Password string `json:"password" binding:"required,max=255"` + OTP string `json:"otp"` + RecoveryCode string `json:"recovery_code"` } const ( ErrPasswordIncorrect = 4031 ErrMaxAttempts = 4291 ErrUserBanned = 4033 + Enabled2FA = 199 + Error2FACode = 4034 + LoginSuccess = 200 ) type LoginResponse struct { @@ -80,11 +85,32 @@ func Login(c *gin.Context) { return } + // Check if the user enables 2FA + if len(u.OTPSecret) > 0 { + if json.OTP == "" && json.RecoveryCode == "" { + c.JSON(http.StatusOK, LoginResponse{ + Message: "The user has enabled 2FA", + Code: Enabled2FA, + }) + user.BanIP(clientIP) + return + } + + if err = user.VerifyOTP(u, json.OTP, json.RecoveryCode); err != nil { + c.JSON(http.StatusForbidden, LoginResponse{ + Message: "Invalid 2FA or recovery code", + Code: Error2FACode, + }) + user.BanIP(clientIP) + return + } + } + // login success, clear banned record _, _ = b.Where(b.IP.Eq(clientIP)).Delete() logger.Info("[User Login]", u.Name) - token, err := user.GenerateJWT(u.Name) + token, err := user.GenerateJWT(u) if err != nil { c.JSON(http.StatusInternalServerError, LoginResponse{ Message: err.Error(), @@ -93,6 +119,7 @@ func Login(c *gin.Context) { } c.JSON(http.StatusOK, LoginResponse{ + Code: LoginSuccess, Message: "ok", Token: token, }) @@ -101,13 +128,7 @@ func Login(c *gin.Context) { func Logout(c *gin.Context) { token := c.GetHeader("Authorization") if token != "" { - err := user.DeleteToken(token) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{ - "message": err.Error(), - }) - return - } + user.DeleteToken(token) } c.JSON(http.StatusNoContent, nil) } diff --git a/api/user/casdoor.go b/api/user/casdoor.go index 3fb105063..d8e210cb9 100644 --- a/api/user/casdoor.go +++ b/api/user/casdoor.go @@ -65,7 +65,7 @@ func CasdoorCallback(c *gin.Context) { return } - userToken, err := user.GenerateJWT(u.Name) + userToken, err := user.GenerateJWT(u) if err != nil { api.ErrHandler(c, err) return diff --git a/api/user/otp.go b/api/user/otp.go new file mode 100644 index 000000000..13bcc3e84 --- /dev/null +++ b/api/user/otp.go @@ -0,0 +1,163 @@ +package user + +import ( + "bytes" + "crypto/sha1" + "encoding/base64" + "encoding/hex" + "fmt" + "github.com/0xJacky/Nginx-UI/api" + "github.com/0xJacky/Nginx-UI/internal/crypto" + "github.com/0xJacky/Nginx-UI/query" + "github.com/0xJacky/Nginx-UI/settings" + "github.com/gin-gonic/gin" + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" + "image/jpeg" + "net/http" + "strings" +) + +func GenerateTOTP(c *gin.Context) { + user := api.CurrentUser(c) + + issuer := fmt.Sprintf("Nginx UI %s", settings.ServerSettings.Name) + issuer = strings.TrimSpace(issuer) + + otpOpts := totp.GenerateOpts{ + Issuer: issuer, + AccountName: user.Name, + Period: 30, // seconds + Digits: otp.DigitsSix, + Algorithm: otp.AlgorithmSHA1, + } + otpKey, err := totp.Generate(otpOpts) + if err != nil { + api.ErrHandler(c, err) + return + } + ciphertext, err := crypto.AesEncrypt([]byte(otpKey.Secret())) + if err != nil { + api.ErrHandler(c, err) + return + } + + qrCode, err := otpKey.Image(512, 512) + if err != nil { + api.ErrHandler(c, err) + return + } + + // Encode the image to a buffer + var buf []byte + buffer := bytes.NewBuffer(buf) + err = jpeg.Encode(buffer, qrCode, nil) + if err != nil { + fmt.Println("Error encoding image:", err) + return + } + + // Convert the buffer to a base64 string + base64Str := "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(buffer.Bytes()) + + c.JSON(http.StatusOK, gin.H{ + "secret": base64.StdEncoding.EncodeToString(ciphertext), + "qr_code": base64Str, + }) +} + +func EnrollTOTP(c *gin.Context) { + user := api.CurrentUser(c) + if len(user.OTPSecret) > 0 { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "User already enrolled", + }) + return + } + + var json struct { + Secret string `json:"secret" binding:"required"` + Passcode string `json:"passcode" binding:"required"` + } + if !api.BindAndValid(c, &json) { + return + } + + secret, err := base64.StdEncoding.DecodeString(json.Secret) + if err != nil { + api.ErrHandler(c, err) + return + } + + decrypted, err := crypto.AesDecrypt(secret) + if err != nil { + api.ErrHandler(c, err) + return + } + + if ok := totp.Validate(json.Passcode, string(decrypted)); !ok { + c.JSON(http.StatusNotAcceptable, gin.H{ + "message": "Invalid passcode", + }) + return + } + + ciphertext, err := crypto.AesEncrypt(decrypted) + if err != nil { + api.ErrHandler(c, err) + return + } + + u := query.Auth + _, err = u.Where(u.ID.Eq(user.ID)).Update(u.OTPSecret, ciphertext) + if err != nil { + api.ErrHandler(c, err) + return + } + + recoveryCode := sha1.Sum(ciphertext) + + c.JSON(http.StatusOK, gin.H{ + "message": "ok", + "recovery_code": hex.EncodeToString(recoveryCode[:]), + }) +} + +func ResetOTP(c *gin.Context) { + var json struct { + RecoveryCode string `json:"recovery_code"` + } + if !api.BindAndValid(c, &json) { + return + } + recoverCode, err := hex.DecodeString(json.RecoveryCode) + if err != nil { + api.ErrHandler(c, err) + return + } + user := api.CurrentUser(c) + k := sha1.Sum(user.OTPSecret) + if !bytes.Equal(k[:], recoverCode) { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "Invalid recovery code", + }) + return + } + + u := query.Auth + _, err = u.Where(u.ID.Eq(user.ID)).UpdateSimple(u.OTPSecret.Null()) + if err != nil { + api.ErrHandler(c, err) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "ok", + }) +} + +func OTPStatus(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "status": len(api.CurrentUser(c).OTPSecret) > 0, + }) +} diff --git a/api/user/router.go b/api/user/router.go index ee50f7029..565985ddf 100644 --- a/api/user/router.go +++ b/api/user/router.go @@ -2,18 +2,25 @@ package user import "github.com/gin-gonic/gin" -func InitAuthRouter(r *gin.RouterGroup) { - r.POST("/login", Login) - r.DELETE("/logout", Logout) +func InitAuthRouter(r *gin.RouterGroup) { + r.POST("/login", Login) + r.DELETE("/logout", Logout) - r.GET("/casdoor_uri", GetCasdoorUri) - r.POST("/casdoor_callback", CasdoorCallback) + r.GET("/casdoor_uri", GetCasdoorUri) + r.POST("/casdoor_callback", CasdoorCallback) } func InitManageUserRouter(r *gin.RouterGroup) { - r.GET("users", GetUsers) - r.GET("user/:id", GetUser) - r.POST("user", AddUser) - r.POST("user/:id", EditUser) - r.DELETE("user/:id", DeleteUser) + r.GET("users", GetUsers) + r.GET("user/:id", GetUser) + r.POST("user", AddUser) + r.POST("user/:id", EditUser) + r.DELETE("user/:id", DeleteUser) +} + +func InitUserRouter(r *gin.RouterGroup) { + r.GET("/otp_status", OTPStatus) + r.GET("/otp_secret", GenerateTOTP) + r.POST("/otp_enroll", EnrollTOTP) + r.POST("/otp_reset", ResetOTP) } diff --git a/app.example.ini b/app.example.ini index ca687a996..ec86357f9 100644 --- a/app.example.ini +++ b/app.example.ini @@ -51,3 +51,11 @@ Interval = 1440 Node = http://10.0.0.1:9000?name=node1&node_secret=my-node-secret&enabled=true Node = http://10.0.0.2:9000?name=node2&node_secret=my-node-secret&enabled=true Node = http://10.0.0.3?name=node3&node_secret=my-node-secret&enabled=true + +[auth] +IPWhiteList = +BanThresholdMinutes = 10 +MaxAttempts = 10 + +[crypto] +Secret = secret2 diff --git a/app/components.d.ts b/app/components.d.ts index 2d3e57bd3..507cb693e 100644 --- a/app/components.d.ts +++ b/app/components.d.ts @@ -78,6 +78,8 @@ declare module 'vue' { NginxControlNginxControl: typeof import('./src/components/NginxControl/NginxControl.vue')['default'] NodeSelectorNodeSelector: typeof import('./src/components/NodeSelector/NodeSelector.vue')['default'] NotificationNotification: typeof import('./src/components/Notification/Notification.vue')['default'] + OTPInput: typeof import('./src/components/OTPInput.vue')['default'] + OTPInputOTPInput: typeof import('./src/components/OTPInput/OTPInput.vue')['default'] PageHeaderPageHeader: typeof import('./src/components/PageHeader/PageHeader.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] diff --git a/app/package.json b/app/package.json index a0806938c..5ca186b0a 100644 --- a/app/package.json +++ b/app/package.json @@ -38,6 +38,7 @@ "vue3-ace-editor": "2.2.4", "vue3-apexcharts": "1.4.4", "vue3-gettext": "3.0.0-beta.4", + "vue3-otp-input": "^0.5.21", "vuedraggable": "^4.1.0" }, "devDependencies": { diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 865f2fa92..ac629e395 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -89,6 +89,9 @@ importers: vue3-gettext: specifier: 3.0.0-beta.4 version: 3.0.0-beta.4(@vue/compiler-sfc@3.4.33)(typescript@5.3.3)(vue@3.4.33(typescript@5.3.3)) + vue3-otp-input: + specifier: ^0.5.21 + version: 0.5.21(vue@3.4.33(typescript@5.3.3)) vuedraggable: specifier: ^4.1.0 version: 4.1.0(vue@3.4.33(typescript@5.3.3)) @@ -2969,6 +2972,11 @@ packages: '@vue/compiler-sfc': '>=3.0.0' vue: '>=3.0.0' + vue3-otp-input@0.5.21: + resolution: {integrity: sha512-dRxmGJqXlU+U5dCijNCyY7ird49+pyfeQspSTqvIp2Xs+VByIluNlTOjgHrftzSdeVZggtx+Ojb8uKiRLaob4Q==} + peerDependencies: + vue: ^3.0.* + vue@3.4.33: resolution: {integrity: sha512-VdMCWQOummbhctl4QFMcW6eNtXHsFyDlX60O/tsSQuCcuDOnJ1qPOhhVla65Niece7xq/P2zyZReIO5mP+LGTQ==} peerDependencies: @@ -6175,6 +6183,10 @@ snapshots: transitivePeerDependencies: - typescript + vue3-otp-input@0.5.21(vue@3.4.33(typescript@5.3.3)): + dependencies: + vue: 3.4.33(typescript@5.3.3) + vue@3.4.33(typescript@5.3.3): dependencies: '@vue/compiler-dom': 3.4.33 diff --git a/app/src/api/auth.ts b/app/src/api/auth.ts index 7541a9653..7381ed94d 100644 --- a/app/src/api/auth.ts +++ b/app/src/api/auth.ts @@ -6,15 +6,16 @@ const { login, logout } = useUserStore() export interface AuthResponse { message: string token: string + code: number } const auth = { - async login(name: string, password: string) { + async login(name: string, password: string, otp: string, recoveryCode: string): Promise { return http.post('/login', { name, password, - }).then((r: AuthResponse) => { - login(r.token) + otp, + recovery_code: recoveryCode, }) }, async casdoor_login(code?: string, state?: string) { diff --git a/app/src/api/otp.ts b/app/src/api/otp.ts new file mode 100644 index 000000000..ba8f01806 --- /dev/null +++ b/app/src/api/otp.ts @@ -0,0 +1,23 @@ +import http from '@/lib/http' + +export interface OTPGenerateSecretResponse { + secret: string + qr_code: string +} + +const otp = { + status(): Promise<{ status: boolean }> { + return http.get('/otp_status') + }, + generate_secret(): Promise { + return http.get('/otp_secret') + }, + enroll_otp(secret: string, passcode: string): Promise<{ recovery_code: string }> { + return http.post('/otp_enroll', { secret, passcode }) + }, + reset(recovery_code: string) { + return http.post('/otp_reset', { recovery_code }) + }, +} + +export default otp diff --git a/app/src/components/OTPInput/OTPInput.vue b/app/src/components/OTPInput/OTPInput.vue new file mode 100644 index 000000000..39fc6947e --- /dev/null +++ b/app/src/components/OTPInput/OTPInput.vue @@ -0,0 +1,72 @@ + + + + + + + diff --git a/app/src/views/other/Login.vue b/app/src/views/other/Login.vue index 96bfbdf30..020227931 100644 --- a/app/src/views/other/Login.vue +++ b/app/src/views/other/Login.vue @@ -1,13 +1,14 @@ +
+
+

{{ $gettext('Please enter the 2FA code:') }}

+ + + +
+ +
- - - - - - - - - +

{{ $gettext('Input the recovery code:') }}

+ + + + {{ $gettext('Recovery') }} + + +
+
+ + + + diff --git a/app/src/views/preference/components/TOTP.vue b/app/src/views/preference/components/TOTP.vue new file mode 100644 index 000000000..59b2dfc05 --- /dev/null +++ b/app/src/views/preference/components/TOTP.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/go.mod b/go.mod index d923f55a8..d72ce386c 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/caarlos0/env/v11 v11.1.0 github.com/casdoor/casdoor-go-sdk v0.47.0 github.com/creack/pty v1.1.21 + github.com/dgraph-io/ristretto v0.1.1 github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.17.0 @@ -44,7 +45,7 @@ require ( require ( aead.dev/minisign v0.3.0 // indirect - cloud.google.com/go/auth v0.7.1 // indirect + cloud.google.com/go/auth v0.7.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect cloud.google.com/go/compute/metadata v0.5.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect @@ -73,7 +74,7 @@ require ( github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect github.com/StackExchange/wmi v1.2.1 // indirect github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect - github.com/aliyun/alibaba-cloud-sdk-go v1.62.793 // indirect + github.com/aliyun/alibaba-cloud-sdk-go v1.62.795 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect @@ -97,6 +98,8 @@ require ( github.com/bytedance/sonic v1.11.9 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash v1.1.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/civo/civogo v0.3.73 // indirect github.com/cloudflare/cloudflare-go v0.100.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect @@ -125,20 +128,21 @@ require ( github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-viper/mapstructure/v2 v2.0.0 // indirect github.com/goccy/go-json v0.10.3 // indirect - github.com/gofrs/flock v0.12.0 // indirect + github.com/gofrs/flock v0.12.1 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/golang/glog v1.2.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/gomarkdown/markdown v0.0.0-20240626202925-2eda941fd024 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/s2a-go v0.1.7 // indirect + github.com/google/s2a-go v0.1.8 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.5 // indirect + github.com/googleapis/gax-go/v2 v2.13.0 // indirect github.com/gophercloud/gophercloud v1.13.0 // indirect github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect github.com/gorilla/css v1.0.1 // indirect @@ -228,8 +232,8 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/tdewolff/minify/v2 v2.20.37 // indirect github.com/tdewolff/parse/v2 v2.7.15 // indirect - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.968 // indirect - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.968 // indirect + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.969 // indirect + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.969 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/numcpus v0.8.0 // indirect github.com/transip/gotransip/v6 v6.25.0 // indirect @@ -242,7 +246,7 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/vultr/govultr/v2 v2.17.2 // indirect - github.com/yandex-cloud/go-genproto v0.0.0-20240715115219-0c1e192fbf5c // indirect + github.com/yandex-cloud/go-genproto v0.0.0-20240722173647-40d4f9e8b9fa // indirect github.com/yandex-cloud/go-sdk v0.0.0-20240701143239-7326d2d09169 // indirect github.com/yosssi/ace v0.0.5 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect @@ -264,10 +268,10 @@ require ( golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.23.0 // indirect - google.golang.org/api v0.188.0 // indirect - google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect + google.golang.org/api v0.189.0 // indirect + google.golang.org/genproto v0.0.0-20240722135656-d784300faade // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade // indirect google.golang.org/grpc v1.65.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect @@ -278,6 +282,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/datatypes v1.2.1 // indirect gorm.io/driver/mysql v1.5.7 // indirect + gorm.io/driver/postgres v1.5.6 // indirect gorm.io/hints v1.1.2 // indirect k8s.io/api v0.30.3 // indirect k8s.io/apimachinery v0.30.3 // indirect diff --git a/go.sum b/go.sum index 95560c5c7..25f77909e 100644 --- a/go.sum +++ b/go.sum @@ -101,8 +101,8 @@ cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVo cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= -cloud.google.com/go/auth v0.7.1 h1:Iv1bbpzJ2OIg16m94XI9/tlzZZl3cdeR3nGVGj78N7s= -cloud.google.com/go/auth v0.7.1/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs= +cloud.google.com/go/auth v0.7.2 h1:uiha352VrCDMXg+yoBtaD0tUF4Kv9vrtrWPYXwutnDE= +cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs= cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI= cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I= cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= @@ -672,6 +672,7 @@ github.com/Joker/hpp v1.0.0 h1:65+iuJYdRXv/XyN62C1uEmmOx3432rNG/rKlX6V7Kkc= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk= github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 h1:xPMsUicZ3iosVPSIP7bW5EcGUzjiiMl1OYTe14y/R24= github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks= @@ -694,8 +695,10 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/aliyun/alibaba-cloud-sdk-go v1.62.793 h1:7FmdfF5fZMxM8Y0YtwrnMLkwud+egvoB5X5xczqISNQ= -github.com/aliyun/alibaba-cloud-sdk-go v1.62.793/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= +github.com/aliyun/alibaba-cloud-sdk-go v1.62.794 h1:M6YtlJdCobRVlJaILK4Eia5aMtDSpeQtxFRl4hSi+DU= +github.com/aliyun/alibaba-cloud-sdk-go v1.62.794/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= +github.com/aliyun/alibaba-cloud-sdk-go v1.62.795 h1:DjIaInK6Ru+fPnOX0Ef4ux5tkp/dCPI3pAZEijEvlvo= +github.com/aliyun/alibaba-cloud-sdk-go v1.62.795/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= @@ -772,9 +775,12 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -821,8 +827,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deepmap/oapi-codegen v1.16.3 h1:GT9G86SbQtT1r8ZB+4Cybi9VGdu1P5ieNvNdEoCSbrA= github.com/deepmap/oapi-codegen v1.16.3/go.mod h1:JD6ErqeX0nYnhdciLc61Konj3NBASREMlkHOgHn8WAM= +github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de h1:t0UHb5vdojIDUqktM6+xJAfScFBsVpXZmqC9dsgJmeA= +github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= @@ -949,6 +961,8 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/flock v0.12.0 h1:xHW8t8GPAiGtqz7KxiSqfOEXwpOaqhpYZrTE2MQBgXY= github.com/gofrs/flock v0.12.0/go.mod h1:FirDy1Ing0mI2+kB6wk+vyyAH+e6xiE+EYA0jnzV9jc= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -972,6 +986,8 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGw github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= +github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= +github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -1065,6 +1081,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -1092,6 +1110,8 @@ github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38 github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA= github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E= +github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= +github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gophercloud/gophercloud v1.3.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= @@ -1561,6 +1581,7 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ= github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= @@ -1614,14 +1635,14 @@ github.com/tdewolff/parse/v2 v2.7.15/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo= github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.967 h1:ui73H/2pKk2aDCxaBCLAeMB3JlNgdCkn0nx1x0pqvf0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.967/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.968 h1:SdgunZB3WU2vNn3H9dJQ1Z2cQK61vN79zCfnHk3Cu3Y= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.968/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.967 h1:4w33xHFgyrlFZYoGkPQ3uhld8tqoezpObfmCBrdlFBY= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.967/go.mod h1:T0RlPIT2imBeCxLkWfzoiEVP1r5WwzC6becSq7wvSgU= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.969 h1:rJlV77WbjuJ5uGBi+THOk09Cfp8Kskz9HgExq0enTmY= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.969/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.968 h1:h7voJALWRkUX6w7obk9CWHppnJwZuQlreQJVDldVRxY= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.968/go.mod h1:3cwvPwyqYaYkzAsR4vbrE6mb3Ju9uY7Pj+wHYSVd3aw= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.969 h1:W2DHKBCSLjpHoQjqgAkyUu7lV8deIW+FBZS95iNRf1A= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.969/go.mod h1:jIxuhjYsAyTTErdwvaX1ay+FHH021fmjdlsbnkaOgfs= github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY= @@ -1664,6 +1685,8 @@ github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmv github.com/yandex-cloud/go-genproto v0.0.0-20240701142715-6a03f33f8ec8/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE= github.com/yandex-cloud/go-genproto v0.0.0-20240715115219-0c1e192fbf5c h1:GzMfpQ/oAP93MOQb5/B+3daDzdcLRRqetZ8radtnJJ4= github.com/yandex-cloud/go-genproto v0.0.0-20240715115219-0c1e192fbf5c/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE= +github.com/yandex-cloud/go-genproto v0.0.0-20240722173647-40d4f9e8b9fa h1:MFb4Q81BMqa0vL64v/i3mel9C+XQkVnwgWqWbmqv10U= +github.com/yandex-cloud/go-genproto v0.0.0-20240722173647-40d4f9e8b9fa/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE= github.com/yandex-cloud/go-sdk v0.0.0-20240701143239-7326d2d09169 h1:5LGYQ/0h1uUo3HH8MsG6R40gvSVPj/7r4D1sKVMa370= github.com/yandex-cloud/go-sdk v0.0.0-20240701143239-7326d2d09169/go.mod h1:kRqpmRyPs8rzXuYEJe57AH546a3VcSjEIzdFa1V66hY= github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA= @@ -2051,6 +2074,7 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2258,6 +2282,8 @@ google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45 google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= google.golang.org/api v0.188.0 h1:51y8fJ/b1AaaBRJr4yWm96fPcuxSo0JcegXE3DaHQHw= google.golang.org/api v0.188.0/go.mod h1:VR0d+2SIiWOYG3r/jdm7adPW9hI2aRv9ETOSCQ9Beag= +google.golang.org/api v0.189.0 h1:equMo30LypAkdkLMBqfeIqtyAnlyig1JSZArl4XPwdI= +google.golang.org/api v0.189.0/go.mod h1:FLWGJKb0hb+pU2j+rJqwbnsF+ym+fQs73rbJ+KAUgy8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -2400,10 +2426,16 @@ google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOl google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d h1:/hmn0Ku5kWij/kjGsrcJeC1T/MrJi2iNWwgAqrihFwc= google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY= +google.golang.org/genproto v0.0.0-20240722135656-d784300faade h1:lKFsS7wpngDgSCeFn7MoLy+wBDQZ1UQIJD4UNM1Qvkg= +google.golang.org/genproto v0.0.0-20240722135656-d784300faade/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY= google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY= google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= +google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade h1:WxZOF2yayUHpHSbUE6NMzumUzBxYc3YGwo0YHnbzsJY= +google.golang.org/genproto/googleapis/api v0.0.0-20240722135656-d784300faade/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A= google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade h1:oCRSWfwGXQsqlVdErcyTt4A93Y8fo0/9D4b1gnI++qo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -2515,8 +2547,8 @@ gorm.io/datatypes v1.2.1/go.mod h1:hYK6OTb/1x+m96PgoZZq10UXJ6RvEBb9kRDQ2yyhzGs= gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= -gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= -gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= +gorm.io/driver/postgres v1.5.6 h1:ydr9xEd5YAM0vxVDY0X139dyzNz10spDiDlC7+ibLeU= +gorm.io/driver/postgres v1.5.6/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I= gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= diff --git a/internal/cron/cron.go b/internal/cron/cron.go index a6b1e1201..decda2e26 100644 --- a/internal/cron/cron.go +++ b/internal/cron/cron.go @@ -4,6 +4,7 @@ import ( "github.com/0xJacky/Nginx-UI/internal/cert" "github.com/0xJacky/Nginx-UI/internal/logger" "github.com/0xJacky/Nginx-UI/internal/logrotate" + "github.com/0xJacky/Nginx-UI/query" "github.com/0xJacky/Nginx-UI/settings" "github.com/go-co-op/gocron" "time" @@ -25,6 +26,7 @@ func InitCronJobs() { } startLogrotate() + cleanExpiredAuthToken() s.StartAsync() } @@ -43,10 +45,20 @@ func startLogrotate() { return } var err error - logrotateJob, err = s.Every(settings.LogrotateSettings.Interval).Minute().SingletonMode().Do(logrotate.Exec) - if err != nil { logger.Fatalf("LogRotate Job: %v, Err: %v\n", logrotateJob, err) } } + +func cleanExpiredAuthToken() { + job, err := s.Every(5).Minute().SingletonMode().Do(func() { + logger.Info("clean expired auth tokens") + q := query.AuthToken + _, _ = q.Where(q.ExpiredAt.Lt(time.Now().Unix())).Delete() + }) + + if err != nil { + logger.Fatalf("CleanExpiredAuthToken Job: %v, Err: %v\n", job, err) + } +} diff --git a/internal/crypto/aes.go b/internal/crypto/aes.go new file mode 100644 index 000000000..1e76d2df8 --- /dev/null +++ b/internal/crypto/aes.go @@ -0,0 +1,59 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "fmt" + "github.com/0xJacky/Nginx-UI/settings" + "github.com/pkg/errors" + "io" +) + +// AesEncrypt encrypts text and given key with AES. +func AesEncrypt(text []byte) ([]byte, error) { + if len(text) == 0 { + return nil, errors.New("AesEncrypt text is empty") + } + block, err := aes.NewCipher(settings.CryptoSettings.GetSecretMd5()) + if err != nil { + return nil, fmt.Errorf("AesEncrypt invalid key: %v", err) + } + + b := base64.StdEncoding.EncodeToString(text) + ciphertext := make([]byte, aes.BlockSize+len(b)) + iv := ciphertext[:aes.BlockSize] + if _, err = io.ReadFull(rand.Reader, iv); err != nil { + return nil, fmt.Errorf("AesEncrypt unable to read IV: %w", err) + } + + cfb := cipher.NewCFBEncrypter(block, iv) + cfb.XORKeyStream(ciphertext[aes.BlockSize:], []byte(b)) + + return ciphertext, nil +} + +// AesDecrypt decrypts text and given key with AES. +func AesDecrypt(text []byte) ([]byte, error) { + block, err := aes.NewCipher(settings.CryptoSettings.GetSecretMd5()) + if err != nil { + return nil, err + } + + if len(text) < aes.BlockSize { + return nil, errors.New("AesDecrypt ciphertext too short") + } + + iv := text[:aes.BlockSize] + text = text[aes.BlockSize:] + cfb := cipher.NewCFBDecrypter(block, iv) + cfb.XORKeyStream(text, text) + + data, err := base64.StdEncoding.DecodeString(string(text)) + if err != nil { + return nil, fmt.Errorf("AesDecrypt invalid decrypted base64 string: %w", err) + } + + return data, nil +} diff --git a/internal/crypto/aes_test.go b/internal/crypto/aes_test.go new file mode 100644 index 000000000..805214c08 --- /dev/null +++ b/internal/crypto/aes_test.go @@ -0,0 +1,76 @@ +package crypto + +import ( + "github.com/0xJacky/Nginx-UI/settings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func EncryptDecryptRoundTrip(text string) bool { + encrypted, err := AesEncrypt([]byte(text)) + if err != nil { + return false + } + + decrypted, err := AesDecrypt(encrypted) + if err != nil { + return false + } + + return text == string(decrypted) +} + +func EncryptsNonEmptyStringWithoutError(text string) bool { + _, err := AesEncrypt([]byte(text)) + return err == nil +} + +func DecryptsToOriginalTextAfterEncryption(text string) bool { + encrypted, _ := AesEncrypt([]byte(text)) + decrypted, err := AesDecrypt(encrypted) + if err != nil { + return false + } + + return text == string(decrypted) +} + +func FailsToDecryptWithModifiedCiphertext(text string) bool { + encrypted, _ := AesEncrypt([]byte(text)) + // Modify the ciphertext + encrypted[0] ^= 0xff + _, err := AesDecrypt(encrypted) + return err != nil +} + +func FailsToDecryptShortCiphertext() bool { + _, err := AesDecrypt([]byte("short")) + return err != nil +} + +func TestAesEncryptionDecryption(t *testing.T) { + settings.CryptoSettings.Secret = "test" + assert.True(t, EncryptDecryptRoundTrip("Hello, world!"), "should encrypt and decrypt to the original text") + assert.True(t, EncryptsNonEmptyStringWithoutError("Test String"), "should encrypt a non-empty string without error") + assert.True(t, DecryptsToOriginalTextAfterEncryption("Another Test String"), "should decrypt to the original text after encryption") + assert.True(t, FailsToDecryptWithModifiedCiphertext("Sensitive Data"), "should fail to decrypt with modified ciphertext") + assert.True(t, FailsToDecryptShortCiphertext(), "should fail to decrypt short ciphertext") +} + +func TestAesEncrypt_WithEmptyString_ReturnsError(t *testing.T) { + settings.CryptoSettings.Secret = "test" + _, err := AesEncrypt([]byte("")) + require.Error(t, err, "encrypting an empty string should return an error") +} + +func TestAesDecrypt_WithInvalidBase64_ReturnsError(t *testing.T) { + settings.CryptoSettings.Secret = "test" + // Assuming the function is modified to handle this case explicitly + encrypted, _ := AesEncrypt([]byte("valid text")) + // Invalidate the base64 encoding + encrypted[len(encrypted)-1] = '!' + _, err := AesDecrypt(encrypted) + require.Error(t, err, "decrypting an invalid base64 string should return an error") +} diff --git a/internal/kernal/boot.go b/internal/kernal/boot.go index 83f81ff07..aad0496fd 100644 --- a/internal/kernal/boot.go +++ b/internal/kernal/boot.go @@ -1,20 +1,20 @@ package kernal import ( + "crypto/rand" + "encoding/hex" "github.com/0xJacky/Nginx-UI/internal/analytic" "github.com/0xJacky/Nginx-UI/internal/cert" "github.com/0xJacky/Nginx-UI/internal/cluster" + "github.com/0xJacky/Nginx-UI/internal/cron" "github.com/0xJacky/Nginx-UI/internal/logger" - "github.com/0xJacky/Nginx-UI/internal/logrotate" "github.com/0xJacky/Nginx-UI/internal/validation" "github.com/0xJacky/Nginx-UI/model" "github.com/0xJacky/Nginx-UI/query" "github.com/0xJacky/Nginx-UI/settings" - "github.com/go-co-op/gocron" "github.com/google/uuid" "mime" "runtime" - "time" ) func Boot() { @@ -24,6 +24,7 @@ func Boot() { InitJsExtensionType, InitDatabase, InitNodeSecret, + InitCryptoSecret, validation.Init, } @@ -44,7 +45,7 @@ func InitAfterDatabase() { syncs := []func(){ registerPredefinedUser, cert.InitRegister, - InitCronJobs, + cron.InitCronJobs, cluster.RegisterPredefinedNodes, analytic.RetrieveNodesStatus, } @@ -83,31 +84,34 @@ func InitNodeSecret() { err := settings.Save() if err != nil { - logger.Error("Error save settings") + logger.Error("Error save settings", err) } logger.Warn("Generated NodeSecret: ", settings.ServerSettings.NodeSecret) } } -func InitJsExtensionType() { - // Hack: fix wrong Content Type of .js file on some OS platforms - // See https://github.com/golang/go/issues/32350 - _ = mime.AddExtensionType(".js", "text/javascript; charset=utf-8") -} - -func InitCronJobs() { - s := gocron.NewScheduler(time.UTC) - job, err := s.Every(6).Hours().SingletonMode().Do(cert.AutoCert) +func InitCryptoSecret() { + if "" == settings.CryptoSettings.Secret { + logger.Warn("Secret is empty, generating...") - if err != nil { - logger.Fatalf("AutoCert Job: %v, Err: %v\n", job, err) - } + key := make([]byte, 32) + if _, err := rand.Read(key); err != nil { + logger.Error("Generate Secret failed: ", err) + return + } - job, err = s.Every(settings.LogrotateSettings.Interval).Minute().SingletonMode().Do(logrotate.Exec) + settings.CryptoSettings.Secret = hex.EncodeToString(key) - if err != nil { - logger.Fatalf("LogRotate Job: %v, Err: %v\n", job, err) + err := settings.Save() + if err != nil { + logger.Error("Error save settings", err) + } + logger.Warn("Secret Generated") } +} - s.StartAsync() +func InitJsExtensionType() { + // Hack: fix wrong Content Type of .js file on some OS platforms + // See https://github.com/golang/go/issues/32350 + _ = mime.AddExtensionType(".js", "text/javascript; charset=utf-8") } diff --git a/internal/user/otp.go b/internal/user/otp.go new file mode 100644 index 000000000..1cfed06d6 --- /dev/null +++ b/internal/user/otp.go @@ -0,0 +1,39 @@ +package user + +import ( + "bytes" + "crypto/sha1" + "encoding/hex" + "github.com/0xJacky/Nginx-UI/internal/crypto" + "github.com/0xJacky/Nginx-UI/model" + "github.com/pkg/errors" + "github.com/pquerna/otp/totp" +) + +var ( + ErrOTPCode = errors.New("invalid otp code") + ErrRecoveryCode = errors.New("invalid recovery code") +) + +func VerifyOTP(user *model.Auth, otp, recoveryCode string) (err error) { + if otp != "" { + decrypted, err := crypto.AesDecrypt(user.OTPSecret) + if err != nil { + return err + } + + if ok := totp.Validate(otp, string(decrypted)); !ok { + return ErrOTPCode + } + } else { + recoverCode, err := hex.DecodeString(recoveryCode) + if err != nil { + return err + } + k := sha1.Sum(user.OTPSecret) + if !bytes.Equal(k[:], recoverCode) { + return ErrRecoveryCode + } + } + return +} diff --git a/internal/user/user.go b/internal/user/user.go index 325fe7da8..75b37c5ed 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -1,53 +1,84 @@ package user import ( + "github.com/0xJacky/Nginx-UI/internal/logger" "github.com/0xJacky/Nginx-UI/model" + "github.com/0xJacky/Nginx-UI/query" "github.com/0xJacky/Nginx-UI/settings" "github.com/dgrijalva/jwt-go" + "github.com/pkg/errors" + "strings" "time" ) +const ExpiredTime = 24 * time.Hour + type JWTClaims struct { - Name string `json:"name"` + Name string `json:"name"` + UserID int `json:"user_id"` jwt.StandardClaims } -func GetUser(name string) (user model.Auth, err error) { +func BuildCacheTokenKey(token string) string { + var sb strings.Builder + sb.WriteString("token:") + sb.WriteString(token) + return sb.String() +} + +func GetUser(name string) (user *model.Auth, err error) { db := model.UseDB() - err = db.Where("name", name).First(&user).Error + user = &model.Auth{} + err = db.Where("name", name).First(user).Error if err != nil { return } return } -func DeleteToken(token string) error { - db := model.UseDB() - return db.Where("token", token).Delete(&model.AuthToken{}).Error +func DeleteToken(token string) { + q := query.AuthToken + _, _ = q.Where(q.Token.Eq(token)).Delete() } -func CheckToken(token string) int64 { - db := model.UseDB() - return db.Where("token", token).Find(&model.AuthToken{}).RowsAffected +func GetTokenUser(token string) (*model.Auth, bool) { + q := query.AuthToken + authToken, err := q.Where(q.Token.Eq(token)).First() + if err != nil { + return nil, false + } + + if authToken.ExpiredAt < time.Now().Unix() { + DeleteToken(token) + return nil, false + } + + u := query.Auth + user, err := u.FirstByID(authToken.UserID) + return user, err == nil } -func GenerateJWT(name string) (string, error) { +func GenerateJWT(user *model.Auth) (string, error) { claims := JWTClaims{ - Name: name, + Name: user.Name, + UserID: user.ID, StandardClaims: jwt.StandardClaims{ - ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), + ExpiresAt: time.Now().Add(ExpiredTime).Unix(), }, } + unsignedToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) signedToken, err := unsignedToken.SignedString([]byte(settings.ServerSettings.JwtSecret)) if err != nil { return "", err } - db := model.UseDB() - err = db.Create(&model.AuthToken{ - Token: signedToken, - }).Error + q := query.AuthToken + err = q.Create(&model.AuthToken{ + UserID: user.ID, + Token: signedToken, + ExpiredAt: time.Now().Add(ExpiredTime).Unix(), + }) if err != nil { return "", err @@ -55,3 +86,50 @@ func GenerateJWT(name string) (string, error) { return signedToken, err } + +func ValidateJWT(token string) (claims *JWTClaims, err error) { + if token == "" { + err = errors.New("token is empty") + return + } + unsignedToken, err := jwt.ParseWithClaims( + token, + &JWTClaims{}, + func(token *jwt.Token) (interface{}, error) { + return []byte(settings.ServerSettings.JwtSecret), nil + }, + ) + if err != nil { + err = errors.New("parse with claims error") + return + } + claims, ok := unsignedToken.Claims.(*JWTClaims) + if !ok { + err = errors.New("convert to jwt claims error") + return + } + if claims.ExpiresAt < time.Now().UTC().Unix() { + err = errors.New("jwt is expired") + } + return +} + +func CurrentUser(token string) (u *model.Auth, err error) { + // validate token + var claims *JWTClaims + claims, err = ValidateJWT(token) + if err != nil { + return + } + + // get user by id + user := query.Auth + u, err = user.FirstByID(claims.UserID) + if err != nil { + return + } + + logger.Info("[Current User]", u.Name) + + return +} diff --git a/model/auth.go b/model/auth.go index 45720b522..7b361b3cd 100644 --- a/model/auth.go +++ b/model/auth.go @@ -1,13 +1,16 @@ package model type Auth struct { - Model + Model - Name string `json:"name"` - Password string `json:"-"` - Status bool `json:"status" gorm:"default:1"` + Name string `json:"name"` + Password string `json:"-"` + Status bool `json:"status" gorm:"default:1"` + OTPSecret []byte `json:"-" gorm:"type:blob"` } type AuthToken struct { - Token string `json:"token"` + UserID int `json:"user_id"` + Token string `json:"token"` + ExpiredAt int64 `json:"expired_at" gorm:"default:0"` } diff --git a/query/auth_tokens.gen.go b/query/auth_tokens.gen.go index b06d7ced8..56b24d8da 100644 --- a/query/auth_tokens.gen.go +++ b/query/auth_tokens.gen.go @@ -28,7 +28,9 @@ func newAuthToken(db *gorm.DB, opts ...gen.DOOption) authToken { tableName := _authToken.authTokenDo.TableName() _authToken.ALL = field.NewAsterisk(tableName) + _authToken.UserID = field.NewInt(tableName, "user_id") _authToken.Token = field.NewString(tableName, "token") + _authToken.ExpiredAt = field.NewInt64(tableName, "expired_at") _authToken.fillFieldMap() @@ -38,8 +40,10 @@ func newAuthToken(db *gorm.DB, opts ...gen.DOOption) authToken { type authToken struct { authTokenDo - ALL field.Asterisk - Token field.String + ALL field.Asterisk + UserID field.Int + Token field.String + ExpiredAt field.Int64 fieldMap map[string]field.Expr } @@ -56,7 +60,9 @@ func (a authToken) As(alias string) *authToken { func (a *authToken) updateTableName(table string) *authToken { a.ALL = field.NewAsterisk(table) + a.UserID = field.NewInt(table, "user_id") a.Token = field.NewString(table, "token") + a.ExpiredAt = field.NewInt64(table, "expired_at") a.fillFieldMap() @@ -73,8 +79,10 @@ func (a *authToken) GetFieldByName(fieldName string) (field.OrderExpr, bool) { } func (a *authToken) fillFieldMap() { - a.fieldMap = make(map[string]field.Expr, 1) + a.fieldMap = make(map[string]field.Expr, 3) + a.fieldMap["user_id"] = a.UserID a.fieldMap["token"] = a.Token + a.fieldMap["expired_at"] = a.ExpiredAt } func (a authToken) clone(db *gorm.DB) authToken { diff --git a/query/auths.gen.go b/query/auths.gen.go index d27499dbb..1295fb716 100644 --- a/query/auths.gen.go +++ b/query/auths.gen.go @@ -35,6 +35,7 @@ func newAuth(db *gorm.DB, opts ...gen.DOOption) auth { _auth.Name = field.NewString(tableName, "name") _auth.Password = field.NewString(tableName, "password") _auth.Status = field.NewBool(tableName, "status") + _auth.OTPSecret = field.NewBytes(tableName, "otp_secret") _auth.fillFieldMap() @@ -52,6 +53,7 @@ type auth struct { Name field.String Password field.String Status field.Bool + OTPSecret field.Bytes fieldMap map[string]field.Expr } @@ -75,6 +77,7 @@ func (a *auth) updateTableName(table string) *auth { a.Name = field.NewString(table, "name") a.Password = field.NewString(table, "password") a.Status = field.NewBool(table, "status") + a.OTPSecret = field.NewBytes(table, "otp_secret") a.fillFieldMap() @@ -91,7 +94,7 @@ func (a *auth) GetFieldByName(fieldName string) (field.OrderExpr, bool) { } func (a *auth) fillFieldMap() { - a.fieldMap = make(map[string]field.Expr, 7) + a.fieldMap = make(map[string]field.Expr, 8) a.fieldMap["id"] = a.ID a.fieldMap["created_at"] = a.CreatedAt a.fieldMap["updated_at"] = a.UpdatedAt @@ -99,6 +102,7 @@ func (a *auth) fillFieldMap() { a.fieldMap["name"] = a.Name a.fieldMap["password"] = a.Password a.fieldMap["status"] = a.Status + a.fieldMap["otp_secret"] = a.OTPSecret } func (a auth) clone(db *gorm.DB) auth { diff --git a/router/middleware.go b/router/middleware.go index bcf7944cc..18ad67536 100644 --- a/router/middleware.go +++ b/router/middleware.go @@ -58,11 +58,14 @@ func authRequired() gin.HandlerFunc { } } - if user.CheckToken(token) < 1 { + u, ok := user.GetTokenUser(token) + if !ok { abortWithAuthFailure() return } + c.Set("user", u) + if nodeID := c.GetHeader("X-Node-ID"); nodeID != "" { c.Set("ProxyNodeID", nodeID) } diff --git a/router/routers.go b/router/routers.go index eb15a257a..d1851ce9a 100644 --- a/router/routers.go +++ b/router/routers.go @@ -46,6 +46,7 @@ func InitRouter() *gin.Engine { // Authorization required not websocket request g := root.Group("/", authRequired(), proxy()) { + user.InitUserRouter(g) analytic.InitRouter(g) user.InitManageUserRouter(g) nginx.InitRouter(g) diff --git a/settings/cluster_test.go b/settings/cluster_test.go index 1d01ed0c7..235b47b6d 100644 --- a/settings/cluster_test.go +++ b/settings/cluster_test.go @@ -11,5 +11,6 @@ func TestCluster(t *testing.T) { assert.Equal(t, []string{ "http://10.0.0.1:9000?name=node1&node_secret=my-node-secret&enabled=true", "http://10.0.0.2:9000?name=node2&node_secret=my-node-secret&enabled=true", + "http://10.0.0.3?name=node3&node_secret=my-node-secret&enabled=true", }, ClusterSettings.Node) } diff --git a/settings/crypto.go b/settings/crypto.go new file mode 100644 index 000000000..57d5e0c66 --- /dev/null +++ b/settings/crypto.go @@ -0,0 +1,14 @@ +package settings + +import "crypto/md5" + +type Crypto struct { + Secret string +} + +var CryptoSettings = Crypto{} + +func (c *Crypto) GetSecretMd5() []byte { + k := md5.Sum([]byte(c.Secret)) + return k[:] +} diff --git a/settings/crypto_test.go b/settings/crypto_test.go new file mode 100644 index 000000000..db10bae5a --- /dev/null +++ b/settings/crypto_test.go @@ -0,0 +1,48 @@ +package settings + +import ( + "crypto/md5" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetSecretMd5_WithNonEmptySecret_ReturnsExpectedMd5Hash(t *testing.T) { + // Setup + CryptoSettings.Secret = "testSecret" + expectedMd5 := md5.Sum([]byte("testSecret")) + expectedMd5String := hex.EncodeToString(expectedMd5[:]) + + // Execute + resultMd5 := CryptoSettings.GetSecretMd5() + resultMd5String := hex.EncodeToString(resultMd5[:]) + + // Verify + assert.Equal(t, expectedMd5String, resultMd5String, "MD5 hash should match for non-empty secret") +} + +func TestGetSecretMd5_WithEmptySecret_ReturnsMd5OfEmptyString(t *testing.T) { + // Setup + CryptoSettings.Secret = "" + expectedMd5 := md5.Sum([]byte("")) + expectedMd5String := hex.EncodeToString(expectedMd5[:]) + + // Execute + resultMd5 := CryptoSettings.GetSecretMd5() + resultMd5String := hex.EncodeToString(resultMd5[:]) + + // Verify + assert.Equal(t, expectedMd5String, resultMd5String, "MD5 hash of an empty string should be returned for empty secret") +} + +func TestGetSecretMd5_WithDifferentSecrets_ReturnsDifferentMd5Hashes(t *testing.T) { + // Setup + CryptoSettings.Secret = "secret1" + firstMd5 := CryptoSettings.GetSecretMd5() + CryptoSettings.Secret = "secret2" + secondMd5 := CryptoSettings.GetSecretMd5() + + // Verify + assert.NotEqual(t, firstMd5, secondMd5, "Different secrets should produce different MD5 hashes") +} diff --git a/settings/settings.go b/settings/settings.go index 12c3c6bfc..b29bbbc43 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -28,6 +28,7 @@ var sections = map[string]interface{}{ "logrotate": &LogrotateSettings, "cluster": &ClusterSettings, "auth": &AuthSettings, + "crypto": &CryptoSettings, } func init() { @@ -64,6 +65,7 @@ func Setup() { parseEnv(&CasdoorSettings, "CASDOOR_") parseEnv(&LogrotateSettings, "LOGROTATE_") parseEnv(&AuthSettings, "AUTH_") + parseEnv(&CryptoSettings, "CRYPTO_") // if in official docker, set the restart cmd of nginx to "nginx -s stop", // then the supervisor of s6-overlay will start the nginx again. From 802d05f692b18ef883b86f631ea0f55ffc2407a5 Mon Sep 17 00:00:00 2001 From: Jacky Date: Tue, 23 Jul 2024 17:33:22 +0800 Subject: [PATCH 2/8] chore: update translations --- app/src/language/LINGUAS | 2 +- app/src/language/en/app.po | 153 +++++++++++++++++++++++++-------- app/src/language/es/app.po | 154 ++++++++++++++++++++++++++-------- app/src/language/fr_FR/app.po | 154 ++++++++++++++++++++++++++-------- app/src/language/ko_KR/app.po | 154 ++++++++++++++++++++++++++-------- app/src/language/messages.pot | 150 +++++++++++++++++++++++++-------- app/src/language/ru_RU/app.po | 154 ++++++++++++++++++++++++++-------- app/src/language/vi_VN/app.po | 154 ++++++++++++++++++++++++++-------- app/src/language/zh_CN/app.mo | Bin 28545 -> 30358 bytes app/src/language/zh_CN/app.po | 152 +++++++++++++++++++++++++-------- app/src/language/zh_TW/app.po | 154 ++++++++++++++++++++++++++-------- 11 files changed, 1081 insertions(+), 300 deletions(-) diff --git a/app/src/language/LINGUAS b/app/src/language/LINGUAS index 052f2613d..1dc98da20 100644 --- a/app/src/language/LINGUAS +++ b/app/src/language/LINGUAS @@ -1 +1 @@ -es fr_FR ko_KR ru_RU vi_VN zh_CN zh_TW +en zh_CN zh_TW fr_FR es ru_RU vi_VN ko_KR \ No newline at end of file diff --git a/app/src/language/en/app.po b/app/src/language/en/app.po index 895a69355..f2f7349f4 100644 --- a/app/src/language/en/app.po +++ b/app/src/language/en/app.po @@ -9,7 +9,11 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: src/routes/index.ts:256 +#: src/views/preference/components/TOTP.vue:88 +msgid "2FA Settings" +msgstr "" + +#: src/routes/index.ts:261 msgid "About" msgstr "About" @@ -28,7 +32,7 @@ msgstr "Username" #: src/views/certificate/DNSCredential.vue:33 src/views/config/config.ts:34 #: src/views/domain/DomainList.vue:47 src/views/environment/envColumns.tsx:131 #: src/views/notification/Notification.vue:37 -#: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47 +#: src/views/preference/AuthSettings.vue:27 src/views/stream/StreamList.vue:47 #: src/views/user/User.vue:43 msgid "Action" msgstr "Action" @@ -95,7 +99,7 @@ msgstr "" msgid "Arch" msgstr "" -#: src/views/preference/AuthSettings.vue:94 +#: src/views/preference/AuthSettings.vue:95 #, fuzzy msgid "Are you sure to delete this banned IP immediately?" msgstr "Are you sure you want to remove this directive?" @@ -153,7 +157,7 @@ msgstr "" msgid "Assistant" msgstr "" -#: src/views/preference/AuthSettings.vue:17 +#: src/views/preference/AuthSettings.vue:18 msgid "Attempts" msgstr "" @@ -198,15 +202,15 @@ msgstr "Back" msgid "Back to list" msgstr "" -#: src/views/preference/AuthSettings.vue:68 +#: src/views/preference/AuthSettings.vue:69 msgid "Ban Threshold Minutes" msgstr "" -#: src/views/preference/AuthSettings.vue:82 +#: src/views/preference/AuthSettings.vue:83 msgid "Banned IPs" msgstr "" -#: src/views/preference/AuthSettings.vue:20 +#: src/views/preference/AuthSettings.vue:21 msgid "Banned Until" msgstr "" @@ -405,6 +409,14 @@ msgstr "" msgid "Credentials" msgstr "" +#: src/views/preference/components/TOTP.vue:96 +msgid "Current account is enabled 2FA." +msgstr "" + +#: src/views/preference/components/TOTP.vue:93 +msgid "Current account is not enabled 2FA." +msgstr "" + #: src/views/system/Upgrade.vue:167 msgid "Current Version" msgstr "" @@ -692,6 +704,16 @@ msgstr "" msgid "Enable %{conf_name} in %{node_name} successfully" msgstr "" +#: src/views/preference/components/TOTP.vue:122 +#, fuzzy +msgid "Enable 2FA" +msgstr "Enabled" + +#: src/views/preference/components/TOTP.vue:52 +#, fuzzy +msgid "Enable 2FA successfully" +msgstr "Enabled successfully" + #: src/views/domain/cert/components/ObtainCert.vue:70 msgid "Enable auto-renewal failed for %{name}" msgstr "Enable auto-renewal failed for %{name}" @@ -893,12 +915,18 @@ msgstr "" msgid "If left blank, the default CA Dir will be used." msgstr "" -#: src/views/preference/AuthSettings.vue:60 +#: src/views/preference/AuthSettings.vue:61 msgid "" "If the number of login failed attempts from a ip reach the max attempts in " "ban threshold minutes, the ip will be banned for a period of time." msgstr "" +#: src/views/preference/components/TOTP.vue:108 +msgid "" +"If you lose your mobile phone, you can use the recovery code to reset your " +"2FA." +msgstr "" + #: src/views/certificate/Certificate.vue:136 msgid "Import" msgstr "" @@ -908,7 +936,7 @@ msgstr "" msgid "Import Certificate" msgstr "Certificate Status" -#: src/views/other/Login.vue:59 +#: src/views/other/Login.vue:73 msgid "Incorrect username or password" msgstr "" @@ -924,7 +952,15 @@ msgstr "" msgid "Initialing core upgrader" msgstr "" -#: src/routes/index.ts:273 src/views/other/Install.vue:135 +#: src/views/preference/components/TOTP.vue:144 +msgid "Input the code from the app:" +msgstr "" + +#: src/views/other/Login.vue:194 src/views/preference/components/TOTP.vue:157 +msgid "Input the recovery code:" +msgstr "" + +#: src/routes/index.ts:283 src/views/other/Install.vue:135 msgid "Install" msgstr "Install" @@ -946,7 +982,11 @@ msgstr "" msgid "Invalid" msgstr "Invalid E-mail!" -#: src/views/preference/AuthSettings.vue:14 +#: src/views/other/Login.vue:83 +msgid "Invalid 2FA or recovery code" +msgstr "" + +#: src/views/preference/AuthSettings.vue:15 msgid "IP" msgstr "" @@ -1040,11 +1080,11 @@ msgstr "Locations" msgid "Log" msgstr "Login" -#: src/routes/index.ts:279 src/views/other/Login.vue:159 +#: src/routes/index.ts:289 src/views/other/Login.vue:218 msgid "Login" msgstr "Login" -#: src/views/other/Login.vue:109 src/views/other/Login.vue:51 +#: src/views/other/Login.vue:127 src/views/other/Login.vue:62 msgid "Login successful" msgstr "Login successful" @@ -1088,7 +1128,7 @@ msgstr "Manage Sites" msgid "Manage Streams" msgstr "Manage Sites" -#: src/routes/index.ts:230 src/views/user/User.vue:50 +#: src/routes/index.ts:235 src/views/user/User.vue:50 msgid "Manage Users" msgstr "Manage Users" @@ -1097,7 +1137,7 @@ msgstr "Manage Users" msgid "Managed Certificate" msgstr "Certificate is valid" -#: src/views/preference/AuthSettings.vue:74 +#: src/views/preference/AuthSettings.vue:75 msgid "Max Attempts" msgstr "" @@ -1231,7 +1271,7 @@ msgstr "Saved successfully" #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90 #: src/views/domain/ngx_conf/LocationEditor.vue:71 #: src/views/notification/Notification.vue:70 -#: src/views/preference/AuthSettings.vue:96 +#: src/views/preference/AuthSettings.vue:97 #: src/views/preference/BasicSettings.vue:101 #: src/views/stream/StreamList.vue:165 msgid "No" @@ -1245,7 +1285,7 @@ msgstr "" msgid "Not After" msgstr "" -#: src/routes/index.ts:285 +#: src/routes/index.ts:295 msgid "Not Found" msgstr "Not Found" @@ -1263,7 +1303,7 @@ msgstr "" msgid "Notification" msgstr "Certificate is valid" -#: src/components/Notification/Notification.vue:82 src/routes/index.ts:221 +#: src/components/Notification/Notification.vue:82 src/routes/index.ts:226 #, fuzzy msgid "Notifications" msgstr "Certificate is valid" @@ -1346,7 +1386,7 @@ msgstr "" msgid "Params" msgstr "Params" -#: src/views/other/Login.vue:144 src/views/user/User.vue:18 +#: src/views/other/Login.vue:167 src/views/user/User.vue:18 msgid "Password" msgstr "Password" @@ -1372,6 +1412,10 @@ msgstr "" msgid "Performing core upgrade" msgstr "" +#: src/views/other/Login.vue:177 +msgid "Please enter the 2FA code:" +msgstr "" + #: src/views/certificate/DNSCredential.vue:53 msgid "" "Please fill in the API authentication credentials provided by your DNS " @@ -1399,11 +1443,11 @@ msgstr "" msgid "Please input your E-mail!" msgstr "Please input your E-mail!" -#: src/views/other/Install.vue:45 src/views/other/Login.vue:39 +#: src/views/other/Install.vue:45 src/views/other/Login.vue:45 msgid "Please input your password!" msgstr "Please input your password!" -#: src/views/other/Install.vue:39 src/views/other/Login.vue:33 +#: src/views/other/Install.vue:39 src/views/other/Login.vue:39 msgid "Please input your username!" msgstr "Please input your username!" @@ -1423,7 +1467,7 @@ msgstr "" msgid "Pre-release" msgstr "" -#: src/routes/index.ts:239 src/views/preference/Preference.vue:105 +#: src/routes/index.ts:244 src/views/preference/Preference.vue:105 msgid "Preference" msgstr "" @@ -1467,6 +1511,18 @@ msgstr "" msgid "Recovered Successfully" msgstr "Saved successfully" +#: src/views/other/Login.vue:204 src/views/preference/components/TOTP.vue:167 +msgid "Recovery" +msgstr "" + +#: src/views/preference/components/TOTP.vue:101 +msgid "Recovery Code" +msgstr "" + +#: src/views/preference/components/TOTP.vue:110 +msgid "Recovery Code:" +msgstr "" + #: src/views/preference/BasicSettings.vue:68 msgid "Recursive Nameservers" msgstr "" @@ -1519,11 +1575,11 @@ msgstr "" msgid "Reloading nginx" msgstr "" -#: src/views/preference/AuthSettings.vue:101 +#: src/views/preference/AuthSettings.vue:102 msgid "Remove" msgstr "" -#: src/views/preference/AuthSettings.vue:47 +#: src/views/preference/AuthSettings.vue:48 #, fuzzy msgid "Remove successfully" msgstr "Saved successfully" @@ -1568,6 +1624,10 @@ msgstr "" msgid "Reset" msgstr "" +#: src/views/preference/components/TOTP.vue:130 +msgid "Reset 2FA" +msgstr "" + #: src/components/NginxControl/NginxControl.vue:93 msgid "Restart" msgstr "" @@ -1617,6 +1677,10 @@ msgstr "Saved successfully" msgid "Saved successfully" msgstr "Saved successfully" +#: src/views/preference/components/TOTP.vue:91 +msgid "Scan the QR code with your mobile phone to add the account to the app." +msgstr "" + #: src/views/certificate/DNSChallenge.vue:89 msgid "SDK" msgstr "" @@ -1640,7 +1704,9 @@ msgstr "Send" #: src/views/config/ConfigEdit.vue:40 src/views/domain/DomainList.vue:81 #: src/views/environment/BatchUpgrader.vue:57 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:69 -#: src/views/preference/AuthSettings.vue:49 +#: src/views/preference/AuthSettings.vue:50 +#: src/views/preference/components/TOTP.vue:42 +#: src/views/preference/components/TOTP.vue:55 #: src/views/preference/Preference.vue:78 src/views/stream/StreamList.vue:113 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42 msgid "Server error" @@ -1717,7 +1783,7 @@ msgstr "Certificate Status" msgid "SSL Certificate Path" msgstr "Certificate Status" -#: src/views/other/Login.vue:170 +#: src/views/other/Login.vue:229 #, fuzzy msgid "SSO Login" msgstr "Login" @@ -1802,7 +1868,7 @@ msgstr "Certificate is valid" msgid "Sync to" msgstr "" -#: src/routes/index.ts:248 +#: src/routes/index.ts:253 msgid "System" msgstr "" @@ -1856,6 +1922,11 @@ msgstr "Certificate Status" msgid "The path exists, but the file is not a private key" msgstr "" +#: src/views/preference/components/TOTP.vue:109 +msgid "" +"The recovery code is only displayed once, please save it in a safe place." +msgstr "" + #: src/views/dashboard/Environments.vue:148 msgid "" "The remote Nginx UI version is not compatible with the local Nginx UI " @@ -1915,7 +1986,7 @@ msgid "" "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}." msgstr "" -#: src/views/preference/AuthSettings.vue:59 +#: src/views/preference/AuthSettings.vue:60 #: src/views/preference/LogrotateSettings.vue:12 msgid "Tips" msgstr "" @@ -1924,6 +1995,12 @@ msgstr "" msgid "Title" msgstr "" +#: src/views/preference/components/TOTP.vue:90 +msgid "" +"To enable it, you need to install the Google or Microsoft Authenticator app " +"on your mobile phone." +msgstr "" + #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44 msgid "" "To make sure the certification auto-renewal can work normally, we need to " @@ -1936,10 +2013,16 @@ msgstr "" msgid "Token is not valid" msgstr "" -#: src/views/other/Login.vue:62 +#: src/views/other/Login.vue:76 msgid "Too many login failed attempts, please try again later" msgstr "" +#: src/views/preference/components/TOTP.vue:89 +msgid "" +"TOTP is a two-factor authentication method that uses a time-based one-time " +"password algorithm." +msgstr "" + #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:209 msgid "Trash" msgstr "" @@ -1964,7 +2047,7 @@ msgstr "Updated at" msgid "Updated successfully" msgstr "Saved successfully" -#: src/routes/index.ts:263 src/views/environment/Environment.vue:50 +#: src/routes/index.ts:268 src/views/environment/Environment.vue:50 #: src/views/system/Upgrade.vue:145 src/views/system/Upgrade.vue:228 msgid "Upgrade" msgstr "" @@ -1995,16 +2078,20 @@ msgstr "Uptime:" msgid "URL" msgstr "" +#: src/views/other/Login.vue:186 +msgid "Use recovery code" +msgstr "" + #: src/components/ChatGPT/ChatGPT.vue:229 #, fuzzy msgid "User" msgstr "Username" -#: src/views/other/Login.vue:65 +#: src/views/other/Login.vue:79 msgid "User is banned" msgstr "" -#: src/views/other/Login.vue:134 src/views/user/User.vue:9 +#: src/views/other/Login.vue:157 src/views/user/User.vue:9 msgid "Username" msgstr "Username" @@ -2073,7 +2160,7 @@ msgstr "" #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89 #: src/views/domain/ngx_conf/LocationEditor.vue:70 -#: src/views/preference/AuthSettings.vue:95 +#: src/views/preference/AuthSettings.vue:96 #: src/views/preference/BasicSettings.vue:100 msgid "Yes" msgstr "Yes" diff --git a/app/src/language/es/app.po b/app/src/language/es/app.po index 2fd7b9a27..2c1c78748 100644 --- a/app/src/language/es/app.po +++ b/app/src/language/es/app.po @@ -14,7 +14,11 @@ msgstr "" "Plural-Forms: nplurals=2; plural=n != 1;\n" "X-Generator: Weblate 5.3.1\n" -#: src/routes/index.ts:256 +#: src/views/preference/components/TOTP.vue:88 +msgid "2FA Settings" +msgstr "" + +#: src/routes/index.ts:261 msgid "About" msgstr "Acerca de" @@ -33,7 +37,7 @@ msgstr "Usuario" #: src/views/certificate/DNSCredential.vue:33 src/views/config/config.ts:34 #: src/views/domain/DomainList.vue:47 src/views/environment/envColumns.tsx:131 #: src/views/notification/Notification.vue:37 -#: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47 +#: src/views/preference/AuthSettings.vue:27 src/views/stream/StreamList.vue:47 #: src/views/user/User.vue:43 msgid "Action" msgstr "Acción" @@ -98,7 +102,7 @@ msgstr "Token de la API" msgid "Arch" msgstr "Arquitectura" -#: src/views/preference/AuthSettings.vue:94 +#: src/views/preference/AuthSettings.vue:95 #, fuzzy msgid "Are you sure to delete this banned IP immediately?" msgstr "¿Está seguro de que quiere borrar?" @@ -152,7 +156,7 @@ msgstr "Preguntar por ayuda a ChatGPT" msgid "Assistant" msgstr "Asistente" -#: src/views/preference/AuthSettings.vue:17 +#: src/views/preference/AuthSettings.vue:18 msgid "Attempts" msgstr "" @@ -197,15 +201,15 @@ msgstr "Volver al Inicio" msgid "Back to list" msgstr "" -#: src/views/preference/AuthSettings.vue:68 +#: src/views/preference/AuthSettings.vue:69 msgid "Ban Threshold Minutes" msgstr "" -#: src/views/preference/AuthSettings.vue:82 +#: src/views/preference/AuthSettings.vue:83 msgid "Banned IPs" msgstr "" -#: src/views/preference/AuthSettings.vue:20 +#: src/views/preference/AuthSettings.vue:21 msgid "Banned Until" msgstr "" @@ -397,6 +401,14 @@ msgstr "Credencial" msgid "Credentials" msgstr "Credenciales" +#: src/views/preference/components/TOTP.vue:96 +msgid "Current account is enabled 2FA." +msgstr "" + +#: src/views/preference/components/TOTP.vue:93 +msgid "Current account is not enabled 2FA." +msgstr "" + #: src/views/system/Upgrade.vue:167 msgid "Current Version" msgstr "Versión actual" @@ -668,6 +680,16 @@ msgstr "Falló el habilitado de %{conf_name} en %{node_name}" msgid "Enable %{conf_name} in %{node_name} successfully" msgstr "Habilitado exitoso de %{conf_name} en %{node_name}" +#: src/views/preference/components/TOTP.vue:122 +#, fuzzy +msgid "Enable 2FA" +msgstr "Habilitar" + +#: src/views/preference/components/TOTP.vue:52 +#, fuzzy +msgid "Enable 2FA successfully" +msgstr "Habilitado con Éxito" + #: src/views/domain/cert/components/ObtainCert.vue:70 msgid "Enable auto-renewal failed for %{name}" msgstr "No se pudo activar la renovación automática por %{name}" @@ -863,12 +885,18 @@ msgstr "HTTP01" msgid "If left blank, the default CA Dir will be used." msgstr "" -#: src/views/preference/AuthSettings.vue:60 +#: src/views/preference/AuthSettings.vue:61 msgid "" "If the number of login failed attempts from a ip reach the max attempts in " "ban threshold minutes, the ip will be banned for a period of time." msgstr "" +#: src/views/preference/components/TOTP.vue:108 +msgid "" +"If you lose your mobile phone, you can use the recovery code to reset your " +"2FA." +msgstr "" + #: src/views/certificate/Certificate.vue:136 msgid "Import" msgstr "Importar" @@ -877,7 +905,7 @@ msgstr "Importar" msgid "Import Certificate" msgstr "Importar Certificado" -#: src/views/other/Login.vue:59 +#: src/views/other/Login.vue:73 #, fuzzy msgid "Incorrect username or password" msgstr "El nombre de usuario o contraseña son incorrectos" @@ -894,7 +922,15 @@ msgstr "Error de actualización de kernel inicial" msgid "Initialing core upgrader" msgstr "Inicializando la actualización del kernel" -#: src/routes/index.ts:273 src/views/other/Install.vue:135 +#: src/views/preference/components/TOTP.vue:144 +msgid "Input the code from the app:" +msgstr "" + +#: src/views/other/Login.vue:194 src/views/preference/components/TOTP.vue:157 +msgid "Input the recovery code:" +msgstr "" + +#: src/routes/index.ts:283 src/views/other/Install.vue:135 msgid "Install" msgstr "Instalar" @@ -915,7 +951,11 @@ msgstr "" msgid "Invalid" msgstr "Válido" -#: src/views/preference/AuthSettings.vue:14 +#: src/views/other/Login.vue:83 +msgid "Invalid 2FA or recovery code" +msgstr "" + +#: src/views/preference/AuthSettings.vue:15 msgid "IP" msgstr "" @@ -1002,11 +1042,11 @@ msgstr "Ubicaciones" msgid "Log" msgstr "Registro" -#: src/routes/index.ts:279 src/views/other/Login.vue:159 +#: src/routes/index.ts:289 src/views/other/Login.vue:218 msgid "Login" msgstr "Acceso" -#: src/views/other/Login.vue:109 src/views/other/Login.vue:51 +#: src/views/other/Login.vue:127 src/views/other/Login.vue:62 msgid "Login successful" msgstr "Acceso exitoso" @@ -1048,7 +1088,7 @@ msgstr "Administrar sitios" msgid "Manage Streams" msgstr "Administrar Transmisiones" -#: src/routes/index.ts:230 src/views/user/User.vue:50 +#: src/routes/index.ts:235 src/views/user/User.vue:50 msgid "Manage Users" msgstr "Administrar usuarios" @@ -1056,7 +1096,7 @@ msgstr "Administrar usuarios" msgid "Managed Certificate" msgstr "Certificado Administrado" -#: src/views/preference/AuthSettings.vue:74 +#: src/views/preference/AuthSettings.vue:75 msgid "Max Attempts" msgstr "" @@ -1184,7 +1224,7 @@ msgstr "Nginx reiniciado con éxito" #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90 #: src/views/domain/ngx_conf/LocationEditor.vue:71 #: src/views/notification/Notification.vue:70 -#: src/views/preference/AuthSettings.vue:96 +#: src/views/preference/AuthSettings.vue:97 #: src/views/preference/BasicSettings.vue:101 #: src/views/stream/StreamList.vue:165 msgid "No" @@ -1198,7 +1238,7 @@ msgstr "Secreto del nodo" msgid "Not After" msgstr "No después de" -#: src/routes/index.ts:285 +#: src/routes/index.ts:295 msgid "Not Found" msgstr "No encontrado" @@ -1215,7 +1255,7 @@ msgstr "Nota" msgid "Notification" msgstr "Notificación" -#: src/components/Notification/Notification.vue:82 src/routes/index.ts:221 +#: src/components/Notification/Notification.vue:82 src/routes/index.ts:226 msgid "Notifications" msgstr "Notificaciones" @@ -1295,7 +1335,7 @@ msgstr "Sobrescribir archivo existente" msgid "Params" msgstr "Parámetros" -#: src/views/other/Login.vue:144 src/views/user/User.vue:18 +#: src/views/other/Login.vue:167 src/views/user/User.vue:18 msgid "Password" msgstr "Contraseña" @@ -1321,6 +1361,10 @@ msgstr "Error al ejecutar la actualización del kernel" msgid "Performing core upgrade" msgstr "Realizando la actualizaciónd el kernel" +#: src/views/other/Login.vue:177 +msgid "Please enter the 2FA code:" +msgstr "" + #: src/views/certificate/DNSCredential.vue:53 msgid "" "Please fill in the API authentication credentials provided by your DNS " @@ -1355,11 +1399,11 @@ msgstr "" msgid "Please input your E-mail!" msgstr "¡Por favor ingrese su correo electrónico!" -#: src/views/other/Install.vue:45 src/views/other/Login.vue:39 +#: src/views/other/Install.vue:45 src/views/other/Login.vue:45 msgid "Please input your password!" msgstr "¡Por favor ingrese su contraseña!" -#: src/views/other/Install.vue:39 src/views/other/Login.vue:33 +#: src/views/other/Install.vue:39 src/views/other/Login.vue:39 msgid "Please input your username!" msgstr "¡Por favor ingrese su nombre de usuario!" @@ -1381,7 +1425,7 @@ msgstr "¡Seleccione al menos un nodo!" msgid "Pre-release" msgstr "Prelanzamiento" -#: src/routes/index.ts:239 src/views/preference/Preference.vue:105 +#: src/routes/index.ts:244 src/views/preference/Preference.vue:105 msgid "Preference" msgstr "Configuración" @@ -1424,6 +1468,18 @@ msgstr "" msgid "Recovered Successfully" msgstr "Eliminado con éxito" +#: src/views/other/Login.vue:204 src/views/preference/components/TOTP.vue:167 +msgid "Recovery" +msgstr "" + +#: src/views/preference/components/TOTP.vue:101 +msgid "Recovery Code" +msgstr "" + +#: src/views/preference/components/TOTP.vue:110 +msgid "Recovery Code:" +msgstr "" + #: src/views/preference/BasicSettings.vue:68 msgid "Recursive Nameservers" msgstr "" @@ -1477,11 +1533,11 @@ msgstr "Recargando" msgid "Reloading nginx" msgstr "Recargando Nginx" -#: src/views/preference/AuthSettings.vue:101 +#: src/views/preference/AuthSettings.vue:102 msgid "Remove" msgstr "" -#: src/views/preference/AuthSettings.vue:47 +#: src/views/preference/AuthSettings.vue:48 #, fuzzy msgid "Remove successfully" msgstr "Eliminado con éxito" @@ -1520,6 +1576,11 @@ msgstr "Pedido con parámetros incorrectos" msgid "Reset" msgstr "Limpiar" +#: src/views/preference/components/TOTP.vue:130 +#, fuzzy +msgid "Reset 2FA" +msgstr "Limpiar" + #: src/components/NginxControl/NginxControl.vue:93 msgid "Restart" msgstr "Reiniciar" @@ -1567,6 +1628,10 @@ msgstr "Guardado con éxito" msgid "Saved successfully" msgstr "Guardado con éxito" +#: src/views/preference/components/TOTP.vue:91 +msgid "Scan the QR code with your mobile phone to add the account to the app." +msgstr "" + #: src/views/certificate/DNSChallenge.vue:89 msgid "SDK" msgstr "" @@ -1590,7 +1655,9 @@ msgstr "Enviado" #: src/views/config/ConfigEdit.vue:40 src/views/domain/DomainList.vue:81 #: src/views/environment/BatchUpgrader.vue:57 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:69 -#: src/views/preference/AuthSettings.vue:49 +#: src/views/preference/AuthSettings.vue:50 +#: src/views/preference/components/TOTP.vue:42 +#: src/views/preference/components/TOTP.vue:55 #: src/views/preference/Preference.vue:78 src/views/stream/StreamList.vue:113 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42 msgid "Server error" @@ -1662,7 +1729,7 @@ msgstr "Ruta de la llave del certificado SSL" msgid "SSL Certificate Path" msgstr "Ruta del certificado SSL" -#: src/views/other/Login.vue:170 +#: src/views/other/Login.vue:229 msgid "SSO Login" msgstr "Acceso SSO" @@ -1744,7 +1811,7 @@ msgstr "Renovado de Certificado exitoso" msgid "Sync to" msgstr "" -#: src/routes/index.ts:248 +#: src/routes/index.ts:253 msgid "System" msgstr "Sistema" @@ -1797,6 +1864,11 @@ msgstr "La ruta existe, pero el archivo no es una clave privada" msgid "The path exists, but the file is not a private key" msgstr "La ruta existe, pero el archivo no es una clave privada" +#: src/views/preference/components/TOTP.vue:109 +msgid "" +"The recovery code is only displayed once, please save it in a safe place." +msgstr "" + #: src/views/dashboard/Environments.vue:148 msgid "" "The remote Nginx UI version is not compatible with the local Nginx UI " @@ -1857,7 +1929,7 @@ msgid "" "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}." msgstr "" -#: src/views/preference/AuthSettings.vue:59 +#: src/views/preference/AuthSettings.vue:60 #: src/views/preference/LogrotateSettings.vue:12 msgid "Tips" msgstr "" @@ -1866,6 +1938,12 @@ msgstr "" msgid "Title" msgstr "Título" +#: src/views/preference/components/TOTP.vue:90 +msgid "" +"To enable it, you need to install the Google or Microsoft Authenticator app " +"on your mobile phone." +msgstr "" + #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44 msgid "" "To make sure the certification auto-renewal can work normally, we need to " @@ -1882,10 +1960,16 @@ msgstr "" msgid "Token is not valid" msgstr "El token no es válido" -#: src/views/other/Login.vue:62 +#: src/views/other/Login.vue:76 msgid "Too many login failed attempts, please try again later" msgstr "" +#: src/views/preference/components/TOTP.vue:89 +msgid "" +"TOTP is a two-factor authentication method that uses a time-based one-time " +"password algorithm." +msgstr "" + #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:209 msgid "Trash" msgstr "" @@ -1909,7 +1993,7 @@ msgstr "Actualizado a" msgid "Updated successfully" msgstr "Actualización exitosa" -#: src/routes/index.ts:263 src/views/environment/Environment.vue:50 +#: src/routes/index.ts:268 src/views/environment/Environment.vue:50 #: src/views/system/Upgrade.vue:145 src/views/system/Upgrade.vue:228 msgid "Upgrade" msgstr "Actualizar" @@ -1939,15 +2023,19 @@ msgstr "Tiempo encendido:" msgid "URL" msgstr "URL" +#: src/views/other/Login.vue:186 +msgid "Use recovery code" +msgstr "" + #: src/components/ChatGPT/ChatGPT.vue:229 msgid "User" msgstr "Usuario" -#: src/views/other/Login.vue:65 +#: src/views/other/Login.vue:79 msgid "User is banned" msgstr "" -#: src/views/other/Login.vue:134 src/views/user/User.vue:9 +#: src/views/other/Login.vue:157 src/views/user/User.vue:9 msgid "Username" msgstr "Nombre de usuario" @@ -2021,7 +2109,7 @@ msgstr "Escribir certificado a disco" #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89 #: src/views/domain/ngx_conf/LocationEditor.vue:70 -#: src/views/preference/AuthSettings.vue:95 +#: src/views/preference/AuthSettings.vue:96 #: src/views/preference/BasicSettings.vue:100 msgid "Yes" msgstr "Si" diff --git a/app/src/language/fr_FR/app.po b/app/src/language/fr_FR/app.po index 33933d39a..f0a58f61c 100644 --- a/app/src/language/fr_FR/app.po +++ b/app/src/language/fr_FR/app.po @@ -11,7 +11,11 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 3.3\n" -#: src/routes/index.ts:256 +#: src/views/preference/components/TOTP.vue:88 +msgid "2FA Settings" +msgstr "" + +#: src/routes/index.ts:261 msgid "About" msgstr "À propos" @@ -30,7 +34,7 @@ msgstr "Nom d'utilisateur" #: src/views/certificate/DNSCredential.vue:33 src/views/config/config.ts:34 #: src/views/domain/DomainList.vue:47 src/views/environment/envColumns.tsx:131 #: src/views/notification/Notification.vue:37 -#: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47 +#: src/views/preference/AuthSettings.vue:27 src/views/stream/StreamList.vue:47 #: src/views/user/User.vue:43 msgid "Action" msgstr "Action" @@ -99,7 +103,7 @@ msgstr "Jeton d'API" msgid "Arch" msgstr "Arch" -#: src/views/preference/AuthSettings.vue:94 +#: src/views/preference/AuthSettings.vue:95 #, fuzzy msgid "Are you sure to delete this banned IP immediately?" msgstr "Etes-vous sûr que vous voulez supprimer ?" @@ -155,7 +159,7 @@ msgstr "Modèle ChatGPT" msgid "Assistant" msgstr "" -#: src/views/preference/AuthSettings.vue:17 +#: src/views/preference/AuthSettings.vue:18 msgid "Attempts" msgstr "" @@ -200,15 +204,15 @@ msgstr "Retour au menu principal" msgid "Back to list" msgstr "" -#: src/views/preference/AuthSettings.vue:68 +#: src/views/preference/AuthSettings.vue:69 msgid "Ban Threshold Minutes" msgstr "" -#: src/views/preference/AuthSettings.vue:82 +#: src/views/preference/AuthSettings.vue:83 msgid "Banned IPs" msgstr "" -#: src/views/preference/AuthSettings.vue:20 +#: src/views/preference/AuthSettings.vue:21 msgid "Banned Until" msgstr "" @@ -405,6 +409,14 @@ msgstr "Identifiant" msgid "Credentials" msgstr "Identifiants" +#: src/views/preference/components/TOTP.vue:96 +msgid "Current account is enabled 2FA." +msgstr "" + +#: src/views/preference/components/TOTP.vue:93 +msgid "Current account is not enabled 2FA." +msgstr "" + #: src/views/system/Upgrade.vue:167 msgid "Current Version" msgstr "Version actuelle" @@ -692,6 +704,16 @@ msgstr "" msgid "Enable %{conf_name} in %{node_name} successfully" msgstr "" +#: src/views/preference/components/TOTP.vue:122 +#, fuzzy +msgid "Enable 2FA" +msgstr "Activé" + +#: src/views/preference/components/TOTP.vue:52 +#, fuzzy +msgid "Enable 2FA successfully" +msgstr "Activé avec succès" + #: src/views/domain/cert/components/ObtainCert.vue:70 msgid "Enable auto-renewal failed for %{name}" msgstr "Échec de l'activation du renouvellement automatique pour %{name}" @@ -894,12 +916,18 @@ msgstr "HTTP01" msgid "If left blank, the default CA Dir will be used." msgstr "" -#: src/views/preference/AuthSettings.vue:60 +#: src/views/preference/AuthSettings.vue:61 msgid "" "If the number of login failed attempts from a ip reach the max attempts in " "ban threshold minutes, the ip will be banned for a period of time." msgstr "" +#: src/views/preference/components/TOTP.vue:108 +msgid "" +"If you lose your mobile phone, you can use the recovery code to reset your " +"2FA." +msgstr "" + #: src/views/certificate/Certificate.vue:136 #, fuzzy msgid "Import" @@ -910,7 +938,7 @@ msgstr "Exporter" msgid "Import Certificate" msgstr "État du certificat" -#: src/views/other/Login.vue:59 +#: src/views/other/Login.vue:73 #, fuzzy msgid "Incorrect username or password" msgstr "Le pseudo ou mot de passe est incorect" @@ -927,7 +955,15 @@ msgstr "Erreur du programme de mise à niveau initial du core" msgid "Initialing core upgrader" msgstr "Initialisation du programme de mise à niveau du core" -#: src/routes/index.ts:273 src/views/other/Install.vue:135 +#: src/views/preference/components/TOTP.vue:144 +msgid "Input the code from the app:" +msgstr "" + +#: src/views/other/Login.vue:194 src/views/preference/components/TOTP.vue:157 +msgid "Input the recovery code:" +msgstr "" + +#: src/routes/index.ts:283 src/views/other/Install.vue:135 msgid "Install" msgstr "Installer" @@ -947,7 +983,11 @@ msgstr "" msgid "Invalid" msgstr "" -#: src/views/preference/AuthSettings.vue:14 +#: src/views/other/Login.vue:83 +msgid "Invalid 2FA or recovery code" +msgstr "" + +#: src/views/preference/AuthSettings.vue:15 msgid "IP" msgstr "" @@ -1043,11 +1083,11 @@ msgstr "Localisations" msgid "Log" msgstr "Connexion" -#: src/routes/index.ts:279 src/views/other/Login.vue:159 +#: src/routes/index.ts:289 src/views/other/Login.vue:218 msgid "Login" msgstr "Connexion" -#: src/views/other/Login.vue:109 src/views/other/Login.vue:51 +#: src/views/other/Login.vue:127 src/views/other/Login.vue:62 msgid "Login successful" msgstr "Connexion réussie" @@ -1091,7 +1131,7 @@ msgstr "Gérer les sites" msgid "Manage Streams" msgstr "Gérer les sites" -#: src/routes/index.ts:230 src/views/user/User.vue:50 +#: src/routes/index.ts:235 src/views/user/User.vue:50 msgid "Manage Users" msgstr "Gérer les utilisateurs" @@ -1100,7 +1140,7 @@ msgstr "Gérer les utilisateurs" msgid "Managed Certificate" msgstr "Changer de certificat" -#: src/views/preference/AuthSettings.vue:74 +#: src/views/preference/AuthSettings.vue:75 msgid "Max Attempts" msgstr "" @@ -1230,7 +1270,7 @@ msgstr "Nginx a redémarré avec succès" #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90 #: src/views/domain/ngx_conf/LocationEditor.vue:71 #: src/views/notification/Notification.vue:70 -#: src/views/preference/AuthSettings.vue:96 +#: src/views/preference/AuthSettings.vue:97 #: src/views/preference/BasicSettings.vue:101 #: src/views/stream/StreamList.vue:165 msgid "No" @@ -1245,7 +1285,7 @@ msgstr "Secret Jwt" msgid "Not After" msgstr "" -#: src/routes/index.ts:285 +#: src/routes/index.ts:295 msgid "Not Found" msgstr "Introuvable" @@ -1263,7 +1303,7 @@ msgstr "Note" msgid "Notification" msgstr "Certification" -#: src/components/Notification/Notification.vue:82 src/routes/index.ts:221 +#: src/components/Notification/Notification.vue:82 src/routes/index.ts:226 #, fuzzy msgid "Notifications" msgstr "Certification" @@ -1344,7 +1384,7 @@ msgstr "" msgid "Params" msgstr "Paramètres" -#: src/views/other/Login.vue:144 src/views/user/User.vue:18 +#: src/views/other/Login.vue:167 src/views/user/User.vue:18 msgid "Password" msgstr "Mot de passe" @@ -1370,6 +1410,10 @@ msgstr "Erreur lors de la mise a niveau du core" msgid "Performing core upgrade" msgstr "Exécution de la mise à niveau du core" +#: src/views/other/Login.vue:177 +msgid "Please enter the 2FA code:" +msgstr "" + #: src/views/certificate/DNSCredential.vue:53 msgid "" "Please fill in the API authentication credentials provided by your DNS " @@ -1404,11 +1448,11 @@ msgstr "" msgid "Please input your E-mail!" msgstr "Veuillez saisir votre e-mail !" -#: src/views/other/Install.vue:45 src/views/other/Login.vue:39 +#: src/views/other/Install.vue:45 src/views/other/Login.vue:45 msgid "Please input your password!" msgstr "Veuillez saisir votre mot de passe !" -#: src/views/other/Install.vue:39 src/views/other/Login.vue:33 +#: src/views/other/Install.vue:39 src/views/other/Login.vue:39 msgid "Please input your username!" msgstr "Veuillez saisir votre nom d'utilisateur !" @@ -1428,7 +1472,7 @@ msgstr "" msgid "Pre-release" msgstr "" -#: src/routes/index.ts:239 src/views/preference/Preference.vue:105 +#: src/routes/index.ts:244 src/views/preference/Preference.vue:105 msgid "Preference" msgstr "Préférence" @@ -1474,6 +1518,18 @@ msgstr "" msgid "Recovered Successfully" msgstr "Enregistré avec succès" +#: src/views/other/Login.vue:204 src/views/preference/components/TOTP.vue:167 +msgid "Recovery" +msgstr "" + +#: src/views/preference/components/TOTP.vue:101 +msgid "Recovery Code" +msgstr "" + +#: src/views/preference/components/TOTP.vue:110 +msgid "Recovery Code:" +msgstr "" + #: src/views/preference/BasicSettings.vue:68 msgid "Recursive Nameservers" msgstr "" @@ -1527,11 +1583,11 @@ msgstr "Rechargement" msgid "Reloading nginx" msgstr "Rechargement de nginx" -#: src/views/preference/AuthSettings.vue:101 +#: src/views/preference/AuthSettings.vue:102 msgid "Remove" msgstr "" -#: src/views/preference/AuthSettings.vue:47 +#: src/views/preference/AuthSettings.vue:48 #, fuzzy msgid "Remove successfully" msgstr "Enregistré avec succès" @@ -1576,6 +1632,11 @@ msgstr "" msgid "Reset" msgstr "Réinitialiser" +#: src/views/preference/components/TOTP.vue:130 +#, fuzzy +msgid "Reset 2FA" +msgstr "Réinitialiser" + #: src/components/NginxControl/NginxControl.vue:93 msgid "Restart" msgstr "Redémarrer" @@ -1623,6 +1684,10 @@ msgstr "Sauvegarde réussie" msgid "Saved successfully" msgstr "Enregistré avec succès" +#: src/views/preference/components/TOTP.vue:91 +msgid "Scan the QR code with your mobile phone to add the account to the app." +msgstr "" + #: src/views/certificate/DNSChallenge.vue:89 msgid "SDK" msgstr "" @@ -1646,7 +1711,9 @@ msgstr "Envoyer" #: src/views/config/ConfigEdit.vue:40 src/views/domain/DomainList.vue:81 #: src/views/environment/BatchUpgrader.vue:57 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:69 -#: src/views/preference/AuthSettings.vue:49 +#: src/views/preference/AuthSettings.vue:50 +#: src/views/preference/components/TOTP.vue:42 +#: src/views/preference/components/TOTP.vue:55 #: src/views/preference/Preference.vue:78 src/views/stream/StreamList.vue:113 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42 msgid "Server error" @@ -1722,7 +1789,7 @@ msgstr "Chemin de la clé du certificat SSL" msgid "SSL Certificate Path" msgstr "Chemin du certificat SSL" -#: src/views/other/Login.vue:170 +#: src/views/other/Login.vue:229 #, fuzzy msgid "SSO Login" msgstr "Connexion" @@ -1808,7 +1875,7 @@ msgstr "Changer de certificat" msgid "Sync to" msgstr "" -#: src/routes/index.ts:248 +#: src/routes/index.ts:253 msgid "System" msgstr "Système" @@ -1862,6 +1929,11 @@ msgstr "Chemin de la clé du certificat SSL" msgid "The path exists, but the file is not a private key" msgstr "" +#: src/views/preference/components/TOTP.vue:109 +msgid "" +"The recovery code is only displayed once, please save it in a safe place." +msgstr "" + #: src/views/dashboard/Environments.vue:148 msgid "" "The remote Nginx UI version is not compatible with the local Nginx UI " @@ -1925,7 +1997,7 @@ msgid "" "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}." msgstr "" -#: src/views/preference/AuthSettings.vue:59 +#: src/views/preference/AuthSettings.vue:60 #: src/views/preference/LogrotateSettings.vue:12 msgid "Tips" msgstr "" @@ -1934,6 +2006,12 @@ msgstr "" msgid "Title" msgstr "" +#: src/views/preference/components/TOTP.vue:90 +msgid "" +"To enable it, you need to install the Google or Microsoft Authenticator app " +"on your mobile phone." +msgstr "" + #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44 msgid "" "To make sure the certification auto-renewal can work normally, we need to " @@ -1950,10 +2028,16 @@ msgstr "" msgid "Token is not valid" msgstr "" -#: src/views/other/Login.vue:62 +#: src/views/other/Login.vue:76 msgid "Too many login failed attempts, please try again later" msgstr "" +#: src/views/preference/components/TOTP.vue:89 +msgid "" +"TOTP is a two-factor authentication method that uses a time-based one-time " +"password algorithm." +msgstr "" + #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:209 msgid "Trash" msgstr "" @@ -1977,7 +2061,7 @@ msgstr "Mis à jour le" msgid "Updated successfully" msgstr "Mis à jour avec succés" -#: src/routes/index.ts:263 src/views/environment/Environment.vue:50 +#: src/routes/index.ts:268 src/views/environment/Environment.vue:50 #: src/views/system/Upgrade.vue:145 src/views/system/Upgrade.vue:228 msgid "Upgrade" msgstr "Mettre à niveau" @@ -2007,16 +2091,20 @@ msgstr "Disponibilité :" msgid "URL" msgstr "" +#: src/views/other/Login.vue:186 +msgid "Use recovery code" +msgstr "" + #: src/components/ChatGPT/ChatGPT.vue:229 #, fuzzy msgid "User" msgstr "Nom d'utilisateur" -#: src/views/other/Login.vue:65 +#: src/views/other/Login.vue:79 msgid "User is banned" msgstr "" -#: src/views/other/Login.vue:134 src/views/user/User.vue:9 +#: src/views/other/Login.vue:157 src/views/user/User.vue:9 msgid "Username" msgstr "Nom d'utilisateur" @@ -2088,7 +2176,7 @@ msgstr "Écriture du certificat sur le disque" #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89 #: src/views/domain/ngx_conf/LocationEditor.vue:70 -#: src/views/preference/AuthSettings.vue:95 +#: src/views/preference/AuthSettings.vue:96 #: src/views/preference/BasicSettings.vue:100 msgid "Yes" msgstr "Oui" diff --git a/app/src/language/ko_KR/app.po b/app/src/language/ko_KR/app.po index 2afc96719..7bd475846 100644 --- a/app/src/language/ko_KR/app.po +++ b/app/src/language/ko_KR/app.po @@ -13,7 +13,11 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Weblate 5.3.1\n" -#: src/routes/index.ts:256 +#: src/views/preference/components/TOTP.vue:88 +msgid "2FA Settings" +msgstr "" + +#: src/routes/index.ts:261 msgid "About" msgstr "소개" @@ -32,7 +36,7 @@ msgstr "사용자 이름" #: src/views/certificate/DNSCredential.vue:33 src/views/config/config.ts:34 #: src/views/domain/DomainList.vue:47 src/views/environment/envColumns.tsx:131 #: src/views/notification/Notification.vue:37 -#: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47 +#: src/views/preference/AuthSettings.vue:27 src/views/stream/StreamList.vue:47 #: src/views/user/User.vue:43 msgid "Action" msgstr "작업" @@ -97,7 +101,7 @@ msgstr "API 토큰" msgid "Arch" msgstr "아키텍처" -#: src/views/preference/AuthSettings.vue:94 +#: src/views/preference/AuthSettings.vue:95 #, fuzzy msgid "Are you sure to delete this banned IP immediately?" msgstr "정말 삭제하시겠습니까?" @@ -151,7 +155,7 @@ msgstr "ChatGPT에게 도움 요청" msgid "Assistant" msgstr "조수" -#: src/views/preference/AuthSettings.vue:17 +#: src/views/preference/AuthSettings.vue:18 msgid "Attempts" msgstr "" @@ -196,15 +200,15 @@ msgstr "홈으로" msgid "Back to list" msgstr "" -#: src/views/preference/AuthSettings.vue:68 +#: src/views/preference/AuthSettings.vue:69 msgid "Ban Threshold Minutes" msgstr "" -#: src/views/preference/AuthSettings.vue:82 +#: src/views/preference/AuthSettings.vue:83 msgid "Banned IPs" msgstr "" -#: src/views/preference/AuthSettings.vue:20 +#: src/views/preference/AuthSettings.vue:21 msgid "Banned Until" msgstr "" @@ -395,6 +399,14 @@ msgstr "인증 정보" msgid "Credentials" msgstr "인증 정보들" +#: src/views/preference/components/TOTP.vue:96 +msgid "Current account is enabled 2FA." +msgstr "" + +#: src/views/preference/components/TOTP.vue:93 +msgid "Current account is not enabled 2FA." +msgstr "" + #: src/views/system/Upgrade.vue:167 msgid "Current Version" msgstr "현재 버전" @@ -666,6 +678,16 @@ msgstr "%{node_name}에서 %{conf_name} 활성화 실패" msgid "Enable %{conf_name} in %{node_name} successfully" msgstr "%{node_name}에서 %{conf_name} 성공적으로 활성화됨" +#: src/views/preference/components/TOTP.vue:122 +#, fuzzy +msgid "Enable 2FA" +msgstr "활성화" + +#: src/views/preference/components/TOTP.vue:52 +#, fuzzy +msgid "Enable 2FA successfully" +msgstr "성공적으로 활성화" + #: src/views/domain/cert/components/ObtainCert.vue:70 msgid "Enable auto-renewal failed for %{name}" msgstr "%{name}에 대한 자동 갱신 활성화 실패" @@ -866,12 +888,18 @@ msgstr "HTTP01" msgid "If left blank, the default CA Dir will be used." msgstr "" -#: src/views/preference/AuthSettings.vue:60 +#: src/views/preference/AuthSettings.vue:61 msgid "" "If the number of login failed attempts from a ip reach the max attempts in " "ban threshold minutes, the ip will be banned for a period of time." msgstr "" +#: src/views/preference/components/TOTP.vue:108 +msgid "" +"If you lose your mobile phone, you can use the recovery code to reset your " +"2FA." +msgstr "" + #: src/views/certificate/Certificate.vue:136 msgid "Import" msgstr "가져오기" @@ -881,7 +909,7 @@ msgstr "가져오기" msgid "Import Certificate" msgstr "인증서 상태" -#: src/views/other/Login.vue:59 +#: src/views/other/Login.vue:73 #, fuzzy msgid "Incorrect username or password" msgstr "사용자 이름 또는 비밀번호가 올바르지 않습니다" @@ -898,7 +926,15 @@ msgstr "초기 코어 업그레이더 오류" msgid "Initialing core upgrader" msgstr "코어 업그레이더 초기화" -#: src/routes/index.ts:273 src/views/other/Install.vue:135 +#: src/views/preference/components/TOTP.vue:144 +msgid "Input the code from the app:" +msgstr "" + +#: src/views/other/Login.vue:194 src/views/preference/components/TOTP.vue:157 +msgid "Input the recovery code:" +msgstr "" + +#: src/routes/index.ts:283 src/views/other/Install.vue:135 msgid "Install" msgstr "설치" @@ -920,7 +956,11 @@ msgstr "간격" msgid "Invalid" msgstr "유효함" -#: src/views/preference/AuthSettings.vue:14 +#: src/views/other/Login.vue:83 +msgid "Invalid 2FA or recovery code" +msgstr "" + +#: src/views/preference/AuthSettings.vue:15 msgid "IP" msgstr "" @@ -1014,11 +1054,11 @@ msgstr "위치들" msgid "Log" msgstr "로그인" -#: src/routes/index.ts:279 src/views/other/Login.vue:159 +#: src/routes/index.ts:289 src/views/other/Login.vue:218 msgid "Login" msgstr "로그인" -#: src/views/other/Login.vue:109 src/views/other/Login.vue:51 +#: src/views/other/Login.vue:127 src/views/other/Login.vue:62 msgid "Login successful" msgstr "로그인 성공" @@ -1067,7 +1107,7 @@ msgstr "사이트 관리" msgid "Manage Streams" msgstr "스트림 관리" -#: src/routes/index.ts:230 src/views/user/User.vue:50 +#: src/routes/index.ts:235 src/views/user/User.vue:50 msgid "Manage Users" msgstr "사용자 관리" @@ -1076,7 +1116,7 @@ msgstr "사용자 관리" msgid "Managed Certificate" msgstr "인증서 유효" -#: src/views/preference/AuthSettings.vue:74 +#: src/views/preference/AuthSettings.vue:75 msgid "Max Attempts" msgstr "" @@ -1210,7 +1250,7 @@ msgstr "Nginx가 성공적으로 재시작됨" #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90 #: src/views/domain/ngx_conf/LocationEditor.vue:71 #: src/views/notification/Notification.vue:70 -#: src/views/preference/AuthSettings.vue:96 +#: src/views/preference/AuthSettings.vue:97 #: src/views/preference/BasicSettings.vue:101 #: src/views/stream/StreamList.vue:165 msgid "No" @@ -1224,7 +1264,7 @@ msgstr "노드 시크릿" msgid "Not After" msgstr "만료일" -#: src/routes/index.ts:285 +#: src/routes/index.ts:295 msgid "Not Found" msgstr "찾을 수 없음" @@ -1242,7 +1282,7 @@ msgstr "참고" msgid "Notification" msgstr "알림" -#: src/components/Notification/Notification.vue:82 src/routes/index.ts:221 +#: src/components/Notification/Notification.vue:82 src/routes/index.ts:226 #, fuzzy msgid "Notifications" msgstr "알림" @@ -1325,7 +1365,7 @@ msgstr "기존 파일 덮어쓰기" msgid "Params" msgstr "파라미터" -#: src/views/other/Login.vue:144 src/views/user/User.vue:18 +#: src/views/other/Login.vue:167 src/views/user/User.vue:18 msgid "Password" msgstr "비밀번호" @@ -1351,6 +1391,10 @@ msgstr "핵심 업그레이드 오류 수행" msgid "Performing core upgrade" msgstr "핵심 업그레이드 수행 중" +#: src/views/other/Login.vue:177 +msgid "Please enter the 2FA code:" +msgstr "" + #: src/views/certificate/DNSCredential.vue:53 msgid "" "Please fill in the API authentication credentials provided by your DNS " @@ -1380,11 +1424,11 @@ msgstr "이름을 입력해주세요, 이것은 새 구성의 파일 이름으 msgid "Please input your E-mail!" msgstr "이메일을 입력해주세요!" -#: src/views/other/Install.vue:45 src/views/other/Login.vue:39 +#: src/views/other/Install.vue:45 src/views/other/Login.vue:45 msgid "Please input your password!" msgstr "비밀번호를 입력해주세요!" -#: src/views/other/Install.vue:39 src/views/other/Login.vue:33 +#: src/views/other/Install.vue:39 src/views/other/Login.vue:39 msgid "Please input your username!" msgstr "사용자 이름을 입력해주세요!" @@ -1404,7 +1448,7 @@ msgstr "적어도 하나의 노드를 선택해주세요!" msgid "Pre-release" msgstr "사전 출시" -#: src/routes/index.ts:239 src/views/preference/Preference.vue:105 +#: src/routes/index.ts:244 src/views/preference/Preference.vue:105 msgid "Preference" msgstr "환경설정" @@ -1448,6 +1492,18 @@ msgstr "" msgid "Recovered Successfully" msgstr "성공적으로 제거됨" +#: src/views/other/Login.vue:204 src/views/preference/components/TOTP.vue:167 +msgid "Recovery" +msgstr "" + +#: src/views/preference/components/TOTP.vue:101 +msgid "Recovery Code" +msgstr "" + +#: src/views/preference/components/TOTP.vue:110 +msgid "Recovery Code:" +msgstr "" + #: src/views/preference/BasicSettings.vue:68 msgid "Recursive Nameservers" msgstr "" @@ -1502,11 +1558,11 @@ msgstr "리로딩 중" msgid "Reloading nginx" msgstr "Nginx 리로딩 중" -#: src/views/preference/AuthSettings.vue:101 +#: src/views/preference/AuthSettings.vue:102 msgid "Remove" msgstr "" -#: src/views/preference/AuthSettings.vue:47 +#: src/views/preference/AuthSettings.vue:48 #, fuzzy msgid "Remove successfully" msgstr "성공적으로 제거됨" @@ -1551,6 +1607,11 @@ msgstr "잘못된 매개변수로 요청됨" msgid "Reset" msgstr "재설정" +#: src/views/preference/components/TOTP.vue:130 +#, fuzzy +msgid "Reset 2FA" +msgstr "재설정" + #: src/components/NginxControl/NginxControl.vue:93 msgid "Restart" msgstr "재시작" @@ -1600,6 +1661,10 @@ msgstr "성공적으로 저장됨" msgid "Saved successfully" msgstr "성공적으로 저장됨" +#: src/views/preference/components/TOTP.vue:91 +msgid "Scan the QR code with your mobile phone to add the account to the app." +msgstr "" + #: src/views/certificate/DNSChallenge.vue:89 msgid "SDK" msgstr "" @@ -1623,7 +1688,9 @@ msgstr "보내기" #: src/views/config/ConfigEdit.vue:40 src/views/domain/DomainList.vue:81 #: src/views/environment/BatchUpgrader.vue:57 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:69 -#: src/views/preference/AuthSettings.vue:49 +#: src/views/preference/AuthSettings.vue:50 +#: src/views/preference/components/TOTP.vue:42 +#: src/views/preference/components/TOTP.vue:55 #: src/views/preference/Preference.vue:78 src/views/stream/StreamList.vue:113 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42 msgid "Server error" @@ -1699,7 +1766,7 @@ msgstr "SSL 인증서 키 경로" msgid "SSL Certificate Path" msgstr "SSL 인증서 경로" -#: src/views/other/Login.vue:170 +#: src/views/other/Login.vue:229 #, fuzzy msgid "SSO Login" msgstr "SSO 로그인" @@ -1784,7 +1851,7 @@ msgstr "인증서 갱신 성공" msgid "Sync to" msgstr "" -#: src/routes/index.ts:248 +#: src/routes/index.ts:253 msgid "System" msgstr "시스템" @@ -1838,6 +1905,11 @@ msgstr "Certificate Status" msgid "The path exists, but the file is not a private key" msgstr "경로는 존재하지만 파일은 개인 키가 아닙니다" +#: src/views/preference/components/TOTP.vue:109 +msgid "" +"The recovery code is only displayed once, please save it in a safe place." +msgstr "" + #: src/views/dashboard/Environments.vue:148 msgid "" "The remote Nginx UI version is not compatible with the local Nginx UI " @@ -1899,7 +1971,7 @@ msgid "" "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}." msgstr "" -#: src/views/preference/AuthSettings.vue:59 +#: src/views/preference/AuthSettings.vue:60 #: src/views/preference/LogrotateSettings.vue:12 msgid "Tips" msgstr "팁" @@ -1908,6 +1980,12 @@ msgstr "팁" msgid "Title" msgstr "제목" +#: src/views/preference/components/TOTP.vue:90 +msgid "" +"To enable it, you need to install the Google or Microsoft Authenticator app " +"on your mobile phone." +msgstr "" + #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44 msgid "" "To make sure the certification auto-renewal can work normally, we need to " @@ -1923,10 +2001,16 @@ msgstr "" msgid "Token is not valid" msgstr "토큰이 유효하지 않습니다" -#: src/views/other/Login.vue:62 +#: src/views/other/Login.vue:76 msgid "Too many login failed attempts, please try again later" msgstr "" +#: src/views/preference/components/TOTP.vue:89 +msgid "" +"TOTP is a two-factor authentication method that uses a time-based one-time " +"password algorithm." +msgstr "" + #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:209 msgid "Trash" msgstr "" @@ -1951,7 +2035,7 @@ msgstr "업데이트됨" msgid "Updated successfully" msgstr "성공적으로 저장되었습니다" -#: src/routes/index.ts:263 src/views/environment/Environment.vue:50 +#: src/routes/index.ts:268 src/views/environment/Environment.vue:50 #: src/views/system/Upgrade.vue:145 src/views/system/Upgrade.vue:228 msgid "Upgrade" msgstr "업그레이드" @@ -1982,16 +2066,20 @@ msgstr "가동 시간:" msgid "URL" msgstr "URL" +#: src/views/other/Login.vue:186 +msgid "Use recovery code" +msgstr "" + #: src/components/ChatGPT/ChatGPT.vue:229 #, fuzzy msgid "User" msgstr "사용자 이름" -#: src/views/other/Login.vue:65 +#: src/views/other/Login.vue:79 msgid "User is banned" msgstr "" -#: src/views/other/Login.vue:134 src/views/user/User.vue:9 +#: src/views/other/Login.vue:157 src/views/user/User.vue:9 msgid "Username" msgstr "사용자 이름" @@ -2066,7 +2154,7 @@ msgstr "인증서를 디스크에 쓰기" #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89 #: src/views/domain/ngx_conf/LocationEditor.vue:70 -#: src/views/preference/AuthSettings.vue:95 +#: src/views/preference/AuthSettings.vue:96 #: src/views/preference/BasicSettings.vue:100 msgid "Yes" msgstr "예" diff --git a/app/src/language/messages.pot b/app/src/language/messages.pot index 97f442fec..b0ebf051e 100644 --- a/app/src/language/messages.pot +++ b/app/src/language/messages.pot @@ -2,7 +2,11 @@ msgid "" msgstr "" "Content-Type: text/plain; charset=UTF-8\n" -#: src/routes/index.ts:256 +#: src/views/preference/components/TOTP.vue:88 +msgid "2FA Settings" +msgstr "" + +#: src/routes/index.ts:261 msgid "About" msgstr "" @@ -24,7 +28,7 @@ msgstr "" #: src/views/domain/DomainList.vue:47 #: src/views/environment/envColumns.tsx:131 #: src/views/notification/Notification.vue:37 -#: src/views/preference/AuthSettings.vue:26 +#: src/views/preference/AuthSettings.vue:27 #: src/views/stream/StreamList.vue:47 #: src/views/user/User.vue:43 msgid "Action" @@ -91,7 +95,7 @@ msgstr "" msgid "Arch" msgstr "" -#: src/views/preference/AuthSettings.vue:94 +#: src/views/preference/AuthSettings.vue:95 msgid "Are you sure to delete this banned IP immediately?" msgstr "" @@ -141,7 +145,7 @@ msgstr "" msgid "Assistant" msgstr "" -#: src/views/preference/AuthSettings.vue:17 +#: src/views/preference/AuthSettings.vue:18 msgid "Attempts" msgstr "" @@ -187,15 +191,15 @@ msgstr "" msgid "Back to list" msgstr "" -#: src/views/preference/AuthSettings.vue:68 +#: src/views/preference/AuthSettings.vue:69 msgid "Ban Threshold Minutes" msgstr "" -#: src/views/preference/AuthSettings.vue:82 +#: src/views/preference/AuthSettings.vue:83 msgid "Banned IPs" msgstr "" -#: src/views/preference/AuthSettings.vue:20 +#: src/views/preference/AuthSettings.vue:21 msgid "Banned Until" msgstr "" @@ -388,6 +392,14 @@ msgstr "" msgid "Credentials" msgstr "" +#: src/views/preference/components/TOTP.vue:96 +msgid "Current account is enabled 2FA." +msgstr "" + +#: src/views/preference/components/TOTP.vue:93 +msgid "Current account is not enabled 2FA." +msgstr "" + #: src/views/system/Upgrade.vue:167 msgid "Current Version" msgstr "" @@ -662,6 +674,14 @@ msgstr "" msgid "Enable %{conf_name} in %{node_name} successfully" msgstr "" +#: src/views/preference/components/TOTP.vue:122 +msgid "Enable 2FA" +msgstr "" + +#: src/views/preference/components/TOTP.vue:52 +msgid "Enable 2FA successfully" +msgstr "" + #: src/views/domain/cert/components/ObtainCert.vue:70 msgid "Enable auto-renewal failed for %{name}" msgstr "" @@ -863,10 +883,14 @@ msgstr "" msgid "If left blank, the default CA Dir will be used." msgstr "" -#: src/views/preference/AuthSettings.vue:60 +#: src/views/preference/AuthSettings.vue:61 msgid "If the number of login failed attempts from a ip reach the max attempts in ban threshold minutes, the ip will be banned for a period of time." msgstr "" +#: src/views/preference/components/TOTP.vue:108 +msgid "If you lose your mobile phone, you can use the recovery code to reset your 2FA." +msgstr "" + #: src/views/certificate/Certificate.vue:136 msgid "Import" msgstr "" @@ -876,7 +900,7 @@ msgstr "" msgid "Import Certificate" msgstr "" -#: src/views/other/Login.vue:59 +#: src/views/other/Login.vue:73 msgid "Incorrect username or password" msgstr "" @@ -892,7 +916,16 @@ msgstr "" msgid "Initialing core upgrader" msgstr "" -#: src/routes/index.ts:273 +#: src/views/preference/components/TOTP.vue:144 +msgid "Input the code from the app:" +msgstr "" + +#: src/views/other/Login.vue:194 +#: src/views/preference/components/TOTP.vue:157 +msgid "Input the recovery code:" +msgstr "" + +#: src/routes/index.ts:283 #: src/views/other/Install.vue:135 msgid "Install" msgstr "" @@ -913,7 +946,11 @@ msgstr "" msgid "Invalid" msgstr "" -#: src/views/preference/AuthSettings.vue:14 +#: src/views/other/Login.vue:83 +msgid "Invalid 2FA or recovery code" +msgstr "" + +#: src/views/preference/AuthSettings.vue:15 msgid "IP" msgstr "" @@ -998,13 +1035,13 @@ msgstr "" msgid "Log" msgstr "" -#: src/routes/index.ts:279 -#: src/views/other/Login.vue:159 +#: src/routes/index.ts:289 +#: src/views/other/Login.vue:218 msgid "Login" msgstr "" -#: src/views/other/Login.vue:109 -#: src/views/other/Login.vue:51 +#: src/views/other/Login.vue:127 +#: src/views/other/Login.vue:62 msgid "Login successful" msgstr "" @@ -1038,7 +1075,7 @@ msgstr "" msgid "Manage Streams" msgstr "" -#: src/routes/index.ts:230 +#: src/routes/index.ts:235 #: src/views/user/User.vue:50 msgid "Manage Users" msgstr "" @@ -1047,7 +1084,7 @@ msgstr "" msgid "Managed Certificate" msgstr "" -#: src/views/preference/AuthSettings.vue:74 +#: src/views/preference/AuthSettings.vue:75 msgid "Max Attempts" msgstr "" @@ -1178,7 +1215,7 @@ msgstr "" #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90 #: src/views/domain/ngx_conf/LocationEditor.vue:71 #: src/views/notification/Notification.vue:70 -#: src/views/preference/AuthSettings.vue:96 +#: src/views/preference/AuthSettings.vue:97 #: src/views/preference/BasicSettings.vue:101 #: src/views/stream/StreamList.vue:165 msgid "No" @@ -1192,7 +1229,7 @@ msgstr "" msgid "Not After" msgstr "" -#: src/routes/index.ts:285 +#: src/routes/index.ts:295 msgid "Not Found" msgstr "" @@ -1210,7 +1247,7 @@ msgid "Notification" msgstr "" #: src/components/Notification/Notification.vue:82 -#: src/routes/index.ts:221 +#: src/routes/index.ts:226 msgid "Notifications" msgstr "" @@ -1290,7 +1327,7 @@ msgstr "" msgid "Params" msgstr "" -#: src/views/other/Login.vue:144 +#: src/views/other/Login.vue:167 #: src/views/user/User.vue:18 msgid "Password" msgstr "" @@ -1317,6 +1354,10 @@ msgstr "" msgid "Performing core upgrade" msgstr "" +#: src/views/other/Login.vue:177 +msgid "Please enter the 2FA code:" +msgstr "" + #: src/views/certificate/DNSCredential.vue:53 msgid "Please fill in the API authentication credentials provided by your DNS provider." msgstr "" @@ -1339,12 +1380,12 @@ msgid "Please input your E-mail!" msgstr "" #: src/views/other/Install.vue:45 -#: src/views/other/Login.vue:39 +#: src/views/other/Login.vue:45 msgid "Please input your password!" msgstr "" #: src/views/other/Install.vue:39 -#: src/views/other/Login.vue:33 +#: src/views/other/Login.vue:39 msgid "Please input your username!" msgstr "" @@ -1364,7 +1405,7 @@ msgstr "" msgid "Pre-release" msgstr "" -#: src/routes/index.ts:239 +#: src/routes/index.ts:244 #: src/views/preference/Preference.vue:105 msgid "Preference" msgstr "" @@ -1407,6 +1448,19 @@ msgstr "" msgid "Recovered Successfully" msgstr "" +#: src/views/other/Login.vue:204 +#: src/views/preference/components/TOTP.vue:167 +msgid "Recovery" +msgstr "" + +#: src/views/preference/components/TOTP.vue:101 +msgid "Recovery Code" +msgstr "" + +#: src/views/preference/components/TOTP.vue:110 +msgid "Recovery Code:" +msgstr "" + #: src/views/preference/BasicSettings.vue:68 msgid "Recursive Nameservers" msgstr "" @@ -1456,11 +1510,11 @@ msgstr "" msgid "Reloading nginx" msgstr "" -#: src/views/preference/AuthSettings.vue:101 +#: src/views/preference/AuthSettings.vue:102 msgid "Remove" msgstr "" -#: src/views/preference/AuthSettings.vue:47 +#: src/views/preference/AuthSettings.vue:48 msgid "Remove successfully" msgstr "" @@ -1498,6 +1552,10 @@ msgstr "" msgid "Reset" msgstr "" +#: src/views/preference/components/TOTP.vue:130 +msgid "Reset 2FA" +msgstr "" + #: src/components/NginxControl/NginxControl.vue:93 msgid "Restart" msgstr "" @@ -1549,6 +1607,10 @@ msgstr "" msgid "Saved successfully" msgstr "" +#: src/views/preference/components/TOTP.vue:91 +msgid "Scan the QR code with your mobile phone to add the account to the app." +msgstr "" + #: src/views/certificate/DNSChallenge.vue:89 msgid "SDK" msgstr "" @@ -1574,7 +1636,9 @@ msgstr "" #: src/views/environment/BatchUpgrader.vue:57 #: src/views/environment/Environment.vue:15 #: src/views/other/Install.vue:69 -#: src/views/preference/AuthSettings.vue:49 +#: src/views/preference/AuthSettings.vue:50 +#: src/views/preference/components/TOTP.vue:42 +#: src/views/preference/components/TOTP.vue:55 #: src/views/preference/Preference.vue:78 #: src/views/stream/StreamList.vue:113 #: src/views/stream/StreamList.vue:81 @@ -1645,7 +1709,7 @@ msgstr "" msgid "SSL Certificate Path" msgstr "" -#: src/views/other/Login.vue:170 +#: src/views/other/Login.vue:229 msgid "SSO Login" msgstr "" @@ -1722,7 +1786,7 @@ msgstr "" msgid "Sync to" msgstr "" -#: src/routes/index.ts:248 +#: src/routes/index.ts:253 msgid "System" msgstr "" @@ -1768,6 +1832,10 @@ msgstr "" msgid "The path exists, but the file is not a private key" msgstr "" +#: src/views/preference/components/TOTP.vue:109 +msgid "The recovery code is only displayed once, please save it in a safe place." +msgstr "" + #: src/views/dashboard/Environments.vue:148 msgid "The remote Nginx UI version is not compatible with the local Nginx UI version. To avoid potential errors, please upgrade the remote Nginx UI to match the local version." msgstr "" @@ -1816,7 +1884,7 @@ msgstr "" msgid "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}." msgstr "" -#: src/views/preference/AuthSettings.vue:59 +#: src/views/preference/AuthSettings.vue:60 #: src/views/preference/LogrotateSettings.vue:12 msgid "Tips" msgstr "" @@ -1825,6 +1893,10 @@ msgstr "" msgid "Title" msgstr "" +#: src/views/preference/components/TOTP.vue:90 +msgid "To enable it, you need to install the Google or Microsoft Authenticator app on your mobile phone." +msgstr "" + #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44 msgid "To make sure the certification auto-renewal can work normally, we need to add a location which can proxy the request from authority to backend, and we need to save this file and reload the Nginx. Are you sure you want to continue?" msgstr "" @@ -1833,10 +1905,14 @@ msgstr "" msgid "Token is not valid" msgstr "" -#: src/views/other/Login.vue:62 +#: src/views/other/Login.vue:76 msgid "Too many login failed attempts, please try again later" msgstr "" +#: src/views/preference/components/TOTP.vue:89 +msgid "TOTP is a two-factor authentication method that uses a time-based one-time password algorithm." +msgstr "" + #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:209 msgid "Trash" msgstr "" @@ -1864,7 +1940,7 @@ msgstr "" msgid "Updated successfully" msgstr "" -#: src/routes/index.ts:263 +#: src/routes/index.ts:268 #: src/views/environment/Environment.vue:50 #: src/views/system/Upgrade.vue:145 #: src/views/system/Upgrade.vue:228 @@ -1896,15 +1972,19 @@ msgstr "" msgid "URL" msgstr "" +#: src/views/other/Login.vue:186 +msgid "Use recovery code" +msgstr "" + #: src/components/ChatGPT/ChatGPT.vue:229 msgid "User" msgstr "" -#: src/views/other/Login.vue:65 +#: src/views/other/Login.vue:79 msgid "User is banned" msgstr "" -#: src/views/other/Login.vue:134 +#: src/views/other/Login.vue:157 #: src/views/user/User.vue:9 msgid "Username" msgstr "" @@ -1969,7 +2049,7 @@ msgstr "" #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89 #: src/views/domain/ngx_conf/LocationEditor.vue:70 -#: src/views/preference/AuthSettings.vue:95 +#: src/views/preference/AuthSettings.vue:96 #: src/views/preference/BasicSettings.vue:100 msgid "Yes" msgstr "" diff --git a/app/src/language/ru_RU/app.po b/app/src/language/ru_RU/app.po index 53780a4e5..4626638b1 100644 --- a/app/src/language/ru_RU/app.po +++ b/app/src/language/ru_RU/app.po @@ -9,7 +9,11 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: src/routes/index.ts:256 +#: src/views/preference/components/TOTP.vue:88 +msgid "2FA Settings" +msgstr "" + +#: src/routes/index.ts:261 msgid "About" msgstr "О проекте" @@ -28,7 +32,7 @@ msgstr "Пользователь" #: src/views/certificate/DNSCredential.vue:33 src/views/config/config.ts:34 #: src/views/domain/DomainList.vue:47 src/views/environment/envColumns.tsx:131 #: src/views/notification/Notification.vue:37 -#: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47 +#: src/views/preference/AuthSettings.vue:27 src/views/stream/StreamList.vue:47 #: src/views/user/User.vue:43 msgid "Action" msgstr "Действие" @@ -95,7 +99,7 @@ msgstr "" msgid "Arch" msgstr "" -#: src/views/preference/AuthSettings.vue:94 +#: src/views/preference/AuthSettings.vue:95 #, fuzzy msgid "Are you sure to delete this banned IP immediately?" msgstr "Вы уверены, что хотите удалить?" @@ -153,7 +157,7 @@ msgstr "Обратитесь за помощью к ChatGPT" msgid "Assistant" msgstr "Ассистент" -#: src/views/preference/AuthSettings.vue:17 +#: src/views/preference/AuthSettings.vue:18 msgid "Attempts" msgstr "" @@ -199,15 +203,15 @@ msgstr "Вернутся" msgid "Back to list" msgstr "" -#: src/views/preference/AuthSettings.vue:68 +#: src/views/preference/AuthSettings.vue:69 msgid "Ban Threshold Minutes" msgstr "" -#: src/views/preference/AuthSettings.vue:82 +#: src/views/preference/AuthSettings.vue:83 msgid "Banned IPs" msgstr "" -#: src/views/preference/AuthSettings.vue:20 +#: src/views/preference/AuthSettings.vue:21 msgid "Banned Until" msgstr "" @@ -407,6 +411,14 @@ msgstr "Учетные данные" msgid "Credentials" msgstr "Учетные данные" +#: src/views/preference/components/TOTP.vue:96 +msgid "Current account is enabled 2FA." +msgstr "" + +#: src/views/preference/components/TOTP.vue:93 +msgid "Current account is not enabled 2FA." +msgstr "" + #: src/views/system/Upgrade.vue:167 msgid "Current Version" msgstr "Текущяя версия" @@ -696,6 +708,16 @@ msgstr "Включение %{conf_name} in %{node_name} нипалучилася msgid "Enable %{conf_name} in %{node_name} successfully" msgstr "Включение %{conf_name} in %{node_name} успешно" +#: src/views/preference/components/TOTP.vue:122 +#, fuzzy +msgid "Enable 2FA" +msgstr "Включить" + +#: src/views/preference/components/TOTP.vue:52 +#, fuzzy +msgid "Enable 2FA successfully" +msgstr "Активировано успешно" + #: src/views/domain/cert/components/ObtainCert.vue:70 msgid "Enable auto-renewal failed for %{name}" msgstr "Не удалось включить автоматическое продление для %{name}" @@ -898,12 +920,18 @@ msgstr "" msgid "If left blank, the default CA Dir will be used." msgstr "" -#: src/views/preference/AuthSettings.vue:60 +#: src/views/preference/AuthSettings.vue:61 msgid "" "If the number of login failed attempts from a ip reach the max attempts in " "ban threshold minutes, the ip will be banned for a period of time." msgstr "" +#: src/views/preference/components/TOTP.vue:108 +msgid "" +"If you lose your mobile phone, you can use the recovery code to reset your " +"2FA." +msgstr "" + #: src/views/certificate/Certificate.vue:136 #, fuzzy msgid "Import" @@ -914,7 +942,7 @@ msgstr "Экспорт" msgid "Import Certificate" msgstr "Статус сертификата" -#: src/views/other/Login.vue:59 +#: src/views/other/Login.vue:73 #, fuzzy msgid "Incorrect username or password" msgstr "Имя пользователя или пароль неверны" @@ -931,7 +959,15 @@ msgstr "Ошибка первоначального обновления ядр msgid "Initialing core upgrader" msgstr "Инициализация программы обновления ядра" -#: src/routes/index.ts:273 src/views/other/Install.vue:135 +#: src/views/preference/components/TOTP.vue:144 +msgid "Input the code from the app:" +msgstr "" + +#: src/views/other/Login.vue:194 src/views/preference/components/TOTP.vue:157 +msgid "Input the recovery code:" +msgstr "" + +#: src/routes/index.ts:283 src/views/other/Install.vue:135 msgid "Install" msgstr "Установить" @@ -953,7 +989,11 @@ msgstr "" msgid "Invalid" msgstr "Действительный" -#: src/views/preference/AuthSettings.vue:14 +#: src/views/other/Login.vue:83 +msgid "Invalid 2FA or recovery code" +msgstr "" + +#: src/views/preference/AuthSettings.vue:15 msgid "IP" msgstr "" @@ -1048,11 +1088,11 @@ msgstr "Locations" msgid "Log" msgstr "Логин" -#: src/routes/index.ts:279 src/views/other/Login.vue:159 +#: src/routes/index.ts:289 src/views/other/Login.vue:218 msgid "Login" msgstr "Логин" -#: src/views/other/Login.vue:109 src/views/other/Login.vue:51 +#: src/views/other/Login.vue:127 src/views/other/Login.vue:62 msgid "Login successful" msgstr "Авторизация успешна" @@ -1096,7 +1136,7 @@ msgstr "Сайты" msgid "Manage Streams" msgstr "Управление потоками" -#: src/routes/index.ts:230 src/views/user/User.vue:50 +#: src/routes/index.ts:235 src/views/user/User.vue:50 msgid "Manage Users" msgstr "Пользователи" @@ -1105,7 +1145,7 @@ msgstr "Пользователи" msgid "Managed Certificate" msgstr "Управление сертификатами" -#: src/views/preference/AuthSettings.vue:74 +#: src/views/preference/AuthSettings.vue:75 msgid "Max Attempts" msgstr "" @@ -1240,7 +1280,7 @@ msgstr "Nginx успешно перезапущен" #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90 #: src/views/domain/ngx_conf/LocationEditor.vue:71 #: src/views/notification/Notification.vue:70 -#: src/views/preference/AuthSettings.vue:96 +#: src/views/preference/AuthSettings.vue:97 #: src/views/preference/BasicSettings.vue:101 #: src/views/stream/StreamList.vue:165 msgid "No" @@ -1254,7 +1294,7 @@ msgstr "" msgid "Not After" msgstr "" -#: src/routes/index.ts:285 +#: src/routes/index.ts:295 msgid "Not Found" msgstr "Не найден" @@ -1272,7 +1312,7 @@ msgstr "Заметка" msgid "Notification" msgstr "Сертификат" -#: src/components/Notification/Notification.vue:82 src/routes/index.ts:221 +#: src/components/Notification/Notification.vue:82 src/routes/index.ts:226 #, fuzzy msgid "Notifications" msgstr "Уведомления" @@ -1355,7 +1395,7 @@ msgstr "" msgid "Params" msgstr "Параметры" -#: src/views/other/Login.vue:144 src/views/user/User.vue:18 +#: src/views/other/Login.vue:167 src/views/user/User.vue:18 msgid "Password" msgstr "Пароль" @@ -1381,6 +1421,10 @@ msgstr "" msgid "Performing core upgrade" msgstr "" +#: src/views/other/Login.vue:177 +msgid "Please enter the 2FA code:" +msgstr "" + #: src/views/certificate/DNSCredential.vue:53 msgid "" "Please fill in the API authentication credentials provided by your DNS " @@ -1410,11 +1454,11 @@ msgstr "" msgid "Please input your E-mail!" msgstr "Введите ваш E-mail!" -#: src/views/other/Install.vue:45 src/views/other/Login.vue:39 +#: src/views/other/Install.vue:45 src/views/other/Login.vue:45 msgid "Please input your password!" msgstr "Введите ваш пароль!" -#: src/views/other/Install.vue:39 src/views/other/Login.vue:33 +#: src/views/other/Install.vue:39 src/views/other/Login.vue:39 msgid "Please input your username!" msgstr "Введите ваше имя пользователя!" @@ -1434,7 +1478,7 @@ msgstr "" msgid "Pre-release" msgstr "" -#: src/routes/index.ts:239 src/views/preference/Preference.vue:105 +#: src/routes/index.ts:244 src/views/preference/Preference.vue:105 msgid "Preference" msgstr "Настройки" @@ -1478,6 +1522,18 @@ msgstr "" msgid "Recovered Successfully" msgstr "Успешно сохранено" +#: src/views/other/Login.vue:204 src/views/preference/components/TOTP.vue:167 +msgid "Recovery" +msgstr "" + +#: src/views/preference/components/TOTP.vue:101 +msgid "Recovery Code" +msgstr "" + +#: src/views/preference/components/TOTP.vue:110 +msgid "Recovery Code:" +msgstr "" + #: src/views/preference/BasicSettings.vue:68 msgid "Recursive Nameservers" msgstr "" @@ -1532,11 +1588,11 @@ msgstr "Перезагружается" msgid "Reloading nginx" msgstr "Перезагружается nginx" -#: src/views/preference/AuthSettings.vue:101 +#: src/views/preference/AuthSettings.vue:102 msgid "Remove" msgstr "" -#: src/views/preference/AuthSettings.vue:47 +#: src/views/preference/AuthSettings.vue:48 #, fuzzy msgid "Remove successfully" msgstr "Успешно сохранено" @@ -1581,6 +1637,11 @@ msgstr "Запрос с неправильными параметрами" msgid "Reset" msgstr "Сброс" +#: src/views/preference/components/TOTP.vue:130 +#, fuzzy +msgid "Reset 2FA" +msgstr "Сброс" + #: src/components/NginxControl/NginxControl.vue:93 msgid "Restart" msgstr "Перезапуск" @@ -1630,6 +1691,10 @@ msgstr "Успешно сохранено" msgid "Saved successfully" msgstr "Успешно сохранено" +#: src/views/preference/components/TOTP.vue:91 +msgid "Scan the QR code with your mobile phone to add the account to the app." +msgstr "" + #: src/views/certificate/DNSChallenge.vue:89 msgid "SDK" msgstr "" @@ -1653,7 +1718,9 @@ msgstr "Отправлено" #: src/views/config/ConfigEdit.vue:40 src/views/domain/DomainList.vue:81 #: src/views/environment/BatchUpgrader.vue:57 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:69 -#: src/views/preference/AuthSettings.vue:49 +#: src/views/preference/AuthSettings.vue:50 +#: src/views/preference/components/TOTP.vue:42 +#: src/views/preference/components/TOTP.vue:55 #: src/views/preference/Preference.vue:78 src/views/stream/StreamList.vue:113 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42 msgid "Server error" @@ -1730,7 +1797,7 @@ msgstr "Путь к ключу сертификата SSL" msgid "SSL Certificate Path" msgstr "Путь к сертификату SSL" -#: src/views/other/Login.vue:170 +#: src/views/other/Login.vue:229 #, fuzzy msgid "SSO Login" msgstr "Логин" @@ -1815,7 +1882,7 @@ msgstr "Сертификат действителен" msgid "Sync to" msgstr "" -#: src/routes/index.ts:248 +#: src/routes/index.ts:253 msgid "System" msgstr "Система" @@ -1869,6 +1936,11 @@ msgstr "Путь к ключу сертификата SSL" msgid "The path exists, but the file is not a private key" msgstr "Путь существует, но файл не является приватным ключом" +#: src/views/preference/components/TOTP.vue:109 +msgid "" +"The recovery code is only displayed once, please save it in a safe place." +msgstr "" + #: src/views/dashboard/Environments.vue:148 msgid "" "The remote Nginx UI version is not compatible with the local Nginx UI " @@ -1931,7 +2003,7 @@ msgid "" "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}." msgstr "" -#: src/views/preference/AuthSettings.vue:59 +#: src/views/preference/AuthSettings.vue:60 #: src/views/preference/LogrotateSettings.vue:12 msgid "Tips" msgstr "" @@ -1940,6 +2012,12 @@ msgstr "" msgid "Title" msgstr "Заголовок" +#: src/views/preference/components/TOTP.vue:90 +msgid "" +"To enable it, you need to install the Google or Microsoft Authenticator app " +"on your mobile phone." +msgstr "" + #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44 msgid "" "To make sure the certification auto-renewal can work normally, we need to " @@ -1952,10 +2030,16 @@ msgstr "" msgid "Token is not valid" msgstr "" -#: src/views/other/Login.vue:62 +#: src/views/other/Login.vue:76 msgid "Too many login failed attempts, please try again later" msgstr "" +#: src/views/preference/components/TOTP.vue:89 +msgid "" +"TOTP is a two-factor authentication method that uses a time-based one-time " +"password algorithm." +msgstr "" + #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:209 msgid "Trash" msgstr "" @@ -1980,7 +2064,7 @@ msgstr "Обновлено в" msgid "Updated successfully" msgstr "Обновлено успешно" -#: src/routes/index.ts:263 src/views/environment/Environment.vue:50 +#: src/routes/index.ts:268 src/views/environment/Environment.vue:50 #: src/views/system/Upgrade.vue:145 src/views/system/Upgrade.vue:228 msgid "Upgrade" msgstr "Обновление" @@ -2011,16 +2095,20 @@ msgstr "Аптайм:" msgid "URL" msgstr "" +#: src/views/other/Login.vue:186 +msgid "Use recovery code" +msgstr "" + #: src/components/ChatGPT/ChatGPT.vue:229 #, fuzzy msgid "User" msgstr "Пользователь" -#: src/views/other/Login.vue:65 +#: src/views/other/Login.vue:79 msgid "User is banned" msgstr "" -#: src/views/other/Login.vue:134 src/views/user/User.vue:9 +#: src/views/other/Login.vue:157 src/views/user/User.vue:9 msgid "Username" msgstr "Имя пользователя" @@ -2092,7 +2180,7 @@ msgstr "Запись сертификата на диск" #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89 #: src/views/domain/ngx_conf/LocationEditor.vue:70 -#: src/views/preference/AuthSettings.vue:95 +#: src/views/preference/AuthSettings.vue:96 #: src/views/preference/BasicSettings.vue:100 msgid "Yes" msgstr "Да" diff --git a/app/src/language/vi_VN/app.po b/app/src/language/vi_VN/app.po index 5a8ff3ada..ea3cc4465 100644 --- a/app/src/language/vi_VN/app.po +++ b/app/src/language/vi_VN/app.po @@ -9,7 +9,11 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: src/routes/index.ts:256 +#: src/views/preference/components/TOTP.vue:88 +msgid "2FA Settings" +msgstr "" + +#: src/routes/index.ts:261 msgid "About" msgstr "Tác giả" @@ -28,7 +32,7 @@ msgstr "Người dùng" #: src/views/certificate/DNSCredential.vue:33 src/views/config/config.ts:34 #: src/views/domain/DomainList.vue:47 src/views/environment/envColumns.tsx:131 #: src/views/notification/Notification.vue:37 -#: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47 +#: src/views/preference/AuthSettings.vue:27 src/views/stream/StreamList.vue:47 #: src/views/user/User.vue:43 msgid "Action" msgstr "Hành động" @@ -95,7 +99,7 @@ msgstr "" msgid "Arch" msgstr "" -#: src/views/preference/AuthSettings.vue:94 +#: src/views/preference/AuthSettings.vue:95 #, fuzzy msgid "Are you sure to delete this banned IP immediately?" msgstr "Bạn chắc chắn muốn xóa nó " @@ -153,7 +157,7 @@ msgstr "Hỏi ChatGPT" msgid "Assistant" msgstr "Trợ lý" -#: src/views/preference/AuthSettings.vue:17 +#: src/views/preference/AuthSettings.vue:18 msgid "Attempts" msgstr "" @@ -199,15 +203,15 @@ msgstr "Quay lại" msgid "Back to list" msgstr "" -#: src/views/preference/AuthSettings.vue:68 +#: src/views/preference/AuthSettings.vue:69 msgid "Ban Threshold Minutes" msgstr "" -#: src/views/preference/AuthSettings.vue:82 +#: src/views/preference/AuthSettings.vue:83 msgid "Banned IPs" msgstr "" -#: src/views/preference/AuthSettings.vue:20 +#: src/views/preference/AuthSettings.vue:21 msgid "Banned Until" msgstr "" @@ -407,6 +411,14 @@ msgstr "Chứng chỉ" msgid "Credentials" msgstr "Chứng chỉ" +#: src/views/preference/components/TOTP.vue:96 +msgid "Current account is enabled 2FA." +msgstr "" + +#: src/views/preference/components/TOTP.vue:93 +msgid "Current account is not enabled 2FA." +msgstr "" + #: src/views/system/Upgrade.vue:167 msgid "Current Version" msgstr "Phiên bản hiện tại" @@ -697,6 +709,16 @@ msgstr "Không thể bật %{conf_name} trên %{node_name}" msgid "Enable %{conf_name} in %{node_name} successfully" msgstr "Đã bật %{conf_name} trên %{node_name}" +#: src/views/preference/components/TOTP.vue:122 +#, fuzzy +msgid "Enable 2FA" +msgstr "Đã bật" + +#: src/views/preference/components/TOTP.vue:52 +#, fuzzy +msgid "Enable 2FA successfully" +msgstr "Đã bật" + #: src/views/domain/cert/components/ObtainCert.vue:70 msgid "Enable auto-renewal failed for %{name}" msgstr "Không thể bật tự động gia hạn SSL cho %{name}" @@ -900,12 +922,18 @@ msgstr "" msgid "If left blank, the default CA Dir will be used." msgstr "" -#: src/views/preference/AuthSettings.vue:60 +#: src/views/preference/AuthSettings.vue:61 msgid "" "If the number of login failed attempts from a ip reach the max attempts in " "ban threshold minutes, the ip will be banned for a period of time." msgstr "" +#: src/views/preference/components/TOTP.vue:108 +msgid "" +"If you lose your mobile phone, you can use the recovery code to reset your " +"2FA." +msgstr "" + #: src/views/certificate/Certificate.vue:136 #, fuzzy msgid "Import" @@ -916,7 +944,7 @@ msgstr "Xuất" msgid "Import Certificate" msgstr "Chứng chỉ" -#: src/views/other/Login.vue:59 +#: src/views/other/Login.vue:73 #, fuzzy msgid "Incorrect username or password" msgstr "Tên người dùng hoặc mật khẩu không chính xác" @@ -933,7 +961,15 @@ msgstr "Không thể khởi tạo trình nâng cấp" msgid "Initialing core upgrader" msgstr "Đang khởi tạo trình nâng cấp" -#: src/routes/index.ts:273 src/views/other/Install.vue:135 +#: src/views/preference/components/TOTP.vue:144 +msgid "Input the code from the app:" +msgstr "" + +#: src/views/other/Login.vue:194 src/views/preference/components/TOTP.vue:157 +msgid "Input the recovery code:" +msgstr "" + +#: src/routes/index.ts:283 src/views/other/Install.vue:135 msgid "Install" msgstr "Cài đặt" @@ -955,7 +991,11 @@ msgstr "" msgid "Invalid" msgstr "Hợp lệ" -#: src/views/preference/AuthSettings.vue:14 +#: src/views/other/Login.vue:83 +msgid "Invalid 2FA or recovery code" +msgstr "" + +#: src/views/preference/AuthSettings.vue:15 msgid "IP" msgstr "" @@ -1050,11 +1090,11 @@ msgstr "Locations" msgid "Log" msgstr "Log" -#: src/routes/index.ts:279 src/views/other/Login.vue:159 +#: src/routes/index.ts:289 src/views/other/Login.vue:218 msgid "Login" msgstr "Đăng nhập" -#: src/views/other/Login.vue:109 src/views/other/Login.vue:51 +#: src/views/other/Login.vue:127 src/views/other/Login.vue:62 msgid "Login successful" msgstr "Đăng nhập thành công" @@ -1098,7 +1138,7 @@ msgstr "Quản lý Website" msgid "Manage Streams" msgstr "Quản lý Website" -#: src/routes/index.ts:230 src/views/user/User.vue:50 +#: src/routes/index.ts:235 src/views/user/User.vue:50 msgid "Manage Users" msgstr "Người dùng" @@ -1106,7 +1146,7 @@ msgstr "Người dùng" msgid "Managed Certificate" msgstr "" -#: src/views/preference/AuthSettings.vue:74 +#: src/views/preference/AuthSettings.vue:75 msgid "Max Attempts" msgstr "" @@ -1240,7 +1280,7 @@ msgstr "Restart Nginx thành công" #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90 #: src/views/domain/ngx_conf/LocationEditor.vue:71 #: src/views/notification/Notification.vue:70 -#: src/views/preference/AuthSettings.vue:96 +#: src/views/preference/AuthSettings.vue:97 #: src/views/preference/BasicSettings.vue:101 #: src/views/stream/StreamList.vue:165 msgid "No" @@ -1254,7 +1294,7 @@ msgstr "" msgid "Not After" msgstr "Không phải sau khi" -#: src/routes/index.ts:285 +#: src/routes/index.ts:295 msgid "Not Found" msgstr "Không tìm thấy" @@ -1272,7 +1312,7 @@ msgstr "Ghi chú" msgid "Notification" msgstr "Thông báo" -#: src/components/Notification/Notification.vue:82 src/routes/index.ts:221 +#: src/components/Notification/Notification.vue:82 src/routes/index.ts:226 #, fuzzy msgid "Notifications" msgstr "Thông báo" @@ -1355,7 +1395,7 @@ msgstr "Ghi đè tập tin đã tồn tại" msgid "Params" msgstr "Tham số" -#: src/views/other/Login.vue:144 src/views/user/User.vue:18 +#: src/views/other/Login.vue:167 src/views/user/User.vue:18 msgid "Password" msgstr "Mật khẩu" @@ -1381,6 +1421,10 @@ msgstr "Nâng cấp core không thành công" msgid "Performing core upgrade" msgstr "Nâng cấp core" +#: src/views/other/Login.vue:177 +msgid "Please enter the 2FA code:" +msgstr "" + #: src/views/certificate/DNSCredential.vue:53 msgid "" "Please fill in the API authentication credentials provided by your DNS " @@ -1412,11 +1456,11 @@ msgstr "" msgid "Please input your E-mail!" msgstr "Vui lòng nhập E-mail của bạn!" -#: src/views/other/Install.vue:45 src/views/other/Login.vue:39 +#: src/views/other/Install.vue:45 src/views/other/Login.vue:45 msgid "Please input your password!" msgstr "Vui lòng nhập mật khẩu!" -#: src/views/other/Install.vue:39 src/views/other/Login.vue:33 +#: src/views/other/Install.vue:39 src/views/other/Login.vue:39 msgid "Please input your username!" msgstr "Vui lòng nhập username!" @@ -1436,7 +1480,7 @@ msgstr "" msgid "Pre-release" msgstr "" -#: src/routes/index.ts:239 src/views/preference/Preference.vue:105 +#: src/routes/index.ts:244 src/views/preference/Preference.vue:105 msgid "Preference" msgstr "Cài đặt" @@ -1480,6 +1524,18 @@ msgstr "" msgid "Recovered Successfully" msgstr "Xoá thành công" +#: src/views/other/Login.vue:204 src/views/preference/components/TOTP.vue:167 +msgid "Recovery" +msgstr "" + +#: src/views/preference/components/TOTP.vue:101 +msgid "Recovery Code" +msgstr "" + +#: src/views/preference/components/TOTP.vue:110 +msgid "Recovery Code:" +msgstr "" + #: src/views/preference/BasicSettings.vue:68 msgid "Recursive Nameservers" msgstr "" @@ -1534,11 +1590,11 @@ msgstr "Đang tải lại" msgid "Reloading nginx" msgstr "Tải lại nginx" -#: src/views/preference/AuthSettings.vue:101 +#: src/views/preference/AuthSettings.vue:102 msgid "Remove" msgstr "" -#: src/views/preference/AuthSettings.vue:47 +#: src/views/preference/AuthSettings.vue:48 #, fuzzy msgid "Remove successfully" msgstr "Xoá thành công" @@ -1583,6 +1639,11 @@ msgstr "Yêu cầu có chứa tham số sai" msgid "Reset" msgstr "Đặt lại" +#: src/views/preference/components/TOTP.vue:130 +#, fuzzy +msgid "Reset 2FA" +msgstr "Đặt lại" + #: src/components/NginxControl/NginxControl.vue:93 msgid "Restart" msgstr "Khởi động lại" @@ -1632,6 +1693,10 @@ msgstr "Lưu thành công" msgid "Saved successfully" msgstr "Lưu thành công" +#: src/views/preference/components/TOTP.vue:91 +msgid "Scan the QR code with your mobile phone to add the account to the app." +msgstr "" + #: src/views/certificate/DNSChallenge.vue:89 msgid "SDK" msgstr "" @@ -1655,7 +1720,9 @@ msgstr "Gửi" #: src/views/config/ConfigEdit.vue:40 src/views/domain/DomainList.vue:81 #: src/views/environment/BatchUpgrader.vue:57 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:69 -#: src/views/preference/AuthSettings.vue:49 +#: src/views/preference/AuthSettings.vue:50 +#: src/views/preference/components/TOTP.vue:42 +#: src/views/preference/components/TOTP.vue:55 #: src/views/preference/Preference.vue:78 src/views/stream/StreamList.vue:113 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42 msgid "Server error" @@ -1729,7 +1796,7 @@ msgstr "" msgid "SSL Certificate Path" msgstr "" -#: src/views/other/Login.vue:170 +#: src/views/other/Login.vue:229 msgid "SSO Login" msgstr "" @@ -1813,7 +1880,7 @@ msgstr "Gia hạn chứng chỉ SSL thành công" msgid "Sync to" msgstr "" -#: src/routes/index.ts:248 +#: src/routes/index.ts:253 msgid "System" msgstr "Thông tin" @@ -1865,6 +1932,11 @@ msgstr "" msgid "The path exists, but the file is not a private key" msgstr "" +#: src/views/preference/components/TOTP.vue:109 +msgid "" +"The recovery code is only displayed once, please save it in a safe place." +msgstr "" + #: src/views/dashboard/Environments.vue:148 msgid "" "The remote Nginx UI version is not compatible with the local Nginx UI " @@ -1923,7 +1995,7 @@ msgid "" "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}." msgstr "" -#: src/views/preference/AuthSettings.vue:59 +#: src/views/preference/AuthSettings.vue:60 #: src/views/preference/LogrotateSettings.vue:12 msgid "Tips" msgstr "" @@ -1932,6 +2004,12 @@ msgstr "" msgid "Title" msgstr "Tiêu đề" +#: src/views/preference/components/TOTP.vue:90 +msgid "" +"To enable it, you need to install the Google or Microsoft Authenticator app " +"on your mobile phone." +msgstr "" + #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44 msgid "" "To make sure the certification auto-renewal can work normally, we need to " @@ -1948,10 +2026,16 @@ msgstr "" msgid "Token is not valid" msgstr "" -#: src/views/other/Login.vue:62 +#: src/views/other/Login.vue:76 msgid "Too many login failed attempts, please try again later" msgstr "" +#: src/views/preference/components/TOTP.vue:89 +msgid "" +"TOTP is a two-factor authentication method that uses a time-based one-time " +"password algorithm." +msgstr "" + #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:209 msgid "Trash" msgstr "" @@ -1976,7 +2060,7 @@ msgstr "Ngày cập nhật" msgid "Updated successfully" msgstr "Cập nhật thành công" -#: src/routes/index.ts:263 src/views/environment/Environment.vue:50 +#: src/routes/index.ts:268 src/views/environment/Environment.vue:50 #: src/views/system/Upgrade.vue:145 src/views/system/Upgrade.vue:228 msgid "Upgrade" msgstr "Cập nhật" @@ -2007,16 +2091,20 @@ msgstr "Thời gian hoạt động:" msgid "URL" msgstr "" +#: src/views/other/Login.vue:186 +msgid "Use recovery code" +msgstr "" + #: src/components/ChatGPT/ChatGPT.vue:229 #, fuzzy msgid "User" msgstr "Người dùng" -#: src/views/other/Login.vue:65 +#: src/views/other/Login.vue:79 msgid "User is banned" msgstr "" -#: src/views/other/Login.vue:134 src/views/user/User.vue:9 +#: src/views/other/Login.vue:157 src/views/user/User.vue:9 msgid "Username" msgstr "Username" @@ -2091,7 +2179,7 @@ msgstr "Ghi chứng chỉ vào disk" #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89 #: src/views/domain/ngx_conf/LocationEditor.vue:70 -#: src/views/preference/AuthSettings.vue:95 +#: src/views/preference/AuthSettings.vue:96 #: src/views/preference/BasicSettings.vue:100 msgid "Yes" msgstr "Có" diff --git a/app/src/language/zh_CN/app.mo b/app/src/language/zh_CN/app.mo index 9f04da9b01cb41cb5df92dce8abb28696184553a..b7c5239b86a604bda372b9e5fd08dd7b64133308 100644 GIT binary patch delta 10977 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 TbK|moy$dED^zja#)ZF*K`b73; diff --git a/app/src/language/zh_CN/app.po b/app/src/language/zh_CN/app.po index 6b8c20496..ec98f5b34 100644 --- a/app/src/language/zh_CN/app.po +++ b/app/src/language/zh_CN/app.po @@ -13,7 +13,11 @@ msgstr "" "Generated-By: easygettext\n" "X-Generator: Poedit 3.4.4\n" -#: src/routes/index.ts:256 +#: src/views/preference/components/TOTP.vue:88 +msgid "2FA Settings" +msgstr "2FA 设置" + +#: src/routes/index.ts:261 msgid "About" msgstr "关于" @@ -31,7 +35,7 @@ msgstr "ACME 用户" #: src/views/certificate/DNSCredential.vue:33 src/views/config/config.ts:34 #: src/views/domain/DomainList.vue:47 src/views/environment/envColumns.tsx:131 #: src/views/notification/Notification.vue:37 -#: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47 +#: src/views/preference/AuthSettings.vue:27 src/views/stream/StreamList.vue:47 #: src/views/user/User.vue:43 msgid "Action" msgstr "操作" @@ -95,7 +99,7 @@ msgstr "API Token" msgid "Arch" msgstr "架构" -#: src/views/preference/AuthSettings.vue:94 +#: src/views/preference/AuthSettings.vue:95 msgid "Are you sure to delete this banned IP immediately?" msgstr "您确定要立即删除这个被禁用的 IP 吗?" @@ -144,7 +148,7 @@ msgstr "与ChatGPT聊天" msgid "Assistant" msgstr "助手" -#: src/views/preference/AuthSettings.vue:17 +#: src/views/preference/AuthSettings.vue:18 msgid "Attempts" msgstr "尝试次数" @@ -188,15 +192,15 @@ msgstr "返回首页" msgid "Back to list" msgstr "返回列表" -#: src/views/preference/AuthSettings.vue:68 +#: src/views/preference/AuthSettings.vue:69 msgid "Ban Threshold Minutes" msgstr "禁止阈值(分钟)" -#: src/views/preference/AuthSettings.vue:82 +#: src/views/preference/AuthSettings.vue:83 msgid "Banned IPs" msgstr "禁止 IP 列表" -#: src/views/preference/AuthSettings.vue:20 +#: src/views/preference/AuthSettings.vue:21 msgid "Banned Until" msgstr "禁用至" @@ -385,6 +389,14 @@ msgstr "DNS 凭证" msgid "Credentials" msgstr "凭证" +#: src/views/preference/components/TOTP.vue:96 +msgid "Current account is enabled 2FA." +msgstr "当前账户已启用二步验证。" + +#: src/views/preference/components/TOTP.vue:93 +msgid "Current account is not enabled 2FA." +msgstr "当前用户未启用二步验证。" + #: src/views/system/Upgrade.vue:167 msgid "Current Version" msgstr "当前版本" @@ -652,6 +664,14 @@ msgstr "在%{node_name}中启用%{conf_name}失败" msgid "Enable %{conf_name} in %{node_name} successfully" msgstr "成功启用%{node_name}中的%{conf_name}" +#: src/views/preference/components/TOTP.vue:122 +msgid "Enable 2FA" +msgstr "启用二步验证" + +#: src/views/preference/components/TOTP.vue:52 +msgid "Enable 2FA successfully" +msgstr "二步验证启用成功" + #: src/views/domain/cert/components/ObtainCert.vue:70 msgid "Enable auto-renewal failed for %{name}" msgstr "启用 %{name} 自动续签失败" @@ -844,7 +864,7 @@ msgstr "HTTP01" msgid "If left blank, the default CA Dir will be used." msgstr "如果留空,则使用默认 CA Dir。" -#: src/views/preference/AuthSettings.vue:60 +#: src/views/preference/AuthSettings.vue:61 msgid "" "If the number of login failed attempts from a ip reach the max attempts in " "ban threshold minutes, the ip will be banned for a period of time." @@ -852,6 +872,12 @@ msgstr "" "如果某个 IP 的登录失败次数达到禁用阈值分钟内的最大尝试次数,该 IP 将被禁止登" "录一段时间。" +#: src/views/preference/components/TOTP.vue:108 +msgid "" +"If you lose your mobile phone, you can use the recovery code to reset your " +"2FA." +msgstr "如果丢失了手机,可以使用恢复代码重置二步验证。" + #: src/views/certificate/Certificate.vue:136 msgid "Import" msgstr "导入" @@ -860,7 +886,7 @@ msgstr "导入" msgid "Import Certificate" msgstr "导入证书" -#: src/views/other/Login.vue:59 +#: src/views/other/Login.vue:73 msgid "Incorrect username or password" msgstr "用户名或密码错误" @@ -876,7 +902,15 @@ msgstr "初始化核心升级程序错误" msgid "Initialing core upgrader" msgstr "初始化核心升级器" -#: src/routes/index.ts:273 src/views/other/Install.vue:135 +#: src/views/preference/components/TOTP.vue:144 +msgid "Input the code from the app:" +msgstr "输入应用程序中的代码:" + +#: src/views/other/Login.vue:194 src/views/preference/components/TOTP.vue:157 +msgid "Input the recovery code:" +msgstr "输入恢复代码:" + +#: src/routes/index.ts:283 src/views/other/Install.vue:135 msgid "Install" msgstr "安装" @@ -896,7 +930,11 @@ msgstr "间隔" msgid "Invalid" msgstr "无效的" -#: src/views/preference/AuthSettings.vue:14 +#: src/views/other/Login.vue:83 +msgid "Invalid 2FA or recovery code" +msgstr "无效的二步验证码或恢复密码" + +#: src/views/preference/AuthSettings.vue:15 msgid "IP" msgstr "IP" @@ -981,11 +1019,11 @@ msgstr "Locations" msgid "Log" msgstr "日志" -#: src/routes/index.ts:279 src/views/other/Login.vue:159 +#: src/routes/index.ts:289 src/views/other/Login.vue:218 msgid "Login" msgstr "登录" -#: src/views/other/Login.vue:109 src/views/other/Login.vue:51 +#: src/views/other/Login.vue:127 src/views/other/Login.vue:62 msgid "Login successful" msgstr "登录成功" @@ -1031,7 +1069,7 @@ msgstr "网站管理" msgid "Manage Streams" msgstr "管理 Stream" -#: src/routes/index.ts:230 src/views/user/User.vue:50 +#: src/routes/index.ts:235 src/views/user/User.vue:50 msgid "Manage Users" msgstr "用户管理" @@ -1039,7 +1077,7 @@ msgstr "用户管理" msgid "Managed Certificate" msgstr "托管证书" -#: src/views/preference/AuthSettings.vue:74 +#: src/views/preference/AuthSettings.vue:75 msgid "Max Attempts" msgstr "最大尝试次数" @@ -1165,7 +1203,7 @@ msgstr "Nginx 重启成功" #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90 #: src/views/domain/ngx_conf/LocationEditor.vue:71 #: src/views/notification/Notification.vue:70 -#: src/views/preference/AuthSettings.vue:96 +#: src/views/preference/AuthSettings.vue:97 #: src/views/preference/BasicSettings.vue:101 #: src/views/stream/StreamList.vue:165 msgid "No" @@ -1179,7 +1217,7 @@ msgstr "节点密钥" msgid "Not After" msgstr "有效期" -#: src/routes/index.ts:285 +#: src/routes/index.ts:295 msgid "Not Found" msgstr "找不到页面" @@ -1196,7 +1234,7 @@ msgstr "注意" msgid "Notification" msgstr "通知" -#: src/components/Notification/Notification.vue:82 src/routes/index.ts:221 +#: src/components/Notification/Notification.vue:82 src/routes/index.ts:226 msgid "Notifications" msgstr "通知" @@ -1276,7 +1314,7 @@ msgstr "覆盖现有文件" msgid "Params" msgstr "参数" -#: src/views/other/Login.vue:144 src/views/user/User.vue:18 +#: src/views/other/Login.vue:167 src/views/user/User.vue:18 msgid "Password" msgstr "密码" @@ -1302,6 +1340,10 @@ msgstr "执行核心升级错误" msgid "Performing core upgrade" msgstr "正在进行核心升级" +#: src/views/other/Login.vue:177 +msgid "Please enter the 2FA code:" +msgstr "请输入二步验证码:" + #: src/views/certificate/DNSCredential.vue:53 msgid "" "Please fill in the API authentication credentials provided by your DNS " @@ -1331,11 +1373,11 @@ msgstr "请输入名称,这将被用作新配置的文件名!" msgid "Please input your E-mail!" msgstr "请输入您的邮箱!" -#: src/views/other/Install.vue:45 src/views/other/Login.vue:39 +#: src/views/other/Install.vue:45 src/views/other/Login.vue:45 msgid "Please input your password!" msgstr "请输入您的密码!" -#: src/views/other/Install.vue:39 src/views/other/Login.vue:33 +#: src/views/other/Install.vue:39 src/views/other/Login.vue:39 msgid "Please input your username!" msgstr "请输入您的用户名!" @@ -1355,7 +1397,7 @@ msgstr "请至少选择一个节点!" msgid "Pre-release" msgstr "预发布" -#: src/routes/index.ts:239 src/views/preference/Preference.vue:105 +#: src/routes/index.ts:244 src/views/preference/Preference.vue:105 msgid "Preference" msgstr "偏好设置" @@ -1397,6 +1439,18 @@ msgstr "恢复" msgid "Recovered Successfully" msgstr "恢复成功" +#: src/views/other/Login.vue:204 src/views/preference/components/TOTP.vue:167 +msgid "Recovery" +msgstr "恢复" + +#: src/views/preference/components/TOTP.vue:101 +msgid "Recovery Code" +msgstr "恢复代码" + +#: src/views/preference/components/TOTP.vue:110 +msgid "Recovery Code:" +msgstr "恢复代码:" + #: src/views/preference/BasicSettings.vue:68 msgid "Recursive Nameservers" msgstr "递归域名服务器" @@ -1446,11 +1500,11 @@ msgstr "重载中" msgid "Reloading nginx" msgstr "正在重载 Nginx" -#: src/views/preference/AuthSettings.vue:101 +#: src/views/preference/AuthSettings.vue:102 msgid "Remove" msgstr "删除" -#: src/views/preference/AuthSettings.vue:47 +#: src/views/preference/AuthSettings.vue:48 msgid "Remove successfully" msgstr "移除成功" @@ -1488,6 +1542,10 @@ msgstr "请求参数错误" msgid "Reset" msgstr "重置" +#: src/views/preference/components/TOTP.vue:130 +msgid "Reset 2FA" +msgstr "重置二步验证" + #: src/components/NginxControl/NginxControl.vue:93 msgid "Restart" msgstr "重启" @@ -1535,6 +1593,10 @@ msgstr "保存成功" msgid "Saved successfully" msgstr "保存成功" +#: src/views/preference/components/TOTP.vue:91 +msgid "Scan the QR code with your mobile phone to add the account to the app." +msgstr "用手机扫描二维码,将账户添加到应用程序中。" + #: src/views/certificate/DNSChallenge.vue:89 msgid "SDK" msgstr "SDK" @@ -1558,7 +1620,9 @@ msgstr "上传" #: src/views/config/ConfigEdit.vue:40 src/views/domain/DomainList.vue:81 #: src/views/environment/BatchUpgrader.vue:57 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:69 -#: src/views/preference/AuthSettings.vue:49 +#: src/views/preference/AuthSettings.vue:50 +#: src/views/preference/components/TOTP.vue:42 +#: src/views/preference/components/TOTP.vue:55 #: src/views/preference/Preference.vue:78 src/views/stream/StreamList.vue:113 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42 msgid "Server error" @@ -1629,7 +1693,7 @@ msgstr "SSL证书密钥路径" msgid "SSL Certificate Path" msgstr "SSL证书路径" -#: src/views/other/Login.vue:170 +#: src/views/other/Login.vue:229 msgid "SSO Login" msgstr "SSO 登录" @@ -1707,7 +1771,7 @@ msgstr "同步证书成功" msgid "Sync to" msgstr "同步到" -#: src/routes/index.ts:248 +#: src/routes/index.ts:253 msgid "System" msgstr "系统" @@ -1758,6 +1822,11 @@ msgstr "路径存在,但文件不是证书" msgid "The path exists, but the file is not a private key" msgstr "路径存在,但文件不是私钥" +#: src/views/preference/components/TOTP.vue:109 +msgid "" +"The recovery code is only displayed once, please save it in a safe place." +msgstr "恢复密码只会显示一次,请妥善保存。" + #: src/views/dashboard/Environments.vue:148 msgid "" "The remote Nginx UI version is not compatible with the local Nginx UI " @@ -1816,7 +1885,7 @@ msgid "" "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}." msgstr "将 %{nodeNames} 上的 Nginx UI 升级或重新安装到 %{version} 版本。" -#: src/views/preference/AuthSettings.vue:59 +#: src/views/preference/AuthSettings.vue:60 #: src/views/preference/LogrotateSettings.vue:12 msgid "Tips" msgstr "提示" @@ -1825,6 +1894,13 @@ msgstr "提示" msgid "Title" msgstr "标题" +#: src/views/preference/components/TOTP.vue:90 +msgid "" +"To enable it, you need to install the Google or Microsoft Authenticator app " +"on your mobile phone." +msgstr "" +"要启用该功能,您需要在手机上安装 Google 或 Microsoft Authenticator 应用程序。" + #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44 msgid "" "To make sure the certification auto-renewal can work normally, we need to " @@ -1839,10 +1915,16 @@ msgstr "" msgid "Token is not valid" msgstr "Token 无效" -#: src/views/other/Login.vue:62 +#: src/views/other/Login.vue:76 msgid "Too many login failed attempts, please try again later" msgstr "登录失败次数过多,请稍后再试" +#: src/views/preference/components/TOTP.vue:89 +msgid "" +"TOTP is a two-factor authentication method that uses a time-based one-time " +"password algorithm." +msgstr "TOTP 是一种使用基于时间的一次性密码算法的双因素身份验证方法。" + #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:209 msgid "Trash" msgstr "回收站" @@ -1866,7 +1948,7 @@ msgstr "修改时间" msgid "Updated successfully" msgstr "更新成功" -#: src/routes/index.ts:263 src/views/environment/Environment.vue:50 +#: src/routes/index.ts:268 src/views/environment/Environment.vue:50 #: src/views/system/Upgrade.vue:145 src/views/system/Upgrade.vue:228 msgid "Upgrade" msgstr "升级" @@ -1895,15 +1977,19 @@ msgstr "运行时间:" msgid "URL" msgstr "URL" +#: src/views/other/Login.vue:186 +msgid "Use recovery code" +msgstr "使用恢复代码" + #: src/components/ChatGPT/ChatGPT.vue:229 msgid "User" msgstr "用户" -#: src/views/other/Login.vue:65 +#: src/views/other/Login.vue:79 msgid "User is banned" msgstr "用户被禁止" -#: src/views/other/Login.vue:134 src/views/user/User.vue:9 +#: src/views/other/Login.vue:157 src/views/user/User.vue:9 msgid "Username" msgstr "用户名" @@ -1971,7 +2057,7 @@ msgstr "正在将证书写入磁盘" #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89 #: src/views/domain/ngx_conf/LocationEditor.vue:70 -#: src/views/preference/AuthSettings.vue:95 +#: src/views/preference/AuthSettings.vue:96 #: src/views/preference/BasicSettings.vue:100 msgid "Yes" msgstr "是的" diff --git a/app/src/language/zh_TW/app.po b/app/src/language/zh_TW/app.po index 4b60fe7ad..e7d8f6b24 100644 --- a/app/src/language/zh_TW/app.po +++ b/app/src/language/zh_TW/app.po @@ -14,7 +14,11 @@ msgstr "" "Generated-By: easygettext\n" "X-Generator: Poedit 3.4.1\n" -#: src/routes/index.ts:256 +#: src/views/preference/components/TOTP.vue:88 +msgid "2FA Settings" +msgstr "" + +#: src/routes/index.ts:261 msgid "About" msgstr "關於" @@ -33,7 +37,7 @@ msgstr "使用者名稱" #: src/views/certificate/DNSCredential.vue:33 src/views/config/config.ts:34 #: src/views/domain/DomainList.vue:47 src/views/environment/envColumns.tsx:131 #: src/views/notification/Notification.vue:37 -#: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47 +#: src/views/preference/AuthSettings.vue:27 src/views/stream/StreamList.vue:47 #: src/views/user/User.vue:43 msgid "Action" msgstr "操作" @@ -100,7 +104,7 @@ msgstr "API Token" msgid "Arch" msgstr "架構" -#: src/views/preference/AuthSettings.vue:94 +#: src/views/preference/AuthSettings.vue:95 #, fuzzy msgid "Are you sure to delete this banned IP immediately?" msgstr "您確定要刪除嗎?" @@ -155,7 +159,7 @@ msgstr "向 ChatGPT 尋求幫助" msgid "Assistant" msgstr "助理" -#: src/views/preference/AuthSettings.vue:17 +#: src/views/preference/AuthSettings.vue:18 msgid "Attempts" msgstr "" @@ -200,15 +204,15 @@ msgstr "返回首頁" msgid "Back to list" msgstr "" -#: src/views/preference/AuthSettings.vue:68 +#: src/views/preference/AuthSettings.vue:69 msgid "Ban Threshold Minutes" msgstr "" -#: src/views/preference/AuthSettings.vue:82 +#: src/views/preference/AuthSettings.vue:83 msgid "Banned IPs" msgstr "" -#: src/views/preference/AuthSettings.vue:20 +#: src/views/preference/AuthSettings.vue:21 msgid "Banned Until" msgstr "" @@ -404,6 +408,14 @@ msgstr "認證" msgid "Credentials" msgstr "認證資訊" +#: src/views/preference/components/TOTP.vue:96 +msgid "Current account is enabled 2FA." +msgstr "" + +#: src/views/preference/components/TOTP.vue:93 +msgid "Current account is not enabled 2FA." +msgstr "" + #: src/views/system/Upgrade.vue:167 msgid "Current Version" msgstr "目前版本" @@ -680,6 +692,16 @@ msgstr "在 %{node_name} 啟用 %{conf_name} 失敗" msgid "Enable %{conf_name} in %{node_name} successfully" msgstr "成功在 %{node_name} 啟用 %{conf_name}" +#: src/views/preference/components/TOTP.vue:122 +#, fuzzy +msgid "Enable 2FA" +msgstr "啟用" + +#: src/views/preference/components/TOTP.vue:52 +#, fuzzy +msgid "Enable 2FA successfully" +msgstr "啟用成功" + #: src/views/domain/cert/components/ObtainCert.vue:70 msgid "Enable auto-renewal failed for %{name}" msgstr "啟用 %{name} 自動續簽失敗" @@ -878,12 +900,18 @@ msgstr "HTTP01" msgid "If left blank, the default CA Dir will be used." msgstr "" -#: src/views/preference/AuthSettings.vue:60 +#: src/views/preference/AuthSettings.vue:61 msgid "" "If the number of login failed attempts from a ip reach the max attempts in " "ban threshold minutes, the ip will be banned for a period of time." msgstr "" +#: src/views/preference/components/TOTP.vue:108 +msgid "" +"If you lose your mobile phone, you can use the recovery code to reset your " +"2FA." +msgstr "" + #: src/views/certificate/Certificate.vue:136 #, fuzzy msgid "Import" @@ -894,7 +922,7 @@ msgstr "匯出" msgid "Import Certificate" msgstr "憑證狀態" -#: src/views/other/Login.vue:59 +#: src/views/other/Login.vue:73 #, fuzzy msgid "Incorrect username or password" msgstr "使用者名稱或密碼不正確" @@ -911,7 +939,15 @@ msgstr "初始化核心升級程式錯誤" msgid "Initialing core upgrader" msgstr "正在初始化核心升級程式" -#: src/routes/index.ts:273 src/views/other/Install.vue:135 +#: src/views/preference/components/TOTP.vue:144 +msgid "Input the code from the app:" +msgstr "" + +#: src/views/other/Login.vue:194 src/views/preference/components/TOTP.vue:157 +msgid "Input the recovery code:" +msgstr "" + +#: src/routes/index.ts:283 src/views/other/Install.vue:135 msgid "Install" msgstr "安裝" @@ -932,7 +968,11 @@ msgstr "" msgid "Invalid" msgstr "無效的郵箱!" -#: src/views/preference/AuthSettings.vue:14 +#: src/views/other/Login.vue:83 +msgid "Invalid 2FA or recovery code" +msgstr "" + +#: src/views/preference/AuthSettings.vue:15 msgid "IP" msgstr "" @@ -1025,11 +1065,11 @@ msgstr "Locations" msgid "Log" msgstr "登入" -#: src/routes/index.ts:279 src/views/other/Login.vue:159 +#: src/routes/index.ts:289 src/views/other/Login.vue:218 msgid "Login" msgstr "登入" -#: src/views/other/Login.vue:109 src/views/other/Login.vue:51 +#: src/views/other/Login.vue:127 src/views/other/Login.vue:62 msgid "Login successful" msgstr "登入成功" @@ -1071,7 +1111,7 @@ msgstr "管理網站" msgid "Manage Streams" msgstr "管理網站" -#: src/routes/index.ts:230 src/views/user/User.vue:50 +#: src/routes/index.ts:235 src/views/user/User.vue:50 msgid "Manage Users" msgstr "管理使用者" @@ -1080,7 +1120,7 @@ msgstr "管理使用者" msgid "Managed Certificate" msgstr "更換憑證" -#: src/views/preference/AuthSettings.vue:74 +#: src/views/preference/AuthSettings.vue:75 msgid "Max Attempts" msgstr "" @@ -1209,7 +1249,7 @@ msgstr "Nginx 重啟成功" #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:90 #: src/views/domain/ngx_conf/LocationEditor.vue:71 #: src/views/notification/Notification.vue:70 -#: src/views/preference/AuthSettings.vue:96 +#: src/views/preference/AuthSettings.vue:97 #: src/views/preference/BasicSettings.vue:101 #: src/views/stream/StreamList.vue:165 msgid "No" @@ -1223,7 +1263,7 @@ msgstr "Node Secret" msgid "Not After" msgstr "" -#: src/routes/index.ts:285 +#: src/routes/index.ts:295 msgid "Not Found" msgstr "找不到頁面" @@ -1241,7 +1281,7 @@ msgstr "備註" msgid "Notification" msgstr "憑證" -#: src/components/Notification/Notification.vue:82 src/routes/index.ts:221 +#: src/components/Notification/Notification.vue:82 src/routes/index.ts:226 #, fuzzy msgid "Notifications" msgstr "憑證" @@ -1322,7 +1362,7 @@ msgstr "覆蓋現有檔案" msgid "Params" msgstr "參數" -#: src/views/other/Login.vue:144 src/views/user/User.vue:18 +#: src/views/other/Login.vue:167 src/views/user/User.vue:18 msgid "Password" msgstr "密碼" @@ -1348,6 +1388,10 @@ msgstr "執行核心升級錯誤" msgid "Performing core upgrade" msgstr "正在執行核心升級" +#: src/views/other/Login.vue:177 +msgid "Please enter the 2FA code:" +msgstr "" + #: src/views/certificate/DNSCredential.vue:53 msgid "" "Please fill in the API authentication credentials provided by your DNS " @@ -1378,11 +1422,11 @@ msgstr "請輸入名稱,這將作為新設定的檔名!" msgid "Please input your E-mail!" msgstr "請輸入您的電子郵件!" -#: src/views/other/Install.vue:45 src/views/other/Login.vue:39 +#: src/views/other/Install.vue:45 src/views/other/Login.vue:45 msgid "Please input your password!" msgstr "請輸入您的密碼!" -#: src/views/other/Install.vue:39 src/views/other/Login.vue:33 +#: src/views/other/Install.vue:39 src/views/other/Login.vue:39 msgid "Please input your username!" msgstr "請輸入您的使用者名稱!" @@ -1402,7 +1446,7 @@ msgstr "請至少選擇一個節點!" msgid "Pre-release" msgstr "預先發布" -#: src/routes/index.ts:239 src/views/preference/Preference.vue:105 +#: src/routes/index.ts:244 src/views/preference/Preference.vue:105 msgid "Preference" msgstr "偏好設定" @@ -1445,6 +1489,18 @@ msgstr "" msgid "Recovered Successfully" msgstr "儲存成功" +#: src/views/other/Login.vue:204 src/views/preference/components/TOTP.vue:167 +msgid "Recovery" +msgstr "" + +#: src/views/preference/components/TOTP.vue:101 +msgid "Recovery Code" +msgstr "" + +#: src/views/preference/components/TOTP.vue:110 +msgid "Recovery Code:" +msgstr "" + #: src/views/preference/BasicSettings.vue:68 #, fuzzy msgid "Recursive Nameservers" @@ -1499,11 +1555,11 @@ msgstr "重新載入中" msgid "Reloading nginx" msgstr "正在重新載入 Nginx" -#: src/views/preference/AuthSettings.vue:101 +#: src/views/preference/AuthSettings.vue:102 msgid "Remove" msgstr "" -#: src/views/preference/AuthSettings.vue:47 +#: src/views/preference/AuthSettings.vue:48 #, fuzzy msgid "Remove successfully" msgstr "儲存成功" @@ -1548,6 +1604,11 @@ msgstr "請求參數錯誤" msgid "Reset" msgstr "重設" +#: src/views/preference/components/TOTP.vue:130 +#, fuzzy +msgid "Reset 2FA" +msgstr "重設" + #: src/components/NginxControl/NginxControl.vue:93 msgid "Restart" msgstr "重新啟動" @@ -1595,6 +1656,10 @@ msgstr "儲存成功" msgid "Saved successfully" msgstr "儲存成功" +#: src/views/preference/components/TOTP.vue:91 +msgid "Scan the QR code with your mobile phone to add the account to the app." +msgstr "" + #: src/views/certificate/DNSChallenge.vue:89 msgid "SDK" msgstr "" @@ -1618,7 +1683,9 @@ msgstr "傳送" #: src/views/config/ConfigEdit.vue:40 src/views/domain/DomainList.vue:81 #: src/views/environment/BatchUpgrader.vue:57 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:69 -#: src/views/preference/AuthSettings.vue:49 +#: src/views/preference/AuthSettings.vue:50 +#: src/views/preference/components/TOTP.vue:42 +#: src/views/preference/components/TOTP.vue:55 #: src/views/preference/Preference.vue:78 src/views/stream/StreamList.vue:113 #: src/views/stream/StreamList.vue:81 src/views/system/Upgrade.vue:42 msgid "Server error" @@ -1694,7 +1761,7 @@ msgstr "SSL 憑證金鑰路徑" msgid "SSL Certificate Path" msgstr "SSL 憑證路徑" -#: src/views/other/Login.vue:170 +#: src/views/other/Login.vue:229 #, fuzzy msgid "SSO Login" msgstr "登入" @@ -1778,7 +1845,7 @@ msgstr "更換憑證" msgid "Sync to" msgstr "" -#: src/routes/index.ts:248 +#: src/routes/index.ts:253 msgid "System" msgstr "系統" @@ -1832,6 +1899,11 @@ msgstr "SSL 憑證金鑰路徑" msgid "The path exists, but the file is not a private key" msgstr "" +#: src/views/preference/components/TOTP.vue:109 +msgid "" +"The recovery code is only displayed once, please save it in a safe place." +msgstr "" + #: src/views/dashboard/Environments.vue:148 msgid "" "The remote Nginx UI version is not compatible with the local Nginx UI " @@ -1892,7 +1964,7 @@ msgid "" "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}." msgstr "" -#: src/views/preference/AuthSettings.vue:59 +#: src/views/preference/AuthSettings.vue:60 #: src/views/preference/LogrotateSettings.vue:12 msgid "Tips" msgstr "" @@ -1901,6 +1973,12 @@ msgstr "" msgid "Title" msgstr "" +#: src/views/preference/components/TOTP.vue:90 +msgid "" +"To enable it, you need to install the Google or Microsoft Authenticator app " +"on your mobile phone." +msgstr "" + #: src/views/domain/ngx_conf/NgxConfigEditor.vue:44 msgid "" "To make sure the certification auto-renewal can work normally, we need to " @@ -1915,10 +1993,16 @@ msgstr "" msgid "Token is not valid" msgstr "" -#: src/views/other/Login.vue:62 +#: src/views/other/Login.vue:76 msgid "Too many login failed attempts, please try again later" msgstr "" +#: src/views/preference/components/TOTP.vue:89 +msgid "" +"TOTP is a two-factor authentication method that uses a time-based one-time " +"password algorithm." +msgstr "" + #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:209 msgid "Trash" msgstr "" @@ -1942,7 +2026,7 @@ msgstr "更新時間" msgid "Updated successfully" msgstr "更新成功" -#: src/routes/index.ts:263 src/views/environment/Environment.vue:50 +#: src/routes/index.ts:268 src/views/environment/Environment.vue:50 #: src/views/system/Upgrade.vue:145 src/views/system/Upgrade.vue:228 msgid "Upgrade" msgstr "升級" @@ -1972,15 +2056,19 @@ msgstr "運作時間:" msgid "URL" msgstr "URL" +#: src/views/other/Login.vue:186 +msgid "Use recovery code" +msgstr "" + #: src/components/ChatGPT/ChatGPT.vue:229 msgid "User" msgstr "使用者名稱" -#: src/views/other/Login.vue:65 +#: src/views/other/Login.vue:79 msgid "User is banned" msgstr "" -#: src/views/other/Login.vue:134 src/views/user/User.vue:9 +#: src/views/other/Login.vue:157 src/views/user/User.vue:9 msgid "Username" msgstr "使用者名稱" @@ -2052,7 +2140,7 @@ msgstr "將憑證寫入磁碟" #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:89 #: src/views/domain/ngx_conf/LocationEditor.vue:70 -#: src/views/preference/AuthSettings.vue:95 +#: src/views/preference/AuthSettings.vue:96 #: src/views/preference/BasicSettings.vue:100 msgid "Yes" msgstr "是的" From 3a22861640f4e19ad6efa60d02fb813a0bd00c5c Mon Sep 17 00:00:00 2001 From: Jacky Date: Tue, 23 Jul 2024 20:35:32 +0800 Subject: [PATCH 3/8] feat: 2FA authorization for web terminal --- api/user/auth.go | 2 +- api/user/otp.go | 50 +++++++++++-- api/user/router.go | 3 +- app/components.d.ts | 3 + app/src/api/otp.ts | 6 ++ app/src/components/OTP/OTPAuthorization.vue | 78 +++++++++++++++++++++ app/src/components/OTP/useOTPModal.ts | 75 ++++++++++++++++++++ app/src/views/other/Login.vue | 50 ++++--------- app/src/views/pty/Terminal.vue | 46 +++++++++--- internal/cache/cache.go | 31 ++++++++ internal/kernal/boot.go | 2 + internal/user/otp.go | 24 +++++++ model/auth.go | 4 ++ router/middleware.go | 36 ++++++++++ router/routers.go | 5 +- 15 files changed, 360 insertions(+), 55 deletions(-) create mode 100644 app/src/components/OTP/OTPAuthorization.vue create mode 100644 app/src/components/OTP/useOTPModal.ts create mode 100644 internal/cache/cache.go diff --git a/api/user/auth.go b/api/user/auth.go index 26ff86cf9..5f750c081 100644 --- a/api/user/auth.go +++ b/api/user/auth.go @@ -86,7 +86,7 @@ func Login(c *gin.Context) { } // Check if the user enables 2FA - if len(u.OTPSecret) > 0 { + if u.EnabledOTP() { if json.OTP == "" && json.RecoveryCode == "" { c.JSON(http.StatusOK, LoginResponse{ Message: "The user has enabled 2FA", diff --git a/api/user/otp.go b/api/user/otp.go index 13bcc3e84..d6afedd4c 100644 --- a/api/user/otp.go +++ b/api/user/otp.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/0xJacky/Nginx-UI/api" "github.com/0xJacky/Nginx-UI/internal/crypto" + "github.com/0xJacky/Nginx-UI/internal/user" "github.com/0xJacky/Nginx-UI/query" "github.com/0xJacky/Nginx-UI/settings" "github.com/gin-gonic/gin" @@ -67,8 +68,8 @@ func GenerateTOTP(c *gin.Context) { } func EnrollTOTP(c *gin.Context) { - user := api.CurrentUser(c) - if len(user.OTPSecret) > 0 { + cUser := api.CurrentUser(c) + if cUser.EnabledOTP() { c.JSON(http.StatusBadRequest, gin.H{ "message": "User already enrolled", }) @@ -109,7 +110,7 @@ func EnrollTOTP(c *gin.Context) { } u := query.Auth - _, err = u.Where(u.ID.Eq(user.ID)).Update(u.OTPSecret, ciphertext) + _, err = u.Where(u.ID.Eq(cUser.ID)).Update(u.OTPSecret, ciphertext) if err != nil { api.ErrHandler(c, err) return @@ -135,8 +136,8 @@ func ResetOTP(c *gin.Context) { api.ErrHandler(c, err) return } - user := api.CurrentUser(c) - k := sha1.Sum(user.OTPSecret) + cUser := api.CurrentUser(c) + k := sha1.Sum(cUser.OTPSecret) if !bytes.Equal(k[:], recoverCode) { c.JSON(http.StatusBadRequest, gin.H{ "message": "Invalid recovery code", @@ -145,7 +146,7 @@ func ResetOTP(c *gin.Context) { } u := query.Auth - _, err = u.Where(u.ID.Eq(user.ID)).UpdateSimple(u.OTPSecret.Null()) + _, err = u.Where(u.ID.Eq(cUser.ID)).UpdateSimple(u.OTPSecret.Null()) if err != nil { api.ErrHandler(c, err) return @@ -161,3 +162,40 @@ func OTPStatus(c *gin.Context) { "status": len(api.CurrentUser(c).OTPSecret) > 0, }) } + +func StartSecure2FASession(c *gin.Context) { + var json struct { + OTP string `json:"otp"` + RecoveryCode string `json:"recovery_code"` + } + if !api.BindAndValid(c, &json) { + return + } + u := api.CurrentUser(c) + if !u.EnabledOTP() { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "User not configured with 2FA", + }) + return + } + + if json.OTP == "" && json.RecoveryCode == "" { + c.JSON(http.StatusBadRequest, LoginResponse{ + Message: "The user has enabled 2FA", + }) + return + } + + if err := user.VerifyOTP(u, json.OTP, json.RecoveryCode); err != nil { + c.JSON(http.StatusBadRequest, LoginResponse{ + Message: "Invalid 2FA or recovery code", + }) + return + } + + sessionId := user.SetSecureSessionID(u.ID) + + c.JSON(http.StatusOK, gin.H{ + "session_id": sessionId, + }) +} diff --git a/api/user/router.go b/api/user/router.go index 565985ddf..f4b8c3543 100644 --- a/api/user/router.go +++ b/api/user/router.go @@ -22,5 +22,6 @@ func InitUserRouter(r *gin.RouterGroup) { r.GET("/otp_status", OTPStatus) r.GET("/otp_secret", GenerateTOTP) r.POST("/otp_enroll", EnrollTOTP) - r.POST("/otp_reset", ResetOTP) + r.POST("/otp_reset", ResetOTP) + r.POST("/otp_secure_session", StartSecure2FASession) } diff --git a/app/components.d.ts b/app/components.d.ts index 507cb693e..a4430ce22 100644 --- a/app/components.d.ts +++ b/app/components.d.ts @@ -78,8 +78,11 @@ declare module 'vue' { NginxControlNginxControl: typeof import('./src/components/NginxControl/NginxControl.vue')['default'] NodeSelectorNodeSelector: typeof import('./src/components/NodeSelector/NodeSelector.vue')['default'] NotificationNotification: typeof import('./src/components/Notification/Notification.vue')['default'] + OTP: typeof import('./src/components/OTP.vue')['default'] OTPInput: typeof import('./src/components/OTPInput.vue')['default'] OTPInputOTPInput: typeof import('./src/components/OTPInput/OTPInput.vue')['default'] + OTPOTPAuthorization: typeof import('./src/components/OTP/OTPAuthorization.vue')['default'] + OTPOTPAuthorizationModal: typeof import('./src/components/OTP/OTPAuthorizationModal.vue')['default'] PageHeaderPageHeader: typeof import('./src/components/PageHeader/PageHeader.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] diff --git a/app/src/api/otp.ts b/app/src/api/otp.ts index ba8f01806..aacd02895 100644 --- a/app/src/api/otp.ts +++ b/app/src/api/otp.ts @@ -18,6 +18,12 @@ const otp = { reset(recovery_code: string) { return http.post('/otp_reset', { recovery_code }) }, + start_secure_session(passcode: string, recovery_code: string): Promise<{ session_id: string }> { + return http.post('/otp_secure_session', { + otp: passcode, + recovery_code, + }) + }, } export default otp diff --git a/app/src/components/OTP/OTPAuthorization.vue b/app/src/components/OTP/OTPAuthorization.vue new file mode 100644 index 000000000..840ebf994 --- /dev/null +++ b/app/src/components/OTP/OTPAuthorization.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/app/src/components/OTP/useOTPModal.ts b/app/src/components/OTP/useOTPModal.ts new file mode 100644 index 000000000..6e2880c46 --- /dev/null +++ b/app/src/components/OTP/useOTPModal.ts @@ -0,0 +1,75 @@ +import { createVNode, render } from 'vue' +import { Modal, message } from 'ant-design-vue' +import OTPAuthorization from '@/components/OTP/OTPAuthorization.vue' +import otp from '@/api/otp' + +export interface OTPModalProps { + onOk?: (secureSessionId: string) => void + onCancel?: () => void +} + +const useOTPModal = () => { + const refOTPAuthorization = ref() + const randomId = Math.random().toString(36).substring(2, 8) + + const injectStyles = () => { + const style = document.createElement('style') + + style.innerHTML = ` + .${randomId} .ant-modal-title { + font-size: 1.125rem; + } + ` + document.head.appendChild(style) + } + + const open = ({ onOk, onCancel }: OTPModalProps) => { + injectStyles() + let container: HTMLDivElement | null = document.createElement('div') + document.body.appendChild(container) + + const close = () => { + render(null, container!) + document.body.removeChild(container!) + container = null + } + + const verify = (passcode: string, recovery: string) => { + otp.start_secure_session(passcode, recovery).then(r => { + onOk?.(r.session_id) + close() + }).catch(async () => { + refOTPAuthorization.value?.clearInput() + await message.error($gettext('Invalid passcode or recovery code')) + }) + } + + const vnode = createVNode(Modal, { + open: true, + title: $gettext('Two-factor authentication required'), + centered: true, + maskClosable: false, + class: randomId, + footer: false, + onCancel: () => { + close() + onCancel?.() + }, + }, { + default: () => h( + OTPAuthorization, + { + ref: refOTPAuthorization, + class: 'mt-3', + onOnSubmit: verify, + }, + ), + }) + + render(vnode, container) + } + + return { open } +} + +export default useOTPModal diff --git a/app/src/views/other/Login.vue b/app/src/views/other/Login.vue index 020227931..b2e3a2ca5 100644 --- a/app/src/views/other/Login.vue +++ b/app/src/views/other/Login.vue @@ -1,14 +1,13 @@ @@ -173,38 +175,10 @@ function clickUseRecoveryCode() {
-
-

{{ $gettext('Please enter the 2FA code:') }}

- - - -
- -
-

{{ $gettext('Input the recovery code:') }}

- - - - {{ $gettext('Recovery') }} - - -
+
diff --git a/app/src/views/pty/Terminal.vue b/app/src/views/pty/Terminal.vue index 4727dd71a..fca172768 100644 --- a/app/src/views/pty/Terminal.vue +++ b/app/src/views/pty/Terminal.vue @@ -2,20 +2,43 @@ import '@xterm/xterm/css/xterm.css' import { Terminal } from '@xterm/xterm' import { FitAddon } from '@xterm/addon-fit' -import { onMounted, onUnmounted } from 'vue' import _ from 'lodash' import ws from '@/lib/websocket' +import useOTPModal from '@/components/OTP/useOTPModal' let term: Terminal | null let ping: NodeJS.Timeout -const websocket = ws('/api/pty') +const router = useRouter() +const websocket = shallowRef() +const lostConnection = ref(false) onMounted(() => { - initTerm() - - websocket.onmessage = wsOnMessage - websocket.onopen = wsOnOpen + const otpModal = useOTPModal() + + otpModal.open({ + onOk(secureSessionId: string) { + websocket.value = ws(`/api/pty?X-Secure-Session-ID=${secureSessionId}`, false) + + nextTick(() => { + initTerm() + websocket.value.onmessage = wsOnMessage + websocket.value.onopen = wsOnOpen + websocket.value.onerror = () => { + lostConnection.value = true + } + websocket.value.onclose = () => { + lostConnection.value = true + } + }) + }, + onCancel() { + if (window.history.length > 1) + router.go(-1) + else + router.push('/') + }, + }) }) interface Message { @@ -65,7 +88,7 @@ function initTerm() { } function sendMessage(data: Message) { - websocket.send(JSON.stringify(data)) + websocket.value.send(JSON.stringify(data)) } function wsOnMessage(msg: { data: string | Uint8Array }) { @@ -82,13 +105,20 @@ onUnmounted(() => { window.removeEventListener('resize', fit) clearInterval(ping) term?.dispose() - websocket.close() + websocket.value?.close() })