mirror of
https://github.com/acepanel/panel.git
synced 2026-02-06 16:21:03 +08:00
feat: 支持绑定域名、IP、UA,close #670
This commit is contained in:
@@ -53,6 +53,10 @@ func initWeb() (*app.Web, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
locale, err := bootstrap.NewT(koanf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger := bootstrap.NewLog(koanf)
|
||||
db, err := bootstrap.NewDB(koanf, logger)
|
||||
if err != nil {
|
||||
@@ -62,10 +66,6 @@ func initWeb() (*app.Web, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
locale, err := bootstrap.NewT(koanf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cacheRepo := data.NewCacheRepo(db)
|
||||
queue := bootstrap.NewQueue()
|
||||
taskRepo := data.NewTaskRepo(locale, db, logger, queue)
|
||||
@@ -145,7 +145,7 @@ func initWeb() (*app.Web, error) {
|
||||
http := route.NewHttp(userService, dashboardService, taskService, websiteService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, loader)
|
||||
wsService := service.NewWsService(locale, koanf, sshRepo)
|
||||
ws := route.NewWs(wsService)
|
||||
mux, err := bootstrap.NewRouter(middlewares, http, ws)
|
||||
mux, err := bootstrap.NewRouter(locale, middlewares, http, ws)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
2
go.sum
2
go.sum
@@ -304,8 +304,6 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
|
||||
github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
|
||||
github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sethvargo/go-limiter v1.0.0 h1:JqW13eWEMn0VFv86OKn8wiYJY/m250WoXdrjRV0kLe4=
|
||||
github.com/sethvargo/go-limiter v1.0.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II=
|
||||
github.com/sethvargo/go-limiter v1.0.1-0.20250412144437-fa26982c7e1a h1:CdCoDHVynJVAQWN7ZQrAUOp0SV5TmRwNOSkF5KedDko=
|
||||
github.com/sethvargo/go-limiter v1.0.1-0.20250412144437-fa26982c7e1a/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/pquerna/otp"
|
||||
|
||||
"github.com/tnb-labs/panel/internal/http/request"
|
||||
)
|
||||
|
||||
@@ -22,15 +20,6 @@ const (
|
||||
SettingKeyMySQLRootPassword SettingKey = "mysql_root_password"
|
||||
SettingKeyOfflineMode SettingKey = "offline_mode"
|
||||
SettingKeyAutoUpdate SettingKey = "auto_update"
|
||||
SettingKeyTwoFA SettingKey = "two_fa"
|
||||
SettingKeyTwoFASecret SettingKey = "two_fa_secret"
|
||||
SettingKeyLoginTimeout SettingKey = "login_timeout"
|
||||
SettingKeyBindDomain SettingKey = "bind_domain"
|
||||
SettingKeyBindIP SettingKey = "bind_ip"
|
||||
SettingKeyBindUA SettingKey = "bind_ua"
|
||||
SettingKeyAPI SettingKey = "api"
|
||||
SettingKeyAPIKey SettingKey = "api_key"
|
||||
SettingKeyAPIWhiteList SettingKey = "api_white_list"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
@@ -51,6 +40,4 @@ type SettingRepo interface {
|
||||
Delete(key SettingKey) error
|
||||
GetPanelSetting(ctx context.Context) (*request.PanelSetting, error)
|
||||
UpdatePanelSetting(ctx context.Context, setting *request.PanelSetting) (bool, error)
|
||||
GenerateTwoFAKey() (*otp.Key, error)
|
||||
GenerateAPIKey() (string, error)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ type User struct {
|
||||
Username string `gorm:"not null;default:'';unique" json:"username"`
|
||||
Password string `gorm:"not null;default:''" json:"password"`
|
||||
Email string `gorm:"not null;default:''" json:"email"`
|
||||
TwoFA string `gorm:"not null;default:''" json:"two_fa"` // 2FA secret,为空表示未开启
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
|
||||
|
||||
21
internal/biz/user_token.go
Normal file
21
internal/biz/user_token.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type UserToken struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"index" json:"user_id"`
|
||||
Token string `gorm:"not null;default:'';unique" json:"token"`
|
||||
IPs []string `gorm:"not null;default:'[]';serializer:json" json:"ips"`
|
||||
ExpiredAt time.Time `json:"expired_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type UserTokenRepo interface {
|
||||
Create(userID uint, ips []string) (*UserToken, error)
|
||||
Get(id uint) (*UserToken, error)
|
||||
Save(user *UserToken) error
|
||||
}
|
||||
@@ -8,16 +8,17 @@ import (
|
||||
"github.com/bddjr/hlfhr"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/knadh/koanf/v2"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/tnb-labs/panel/internal/http/middleware"
|
||||
"github.com/tnb-labs/panel/internal/route"
|
||||
)
|
||||
|
||||
func NewRouter(middlewares *middleware.Middlewares, http *route.Http, ws *route.Ws) (*chi.Mux, error) {
|
||||
func NewRouter(t *gotext.Locale, middlewares *middleware.Middlewares, http *route.Http, ws *route.Ws) (*chi.Mux, error) {
|
||||
r := chi.NewRouter()
|
||||
|
||||
// add middleware
|
||||
r.Use(middlewares.Globals(r)...)
|
||||
r.Use(middlewares.Globals(t, r)...)
|
||||
// add http route
|
||||
http.Register(r)
|
||||
// add ws route
|
||||
|
||||
@@ -9,9 +9,14 @@ import (
|
||||
|
||||
func NewSession(conf *koanf.Koanf, db *gorm.DB) (*sessions.Manager, error) {
|
||||
// initialize session manager
|
||||
lifetime := conf.Int("session.lifetime")
|
||||
// TODO: will remove this fallback in v3
|
||||
if lifetime == 0 {
|
||||
lifetime = 120
|
||||
}
|
||||
manager, err := sessions.NewManager(&sessions.ManagerOptions{
|
||||
Key: conf.MustString("app.key"),
|
||||
Lifetime: conf.MustInt("session.lifetime"),
|
||||
Lifetime: lifetime,
|
||||
GcInterval: 5,
|
||||
DisableDefaultDriver: true,
|
||||
})
|
||||
|
||||
@@ -23,7 +23,6 @@ func NewValidator(conf *koanf.Koanf, db *gorm.DB) *validate.Validation {
|
||||
validate.Config(func(opt *validate.GlobalOption) {
|
||||
opt.StopOnError = false
|
||||
opt.SkipOnEmpty = true
|
||||
opt.FieldTag = "form"
|
||||
})
|
||||
|
||||
// register global rules
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/go-rat/utils/hash"
|
||||
"github.com/go-rat/utils/str"
|
||||
"github.com/knadh/koanf/v2"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/pquerna/otp"
|
||||
@@ -219,22 +218,6 @@ func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSettin
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
twoFA, err := r.GetBool(biz.SettingKeyTwoFA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bindDomain, err := r.GetSlice(biz.SettingKeyBindDomain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bindIP, err := r.GetSlice(biz.SettingKeyBindIP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bindUA, err := r.GetSlice(biz.SettingKeyBindUA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
websitePath, err := r.Get(biz.SettingKeyWebsitePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -243,14 +226,6 @@ func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSettin
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
api, err := r.GetBool(biz.SettingKeyAPI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apiWhiteList, err := r.GetSlice(biz.SettingKeyAPIWhiteList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userID := cast.ToUint(ctx.Value("user_id"))
|
||||
user := new(biz.User)
|
||||
@@ -268,27 +243,24 @@ func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSettin
|
||||
}
|
||||
|
||||
return &request.PanelSetting{
|
||||
Name: name,
|
||||
Channel: channel,
|
||||
Locale: r.conf.String("app.locale"),
|
||||
Entrance: r.conf.String("http.entrance"),
|
||||
OfflineMode: offlineMode,
|
||||
AutoUpdate: autoUpdate,
|
||||
TwoFA: twoFA,
|
||||
Lifetime: uint(r.conf.Int("session.lifetime")),
|
||||
BindDomain: bindDomain,
|
||||
BindIP: bindIP,
|
||||
BindUA: bindUA,
|
||||
WebsitePath: websitePath,
|
||||
BackupPath: backupPath,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Port: uint(r.conf.Int("http.port")),
|
||||
API: api,
|
||||
APIWhiteList: apiWhiteList,
|
||||
HTTPS: r.conf.Bool("http.tls"),
|
||||
Cert: crt,
|
||||
Key: key,
|
||||
Name: name,
|
||||
Channel: channel,
|
||||
Locale: r.conf.String("app.locale"),
|
||||
Entrance: r.conf.String("http.entrance"),
|
||||
OfflineMode: offlineMode,
|
||||
AutoUpdate: autoUpdate,
|
||||
Lifetime: uint(r.conf.Int("session.lifetime")),
|
||||
BindDomain: r.conf.Strings("http.bind_domain"),
|
||||
BindIP: r.conf.Strings("http.bind_ip"),
|
||||
BindUA: r.conf.Strings("http.bind_ua"),
|
||||
WebsitePath: websitePath,
|
||||
BackupPath: backupPath,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Port: uint(r.conf.Int("http.port")),
|
||||
HTTPS: r.conf.Bool("http.tls"),
|
||||
Cert: crt,
|
||||
Key: key,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -305,18 +277,6 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
|
||||
if err := r.Set(biz.SettingKeyAutoUpdate, cast.ToString(setting.AutoUpdate)); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := r.Set(biz.SettingKeyTwoFA, cast.ToString(setting.TwoFA)); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := r.SetSlice(biz.SettingKeyBindDomain, setting.BindDomain); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := r.SetSlice(biz.SettingKeyBindIP, setting.BindIP); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := r.SetSlice(biz.SettingKeyBindUA, setting.BindUA); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := r.Set(biz.SettingKeyWebsitePath, setting.WebsitePath); err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -388,6 +348,9 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
|
||||
config.HTTP.Port = setting.Port
|
||||
config.HTTP.Entrance = setting.Entrance
|
||||
config.HTTP.TLS = setting.HTTPS
|
||||
config.HTTP.BindDomain = setting.BindDomain
|
||||
config.HTTP.BindIP = setting.BindIP
|
||||
config.HTTP.BindUA = setting.BindUA
|
||||
config.Session.Lifetime = setting.Lifetime
|
||||
|
||||
// 放行端口
|
||||
@@ -420,8 +383,9 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
|
||||
return restartFlag, nil
|
||||
}
|
||||
|
||||
// GenerateTwoFAKey 生成两步验证密钥
|
||||
func (r *settingRepo) GenerateTwoFAKey() (*otp.Key, error) {
|
||||
// GetTwoFA 生成两步验证密钥
|
||||
// TODO: 即将废弃
|
||||
func (r *settingRepo) GetTwoFA() (*otp.Key, error) {
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: "RatPanel",
|
||||
AccountName: "admin",
|
||||
@@ -432,24 +396,5 @@ func (r *settingRepo) GenerateTwoFAKey() (*otp.Key, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = r.Set(biz.SettingKeyTwoFASecret, key.Secret()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// GenerateAPIKey 生成API密钥
|
||||
func (r *settingRepo) GenerateAPIKey() (string, error) {
|
||||
key := str.Random(32)
|
||||
hashed, err := hash.NewArgon2id().Make(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = r.Set(biz.SettingKeyAPIKey, hashed); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
"github.com/go-rat/sessions"
|
||||
"github.com/knadh/koanf/v2"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/tnb-labs/panel/pkg/punycode"
|
||||
)
|
||||
|
||||
// Entrance 确保通过正确的入口访问
|
||||
func Entrance(conf *koanf.Koanf, session *sessions.Manager) func(next http.Handler) http.Handler {
|
||||
func Entrance(t *gotext.Locale, conf *koanf.Koanf, session *sessions.Manager) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sess, err := session.GetSession(r)
|
||||
@@ -26,6 +31,50 @@ func Entrance(conf *koanf.Koanf, session *sessions.Manager) func(next http.Handl
|
||||
}
|
||||
|
||||
entrance := conf.String("http.entrance")
|
||||
|
||||
// 情况一:设置了绑定域名、IP、UA,且请求不符合要求,返回错误
|
||||
host, _, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
host = r.Host
|
||||
}
|
||||
if strings.Contains(host, "xn--") {
|
||||
if decoded, err := punycode.DecodeDomain(host); err == nil {
|
||||
host = decoded
|
||||
}
|
||||
}
|
||||
if len(conf.Strings("http.bind_domain")) > 0 && !slices.Contains(conf.Strings("http.bind_domain"), host) {
|
||||
render := chix.NewRender(w)
|
||||
defer render.Release()
|
||||
render.Status(http.StatusTeapot)
|
||||
render.JSON(chix.M{
|
||||
"message": t.Get("invalid request domain: %s", r.Host),
|
||||
})
|
||||
return
|
||||
}
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
ip = r.RemoteAddr
|
||||
}
|
||||
if len(conf.Strings("http.bind_ip")) > 0 && !slices.Contains(conf.Strings("http.bind_ip"), ip) {
|
||||
render := chix.NewRender(w)
|
||||
defer render.Release()
|
||||
render.Status(http.StatusTeapot)
|
||||
render.JSON(chix.M{
|
||||
"message": t.Get("invalid request ip: %s", r.RemoteAddr),
|
||||
})
|
||||
return
|
||||
}
|
||||
if len(conf.Strings("http.bind_ua")) > 0 && !slices.Contains(conf.Strings("http.bind_ua"), r.UserAgent()) {
|
||||
render := chix.NewRender(w)
|
||||
defer render.Release()
|
||||
render.Status(http.StatusTeapot)
|
||||
render.JSON(chix.M{
|
||||
"message": t.Get("invalid request user agent: %s", r.UserAgent()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 情况二:请求路径与入口路径相同,标记通过验证并重定向到登录页面
|
||||
if strings.TrimSuffix(r.URL.Path, "/") == strings.TrimSuffix(entrance, "/") {
|
||||
sess.Put("verify_entrance", true)
|
||||
render := chix.NewRender(w, r)
|
||||
@@ -34,6 +83,14 @@ func Entrance(conf *koanf.Koanf, session *sessions.Manager) func(next http.Handl
|
||||
return
|
||||
}
|
||||
|
||||
// 情况三:通过APIKey+入口路径访问,重写请求路径并跳过验证
|
||||
if strings.HasPrefix(r.URL.Path, entrance) && r.Header.Get("Authorization") != "" {
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, entrance)
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// 情况三:非调试模式且未通过验证的请求,返回错误
|
||||
if !conf.Bool("app.debug") &&
|
||||
!cast.ToBool(sess.Get("verify_entrance", false)) &&
|
||||
r.URL.Path != "/robots.txt" {
|
||||
@@ -41,7 +98,7 @@ func Entrance(conf *koanf.Koanf, session *sessions.Manager) func(next http.Handl
|
||||
defer render.Release()
|
||||
render.Status(http.StatusTeapot)
|
||||
render.JSON(chix.M{
|
||||
"message": "请通过正确的入口访问",
|
||||
"message": t.Get("invalid access entrance"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/golang-cz/httplog"
|
||||
"github.com/google/wire"
|
||||
"github.com/knadh/koanf/v2"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/tnb-labs/panel/internal/biz"
|
||||
)
|
||||
@@ -34,7 +35,7 @@ func NewMiddlewares(conf *koanf.Koanf, log *slog.Logger, session *sessions.Manag
|
||||
}
|
||||
|
||||
// Globals is a collection of global middleware that will be applied to every request.
|
||||
func (r *Middlewares) Globals(mux *chi.Mux) []func(http.Handler) http.Handler {
|
||||
func (r *Middlewares) Globals(t *gotext.Locale, mux *chi.Mux) []func(http.Handler) http.Handler {
|
||||
return []func(http.Handler) http.Handler{
|
||||
sessionmiddleware.StartSession(r.session),
|
||||
//middleware.SupressNotFound(mux),// bug https://github.com/go-chi/chi/pull/940
|
||||
@@ -46,9 +47,9 @@ func (r *Middlewares) Globals(mux *chi.Mux) []func(http.Handler) http.Handler {
|
||||
LogRequestHeaders: []string{"User-Agent"},
|
||||
}),
|
||||
middleware.Recoverer,
|
||||
Status,
|
||||
Entrance(r.conf, r.session),
|
||||
MustLogin(r.session),
|
||||
MustInstall(r.app),
|
||||
Status(t),
|
||||
Entrance(t, r.conf, r.session),
|
||||
MustLogin(t, r.session),
|
||||
MustInstall(t, r.app),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/tnb-labs/panel/internal/biz"
|
||||
)
|
||||
|
||||
// MustInstall 确保已安装应用
|
||||
func MustInstall(app biz.AppRepo) func(next http.Handler) http.Handler {
|
||||
func MustInstall(t *gotext.Locale, app biz.AppRepo) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var slugs []string
|
||||
@@ -26,7 +26,7 @@ func MustInstall(app biz.AppRepo) func(next http.Handler) http.Handler {
|
||||
defer render.Release()
|
||||
render.Status(http.StatusForbidden)
|
||||
render.JSON(chix.M{
|
||||
"message": "应用不存在",
|
||||
"message": t.Get("app not found"),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -45,7 +45,7 @@ func MustInstall(app biz.AppRepo) func(next http.Handler) http.Handler {
|
||||
defer render.Release()
|
||||
render.Status(http.StatusForbidden)
|
||||
render.JSON(chix.M{
|
||||
"message": fmt.Sprintf("应用 %s 未安装", slugs),
|
||||
"message": t.Get("app %s not installed", slugs),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
"github.com/go-rat/sessions"
|
||||
"github.com/go-rat/utils/str"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// MustLogin 确保已登录
|
||||
func MustLogin(session *sessions.Manager) func(next http.Handler) http.Handler {
|
||||
func MustLogin(t *gotext.Locale, session *sessions.Manager) func(next http.Handler) http.Handler {
|
||||
// 白名单
|
||||
whiteList := []string{
|
||||
"/api/user/key",
|
||||
@@ -43,45 +51,101 @@ func MustLogin(session *sessions.Manager) func(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
if sess.Missing("user_id") {
|
||||
render := chix.NewRender(w)
|
||||
defer render.Release()
|
||||
render.Status(http.StatusUnauthorized)
|
||||
render.JSON(chix.M{
|
||||
"message": "会话已过期,请重新登录",
|
||||
})
|
||||
return
|
||||
userID := uint(0)
|
||||
if r.Header.Get("Authorization") != "" {
|
||||
signature := strings.TrimPrefix(r.Header.Get("Authorization"), "HMAC-SHA256 ")
|
||||
|
||||
// 步骤一:构造规范化请求
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
render := chix.NewRender(w)
|
||||
defer render.Release()
|
||||
render.Status(http.StatusInternalServerError)
|
||||
render.JSON(chix.M{
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
r.Body = io.NopCloser(bytes.NewReader(body))
|
||||
canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s", r.Method, r.URL.Path, r.URL.Query().Encode(), str.SHA256(string(body)))
|
||||
|
||||
// 步骤二:构造待签名字符串
|
||||
stringToSign := fmt.Sprintf("%s\n%d\n%s", "HMAC-SHA256", cast.ToInt64(r.Header.Get("X-Timestamp")), str.SHA256(canonicalRequest))
|
||||
|
||||
// 步骤三:计算签名
|
||||
validSignature := hmacsha256(stringToSign, cast.ToString(sess.Get("api_secret")))
|
||||
|
||||
// 步骤四:验证签名
|
||||
if subtle.ConstantTimeCompare([]byte(signature), []byte(validSignature)) != 1 {
|
||||
render := chix.NewRender(w)
|
||||
defer render.Release()
|
||||
render.Status(http.StatusUnauthorized)
|
||||
render.JSON(chix.M{
|
||||
"message": t.Get("invalid api signature"),
|
||||
})
|
||||
return
|
||||
}
|
||||
timestamp := cast.ToInt64(r.Header.Get("X-Timestamp"))
|
||||
if timestamp == 0 || timestamp < (time.Now().Unix()-60) {
|
||||
render := chix.NewRender(w)
|
||||
defer render.Release()
|
||||
render.Status(http.StatusUnauthorized)
|
||||
render.JSON(chix.M{
|
||||
"message": t.Get("api signature expired"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 步骤五:验证通过
|
||||
userID = 1
|
||||
} else {
|
||||
if sess.Missing("user_id") {
|
||||
render := chix.NewRender(w)
|
||||
defer render.Release()
|
||||
render.Status(http.StatusUnauthorized)
|
||||
render.JSON(chix.M{
|
||||
"message": t.Get("session expired, please login again"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
safeLogin := cast.ToBool(sess.Get("safe_login"))
|
||||
if safeLogin {
|
||||
safeClientHash := cast.ToString(sess.Get("safe_client"))
|
||||
ip, _, _ := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr))
|
||||
clientHash := fmt.Sprintf("%x", sha256.Sum256([]byte(ip)))
|
||||
if safeClientHash != clientHash || safeClientHash == "" {
|
||||
render := chix.NewRender(w)
|
||||
defer render.Release()
|
||||
render.Status(http.StatusUnauthorized)
|
||||
render.JSON(chix.M{
|
||||
"message": t.Get("client ip/ua changed, please login again"),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
userID = cast.ToUint(sess.Get("user_id"))
|
||||
}
|
||||
|
||||
userID := cast.ToUint(sess.Get("user_id"))
|
||||
if userID == 0 {
|
||||
render := chix.NewRender(w)
|
||||
defer render.Release()
|
||||
render.Status(http.StatusUnauthorized)
|
||||
render.JSON(chix.M{
|
||||
"message": "会话无效,请重新登录",
|
||||
"message": t.Get("invalid user id, please login again"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
safeLogin := cast.ToBool(sess.Get("safe_login"))
|
||||
if safeLogin {
|
||||
safeClientHash := cast.ToString(sess.Get("safe_client"))
|
||||
ip, _, _ := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr))
|
||||
clientHash := fmt.Sprintf("%x", sha256.Sum256([]byte(ip)))
|
||||
if safeClientHash != clientHash || safeClientHash == "" {
|
||||
render := chix.NewRender(w)
|
||||
defer render.Release()
|
||||
render.Status(http.StatusUnauthorized)
|
||||
render.JSON(chix.M{
|
||||
"message": "客户端IP/UA变化,请重新登录",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
r = r.WithContext(context.WithValue(r.Context(), "user_id", userID)) // nolint:staticcheck
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func hmacsha256(data string, secret string) string {
|
||||
h := hmac.New(sha256.New, []byte(secret))
|
||||
h.Write([]byte(data))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
@@ -4,48 +4,51 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/tnb-labs/panel/internal/app"
|
||||
)
|
||||
|
||||
// Status 检查程序状态
|
||||
func Status(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch app.Status {
|
||||
case app.StatusUpgrade:
|
||||
render := chix.NewRender(w)
|
||||
defer render.Release()
|
||||
render.Status(http.StatusServiceUnavailable)
|
||||
render.JSON(chix.M{
|
||||
"message": "面板更新中,请稍后刷新",
|
||||
})
|
||||
return
|
||||
case app.StatusMaintain:
|
||||
render := chix.NewRender(w)
|
||||
defer render.Release()
|
||||
render.Status(http.StatusServiceUnavailable)
|
||||
render.JSON(chix.M{
|
||||
"message": "面板正在运行维护任务,请稍后刷新",
|
||||
})
|
||||
return
|
||||
case app.StatusClosed:
|
||||
render := chix.NewRender(w)
|
||||
defer render.Release()
|
||||
render.Status(http.StatusForbidden)
|
||||
render.JSON(chix.M{
|
||||
"message": "面板已关闭",
|
||||
})
|
||||
return
|
||||
case app.StatusFailed:
|
||||
render := chix.NewRender(w)
|
||||
defer render.Release()
|
||||
render.Status(http.StatusInternalServerError)
|
||||
render.JSON(chix.M{
|
||||
"message": "面板运行出错,请检查排除或联系支持",
|
||||
})
|
||||
return
|
||||
default:
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
func Status(t *gotext.Locale) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch app.Status {
|
||||
case app.StatusUpgrade:
|
||||
render := chix.NewRender(w)
|
||||
defer render.Release()
|
||||
render.Status(http.StatusServiceUnavailable)
|
||||
render.JSON(chix.M{
|
||||
"message": t.Get("panel is upgrading, please refresh later"),
|
||||
})
|
||||
return
|
||||
case app.StatusMaintain:
|
||||
render := chix.NewRender(w)
|
||||
defer render.Release()
|
||||
render.Status(http.StatusServiceUnavailable)
|
||||
render.JSON(chix.M{
|
||||
"message": t.Get("panel is maintaining, please refresh later"),
|
||||
})
|
||||
return
|
||||
case app.StatusClosed:
|
||||
render := chix.NewRender(w)
|
||||
defer render.Release()
|
||||
render.Status(http.StatusForbidden)
|
||||
render.JSON(chix.M{
|
||||
"message": t.Get("panel is closed"),
|
||||
})
|
||||
return
|
||||
case app.StatusFailed:
|
||||
render := chix.NewRender(w)
|
||||
defer render.Release()
|
||||
render.Status(http.StatusInternalServerError)
|
||||
render.JSON(chix.M{
|
||||
"message": t.Get("panel run error, please check or contact support"),
|
||||
})
|
||||
return
|
||||
default:
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
package request
|
||||
|
||||
import "net/http"
|
||||
|
||||
type PanelSetting struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Channel string `json:"channel" validate:"required|in:stable,beta"`
|
||||
Locale string `json:"locale" validate:"required"`
|
||||
Entrance string `json:"entrance" validate:"required"`
|
||||
OfflineMode bool `json:"offline_mode"`
|
||||
AutoUpdate bool `json:"auto_update"`
|
||||
TwoFA bool `json:"two_fa"`
|
||||
Lifetime uint `json:"lifetime" validate:"required|min:10|max:43200"` // 登录超时,单位:分
|
||||
BindDomain []string `json:"bind_domain"`
|
||||
BindIP []string `json:"bind_ip"`
|
||||
BindUA []string `json:"bind_ua"`
|
||||
WebsitePath string `json:"website_path" validate:"required"`
|
||||
BackupPath string `json:"backup_path" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
Password string `json:"password" validate:"password"`
|
||||
Email string `json:"email" validate:"required"`
|
||||
Port uint `json:"port" validate:"required|min:1|max:65535"`
|
||||
API bool `json:"api"`
|
||||
APIWhiteList []string `json:"api_white_list"`
|
||||
HTTPS bool `json:"https"`
|
||||
Cert string `json:"cert" validate:"required"`
|
||||
Key string `json:"key" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Channel string `json:"channel" validate:"required|in:stable,beta"`
|
||||
Locale string `json:"locale" validate:"required"`
|
||||
Entrance string `json:"entrance" validate:"required"`
|
||||
OfflineMode bool `json:"offline_mode"`
|
||||
AutoUpdate bool `json:"auto_update"`
|
||||
TwoFA bool `json:"two_fa"`
|
||||
Lifetime uint `json:"lifetime" validate:"required|min:10|max:43200"` // 登录超时,单位:分
|
||||
BindDomain []string `json:"bind_domain"`
|
||||
BindIP []string `json:"bind_ip"`
|
||||
BindUA []string `json:"bind_ua"`
|
||||
WebsitePath string `json:"website_path" validate:"required"`
|
||||
BackupPath string `json:"backup_path" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
Password string `json:"password" validate:"password"`
|
||||
Email string `json:"email" validate:"required"`
|
||||
Port uint `json:"port" validate:"required|min:1|max:65535"`
|
||||
HTTPS bool `json:"https"`
|
||||
Cert string `json:"cert" validate:"required"`
|
||||
Key string `json:"key" validate:"required"`
|
||||
}
|
||||
|
||||
func (r *PanelSetting) Rules(_ *http.Request) map[string]string {
|
||||
return map[string]string{
|
||||
"BindDomain.*": "required",
|
||||
"BindIP.*": "required|ip",
|
||||
"BindUA.*": "required",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,4 +93,26 @@ func init() {
|
||||
return tx.Migrator().DropColumn(&biz.Cert{}, "script")
|
||||
},
|
||||
})
|
||||
Migrations = append(Migrations, &gormigrate.Migration{
|
||||
ID: "20250514-user-two-fa",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
&biz.User{},
|
||||
)
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Migrator().DropColumn(&biz.User{}, "two_fa")
|
||||
},
|
||||
})
|
||||
Migrations = append(Migrations, &gormigrate.Migration{
|
||||
ID: "20250514-user-token",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
&biz.UserToken{},
|
||||
)
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Migrator().DropTable(&biz.UserToken{})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -826,15 +826,6 @@ func (s *CliService) Init(ctx context.Context, cmd *cli.Command) error {
|
||||
{Key: biz.SettingKeyWebsitePath, Value: filepath.Join(app.Root, "wwwroot")},
|
||||
{Key: biz.SettingKeyOfflineMode, Value: "false"},
|
||||
{Key: biz.SettingKeyAutoUpdate, Value: "true"},
|
||||
{Key: biz.SettingKeyTwoFA, Value: "false"},
|
||||
{Key: biz.SettingKeyTwoFASecret, Value: ""},
|
||||
{Key: biz.SettingKeyLoginTimeout, Value: "720"},
|
||||
{Key: biz.SettingKeyBindDomain, Value: "[]"},
|
||||
{Key: biz.SettingKeyBindIP, Value: "[]"},
|
||||
{Key: biz.SettingKeyBindUA, Value: "[]"},
|
||||
{Key: biz.SettingKeyAPI, Value: "false"},
|
||||
{Key: biz.SettingKeyAPIKey, Value: ""},
|
||||
{Key: biz.SettingKeyAPIWhiteList, Value: "[]"},
|
||||
}
|
||||
if err := s.db.Create(&settings).Error; err != nil {
|
||||
return errors.New(s.t.Get("Initialization failed: %v", err))
|
||||
|
||||
@@ -17,10 +17,13 @@ type PanelAppConfig struct {
|
||||
}
|
||||
|
||||
type PanelHTTPConfig struct {
|
||||
Debug bool `yaml:"debug"`
|
||||
Port uint `yaml:"port"`
|
||||
Entrance string `yaml:"entrance"`
|
||||
TLS bool `yaml:"tls"`
|
||||
Debug bool `yaml:"debug"`
|
||||
Port uint `yaml:"port"`
|
||||
Entrance string `yaml:"entrance"`
|
||||
TLS bool `yaml:"tls"`
|
||||
BindDomain []string `yaml:"bind_domain"`
|
||||
BindIP []string `yaml:"bind_ip"`
|
||||
BindUA []string `yaml:"bind_ua"`
|
||||
}
|
||||
|
||||
type PanelDatabaseConfig struct {
|
||||
|
||||
@@ -1,24 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import setting from '@/api/panel/setting'
|
||||
|
||||
defineOptions({
|
||||
name: 'setting-index'
|
||||
})
|
||||
|
||||
import TheIcon from '@/components/custom/TheIcon.vue'
|
||||
import { useThemeStore } from '@/store'
|
||||
import SettingBase from '@/views/setting/SettingBase.vue'
|
||||
import SettingHttps from '@/views/setting/SettingHttps.vue'
|
||||
import SettingSafe from '@/views/setting/SettingSafe.vue'
|
||||
import { NButton } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const themeStore = useThemeStore()
|
||||
const currentTab = ref('base')
|
||||
|
||||
const { data: model } = useRequest(setting.list, {
|
||||
initialData: {
|
||||
name: '',
|
||||
channel: 'stable',
|
||||
locale: 'en',
|
||||
channel: 'stable',
|
||||
username: '',
|
||||
password: '',
|
||||
email: '',
|
||||
port: 8888,
|
||||
entrance: '',
|
||||
offline_mode: false,
|
||||
two_fa: false,
|
||||
lifetime: 0,
|
||||
bind_domain: [],
|
||||
bind_ip: [],
|
||||
bind_ua: [],
|
||||
website_path: '',
|
||||
backup_path: '',
|
||||
https: false,
|
||||
cert: '',
|
||||
key: ''
|
||||
}
|
||||
})
|
||||
|
||||
const handleSave = () => {
|
||||
useRequest(setting.update(model.value)).onSuccess(() => {
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
if (model.value.locale !== themeStore.locale) {
|
||||
themeStore.setLocale(model.value.locale)
|
||||
window.$message.info($gettext('Panel is restarting, page will refresh in 3 seconds'))
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 3000)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<template #action>
|
||||
<n-button type="primary" @click="handleSave">
|
||||
<TheIcon :size="18" icon="material-symbols:save-outline" />
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</template>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated>
|
||||
<n-tab-pane name="base" :tab="$gettext('Basic')">
|
||||
<setting-base />
|
||||
<setting-base v-model:model="model" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="https" tab="HTTPS">
|
||||
<setting-https />
|
||||
<n-tab-pane name="safe" :tab="$gettext('Safe')">
|
||||
<setting-safe v-model:model="model" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</common-page>
|
||||
|
||||
@@ -1,29 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import setting from '@/api/panel/setting'
|
||||
import { useThemeStore } from '@/store'
|
||||
import { locales as availableLocales } from '@/utils'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const themeStore = useThemeStore()
|
||||
|
||||
const { data: model } = useRequest(setting.list, {
|
||||
initialData: {
|
||||
name: '',
|
||||
locale: '',
|
||||
username: '',
|
||||
password: '',
|
||||
email: '',
|
||||
port: 8888,
|
||||
entrance: '',
|
||||
offline_mode: false,
|
||||
website_path: '',
|
||||
backup_path: '',
|
||||
https: false,
|
||||
cert: '',
|
||||
key: ''
|
||||
}
|
||||
})
|
||||
const model = defineModel<any>('model', { type: Object, required: true })
|
||||
|
||||
const locales = computed(() => {
|
||||
return Object.entries(availableLocales).map(([code, name]: [string, string]) => {
|
||||
@@ -34,18 +15,16 @@ const locales = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const handleSave = () => {
|
||||
useRequest(setting.update(model.value)).onSuccess(() => {
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
if (model.value.locale !== themeStore.locale) {
|
||||
themeStore.setLocale(model.value.locale)
|
||||
window.$message.info($gettext('Panel is restarting, page will refresh in 3 seconds'))
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 3000)
|
||||
}
|
||||
})
|
||||
}
|
||||
const channels = [
|
||||
{
|
||||
label: $gettext('Stable'),
|
||||
value: 'stable'
|
||||
},
|
||||
{
|
||||
label: $gettext('Beta'),
|
||||
value: 'beta'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -64,6 +43,9 @@ const handleSave = () => {
|
||||
<n-form-item :label="$gettext('Language')">
|
||||
<n-select v-model:value="model.locale" :options="locales"> </n-select>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Update Channel')">
|
||||
<n-select v-model:value="model.channel" :options="channels"> </n-select>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Username')">
|
||||
<n-input v-model:value="model.username" :placeholder="$gettext('admin')" />
|
||||
</n-form-item>
|
||||
@@ -74,16 +56,7 @@ const handleSave = () => {
|
||||
<n-input v-model:value="model.email" :placeholder="$gettext('admin@yourdomain.com')" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Port')">
|
||||
<n-input-number v-model:value="model.port" :placeholder="$gettext('8888')" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Access Entrance')">
|
||||
<n-input v-model:value="model.entrance" :placeholder="$gettext('admin')" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Offline Mode')">
|
||||
<n-switch v-model:value="model.offline_mode" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Auto Update')">
|
||||
<n-switch v-model:value="model.auto_update" />
|
||||
<n-input-number v-model:value="model.port" :placeholder="$gettext('8888')" w-full />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Default Website Directory')">
|
||||
<n-input v-model:value="model.website_path" :placeholder="$gettext('/www/wwwroot')" />
|
||||
@@ -93,9 +66,6 @@ const handleSave = () => {
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-space>
|
||||
<n-button type="primary" @click="handleSave">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import setting from '@/api/panel/setting'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
const { data: model } = useRequest(setting.list, {
|
||||
initialData: {
|
||||
name: '',
|
||||
locale: '',
|
||||
username: '',
|
||||
password: '',
|
||||
email: '',
|
||||
port: 8888,
|
||||
entrance: '',
|
||||
offline_mode: false,
|
||||
website_path: '',
|
||||
backup_path: '',
|
||||
https: false,
|
||||
cert: '',
|
||||
key: ''
|
||||
}
|
||||
})
|
||||
|
||||
const handleSave = () => {
|
||||
useRequest(setting.update(model.value)).onSuccess(() => {
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-space vertical>
|
||||
<n-alert type="warning">
|
||||
{{
|
||||
$gettext(
|
||||
'Incorrect certificates may cause the panel to be inaccessible. Please proceed with caution!'
|
||||
)
|
||||
}}</n-alert
|
||||
>
|
||||
<n-form>
|
||||
<n-form-item :label="$gettext('Panel HTTPS')">
|
||||
<n-switch v-model:value="model.https" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="model.https" :label="$gettext('Certificate')">
|
||||
<n-input
|
||||
v-model:value="model.cert"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 10, maxRows: 15 }"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item v-if="model.https" :label="$gettext('Private Key')">
|
||||
<n-input
|
||||
v-model:value="model.key"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 10, maxRows: 15 }"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-space>
|
||||
<n-button type="primary" @click="handleSave">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
72
web/src/views/setting/SettingSafe.vue
Normal file
72
web/src/views/setting/SettingSafe.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
const model = defineModel<any>('model', { type: Object, required: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-space vertical>
|
||||
<n-form>
|
||||
<n-form-item :label="$gettext('Login Timeout')">
|
||||
<n-input-number
|
||||
v-model:value="model.lifetime"
|
||||
:placeholder="$gettext('120')"
|
||||
:min="10"
|
||||
:max="43200"
|
||||
w-full
|
||||
>
|
||||
<template #suffix>
|
||||
{{ $gettext('minutes') }}
|
||||
</template>
|
||||
</n-input-number>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Access Entrance')">
|
||||
<n-input v-model:value="model.entrance" :placeholder="$gettext('admin')" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Bind Domain')">
|
||||
<n-dynamic-input
|
||||
v-model:value="model.bind_domain"
|
||||
placeholder="example.com"
|
||||
show-sort-button
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Bind IP')">
|
||||
<n-dynamic-input v-model:value="model.bind_ip" placeholder="127.0.0.1" show-sort-button />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Bind UA')">
|
||||
<n-dynamic-input
|
||||
v-model:value="model.bind_ua"
|
||||
placeholder="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36"
|
||||
show-sort-button
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Offline Mode')">
|
||||
<n-switch v-model:value="model.offline_mode" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Auto Update')">
|
||||
<n-switch v-model:value="model.auto_update" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Panel HTTPS')">
|
||||
<n-switch v-model:value="model.https" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="model.https" :label="$gettext('Certificate')">
|
||||
<n-input
|
||||
v-model:value="model.cert"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 10, maxRows: 15 }"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item v-if="model.https" :label="$gettext('Private Key')">
|
||||
<n-input
|
||||
v-model:value="model.key"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 10, maxRows: 15 }"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
Reference in New Issue
Block a user