2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 06:47:20 +08:00

feat: 2fa相关接口

This commit is contained in:
2025-05-14 03:59:22 +08:00
parent 4386334c31
commit daccf651b6
9 changed files with 390 additions and 376 deletions

View File

@@ -1,6 +1,7 @@
package biz
import (
"image"
"time"
"gorm.io/gorm"
@@ -18,8 +19,13 @@ type User struct {
}
type UserRepo interface {
Create(username, password string) (*User, error)
CheckPassword(username, password string) (*User, error)
List(page, limit uint) ([]*User, int64, error)
Get(id uint) (*User, error)
Save(user *User) error
Create(username, password string) (*User, error)
UpdatePassword(id uint, password string) error
Delete(id uint) error
CheckPassword(username, password string) (*User, error)
IsTwoFA(username string) (bool, error)
GenerateTwoFA(id uint) (image.Image, string, string, error)
UpdateTwoFA(id uint, code, secret string) error
}

View File

@@ -10,8 +10,6 @@ import (
"github.com/go-rat/utils/hash"
"github.com/knadh/koanf/v2"
"github.com/leonelquinteros/gotext"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/spf13/cast"
"gopkg.in/yaml.v3"
"gorm.io/gorm"
@@ -382,19 +380,3 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
return restartFlag, nil
}
// GetTwoFA 生成两步验证密钥
// TODO: 即将废弃
func (r *settingRepo) GetTwoFA() (*otp.Key, error) {
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "RatPanel",
AccountName: "admin",
SecretSize: 32,
Algorithm: otp.AlgorithmSHA256,
})
if err != nil {
return nil, err
}
return key, nil
}

View File

@@ -2,9 +2,13 @@ package data
import (
"errors"
"image"
"github.com/go-rat/utils/hash"
"github.com/leonelquinteros/gotext"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/spf13/cast"
"gorm.io/gorm"
"github.com/tnb-labs/panel/internal/biz"
@@ -24,6 +28,22 @@ func NewUserRepo(t *gotext.Locale, db *gorm.DB) biz.UserRepo {
}
}
func (r *userRepo) List(page, limit uint) ([]*biz.User, int64, error) {
users := make([]*biz.User, 0)
var total int64
err := r.db.Model(&biz.User{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&users).Error
return users, total, err
}
func (r *userRepo) Get(id uint) (*biz.User, error) {
user := new(biz.User)
if err := r.db.First(user, id).Error; err != nil {
return nil, err
}
return user, nil
}
func (r *userRepo) Create(username, password string) (*biz.User, error) {
value, err := r.hasher.Make(password)
if err != nil {
@@ -41,6 +61,34 @@ func (r *userRepo) Create(username, password string) (*biz.User, error) {
return user, nil
}
func (r *userRepo) UpdatePassword(id uint, password string) error {
value, err := r.hasher.Make(password)
if err != nil {
return err
}
user, err := r.Get(id)
if err != nil {
return err
}
user.Password = value
return r.db.Save(user).Error
}
func (r *userRepo) Delete(id uint) error {
if id == 1 {
return errors.New(r.t.Get("please don't do this"))
}
user := new(biz.User)
if err := r.db.First(user, id).Error; err != nil {
return err
}
return r.db.Delete(user).Error
}
func (r *userRepo) CheckPassword(username, password string) (*biz.User, error) {
user := new(biz.User)
if err := r.db.Where("username = ?", username).First(user).Error; err != nil {
@@ -58,15 +106,66 @@ func (r *userRepo) CheckPassword(username, password string) (*biz.User, error) {
return user, nil
}
func (r *userRepo) Get(id uint) (*biz.User, error) {
func (r *userRepo) IsTwoFA(username string) (bool, error) {
user := new(biz.User)
if err := r.db.First(user, id).Error; err != nil {
return nil, err
if err := r.db.Where("username = ?", username).First(user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, errors.New(r.t.Get("username or password error"))
} else {
return false, err
}
}
return user, nil
return user.TwoFA != "", nil
}
func (r *userRepo) Save(user *biz.User) error {
func (r *userRepo) GenerateTwoFA(id uint) (image.Image, string, string, error) {
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "RatPanel",
AccountName: cast.ToString(id),
SecretSize: 32,
Algorithm: otp.AlgorithmSHA256,
})
if err != nil {
return nil, "", "", err
}
img, err := key.Image(200, 200)
if err != nil {
return nil, "", "", err
}
return img, key.URL(), key.Secret(), nil
}
func (r *userRepo) UpdateTwoFA(id uint, code, secret string) error {
user, err := r.Get(id)
if err != nil {
return err
}
// 保存前先验证一次,防止错误开启
if !totp.Validate(code, secret) {
return errors.New(r.t.Get("invalid 2fa code"))
}
user.TwoFA = secret
return r.db.Save(user).Error
}
func (r *userRepo) CheckTwoFA(id uint, code string) (bool, error) {
user, err := r.Get(id)
if err != nil {
return false, err
}
if user.TwoFA == "" {
return true, nil // 未开启2FA无需验证
}
if !totp.Validate(code, user.TwoFA) {
return false, errors.New(r.t.Get("invalid 2fa code"))
}
return true, nil
}

View File

@@ -1,7 +1,37 @@
package request
type UserLogin struct {
Username string `json:"username" form:"username" validate:"required"`
Password string `json:"password" form:"password" validate:"required"`
SafeLogin bool `json:"safe_login" form:"safe_login"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
SafeLogin bool `json:"safe_login"`
PassCode string `json:"pass_code"`
}
type UserIsTwoFA struct {
Username string `uri:"username" validate:"required"`
}
type UserCreate struct {
Username string `json:"username" validate:"required|notExists:users,username"`
Password string `json:"password" validate:"required|password"`
}
type UserUpdatePassword struct {
ID uint `json:"id" validate:"required|exists:users,id"`
Password string `json:"password" validate:"required|password"`
}
type UserUpdateEmail struct {
ID uint `json:"id" validate:"required|exists:users,id"`
TwoFA string `json:"two_fa" validate:"required"`
}
type UserUpdateTwoFA struct {
ID uint `json:"id" validate:"required|exists:users,id"`
TwoFA string `json:"two_fa" validate:"required"`
Code string `json:"code" validate:"required"`
}
type UserDelete struct {
ID uint `json:"id" validate:"required|exists:users,id"`
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/go-rat/sessions"
"github.com/knadh/koanf/v2"
"github.com/leonelquinteros/gotext"
"github.com/pquerna/otp/totp"
"github.com/spf13/cast"
"github.com/tnb-labs/panel/internal/biz"
@@ -87,6 +88,13 @@ func (s *UserService) Login(w http.ResponseWriter, r *http.Request) {
return
}
if user.TwoFA != "" {
if !totp.Validate(req.PassCode, user.TwoFA) {
Error(w, http.StatusForbidden, s.t.Get("invalid 2fa code"))
return
}
}
// 安全登录下,将当前客户端与会话绑定
// 安全登录只在未启用面板 HTTPS 时生效
ip, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr))
@@ -128,6 +136,17 @@ func (s *UserService) IsLogin(w http.ResponseWriter, r *http.Request) {
Success(w, sess.Has("user_id"))
}
func (s *UserService) IsTwoFA(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.UserIsTwoFA](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
twoFA, _ := s.userRepo.IsTwoFA(req.Username)
Success(w, twoFA)
}
func (s *UserService) Info(w http.ResponseWriter, r *http.Request) {
userID := cast.ToUint(r.Context().Value("user_id"))
if userID == 0 {
@@ -148,3 +167,53 @@ func (s *UserService) Info(w http.ResponseWriter, r *http.Request) {
"email": user.Email,
})
}
func (s *UserService) List(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.Paginate](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
users, total, err := s.userRepo.List(req.Page, req.Limit)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, chix.M{
"total": total,
"items": users,
})
}
func (s *UserService) Create(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.UserCreate](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
user, err := s.userRepo.Create(req.Username, req.Password)
if err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, user)
}
func (s *UserService) UpdatePassword(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.UserUpdatePassword](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = s.userRepo.UpdatePassword(req.ID, req.Password); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return
}
Success(w, nil)
}

View File

@@ -9,6 +9,8 @@ import (
mock "github.com/stretchr/testify/mock"
otp "github.com/pquerna/otp"
request "github.com/tnb-labs/panel/internal/http/request"
)
@@ -71,6 +73,118 @@ func (_c *SettingRepo_Delete_Call) RunAndReturn(run func(biz.SettingKey) error)
return _c
}
// GenerateAPIKey provides a mock function with no fields
func (_m *SettingRepo) GenerateAPIKey() (string, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GenerateAPIKey")
}
var r0 string
var r1 error
if rf, ok := ret.Get(0).(func() (string, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SettingRepo_GenerateAPIKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GenerateAPIKey'
type SettingRepo_GenerateAPIKey_Call struct {
*mock.Call
}
// GenerateAPIKey is a helper method to define mock.On call
func (_e *SettingRepo_Expecter) GenerateAPIKey() *SettingRepo_GenerateAPIKey_Call {
return &SettingRepo_GenerateAPIKey_Call{Call: _e.mock.On("GenerateAPIKey")}
}
func (_c *SettingRepo_GenerateAPIKey_Call) Run(run func()) *SettingRepo_GenerateAPIKey_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *SettingRepo_GenerateAPIKey_Call) Return(_a0 string, _a1 error) *SettingRepo_GenerateAPIKey_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *SettingRepo_GenerateAPIKey_Call) RunAndReturn(run func() (string, error)) *SettingRepo_GenerateAPIKey_Call {
_c.Call.Return(run)
return _c
}
// GenerateTwoFAKey provides a mock function with no fields
func (_m *SettingRepo) GenerateTwoFAKey() (*otp.Key, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GenerateTwoFAKey")
}
var r0 *otp.Key
var r1 error
if rf, ok := ret.Get(0).(func() (*otp.Key, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() *otp.Key); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*otp.Key)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SettingRepo_GenerateTwoFAKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GenerateTwoFAKey'
type SettingRepo_GenerateTwoFAKey_Call struct {
*mock.Call
}
// GenerateTwoFAKey is a helper method to define mock.On call
func (_e *SettingRepo_Expecter) GenerateTwoFAKey() *SettingRepo_GenerateTwoFAKey_Call {
return &SettingRepo_GenerateTwoFAKey_Call{Call: _e.mock.On("GenerateTwoFAKey")}
}
func (_c *SettingRepo_GenerateTwoFAKey_Call) Run(run func()) *SettingRepo_GenerateTwoFAKey_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *SettingRepo_GenerateTwoFAKey_Call) Return(_a0 *otp.Key, _a1 error) *SettingRepo_GenerateTwoFAKey_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *SettingRepo_GenerateTwoFAKey_Call) RunAndReturn(run func() (*otp.Key, error)) *SettingRepo_GenerateTwoFAKey_Call {
_c.Call.Return(run)
return _c
}
// Get provides a mock function with given fields: key, defaultValue
func (_m *SettingRepo) Get(key biz.SettingKey, defaultValue ...string) (string, error) {
_va := make([]interface{}, len(defaultValue))

View File

@@ -1,198 +0,0 @@
// Code generated by mockery. DO NOT EDIT.
package biz
import (
mock "github.com/stretchr/testify/mock"
biz "github.com/tnb-labs/panel/internal/biz"
)
// UserTokenRepo is an autogenerated mock type for the UserTokenRepo type
type UserTokenRepo struct {
mock.Mock
}
type UserTokenRepo_Expecter struct {
mock *mock.Mock
}
func (_m *UserTokenRepo) EXPECT() *UserTokenRepo_Expecter {
return &UserTokenRepo_Expecter{mock: &_m.Mock}
}
// Create provides a mock function with given fields: userID, ips
func (_m *UserTokenRepo) Create(userID uint, ips []string) (*biz.UserToken, error) {
ret := _m.Called(userID, ips)
if len(ret) == 0 {
panic("no return value specified for Create")
}
var r0 *biz.UserToken
var r1 error
if rf, ok := ret.Get(0).(func(uint, []string) (*biz.UserToken, error)); ok {
return rf(userID, ips)
}
if rf, ok := ret.Get(0).(func(uint, []string) *biz.UserToken); ok {
r0 = rf(userID, ips)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*biz.UserToken)
}
}
if rf, ok := ret.Get(1).(func(uint, []string) error); ok {
r1 = rf(userID, ips)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UserTokenRepo_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create'
type UserTokenRepo_Create_Call struct {
*mock.Call
}
// Create is a helper method to define mock.On call
// - userID uint
// - ips []string
func (_e *UserTokenRepo_Expecter) Create(userID interface{}, ips interface{}) *UserTokenRepo_Create_Call {
return &UserTokenRepo_Create_Call{Call: _e.mock.On("Create", userID, ips)}
}
func (_c *UserTokenRepo_Create_Call) Run(run func(userID uint, ips []string)) *UserTokenRepo_Create_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(uint), args[1].([]string))
})
return _c
}
func (_c *UserTokenRepo_Create_Call) Return(_a0 *biz.UserToken, _a1 error) *UserTokenRepo_Create_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *UserTokenRepo_Create_Call) RunAndReturn(run func(uint, []string) (*biz.UserToken, error)) *UserTokenRepo_Create_Call {
_c.Call.Return(run)
return _c
}
// Get provides a mock function with given fields: id
func (_m *UserTokenRepo) Get(id uint) (*biz.UserToken, error) {
ret := _m.Called(id)
if len(ret) == 0 {
panic("no return value specified for Get")
}
var r0 *biz.UserToken
var r1 error
if rf, ok := ret.Get(0).(func(uint) (*biz.UserToken, error)); ok {
return rf(id)
}
if rf, ok := ret.Get(0).(func(uint) *biz.UserToken); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*biz.UserToken)
}
}
if rf, ok := ret.Get(1).(func(uint) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UserTokenRepo_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get'
type UserTokenRepo_Get_Call struct {
*mock.Call
}
// Get is a helper method to define mock.On call
// - id uint
func (_e *UserTokenRepo_Expecter) Get(id interface{}) *UserTokenRepo_Get_Call {
return &UserTokenRepo_Get_Call{Call: _e.mock.On("Get", id)}
}
func (_c *UserTokenRepo_Get_Call) Run(run func(id uint)) *UserTokenRepo_Get_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(uint))
})
return _c
}
func (_c *UserTokenRepo_Get_Call) Return(_a0 *biz.UserToken, _a1 error) *UserTokenRepo_Get_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *UserTokenRepo_Get_Call) RunAndReturn(run func(uint) (*biz.UserToken, error)) *UserTokenRepo_Get_Call {
_c.Call.Return(run)
return _c
}
// Save provides a mock function with given fields: user
func (_m *UserTokenRepo) Save(user *biz.UserToken) error {
ret := _m.Called(user)
if len(ret) == 0 {
panic("no return value specified for Save")
}
var r0 error
if rf, ok := ret.Get(0).(func(*biz.UserToken) error); ok {
r0 = rf(user)
} else {
r0 = ret.Error(0)
}
return r0
}
// UserTokenRepo_Save_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Save'
type UserTokenRepo_Save_Call struct {
*mock.Call
}
// Save is a helper method to define mock.On call
// - user *biz.UserToken
func (_e *UserTokenRepo_Expecter) Save(user interface{}) *UserTokenRepo_Save_Call {
return &UserTokenRepo_Save_Call{Call: _e.mock.On("Save", user)}
}
func (_c *UserTokenRepo_Save_Call) Run(run func(user *biz.UserToken)) *UserTokenRepo_Save_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*biz.UserToken))
})
return _c
}
func (_c *UserTokenRepo_Save_Call) Return(_a0 error) *UserTokenRepo_Save_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *UserTokenRepo_Save_Call) RunAndReturn(run func(*biz.UserToken) error) *UserTokenRepo_Save_Call {
_c.Call.Return(run)
return _c
}
// NewUserTokenRepo creates a new instance of UserTokenRepo. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewUserTokenRepo(t interface {
mock.TestingT
Cleanup(func())
}) *UserTokenRepo {
mock := &UserTokenRepo{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -495,10 +495,10 @@ msgstr ""
msgid "Index Hit Rate"
msgstr ""
#: internal/service/cli.go:831
#: internal/service/cli.go:836
#: internal/service/cli.go:841
#: internal/service/cli.go:840
#: internal/service/cli.go:845
#: internal/service/cli.go:850
#: internal/service/cli.go:854
msgid "Initialization failed: %v"
msgstr ""
@@ -1196,10 +1196,6 @@ msgstr ""
msgid "Zip is a library for handling ZIP files"
msgstr ""
#: internal/http/middleware/must_login.go:94
msgid "api signature expired"
msgstr ""
#: internal/data/app.go:159
msgid "app %s already installed"
msgstr ""
@@ -1210,7 +1206,6 @@ msgstr ""
#: internal/data/app.go:210
#: internal/data/app.go:265
#: internal/http/middleware/must_install.go:48
msgid "app %s not installed"
msgstr ""
@@ -1225,12 +1220,8 @@ msgstr ""
msgid "app %s requires panel version %s, current version %s"
msgstr ""
#: internal/http/middleware/must_install.go:29
msgid "app not found"
msgstr ""
#: internal/data/setting.go:314
#: internal/data/setting.go:375
#: internal/data/setting.go:354
#: internal/data/setting.go:412
msgid "background task is running, modifying some settings is prohibited, please try again later"
msgstr ""
@@ -1261,10 +1252,6 @@ msgstr ""
msgid "check server connection failed"
msgstr ""
#: internal/http/middleware/must_login.go:122
msgid "client ip/ua changed, please login again"
msgstr ""
#: internal/data/backup.go:564
msgid "could not find .sql backup file"
msgstr ""
@@ -1522,12 +1509,12 @@ msgid "failed to load MySQL root password: %v"
msgstr ""
#: internal/data/cert.go:92
#: internal/data/setting.go:319
#: internal/data/setting.go:359
msgid "failed to parse certificate: %v"
msgstr ""
#: internal/data/cert.go:95
#: internal/data/setting.go:322
#: internal/data/setting.go:362
msgid "failed to parse private key: %v"
msgstr ""
@@ -1639,34 +1626,10 @@ msgstr ""
msgid "get service port failed, please check if it is installed"
msgstr ""
#: internal/http/middleware/entrance.go:101
msgid "invalid access entrance"
msgstr ""
#: internal/http/middleware/must_login.go:84
msgid "invalid api signature"
msgstr ""
#: internal/service/user.go:78
msgid "invalid key, please refresh the page"
msgstr ""
#: internal/http/middleware/entrance.go:50
msgid "invalid request domain: %s"
msgstr ""
#: internal/http/middleware/entrance.go:63
msgid "invalid request ip: %s"
msgstr ""
#: internal/http/middleware/entrance.go:72
msgid "invalid request user agent: %s"
msgstr ""
#: internal/http/middleware/must_login.go:136
msgid "invalid user id, please login again"
msgstr ""
#: internal/apps/php/app.go:473
msgid "ionCube is a professional-grade PHP encryption and decryption tool (must be installed after OPcache)"
msgstr ""
@@ -1712,22 +1675,6 @@ msgstr ""
msgid "open file error: %v"
msgstr ""
#: internal/http/middleware/status.go:38
msgid "panel is closed"
msgstr ""
#: internal/http/middleware/status.go:30
msgid "panel is maintaining, please refresh later"
msgstr ""
#: internal/http/middleware/status.go:22
msgid "panel is upgrading, please refresh later"
msgstr ""
#: internal/http/middleware/status.go:46
msgid "panel run error, please check or contact support"
msgstr ""
#: internal/apps/php/app.go:328
msgid "pdo_pgsql is a PDO driver for connecting to PostgreSQL (requires PostgreSQL installed)"
msgstr ""
@@ -1759,7 +1706,7 @@ msgstr ""
msgid "please retry the manual obtain operation"
msgstr ""
#: internal/data/setting.go:343
#: internal/data/setting.go:383
msgid "port is already in use"
msgstr ""
@@ -1792,10 +1739,6 @@ msgstr ""
msgid "runtime directory does not exist"
msgstr ""
#: internal/http/middleware/must_login.go:107
msgid "session expired, please login again"
msgstr ""
#: internal/apps/php/app.go:333
msgid "sqlsrv is a driver for connecting to SQL Server"
msgstr ""

View File

@@ -64,7 +64,8 @@ msgstr ""
#: src/views/apps/toolbox/IndexView.vue:66
#: src/views/apps/toolbox/IndexView.vue:72
#: src/views/apps/toolbox/IndexView.vue:81
#: src/views/setting/IndexView.vue:46
#: src/views/setting/SettingBase.vue:39
#: src/views/setting/SettingHttps.vue:27
#: src/views/website/EditView.vue:115
msgid "Saved successfully"
msgstr ""
@@ -369,7 +370,7 @@ msgstr ""
#: src/views/cert/CertView.vue:497
#: src/views/cert/CertView.vue:573
#: src/views/cert/UploadCertModal.vue:38
#: src/views/setting/SettingSafe.vue:54
#: src/views/setting/SettingHttps.vue:45
#: src/views/website/EditView.vue:355
msgid "Certificate"
msgstr ""
@@ -911,7 +912,8 @@ msgstr ""
#: src/views/apps/toolbox/IndexView.vue:109
#: src/views/file/EditModal.vue:31
#: src/views/file/ListTable.vue:723
#: src/views/setting/IndexView.vue:63
#: src/views/setting/SettingBase.vue:97
#: src/views/setting/SettingHttps.vue:62
#: src/views/website/EditView.vue:215
msgid "Save"
msgstr ""
@@ -1609,7 +1611,7 @@ msgstr ""
#: src/views/database/UpdateServerModal.vue:86
#: src/views/database/UserList.vue:40
#: src/views/login/IndexView.vue:115
#: src/views/setting/SettingBase.vue:49
#: src/views/setting/SettingBase.vue:67
#: src/views/ssh/CreateModal.vue:83
#: src/views/ssh/UpdateModal.vue:89
msgid "Username"
@@ -1675,7 +1677,7 @@ msgstr ""
#: src/views/database/UpdateUserModal.vue:49
#: src/views/database/UserList.vue:50
#: src/views/login/IndexView.vue:123
#: src/views/setting/SettingBase.vue:52
#: src/views/setting/SettingBase.vue:70
#: src/views/ssh/CreateModal.vue:77
#: src/views/ssh/CreateModal.vue:86
#: src/views/ssh/UpdateModal.vue:83
@@ -2290,7 +2292,7 @@ msgstr ""
#: src/views/cert/CertView.vue:509
#: src/views/cert/CertView.vue:585
#: src/views/cert/UploadCertModal.vue:46
#: src/views/setting/SettingSafe.vue:61
#: src/views/setting/SettingHttps.vue:52
#: src/views/ssh/CreateModal.vue:78
#: src/views/ssh/CreateModal.vue:89
#: src/views/ssh/UpdateModal.vue:84
@@ -3478,7 +3480,7 @@ msgstr ""
#: src/views/database/UpdateServerModal.vue:76
#: src/views/firewall/ForwardView.vue:32
#: src/views/firewall/RuleView.vue:49
#: src/views/setting/SettingBase.vue:58
#: src/views/setting/SettingBase.vue:76
#: src/views/ssh/CreateModal.vue:68
#: src/views/ssh/UpdateModal.vue:74
#: src/views/website/IndexView.vue:420
@@ -4126,115 +4128,78 @@ msgstr ""
msgid "Time Selection"
msgstr ""
#: src/views/setting/IndexView.vue:49
msgid "Panel is restarting, page will refresh in 3 seconds"
msgstr ""
#: src/views/setting/IndexView.vue:67
#: src/views/setting/IndexView.vue:17
msgid "Basic"
msgstr ""
#: src/views/setting/IndexView.vue:70
msgid "Safe"
msgstr ""
#: src/views/setting/SettingBase.vue:20
msgid "Stable"
msgstr ""
#: src/views/setting/SettingBase.vue:24
msgid "Beta"
msgstr ""
#: src/views/setting/SettingBase.vue:34
msgid "Modifying panel port/entrance requires corresponding changes in the browser address bar to access the panel!"
msgstr ""
#: src/views/setting/SettingBase.vue:40
#: src/views/setting/SettingBase.vue:41
msgid "Panel Name"
msgstr ""
#: src/views/setting/SettingBase.vue:43
msgid "Language"
msgstr ""
#: src/views/setting/SettingBase.vue:46
msgid "Update Channel"
msgstr ""
#: src/views/setting/SettingBase.vue:50
#: src/views/setting/SettingBase.vue:53
#: src/views/setting/SettingSafe.vue:26
msgid "admin"
#: src/views/setting/SettingBase.vue:42
msgid "Panel is restarting, page will refresh in 3 seconds"
msgstr ""
#: src/views/setting/SettingBase.vue:55
msgid "Certificate Default Email"
msgstr ""
#: src/views/setting/SettingBase.vue:56
msgid "admin@yourdomain.com"
msgstr ""
#: src/views/setting/SettingBase.vue:59
msgid "8888"
msgid "Modifying panel port/entrance requires corresponding changes in the browser address bar to access the panel!"
msgstr ""
#: src/views/setting/SettingBase.vue:61
msgid "Default Website Directory"
msgstr ""
#: src/views/setting/SettingBase.vue:62
msgid "/www/wwwroot"
msgid "Panel Name"
msgstr ""
#: src/views/setting/SettingBase.vue:64
msgid "Default Backup Directory"
msgid "Language"
msgstr ""
#: src/views/setting/SettingBase.vue:65
msgid "/www/backup"
#: src/views/setting/SettingBase.vue:68
#: src/views/setting/SettingBase.vue:71
#: src/views/setting/SettingBase.vue:80
msgid "admin"
msgstr ""
#: src/views/setting/SettingSafe.vue:12
msgid "Login Timeout"
#: src/views/setting/SettingBase.vue:73
msgid "Certificate Default Email"
msgstr ""
#: src/views/setting/SettingSafe.vue:15
msgid "120"
#: src/views/setting/SettingBase.vue:74
msgid "admin@yourdomain.com"
msgstr ""
#: src/views/setting/SettingSafe.vue:21
#: src/views/website/ProxyBuilderModal.vue:188
msgid "minutes"
#: src/views/setting/SettingBase.vue:77
msgid "8888"
msgstr ""
#: src/views/setting/SettingSafe.vue:25
#: src/views/setting/SettingBase.vue:79
msgid "Access Entrance"
msgstr ""
#: src/views/setting/SettingSafe.vue:28
msgid "Bind Domain"
msgstr ""
#: src/views/setting/SettingSafe.vue:35
msgid "Bind IP"
msgstr ""
#: src/views/setting/SettingSafe.vue:38
msgid "Bind UA"
msgstr ""
#: src/views/setting/SettingSafe.vue:45
#: src/views/setting/SettingBase.vue:82
msgid "Offline Mode"
msgstr ""
#: src/views/setting/SettingSafe.vue:48
#: src/views/setting/SettingBase.vue:85
msgid "Auto Update"
msgstr ""
#: src/views/setting/SettingSafe.vue:51
#: src/views/setting/SettingBase.vue:88
msgid "Default Website Directory"
msgstr ""
#: src/views/setting/SettingBase.vue:89
msgid "/www/wwwroot"
msgstr ""
#: src/views/setting/SettingBase.vue:91
msgid "Default Backup Directory"
msgstr ""
#: src/views/setting/SettingBase.vue:92
msgid "/www/backup"
msgstr ""
#: src/views/setting/SettingHttps.vue:36
msgid "Incorrect certificates may cause the panel to be inaccessible. Please proceed with caution!"
msgstr ""
#: src/views/setting/SettingHttps.vue:42
msgid "Panel HTTPS"
msgstr ""
@@ -4752,6 +4717,10 @@ msgstr ""
msgid "Cache time (minutes)"
msgstr ""
#: src/views/website/ProxyBuilderModal.vue:188
msgid "minutes"
msgstr ""
#: src/views/website/ProxyBuilderModal.vue:191
msgid "Content Replacement"
msgstr ""