diff --git a/.gitignore b/.gitignore index 43a9be62b..65d545a60 100644 --- a/.gitignore +++ b/.gitignore @@ -42,9 +42,17 @@ frontend/components.d.ts frontend/src/xpack frontend/src/xpack-ee agent/xpack -agent/xpack-ee +agent/router/entry_xpack.go +agent/server/init_xpack.go +agent/utils/xpack/xpack.go core/xpack core/xpack-ee +core/router/entry_xpack.go +core/router/entry_xpackee.go +core/server/init_xpack.go +core/server/init_xpackee.go +core/utils/xpack/xpack.go +core/utils/xpack/xpackee.go .history/ dist/ diff --git a/agent/app/api/v2/ai.go b/agent/app/api/v2/ai.go index 11bdd22c7..4837f02d0 100644 --- a/agent/app/api/v2/ai.go +++ b/agent/app/api/v2/ai.go @@ -3,9 +3,6 @@ package v2 import ( "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/dto" - "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu" - "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu/common" - "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/xpu" "github.com/gin-gonic/gin" ) @@ -163,37 +160,6 @@ func (b *BaseApi) DeleteOllamaModel(c *gin.Context) { helper.Success(c) } -// @Tags AI -// @Summary Load gpu / xpu info -// @Accept json -// @Success 200 -// @Security ApiKeyAuth -// @Security Timestamp -// @Router /ai/gpu/load [get] -func (b *BaseApi) LoadGpuInfo(c *gin.Context) { - ok, client := gpu.New() - if ok { - info, err := client.LoadGpuInfo() - if err != nil { - helper.BadRequest(c, err) - return - } - helper.SuccessWithData(c, info) - return - } - xpuOK, xpuClient := xpu.New() - if xpuOK { - info, err := xpuClient.LoadGpuInfo() - if err != nil { - helper.BadRequest(c, err) - return - } - helper.SuccessWithData(c, info) - return - } - helper.SuccessWithData(c, &common.GpuInfo{}) -} - // @Tags AI // @Summary Bind domain // @Accept json diff --git a/agent/app/api/v2/gpu.go b/agent/app/api/v2/gpu.go new file mode 100644 index 000000000..a862c61f8 --- /dev/null +++ b/agent/app/api/v2/gpu.go @@ -0,0 +1,66 @@ +package v2 + +import ( + "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu" + "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu/common" + "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/xpu" + "github.com/gin-gonic/gin" +) + +// @Tags AI +// @Summary Load gpu / xpu info +// @Accept json +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/gpu/load [get] +func (b *BaseApi) LoadGpuInfo(c *gin.Context) { + ok, client := gpu.New() + if ok { + info, err := client.LoadGpuInfo() + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, info) + return + } + xpuOK, xpuClient := xpu.New() + if xpuOK { + info, err := xpuClient.LoadGpuInfo() + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, info) + return + } + helper.SuccessWithData(c, &common.GpuInfo{}) +} + +func (b *BaseApi) GetCPUOptions(c *gin.Context) { + helper.SuccessWithData(c, monitorService.LoadGPUOptions()) +} + +// @Tags Monitor +// @Summary Load monitor data +// @Param request body dto.MonitorGPUSearch true "request" +// @Success 200 {object} dto.MonitorGPUData +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /hosts/monitor/gpu/search [post] +func (b *BaseApi) LoadGPUMonitor(c *gin.Context) { + var req dto.MonitorGPUSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + data, err := monitorService.LoadGPUMonitorData(req) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, data) +} diff --git a/agent/app/api/v2/monitor.go b/agent/app/api/v2/monitor.go index 6543fc00a..cff19b341 100644 --- a/agent/app/api/v2/monitor.go +++ b/agent/app/api/v2/monitor.go @@ -31,27 +31,6 @@ func (b *BaseApi) LoadMonitor(c *gin.Context) { helper.SuccessWithDataGzipped(c, data) } -// @Tags Monitor -// @Summary Load monitor data -// @Param request body dto.MonitorGPUSearch true "request" -// @Success 200 {object} dto.MonitorGPUData -// @Security ApiKeyAuth -// @Security Timestamp -// @Router /hosts/monitor/gpu/search [post] -func (b *BaseApi) LoadGPUMonitor(c *gin.Context) { - var req dto.MonitorGPUSearch - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - data, err := monitorService.LoadGPUMonitorData(req) - if err != nil { - helper.InternalServer(c, err) - return - } - helper.SuccessWithData(c, data) -} - // @Tags Monitor // @Summary Clean monitor data // @Success 200 @@ -126,7 +105,3 @@ func (b *BaseApi) GetIOOptions(c *gin.Context) { sort.Strings(options) helper.SuccessWithData(c, options) } - -func (b *BaseApi) GetCPUOptions(c *gin.Context) { - helper.SuccessWithData(c, monitorService.LoadGPUOptions()) -} diff --git a/agent/app/api/v2/terminal.go b/agent/app/api/v2/terminal.go index 0758e0ae5..0b1e180a0 100644 --- a/agent/app/api/v2/terminal.go +++ b/agent/app/api/v2/terminal.go @@ -9,6 +9,7 @@ import ( "time" "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/service" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/cmd" @@ -19,117 +20,30 @@ import ( "github.com/pkg/errors" ) -func (b *BaseApi) WsSSH(c *gin.Context) { - wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) - if err != nil { - global.LOG.Errorf("gin context http handler failed, err: %v", err) - return - } - defer wsConn.Close() - - if global.CONF.Base.IsDemo { - if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) { - return - } - } - - cols, err := strconv.Atoi(c.DefaultQuery("cols", "80")) - if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) { - return - } - rows, err := strconv.Atoi(c.DefaultQuery("rows", "40")) - if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) { - return - } - - hostID, _ := strconv.Atoi(c.DefaultQuery("id", "0")) - var client *ssh.SSHClient - if hostID > 0 { - host, err := service.GetHostInfo(uint(hostID)) - if wshandleError(wsConn, errors.WithMessage(err, "load host info by id failed")) { - return - } - connInfo := ssh.ConnInfo{ - Addr: host.Addr, - Port: int(host.Port), - User: host.User, - AuthMode: host.AuthMode, - Password: host.Password, - PrivateKey: []byte(host.PrivateKey), - } - if len(host.PassPhrase) != 0 { - connInfo.PassPhrase = []byte(host.PassPhrase) - } - client, err = ssh.NewClient(connInfo) - if wshandleError(wsConn, errors.WithMessage(err, "failed to set up the connection. Please check the host information")) { - return - } - } else { - client, err = loadLocalConn() - if wshandleError(wsConn, errors.WithMessage(err, "failed to set up the connection. Please check the host information")) { - return - } - } - defer client.Close() - command := c.DefaultQuery("command", "") - sws, err := terminal.NewLogicSshWsSession(cols, rows, client.Client, wsConn, command) - if wshandleError(wsConn, err) { - return - } - defer sws.Close() - - quitChan := make(chan bool, 3) - sws.Start(quitChan) - go sws.Wait(quitChan) - - <-quitChan - - dt := time.Now().Add(time.Second) - _ = wsConn.WriteControl(websocket.CloseMessage, nil, dt) +func (b *BaseApi) WsLocalTerminal(c *gin.Context) { + client, err := loadLocalConn() + b.runSSHSession(c, client, err, c.DefaultQuery("command", "")) } -func (b *BaseApi) ContainerWsSSH(c *gin.Context) { - wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) - if err != nil { - global.LOG.Errorf("gin context http handler failed, err: %v", err) +func (b *BaseApi) WsHostSSH(c *gin.Context) { + hostID, _ := strconv.Atoi(c.DefaultQuery("id", "0")) + if hostID <= 0 { + b.runSSHSession(c, nil, errors.New("missing host id"), c.DefaultQuery("command", "")) + return + } + host, err := service.GetHostInfo(uint(hostID)) + client, err := newHostSSHClient(host, err) + b.runSSHSession(c, client, err, c.DefaultQuery("command", "")) +} + +func (b *BaseApi) WsContainerTerminal(c *gin.Context) { + wsConn, cols, rows, ok := prepareTerminalSession(c) + if !ok { return } defer wsConn.Close() - if global.CONF.Base.IsDemo { - if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) { - return - } - } - - cols, err := strconv.Atoi(c.DefaultQuery("cols", "80")) - if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) { - return - } - rows, err := strconv.Atoi(c.DefaultQuery("rows", "40")) - if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) { - return - } - source := c.Query("source") - var initCmd []string - switch source { - case "redis", "redis-cluster": - initCmd, err = loadRedisInitCmd(c, source) - case "ollama": - initCmd, err = loadOllamaInitCmd(c) - case "container": - initCmd, err = loadContainerInitCmd(c) - case "database": - initCmd, err = loadDatabaseInitCmd(c) - default: - if wshandleError(wsConn, fmt.Errorf("not support such source %s", source)) { - return - } - } - if wshandleError(wsConn, err) { - return - } - slave, err := terminal.NewCommand("docker", initCmd...) + slave, err := loadContainerTerminalCommand(c) if wshandleError(wsConn, err) { return } @@ -147,10 +61,107 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) { <-quitChan global.LOG.Info("websocket finished") + closeTerminalConn(wsConn) +} + +func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) { + wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + global.LOG.Errorf("gin context http handler failed, err: %v", err) + return nil, 0, 0, false + } + + if global.CONF.Base.IsDemo { + if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) { + return nil, 0, 0, false + } + } + + cols, err := strconv.Atoi(c.DefaultQuery("cols", "80")) + if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) { + return nil, 0, 0, false + } + rows, err := strconv.Atoi(c.DefaultQuery("rows", "40")) + if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) { + return nil, 0, 0, false + } + return wsConn, cols, rows, true +} + +func (b *BaseApi) runSSHSession(c *gin.Context, client *ssh.SSHClient, clientErr error, command string) { + wsConn, cols, rows, ok := prepareTerminalSession(c) + if !ok { + return + } + defer wsConn.Close() + + if wshandleError(wsConn, errors.WithMessage(clientErr, "failed to set up the connection. Please check the host information")) { + return + } + defer client.Close() + + sws, err := terminal.NewLogicSshWsSession(cols, rows, client.Client, wsConn, command) + if wshandleError(wsConn, err) { + return + } + defer sws.Close() + + quitChan := make(chan bool, 3) + sws.Start(quitChan) + go sws.Wait(quitChan) + + <-quitChan + + closeTerminalConn(wsConn) +} + +func closeTerminalConn(wsConn *websocket.Conn) { dt := time.Now().Add(time.Second) _ = wsConn.WriteControl(websocket.CloseMessage, nil, dt) } +func newHostSSHClient(host *model.Host, err error) (*ssh.SSHClient, error) { + if err != nil { + return nil, errors.WithMessage(err, "load host info by id failed") + } + connInfo := ssh.ConnInfo{ + Addr: host.Addr, + Port: int(host.Port), + User: host.User, + AuthMode: host.AuthMode, + Password: host.Password, + PrivateKey: []byte(host.PrivateKey), + } + if len(host.PassPhrase) != 0 { + connInfo.PassPhrase = []byte(host.PassPhrase) + } + return ssh.NewClient(connInfo) +} + +func loadContainerTerminalCommand(c *gin.Context) (*terminal.LocalCommand, error) { + source := c.Query("source") + var ( + initCmd []string + err error + ) + switch source { + case "redis", "redis-cluster": + initCmd, err = loadRedisInitCmd(c, source) + case "ollama": + initCmd, err = loadOllamaInitCmd(c) + case "container": + initCmd, err = loadContainerInitCmd(c) + case "database": + initCmd, err = loadDatabaseInitCmd(c) + default: + return nil, fmt.Errorf("not support such source %s", source) + } + if err != nil { + return nil, err + } + return terminal.NewCommand("docker", initCmd...) +} + func loadRedisInitCmd(c *gin.Context, redisType string) ([]string, error) { name := c.Query("name") from := c.Query("from") diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go index a1eecacef..67619403c 100644 --- a/agent/app/service/agents.go +++ b/agent/app/service/agents.go @@ -127,7 +127,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) { if installs, _ := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(req.Name)); len(installs) > 0 { return nil, buserr.New("ErrNameIsExist") } - if !xpack.IsXpack() { + if !xpack.MultiNodeProvider.IsXpack() { count, _, err := agentRepo.Page(1, 1) if err != nil { return nil, err diff --git a/agent/app/service/alert.go b/agent/app/service/alert.go index 15da2c773..ff9c67b1d 100644 --- a/agent/app/service/alert.go +++ b/agent/app/service/alert.go @@ -501,7 +501,7 @@ func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) { Body: i18n.GetMsgByKey("TestAlert"), IsHTML: false, } - transport := xpack.LoadRequestTransport() + transport := xpack.MultiNodeProvider.LoadRequestTransport() if err := email.SendMail(cfg, msg, transport); err != nil { return false, err } diff --git a/agent/app/service/alert_helper.go b/agent/app/service/alert_helper.go index adcada572..29c067157 100644 --- a/agent/app/service/alert_helper.go +++ b/agent/app/service/alert_helper.go @@ -526,7 +526,7 @@ func loadSSHLogin(alert dto.AlertDTO) { func loadNodeException(alert dto.AlertDTO) { // only master alert - failCount, err := xpack.GetNodeErrorAlert() + failCount, err := xpack.AlertProvider.GetNodeErrorAlert() if err != nil { global.LOG.Errorf("error getting node, err: %s", err) return @@ -555,7 +555,7 @@ func loadNodeException(alert dto.AlertDTO) { func loadLicenseException(alert dto.AlertDTO) { // only master alert - failCount, err := xpack.GetLicenseErrorAlert() + failCount, err := xpack.AlertProvider.GetLicenseErrorAlert() if err != nil { global.LOG.Errorf("error getting license, err: %s", err) return @@ -604,7 +604,7 @@ func sendAlerts(alert dto.AlertDTO, alertType, quota, quotaType string, params [ AlertId: alert.ID, Count: todayCount + 1, } - alertErr := xpack.CreateSMSAlertLog(alertType, alert, create, quotaType, params, constant.SMS) + alertErr := xpack.AlertProvider.CreateSMSAlertLog(alertType, alert, create, quotaType, params, constant.SMS) if alertErr != nil { global.LOG.Infof("%s alert sms push faild, err: %v", alertType, alertErr.Error()) continue @@ -624,8 +624,8 @@ func sendAlerts(alert dto.AlertDTO, alertType, quota, quotaType string, params [ alertInfo.Type = alertType create.AlertRule = alertUtil.ProcessAlertRule(alert) create.AlertDetail = alertUtil.ProcessAlertDetail(alertInfo, quotaType, params, constant.Email) - transport := xpack.LoadRequestTransport() - agentInfo, _ := xpack.GetAgentInfo() + transport := xpack.MultiNodeProvider.LoadRequestTransport() + agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() alertErr := alertUtil.CreateEmailAlertLog(create, alertInfo, params, transport, agentInfo) if alertErr != nil { global.LOG.Infof("%s alert email push faild, err: %v", alertType, alertErr.Error()) @@ -642,9 +642,9 @@ func sendAlerts(alert dto.AlertDTO, alertType, quota, quotaType string, params [ AlertId: alert.ID, Count: todayCount + 1, } - transport := xpack.LoadRequestTransport() - agentInfo, _ := xpack.GetAgentInfo() - err := xpack.CreateWebhookAlertLog(alertType, alert, create, quotaType, params, m, transport, agentInfo) + transport := xpack.MultiNodeProvider.LoadRequestTransport() + agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() + err := xpack.AlertProvider.CreateWebhookAlertLog(alertType, alert, create, quotaType, params, m, transport, agentInfo) if err != nil { global.LOG.Infof("%s alert webhook %s push faild, err: %v", alertType, m, err) continue @@ -664,8 +664,8 @@ func sendAlerts(alert dto.AlertDTO, alertType, quota, quotaType string, params [ alertInfo.Type = alertType create.AlertRule = alertUtil.ProcessAlertRule(alert) create.AlertDetail = alertUtil.ProcessAlertDetail(alertInfo, quotaType, params, m) - transport := xpack.LoadRequestTransport() - agentInfo, _ := xpack.GetAgentInfo() + transport := xpack.MultiNodeProvider.LoadRequestTransport() + agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() alertErr := alertUtil.CreateBarkAlertLog(create, alertInfo, params, transport, agentInfo) if alertErr != nil { global.LOG.Infof("%s alert %s push failed, err: %v", alertType, m, alertErr.Error()) diff --git a/agent/app/service/alert_sender.go b/agent/app/service/alert_sender.go index 9d1df8f05..b64efa7aa 100644 --- a/agent/app/service/alert_sender.go +++ b/agent/app/service/alert_sender.go @@ -72,7 +72,7 @@ func (s *AlertSender) sendSMS(quota string, params []dto.Param) { Type: s.alert.Type, } - err := xpack.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, constant.SMS) + err := xpack.AlertProvider.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, constant.SMS) if err != nil { global.LOG.Errorf("%s alert sms push failed: %v", s.alert.Type, err) return @@ -95,8 +95,8 @@ func (s *AlertSender) sendEmail(quota string, params []dto.Param) { AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Email), } - transport := xpack.LoadRequestTransport() - agentInfo, _ := xpack.GetAgentInfo() + transport := xpack.MultiNodeProvider.LoadRequestTransport() + agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() err := alertUtil.CreateEmailAlertLog(create, s.alert, params, transport, agentInfo) if err != nil { global.LOG.Errorf("%s alert email push failed: %v", s.alert.Type, err) @@ -120,8 +120,8 @@ func (s *AlertSender) sendBark(quota string, params []dto.Param) { AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Bark), } - transport := xpack.LoadRequestTransport() - agentInfo, _ := xpack.GetAgentInfo() + transport := xpack.MultiNodeProvider.LoadRequestTransport() + agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() err := alertUtil.CreateBarkAlertLog(create, s.alert, params, transport, agentInfo) if err != nil { global.LOG.Errorf("%s alert bark push failed: %v", s.alert.Type, err) @@ -142,9 +142,9 @@ func (s *AlertSender) sendWebhook(quota string, params []dto.Param, method strin AlertId: s.alert.ID, Type: s.alert.Type, } - transport := xpack.LoadRequestTransport() - agentInfo, _ := xpack.GetAgentInfo() - err := xpack.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, method, transport, agentInfo) + transport := xpack.MultiNodeProvider.LoadRequestTransport() + agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() + err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, method, transport, agentInfo) if err != nil { global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, method, err) return @@ -169,7 +169,7 @@ func (s *AlertSender) sendResourceSMS(quota string, params []dto.Param) { Type: s.alert.Type, } - if err := xpack.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, constant.SMS); err != nil { + if err := xpack.AlertProvider.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, constant.SMS); err != nil { global.LOG.Errorf("failed to send SMS alert: %v", err) return } @@ -191,8 +191,8 @@ func (s *AlertSender) sendResourceEmail(quota string, params []dto.Param) { AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Email), } - transport := xpack.LoadRequestTransport() - agentInfo, _ := xpack.GetAgentInfo() + transport := xpack.MultiNodeProvider.LoadRequestTransport() + agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() if err := alertUtil.CreateEmailAlertLog(create, s.alert, params, transport, agentInfo); err != nil { global.LOG.Errorf("failed to send Email alert: %v", err) return @@ -215,8 +215,8 @@ func (s *AlertSender) sendResourceBark(quota string, params []dto.Param) { AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Bark), } - transport := xpack.LoadRequestTransport() - agentInfo, _ := xpack.GetAgentInfo() + transport := xpack.MultiNodeProvider.LoadRequestTransport() + agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() if err := alertUtil.CreateBarkAlertLog(create, s.alert, params, transport, agentInfo); err != nil { global.LOG.Errorf("failed to send Bark alert: %v", err) return @@ -236,9 +236,9 @@ func (s *AlertSender) sendResourceWebhook(quota string, params []dto.Param, meth AlertId: s.alert.ID, Type: s.alert.Type, } - transport := xpack.LoadRequestTransport() - agentInfo, _ := xpack.GetAgentInfo() - if err := xpack.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, method, transport, agentInfo); err != nil { + transport := xpack.MultiNodeProvider.LoadRequestTransport() + agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() + if err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, method, transport, agentInfo); err != nil { global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, method, err) return } diff --git a/agent/app/service/app.go b/agent/app/service/app.go index 8f3bd0e2d..e96a5a51f 100644 --- a/agent/app/service/app.go +++ b/agent/app/service/app.go @@ -966,7 +966,7 @@ func deleteCustomApp() { } func (a AppService) SyncAppListFromRemote(taskID string) (err error) { - if xpack.IsUseCustomApp() { + if xpack.MultiNodeProvider.IsUseCustomApp() { return nil } diff --git a/agent/app/service/app_sync_task.go b/agent/app/service/app_sync_task.go index e9cdf5e83..b05c07f56 100644 --- a/agent/app/service/app_sync_task.go +++ b/agent/app/service/app_sync_task.go @@ -82,7 +82,7 @@ func (a AppService) createSyncAppStoreTask(sharedCtx **appSyncContext) func(t *t ctx := &appSyncContext{ task: t, - httpClient: http.Client{Timeout: time.Duration(constant.TimeOut20s) * time.Second, Transport: xpack.LoadRequestTransport()}, + httpClient: http.Client{Timeout: time.Duration(constant.TimeOut20s) * time.Second, Transport: xpack.MultiNodeProvider.LoadRequestTransport()}, baseRemoteUrl: fmt.Sprintf("%s/%s/1panel", global.AppRepoURL(), global.CONF.Base.Mode), systemVersion: setting.SystemVersion, settingService: settingService, @@ -279,9 +279,9 @@ func (c *appSyncContext) syncAppIconsAndDetails() error { }() var ( - completed int - icon200Count int - icon304Count int + completed int + icon200Count int + icon304Count int iconFailCount int ) milestones := [4]int{totalWork / 4, totalWork / 2, totalWork * 3 / 4, totalWork} diff --git a/agent/app/service/app_utils.go b/agent/app/service/app_utils.go index 806d25d87..ecceca6dc 100644 --- a/agent/app/service/app_utils.go +++ b/agent/app/service/app_utils.go @@ -1178,7 +1178,7 @@ func upApp(task *task.Task, appInstall *model.AppInstall, pullImages bool) error if err != nil { return err } - imagePrefix := xpack.GetImagePrefix() + imagePrefix := xpack.MultiNodeProvider.GetImagePrefix() dockerCLi, err := docker.NewClient() if err != nil { return err @@ -1778,7 +1778,7 @@ func addDockerComposeCommonParam(composeMap map[string]interface{}, serviceName if !serviceValid { return buserr.New("ErrFileParse") } - imagePreFix := xpack.GetImagePrefix() + imagePreFix := xpack.MultiNodeProvider.GetImagePrefix() if imagePreFix != "" { for _, service := range services { serviceValue := service.(map[string]interface{}) diff --git a/agent/app/service/clam.go b/agent/app/service/clam.go index e91f060c4..881435127 100644 --- a/agent/app/service/clam.go +++ b/agent/app/service/clam.go @@ -179,7 +179,7 @@ func (c *ClamService) Create(req dto.ClamCreate) error { clam.InfectedDir = "" } if len(req.Spec) != 0 { - entryID, err := xpack.StartClam(&clam, false) + entryID, err := xpack.MultiNodeProvider.StartClam(&clam, false) if err != nil { return err } @@ -232,7 +232,7 @@ func (c *ClamService) Update(req dto.ClamUpdate) error { upMap["entry_id"] = 0 } if len(req.Spec) != 0 && clam.Status != constant.StatusDisable { - newEntryID, err := xpack.StartClam(&clamItem, true) + newEntryID, err := xpack.MultiNodeProvider.StartClam(&clamItem, true) if err != nil { return err } @@ -276,7 +276,7 @@ func (c *ClamService) UpdateStatus(id uint, status string) error { err error ) if status == constant.StatusEnable { - entryID, err = xpack.StartClam(&clam, true) + entryID, err = xpack.MultiNodeProvider.StartClam(&clam, true) if err != nil { return err } diff --git a/agent/app/service/monitor.go b/agent/app/service/monitor.go index a619c6c5d..1164bff7c 100644 --- a/agent/app/service/monitor.go +++ b/agent/app/service/monitor.go @@ -39,12 +39,13 @@ var monitorCancel context.CancelFunc type IMonitorService interface { Run() LoadMonitorData(req dto.MonitorSearch) ([]dto.MonitorData, error) - LoadGPUOptions() dto.MonitorGPUOptions - LoadGPUMonitorData(req dto.MonitorGPUSearch) (dto.MonitorGPUData, error) LoadSetting() (*dto.MonitorSetting, error) UpdateSetting(key, value string) error CleanData() error + LoadGPUOptions() dto.MonitorGPUOptions + LoadGPUMonitorData(req dto.MonitorGPUSearch) (dto.MonitorGPUData, error) + saveIODataToDB(ctx context.Context, interval float64) saveNetDataToDB(ctx context.Context, interval float64) } diff --git a/agent/app/service/website_ssl.go b/agent/app/service/website_ssl.go index ec5a691f3..6039ef959 100644 --- a/agent/app/service/website_ssl.go +++ b/agent/app/service/website_ssl.go @@ -436,7 +436,7 @@ func (w WebsiteSSLService) obtainSSL(id uint, autoRenew bool) error { reloadSystemSSL(websiteSSL, logger) if websiteSSL.PushNode { printSSLLog(logger, "StartPushSSLToNode", nil) - if err = xpack.PushSSLToNode(websiteSSL); err != nil { + if err = xpack.MultiNodeProvider.PushSSLToNode(websiteSSL); err != nil { printSSLLog(logger, "PushSSLToNodeFailed", map[string]interface{}{"err": err.Error()}) return } diff --git a/agent/app/service/website_utils.go b/agent/app/service/website_utils.go index 42d7ece48..3bca11e8e 100644 --- a/agent/app/service/website_utils.go +++ b/agent/app/service/website_utils.go @@ -576,7 +576,7 @@ func delNginxConfig(website model.Website, force bool) error { } sitePath := GetSiteDir(website.Alias) if fileOp.Stat(sitePath) { - xpack.RemoveTamper(website.Alias) + xpack.MultiNodeProvider.RemoveTamper(website.Alias) _ = fileOp.DeleteDir(sitePath) } diff --git a/agent/go.mod b/agent/go.mod index 8d8310e20..63ca27cbc 100644 --- a/agent/go.mod +++ b/agent/go.mod @@ -68,7 +68,6 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/BurntSushi/toml v1.6.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/STARRY-S/zip v0.2.3 // indirect github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.16 // indirect @@ -221,7 +220,6 @@ require ( github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/sigstore/sigstore v1.10.5 // indirect github.com/sigstore/sigstore-go v1.1.4 // indirect - github.com/sorairolake/lzip-go v0.3.8 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.80 // indirect diff --git a/agent/go.sum b/agent/go.sum index df1501304..0e730d097 100644 --- a/agent/go.sum +++ b/agent/go.sum @@ -55,8 +55,6 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= -github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4= -github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/sarama v1.30.1/go.mod h1:hGgx05L/DiW8XYBXeJdKIN6V2QUy2H6JqME5VT1NLRw= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= @@ -348,8 +346,6 @@ github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9 github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gammazero/toposort v0.1.1 h1:OivGxsWxF3U3+U80VoLJ+f50HcPU1MIqE1JlKzoJ2Eg= github.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw= -github.com/gammazero/toposort v0.2.0 h1:4wndQUKr24ALbkDnYag1bMGJBR+2y++cIGST1lb+L9w= -github.com/gammazero/toposort v0.2.0/go.mod h1:X4xn3isrX0SRJi+T0wfUkmzubOm2uOQynDzAsO4yiMQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko= github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s= @@ -727,8 +723,6 @@ github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04 github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/mholt/archiver/v4 v4.0.0-alpha.8 h1:tRGQuDVPh66WCOelqe6LIGh0gwmfwxUrSSDunscGsRM= github.com/mholt/archiver/v4 v4.0.0-alpha.8/go.mod h1:5f7FUYGXdJWUjESffJaYR4R60VhnHxb2X3T1teMyv5A= -github.com/mholt/archiver/v4 v4.0.0-alpha.9 h1:EZgAsW6DsuawxDgTtIdjCUBa2TQ6AOe9pnCidofSRtE= -github.com/mholt/archiver/v4 v4.0.0-alpha.9/go.mod h1:5D3uct315OMkMRXKwEuMB+wQi/2m5NQngKDmApqwVlo= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= @@ -921,8 +915,6 @@ github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7D github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= github.com/qiniu/go-sdk/v7 v7.26.4 h1:D/k6cPbhjKvSx9hVBQSh97GuPu1ZtM3OLPCclSpusjo= github.com/qiniu/go-sdk/v7 v7.26.4/go.mod h1:ri7fGwbio0pRDFr8EK5TUpx0DbnpIMJ2bMSDxGWfCbk= -github.com/qiniu/go-sdk/v7 v7.26.9 h1:is9th0m6RCq9LBq7Cuh7mmGUTfR2xb68SGO3xH7ZrA0= -github.com/qiniu/go-sdk/v7 v7.26.9/go.mod h1:ri7fGwbio0pRDFr8EK5TUpx0DbnpIMJ2bMSDxGWfCbk= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= @@ -980,8 +972,6 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1 github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= -github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik= -github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spdx/tools-golang v0.5.7 h1:+sWcKGnhwp3vLdMqPcLdA6QK679vd86cK9hQWH3AwCg= github.com/spdx/tools-golang v0.5.7/go.mod h1:jg7w0LOpoNAw6OxKEzCoqPC2GCTj45LyTlVmXubDsYw= diff --git a/agent/init/hook/hook.go b/agent/init/hook/hook.go index 5895c162c..156e6d914 100644 --- a/agent/init/hook/hook.go +++ b/agent/init/hook/hook.go @@ -38,7 +38,7 @@ func initGlobalData() { if err := settingRepo.Update("SystemStatus", "Free"); err != nil { global.LOG.Fatalf("init service before start failed, err: %v", err) } - node, _ := xpack.LoadNodeInfo(false) + node, _ := xpack.MultiNodeProvider.LoadNodeInfo(false) if len(node.Version) != 0 { _ = settingRepo.Update("SystemVersion", node.Version) } diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index ce47fdbde..f94525b8d 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -97,7 +97,7 @@ var InitSetting = &gormigrate.Migration{ ID: "20240722-init-setting", Migrate: func(tx *gorm.DB) error { global.CONF.Base.EncryptKey = common.RandStr(16) - nodeInfo, err := xpack.LoadNodeInfo(true) + nodeInfo, err := xpack.MultiNodeProvider.LoadNodeInfo(true) if err != nil { return err } diff --git a/agent/init/viper/viper.go b/agent/init/viper/viper.go index f1f77cdb4..696c0b48d 100644 --- a/agent/init/viper/viper.go +++ b/agent/init/viper/viper.go @@ -56,7 +56,7 @@ func Init() { } func initBaseInfo() { - nodeInfo, err := xpack.LoadNodeInfo(true) + nodeInfo, err := xpack.MultiNodeProvider.LoadNodeInfo(true) if err != nil { panic(err) } diff --git a/agent/middleware/certificate.go b/agent/middleware/certificate.go index 1a96426fa..634fecd90 100644 --- a/agent/middleware/certificate.go +++ b/agent/middleware/certificate.go @@ -19,7 +19,7 @@ func Certificate() gin.HandlerFunc { c.Next() return } - if !xpack.ValidateCertificate(c) { + if !xpack.MultiNodeProvider.ValidateCertificate(c) { CloseDirectly(c) return } diff --git a/agent/router/entry_xpack.go b/agent/router/entry_xpack.go deleted file mode 100644 index b311f38ec..000000000 --- a/agent/router/entry_xpack.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build xpack - -package router - -import ( - xpackRouter "github.com/1Panel-dev/1Panel/agent/xpack/router" -) - -func RouterGroups() []CommonRouter { - baseRouter := commonGroups() - for _, ro := range xpackRouter.XpackGroups() { - if val, ok := ro.(CommonRouter); ok { - baseRouter = append(baseRouter, val) - } - } - return baseRouter -} - -var RouterGroupApp = RouterGroups() diff --git a/agent/router/entry_xpackee.go b/agent/router/entry_xpackee.go deleted file mode 100644 index f76560a2d..000000000 --- a/agent/router/entry_xpackee.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build xpackee - -package router - -import ( - xpackRouter "github.com/1Panel-dev/1Panel/agent/xpack/router" -) - -func RouterGroups() []CommonRouter { - baseRouter := commonGroups() - for _, ro := range xpackRouter.XpackGroups() { - if val, ok := ro.(CommonRouter); ok { - baseRouter = append(baseRouter, val) - } - } - return baseRouter -} - -var RouterGroupApp = RouterGroups() diff --git a/agent/router/ro_ai.go b/agent/router/ro_ai.go index 1304f9b7e..b62f0e9cd 100644 --- a/agent/router/ro_ai.go +++ b/agent/router/ro_ai.go @@ -20,11 +20,14 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) { aiToolsRouter.POST("/ollama/model/sync", baseApi.SyncOllamaModel) aiToolsRouter.POST("/ollama/model/load", baseApi.LoadOllamaModelDetail) aiToolsRouter.POST("/ollama/model/del", baseApi.DeleteOllamaModel) - aiToolsRouter.GET("/gpu/load", baseApi.LoadGpuInfo) aiToolsRouter.POST("/domain/bind", baseApi.BindDomain) aiToolsRouter.POST("/domain/get", baseApi.GetBindDomain) aiToolsRouter.POST("/domain/update", baseApi.UpdateBindDomain) + aiToolsRouter.GET("/gpu/load", baseApi.LoadGpuInfo) + aiToolsRouter.POST("/gpu/search", baseApi.LoadGPUMonitor) + aiToolsRouter.GET("/gpu/options", baseApi.GetCPUOptions) + aiToolsRouter.POST("/mcp/search", baseApi.PageMcpServers) aiToolsRouter.POST("/mcp/server", baseApi.CreateMcpServer) aiToolsRouter.POST("/mcp/server/update", baseApi.UpdateMcpServer) diff --git a/agent/router/ro_container.go b/agent/router/ro_container.go index dbbc82f5a..906abab72 100644 --- a/agent/router/ro_container.go +++ b/agent/router/ro_container.go @@ -11,7 +11,6 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) { baRouter := Router.Group("containers") baseApi := v2.ApiGroupApp.BaseApi { - baRouter.GET("/exec", baseApi.ContainerWsSSH) baRouter.GET("/stats/:id", baseApi.ContainerStats) baRouter.POST("", baseApi.ContainerCreate) diff --git a/agent/router/ro_host.go b/agent/router/ro_host.go index bb4446842..06bb0a210 100644 --- a/agent/router/ro_host.go +++ b/agent/router/ro_host.go @@ -39,9 +39,7 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) { hostRouter.POST("/firewall/filter/chain/status", baseApi.LoadChainStatus) hostRouter.POST("/monitor/search", baseApi.LoadMonitor) - hostRouter.POST("/monitor/gpu/search", baseApi.LoadGPUMonitor) hostRouter.POST("/monitor/clean", baseApi.CleanMonitor) - hostRouter.GET("/monitor/gpuoptions", baseApi.GetCPUOptions) hostRouter.GET("/monitor/netoptions", baseApi.GetNetworkOptions) hostRouter.GET("/monitor/iooptions", baseApi.GetIOOptions) hostRouter.GET("/monitor/setting", baseApi.LoadMonitorSetting) @@ -69,7 +67,9 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) { hostRouter.GET("/tool/supervisor/process", baseApi.GetProcess) hostRouter.POST("/tool/supervisor/process/file", baseApi.GetProcessFile) - hostRouter.GET("/terminal", baseApi.WsSSH) + hostRouter.GET("/terminal/local", baseApi.WsLocalTerminal) + hostRouter.GET("/terminal/ssh", baseApi.WsHostSSH) + hostRouter.GET("/terminal/container", baseApi.WsContainerTerminal) hostRouter.GET("/disks", baseApi.GetCompleteDiskInfo) hostRouter.POST("/disks/partition", baseApi.PartitionDisk) diff --git a/agent/server/init_xpack.go b/agent/server/init_xpack.go deleted file mode 100644 index f771928e9..000000000 --- a/agent/server/init_xpack.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build xpack - -package server - -import ( - xpack "github.com/1Panel-dev/1Panel/agent/xpack" -) - -func InitOthers() { - xpack.Init() -} diff --git a/agent/server/init_xpackee.go b/agent/server/init_xpackee.go deleted file mode 100644 index 411aaf8f1..000000000 --- a/agent/server/init_xpackee.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build xpackee - -package server - -import ( - xpack "github.com/1Panel-dev/1Panel/agent/xpack" -) - -func InitOthers() { - xpack.Init() -} diff --git a/agent/utils/alert_push/alert_push.go b/agent/utils/alert_push/alert_push.go index e28765d0a..3b149b83e 100644 --- a/agent/utils/alert_push/alert_push.go +++ b/agent/utils/alert_push/alert_push.go @@ -42,7 +42,7 @@ func PushAlert(pushAlert dto.PushAlert) error { AlertId: alert.ID, Count: todayCount + 1, } - err = xpack.CreateTaskScanSMSAlertLog(alert, alert.Type, create, pushAlert, constant.SMS) + err = xpack.AlertProvider.CreateTaskScanSMSAlertLog(alert, alert.Type, create, pushAlert, constant.SMS) if err != nil { global.LOG.Errorf("%s alert sms push failed: %v", alert.Type, err) continue @@ -58,8 +58,8 @@ func PushAlert(pushAlert dto.PushAlert) error { AlertId: alert.ID, Count: todayCount + 1, } - transport := xpack.LoadRequestTransport() - agentInfo, _ := xpack.GetAgentInfo() + transport := xpack.MultiNodeProvider.LoadRequestTransport() + agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() err = alertUtil.CreateTaskScanEmailAlertLog(alert, create, pushAlert, constant.Email, transport, agentInfo) if err != nil { global.LOG.Errorf("%s alert email push failed: %v", alert.Type, err) @@ -76,8 +76,8 @@ func PushAlert(pushAlert dto.PushAlert) error { AlertId: alert.ID, Count: todayCount + 1, } - transport := xpack.LoadRequestTransport() - agentInfo, _ := xpack.GetAgentInfo() + transport := xpack.MultiNodeProvider.LoadRequestTransport() + agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() params := alertUtil.CreateAlertParams(alertUtil.GetCronJobTypeName(pushAlert.Param)) alertDetail := alertUtil.ProcessAlertDetail(alert, pushAlert.TaskName, params, constant.Bark) alertRule := alertUtil.ProcessAlertRule(alert) @@ -99,9 +99,9 @@ func PushAlert(pushAlert dto.PushAlert) error { AlertId: alert.ID, Count: todayCount + 1, } - transport := xpack.LoadRequestTransport() - agentInfo, _ := xpack.GetAgentInfo() - err = xpack.CreateTaskScanWebhookAlertLog(alert, alert.Type, create, pushAlert, m, transport, agentInfo) + transport := xpack.MultiNodeProvider.LoadRequestTransport() + agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() + err = xpack.AlertProvider.CreateTaskScanWebhookAlertLog(alert, alert.Type, create, pushAlert, m, transport, agentInfo) if err != nil { global.LOG.Errorf("%s alert %s webhook push failed: %v", alert.Type, m, err) continue diff --git a/agent/utils/req_helper/request.go b/agent/utils/req_helper/request.go index 828c94678..378c43f96 100644 --- a/agent/utils/req_helper/request.go +++ b/agent/utils/req_helper/request.go @@ -47,7 +47,7 @@ func HandleGet(url string) (*http.Response, error) { } func HandleRequest(url, method string, timeout int) (int, []byte, error) { - transport := xpack.LoadRequestTransport() + transport := xpack.MultiNodeProvider.LoadRequestTransport() client := http.Client{Timeout: time.Duration(timeout) * time.Second, Transport: transport} return HandleRequestWithClient(&client, url, method, timeout) } @@ -130,7 +130,7 @@ func RequestFile(url, method string, timeout int) (io.ReadCloser, context.Cancel return } }() - transport := xpack.LoadRequestTransport() + transport := xpack.MultiNodeProvider.LoadRequestTransport() client := http.Client{Timeout: time.Duration(timeout) * time.Second, Transport: transport} ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) request, err := http.NewRequestWithContext(ctx, method, url, nil) diff --git a/agent/utils/xpack/community.go b/agent/utils/xpack/community.go index f7d137ca9..464635b62 100644 --- a/agent/utils/xpack/community.go +++ b/agent/utils/xpack/community.go @@ -2,92 +2,8 @@ package xpack -import ( - "crypto/tls" - "net" - "net/http" - "time" +import "github.com/1Panel-dev/1Panel/agent/utils/xpack/helper" - "github.com/1Panel-dev/1Panel/agent/app/dto" - "github.com/1Panel-dev/1Panel/agent/app/model" - "github.com/1Panel-dev/1Panel/agent/buserr" - "github.com/1Panel-dev/1Panel/agent/global" - "github.com/1Panel-dev/1Panel/agent/utils/common" - "github.com/gin-gonic/gin" -) +var AlertProvider = helper.NewIAlertProvider() -func RemoveTamper(website string) {} - -func StartClam(startClam *model.Clam, isUpdate bool) (int, error) { - return 0, buserr.New("ErrXpackNotFound") -} - -func LoadNodeInfo(isBase bool) (model.NodeInfo, error) { - var info model.NodeInfo - info.BaseDir = common.LoadParams("BASE_DIR") - info.Version = common.LoadParams("ORIGINAL_VERSION") - info.Scope = "master" - global.IsMaster = true - return info, nil -} - -func GetImagePrefix() string { - return "" -} - -func IsUseCustomApp() bool { - return false -} - -func IsXpack() bool { - return false -} - -func CreateTaskScanSMSAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string) error { - return nil -} - -func CreateSMSAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string) error { - return nil -} - -func CreateTaskScanWebhookAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error { - return nil -} - -func CreateWebhookAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error { - return nil -} - -func GetLicenseErrorAlert() (uint, error) { - return 0, nil -} - -func GetNodeErrorAlert() (uint, error) { - return 0, nil -} - -func LoadRequestTransport() *http.Transport { - return &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - DialContext: (&net.Dialer{ - Timeout: 60 * time.Second, - KeepAlive: 60 * time.Second, - }).DialContext, - TLSHandshakeTimeout: 5 * time.Second, - ResponseHeaderTimeout: 10 * time.Second, - IdleConnTimeout: 15 * time.Second, - } -} - -func ValidateCertificate(c *gin.Context) bool { - return true -} - -func PushSSLToNode(websiteSSL *model.WebsiteSSL) error { - return nil -} - -func GetAgentInfo() (*dto.AgentInfo, error) { - return nil, nil -} +var MultiNodeProvider = helper.NewIMultiNodeProvider() diff --git a/agent/utils/xpack/helper/alert.go b/agent/utils/xpack/helper/alert.go new file mode 100644 index 000000000..f1169fdb0 --- /dev/null +++ b/agent/utils/xpack/helper/alert.go @@ -0,0 +1,38 @@ +package helper + +import ( + "net/http" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/utils/xpack/providers" +) + +type alertHelper struct{} + +func NewIAlertProvider() providers.AlertProvider { + return &alertHelper{} +} + +func (a *alertHelper) CreateTaskScanSMSAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string) error { + return nil +} + +func (a *alertHelper) CreateSMSAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string) error { + return nil +} + +func (a *alertHelper) CreateTaskScanWebhookAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error { + return nil +} + +func (a *alertHelper) CreateWebhookAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error { + return nil +} + +func (a *alertHelper) GetLicenseErrorAlert() (uint, error) { + return 0, nil +} + +func (a *alertHelper) GetNodeErrorAlert() (uint, error) { + return 0, nil +} diff --git a/agent/utils/xpack/helper/multi_node.go b/agent/utils/xpack/helper/multi_node.go new file mode 100644 index 000000000..1c4700711 --- /dev/null +++ b/agent/utils/xpack/helper/multi_node.go @@ -0,0 +1,74 @@ +package helper + +import ( + "crypto/tls" + "net" + "net/http" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/common" + "github.com/1Panel-dev/1Panel/agent/utils/xpack/providers" + "github.com/gin-gonic/gin" +) + +type multiNodeHelper struct{} + +func NewIMultiNodeProvider() providers.MultiNodeProvider { + return &multiNodeHelper{} +} + +func (m *multiNodeHelper) RemoveTamper(website string) {} + +func (m *multiNodeHelper) StartClam(startClam *model.Clam, isUpdate bool) (int, error) { + return 0, buserr.New("ErrXpackNotFound") +} + +func (m *multiNodeHelper) LoadNodeInfo(isBase bool) (model.NodeInfo, error) { + var info model.NodeInfo + info.BaseDir = common.LoadParams("BASE_DIR") + info.Version = common.LoadParams("ORIGINAL_VERSION") + info.Scope = "master" + global.IsMaster = true + return info, nil +} + +func (m *multiNodeHelper) GetImagePrefix() string { + return "" +} + +func (m *multiNodeHelper) IsUseCustomApp() bool { + return false +} + +func (m *multiNodeHelper) IsXpack() bool { + return false +} + +func (m *multiNodeHelper) LoadRequestTransport() *http.Transport { + return &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + DialContext: (&net.Dialer{ + Timeout: 60 * time.Second, + KeepAlive: 60 * time.Second, + }).DialContext, + TLSHandshakeTimeout: 5 * time.Second, + ResponseHeaderTimeout: 10 * time.Second, + IdleConnTimeout: 15 * time.Second, + } +} + +func (m *multiNodeHelper) ValidateCertificate(c *gin.Context) bool { + return true +} + +func (m *multiNodeHelper) PushSSLToNode(websiteSSL *model.WebsiteSSL) error { + return nil +} + +func (m *multiNodeHelper) GetAgentInfo() (*dto.AgentInfo, error) { + return nil, nil +} diff --git a/agent/utils/xpack/providers/alert.go b/agent/utils/xpack/providers/alert.go new file mode 100644 index 000000000..1a09489b5 --- /dev/null +++ b/agent/utils/xpack/providers/alert.go @@ -0,0 +1,17 @@ +package providers + +import ( + "net/http" + + "github.com/1Panel-dev/1Panel/agent/app/dto" +) + +type AlertProvider interface { + GetNodeErrorAlert() (uint, error) + GetLicenseErrorAlert() (uint, error) + + CreateTaskScanSMSAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string) error + CreateSMSAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string) error + CreateTaskScanWebhookAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error + CreateWebhookAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error +} diff --git a/agent/utils/xpack/providers/multi_node.go b/agent/utils/xpack/providers/multi_node.go new file mode 100644 index 000000000..5defd2cc7 --- /dev/null +++ b/agent/utils/xpack/providers/multi_node.go @@ -0,0 +1,23 @@ +package providers + +import ( + "net/http" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/gin-gonic/gin" +) + +type MultiNodeProvider interface { + IsXpack() bool + IsUseCustomApp() bool + GetImagePrefix() string + RemoveTamper(website string) + StartClam(startClam *model.Clam, isUpdate bool) (int, error) + LoadNodeInfo(isBase bool) (model.NodeInfo, error) + + LoadRequestTransport() *http.Transport + ValidateCertificate(c *gin.Context) bool + PushSSLToNode(websiteSSL *model.WebsiteSSL) error + GetAgentInfo() (*dto.AgentInfo, error) +} diff --git a/agent/utils/xpack/xpack.go b/agent/utils/xpack/xpack.go deleted file mode 100644 index ddc303102..000000000 --- a/agent/utils/xpack/xpack.go +++ /dev/null @@ -1,74 +0,0 @@ -//go:build xpack - -package xpack - -import ( - "net/http" - - "github.com/1Panel-dev/1Panel/agent/app/dto" - "github.com/1Panel-dev/1Panel/agent/app/model" - edition "github.com/1Panel-dev/1Panel/agent/xpack/edition" - "github.com/gin-gonic/gin" -) - -func RemoveTamper(website string) { - edition.RemoveTamper(website) -} - -func StartClam(startClam *model.Clam, isUpdate bool) (int, error) { - return edition.StartClam(startClam, isUpdate) -} - -func LoadNodeInfo(isBase bool) (model.NodeInfo, error) { - return edition.LoadNodeInfo(isBase) -} - -func GetImagePrefix() string { - return edition.GetImagePrefix() -} - -func IsUseCustomApp() bool { - return edition.IsUseCustomApp() -} - -func IsXpack() bool { - return edition.IsXpack() -} - -func CreateTaskScanSMSAlertLog(info dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string) error { - return edition.CreateTaskScanSMSAlertLog(info, alertType, create, pushAlert, method) -} - -func CreateSMSAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string) error { - return edition.CreateSMSAlertLog(alertType, info, create, project, params, method) -} - -func CreateTaskScanWebhookAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error { - return edition.CreateTaskScanWebhookAlertLog(alert, alertType, create, pushAlert, method, transport, agentInfo) -} - -func CreateWebhookAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error { - return edition.CreateWebhookAlertLog(alertType, info, create, project, params, method, transport, agentInfo) -} - -func GetLicenseErrorAlert() (uint, error) { - return edition.GetLicenseErrorAlert() -} - -func GetNodeErrorAlert() (uint, error) { - return edition.GetNodeErrorAlert() -} - -func LoadRequestTransport() *http.Transport { return edition.LoadRequestTransport() } - -func ValidateCertificate(c *gin.Context) bool { - return edition.ValidateCertificate(c) -} - -func PushSSLToNode(websiteSSL *model.WebsiteSSL) error { - return edition.PushSSLToNode(websiteSSL) -} - -func GetAgentInfo() (*dto.AgentInfo, error) { - return edition.GetAgentInfo() -} diff --git a/agent/utils/xpack/xpackee.go b/agent/utils/xpack/xpackee.go deleted file mode 100644 index 69df0ddbd..000000000 --- a/agent/utils/xpack/xpackee.go +++ /dev/null @@ -1,74 +0,0 @@ -//go:build xpackee - -package xpack - -import ( - "net/http" - - "github.com/1Panel-dev/1Panel/agent/app/dto" - "github.com/1Panel-dev/1Panel/agent/app/model" - edition "github.com/1Panel-dev/1Panel/agent/xpack/edition" - "github.com/gin-gonic/gin" -) - -func RemoveTamper(website string) { - edition.RemoveTamper(website) -} - -func StartClam(startClam *model.Clam, isUpdate bool) (int, error) { - return edition.StartClam(startClam, isUpdate) -} - -func LoadNodeInfo(isBase bool) (model.NodeInfo, error) { - return edition.LoadNodeInfo(isBase) -} - -func GetImagePrefix() string { - return edition.GetImagePrefix() -} - -func IsUseCustomApp() bool { - return edition.IsUseCustomApp() -} - -func IsXpack() bool { - return edition.IsXpack() -} - -func CreateTaskScanSMSAlertLog(info dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string) error { - return edition.CreateTaskScanSMSAlertLog(info, alertType, create, pushAlert, method) -} - -func CreateSMSAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string) error { - return edition.CreateSMSAlertLog(alertType, info, create, project, params, method) -} - -func CreateTaskScanWebhookAlertLog(alert dto.AlertDTO, alertType string, create dto.AlertLogCreate, pushAlert dto.PushAlert, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error { - return edition.CreateTaskScanWebhookAlertLog(alert, alertType, create, pushAlert, method, transport, agentInfo) -} - -func CreateWebhookAlertLog(alertType string, info dto.AlertDTO, create dto.AlertLogCreate, project string, params []dto.Param, method string, transport *http.Transport, agentInfo *dto.AgentInfo) error { - return edition.CreateWebhookAlertLog(alertType, info, create, project, params, method, transport, agentInfo) -} - -func GetLicenseErrorAlert() (uint, error) { - return edition.GetLicenseErrorAlert() -} - -func GetNodeErrorAlert() (uint, error) { - return edition.GetNodeErrorAlert() -} - -func LoadRequestTransport() *http.Transport { return edition.LoadRequestTransport() } - -func ValidateCertificate(c *gin.Context) bool { - return edition.ValidateCertificate(c) -} - -func PushSSLToNode(websiteSSL *model.WebsiteSSL) error { - return edition.PushSSLToNode(websiteSSL) -} - -func GetAgentInfo() (*dto.AgentInfo, error) { - return edition.GetAgentInfo() -} diff --git a/core/app/api/v2/auth.go b/core/app/api/v2/auth.go index 582a58833..6493d0c6a 100644 --- a/core/app/api/v2/auth.go +++ b/core/app/api/v2/auth.go @@ -15,6 +15,7 @@ import ( initauth "github.com/1Panel-dev/1Panel/core/init/auth" "github.com/1Panel-dev/1Panel/core/utils/captcha" "github.com/1Panel-dev/1Panel/core/utils/common" + "github.com/1Panel-dev/1Panel/core/utils/xpack" "github.com/gin-gonic/gin" ) @@ -58,7 +59,7 @@ func (b *BaseApi) Login(c *gin.Context) { } } - user, msgKey, err := authService.Login(c, req, string(entrance)) + user, msgKey, err := xpack.AuthProvider.Login(c, req, string(entrance)) if user == nil || user.MfaStatus != constant.StatusEnable { go saveLoginLogs(c, wrapLoginErr(msgKey, err)) } @@ -106,7 +107,7 @@ func (b *BaseApi) MFALogin(c *gin.Context) { entrance, _ = base64.StdEncoding.DecodeString(entranceItem) } - user, msgKey, err := authService.MFALogin(c, req, string(entrance)) + user, msgKey, err := xpack.AuthProvider.MFALogin(c, req, string(entrance)) go saveLoginLogs(c, wrapLoginErr(msgKey, err)) if msgKey == "ErrMFA" { global.IPTracker.RecordFailure(ip) @@ -134,7 +135,7 @@ func (b *BaseApi) MFALogin(c *gin.Context) { // @Router /core/auth/passkey/begin [post] func (b *BaseApi) PasskeyBeginLogin(c *gin.Context) { entrance := loadEntranceFromRequest(c) - res, msgKey, err := authService.PasskeyBeginLogin(c, entrance) + res, msgKey, err := xpack.AuthProvider.PasskeyBeginLogin(c, entrance) if msgKey != "" { if msgKey == "ErrEntrance" { helper.BadAuth(c, msgKey, err) @@ -161,7 +162,7 @@ func (b *BaseApi) PasskeyBeginLogin(c *gin.Context) { func (b *BaseApi) PasskeyFinishLogin(c *gin.Context) { sessionID := c.GetHeader("Passkey-Session") entrance := loadEntranceFromRequest(c) - user, msgKey, err := authService.PasskeyFinishLogin(c, sessionID, entrance) + user, msgKey, err := xpack.AuthProvider.PasskeyFinishLogin(c, sessionID, entrance) go saveLoginLogs(c, wrapLoginErr(msgKey, err)) if msgKey == "ErrAuth" || msgKey == "ErrEntrance" { if msgKey == "ErrAuth" { @@ -248,10 +249,218 @@ func (b *BaseApi) GetLoginSetting(c *gin.Context) { Theme: settingInfo.Theme, NeedCaptcha: needCaptcha, } - res.PasskeySetting = authService.PasskeyStatus(c) + res.PasskeySetting = xpack.AuthProvider.PasskeyStatus(c) helper.SuccessWithData(c, res) } +// @Tags Auth +// @Summary Begin passkey registration +// @Accept json +// @Param request body dto.PasskeyRegisterRequest true "request" +// @Success 200 {object} dto.PasskeyBeginResponse +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /core/auth/passkey/register/begin [post] +func (b *BaseApi) PasskeyRegisterBegin(c *gin.Context) { + var req dto.PasskeyRegisterRequest + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, msgKey, err := xpack.AuthProvider.PasskeyBeginRegister(c, req.Name) + if msgKey != "" { + helper.ErrorWithDetail(c, http.StatusBadRequest, msgKey, err) + return + } + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags Auth +// @Summary Finish passkey registration +// @Accept json +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /core/auth/passkey/register/finish [post] +func (b *BaseApi) PasskeyRegisterFinish(c *gin.Context) { + sessionID := c.GetHeader("Passkey-Session") + msgKey, err := xpack.AuthProvider.PasskeyFinishRegister(c, sessionID) + if msgKey != "" { + helper.ErrorWithDetail(c, http.StatusBadRequest, msgKey, err) + return + } + if err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +// @Tags Auth +// @Summary List passkeys +// @Success 200 {array} dto.PasskeyInfo +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /core/auth/passkey/list [get] +func (b *BaseApi) PasskeyList(c *gin.Context) { + list, err := xpack.AuthProvider.PasskeyList(c) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, list) +} + +// @Tags Auth +// @Summary Delete passkey +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /core/auth/passkey/del [post] +func (b *BaseApi) PasskeyDelete(c *gin.Context) { + var req dto.PasskeyID + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := xpack.AuthProvider.PasskeyDelete(c, req.ID); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +// @Tags System Setting +// @Summary Load mfa info +// @Accept json +// @Param request body dto.MfaCredential true "request" +// @Success 200 {object} mfa.Otp +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /core/auth/mfa [post] +func (b *BaseApi) LoadMFA(c *gin.Context) { + var req dto.MfaRequest + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + otp, err := xpack.AuthProvider.LoadMFA(c, req) + if err != nil { + helper.InternalServer(c, err) + return + } + + helper.SuccessWithData(c, otp) +} + +// @Tags System Setting +// @Summary Bind mfa +// @Accept json +// @Param request body dto.MfaCredential true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /core/auth/mfa/bind [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"mfa 绑定","formatEN":"bind mfa"} +func (b *BaseApi) MFABind(c *gin.Context) { + var req dto.MfaCredential + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := xpack.AuthProvider.MFABind(c, req); err != nil { + helper.InternalServer(c, err) + return + } + + helper.Success(c) +} + +// @Tags Auth +// @Summary generate api key +// @Accept json +// @Success 200 {string} key +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /core/auth/api/generate [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成 API 接口密钥","formatEN":"generate api key"} +func (b *BaseApi) GenerateApiKey(c *gin.Context) { + panelToken := c.GetHeader("1Panel-Token") + if panelToken != "" { + helper.BadAuth(c, "ErrApiConfigDisable", nil) + return + } + apiKey, err := xpack.AuthProvider.GenerateApiKey(c) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, apiKey) +} + +// @Tags Auth +// @Summary Update api config +// @Accept json +// @Param request body dto.ApiInterfaceConfig true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /core/auth/api/update [post] +// @x-panel-log {"bodyKeys":["ipWhiteList"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 API 接口配置 => IP 白名单: [ipWhiteList]","formatEN":"update api config => IP White List: [ipWhiteList]"} +func (b *BaseApi) UpdateApiConfig(c *gin.Context) { + panelToken := c.GetHeader("1Panel-Token") + if panelToken != "" { + helper.BadAuth(c, "ErrApiConfigDisable", nil) + return + } + var req dto.ApiInterfaceConfig + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := xpack.AuthProvider.UpdateApiConfig(c, req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +// @Tags Auth +// @Summary Load current user info +// @Success 200 {object} dto.CurrentUserInfo +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /core/auth/current [get] +func (b *BaseApi) GetCurrentUser(c *gin.Context) { + userInfo, err := xpack.AuthProvider.GetCurrentUserInfo(c) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, userInfo) +} + +// @Tags Auth +// @Summary Update current user info +// @Accept json +// @Param request body dto.CurrentUserUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /core/auth/current/update [post] +func (b *BaseApi) UpdateCurrentUser(c *gin.Context) { + var req dto.CurrentUserUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := xpack.AuthProvider.UpdateCurrentUserInfo(c, req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + func saveLoginLogs(c *gin.Context, err error) { var logs model.LoginLog if err != nil { diff --git a/core/app/api/v2/script_library.go b/core/app/api/v2/script_library.go index 7457323b8..6678cfc17 100644 --- a/core/app/api/v2/script_library.go +++ b/core/app/api/v2/script_library.go @@ -186,7 +186,7 @@ func (b *BaseApi) RunScript(c *gin.Context) { tty.Start(quitChan) go slave.Wait(quitChan) } else { - connInfo, _, err := xpack.LoadNodeInfo(currentNode) + connInfo, _, err := xpack.MultiNodeProvider.LoadNodeInfo(currentNode) if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) { return } diff --git a/core/app/api/v2/setting.go b/core/app/api/v2/setting.go index 11b40d242..0b2544f61 100644 --- a/core/app/api/v2/setting.go +++ b/core/app/api/v2/setting.go @@ -10,14 +10,14 @@ import ( "strings" "github.com/1Panel-dev/1Panel/core/app/api/v2/helper" + appauth "github.com/1Panel-dev/1Panel/core/app/auth" "github.com/1Panel-dev/1Panel/core/app/dto" "github.com/1Panel-dev/1Panel/core/app/repo" - "github.com/1Panel-dev/1Panel/core/app/service" "github.com/1Panel-dev/1Panel/core/buserr" "github.com/1Panel-dev/1Panel/core/constant" "github.com/1Panel-dev/1Panel/core/global" "github.com/1Panel-dev/1Panel/core/utils/common" - "github.com/1Panel-dev/1Panel/core/utils/mfa" + "github.com/1Panel-dev/1Panel/core/utils/xpack" "github.com/gin-gonic/gin" ) @@ -36,6 +36,21 @@ func (b *BaseApi) GetSettingInfo(c *gin.Context) { helper.SuccessWithData(c, setting) } +// @Tags System Setting +// @Summary Load base system setting info +// @Success 200 {object} dto.SettingBaseInfo +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /core/settings/search/base [post] +func (b *BaseApi) GetSettingBaseInfo(c *gin.Context) { + setting, err := settingService.GetSettingBaseInfo() + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, setting) +} + // @Tags System Setting // @Summary Load system setting by key // @Success 200 {string} info @@ -114,7 +129,7 @@ func (b *BaseApi) UpdateSetting(c *gin.Context) { return } if req.Key == "SecurityEntrance" { - service.SetSecurityEntranceCookie(c, req.Value) + appauth.SetSecurityEntranceCookie(c, req.Value) } helper.Success(c) } @@ -210,28 +225,6 @@ func (b *BaseApi) DefaultMenu(c *gin.Context) { helper.Success(c) } -// @Tags System Setting -// @Summary Update system password -// @Accept json -// @Param request body dto.PasswordUpdate true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Security Timestamp -// @Router /core/settings/password/update [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改系统密码","formatEN":"update system password"} -func (b *BaseApi) UpdatePassword(c *gin.Context) { - var req dto.PasswordUpdate - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := settingService.UpdatePassword(c, req.OldPassword, req.NewPassword); err != nil { - helper.InternalServer(c, err) - return - } - helper.Success(c) -} - // @Tags System Setting // @Summary Update system ssl // @Accept json @@ -360,149 +353,7 @@ func (b *BaseApi) HandlePasswordExpired(c *gin.Context) { return } - if err := settingService.HandlePasswordExpired(c, req.OldPassword, req.NewPassword); err != nil { - helper.InternalServer(c, err) - return - } - helper.Success(c) -} - -// @Tags System Setting -// @Summary Load mfa info -// @Accept json -// @Param request body dto.MfaCredential true "request" -// @Success 200 {object} mfa.Otp -// @Security ApiKeyAuth -// @Security Timestamp -// @Router /core/settings/mfa [post] -func (b *BaseApi) LoadMFA(c *gin.Context) { - var req dto.MfaRequest - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - otp, err := mfa.GetOtp("admin", req.Title, req.Interval) - if err != nil { - helper.InternalServer(c, err) - return - } - - helper.SuccessWithData(c, otp) -} - -// @Tags System Setting -// @Summary Bind mfa -// @Accept json -// @Param request body dto.MfaCredential true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Security Timestamp -// @Router /core/settings/mfa/bind [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"mfa 绑定","formatEN":"bind mfa"} -func (b *BaseApi) MFABind(c *gin.Context) { - var req dto.MfaCredential - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - success := mfa.ValidCode(req.Code, req.Interval, req.Secret) - if !success { - helper.InternalServer(c, errors.New("code is not valid")) - return - } - - if err := settingService.Update(c, "MFAInterval", req.Interval); err != nil { - helper.InternalServer(c, err) - return - } - - if err := settingService.Update(c, "MFAStatus", constant.StatusEnable); err != nil { - helper.InternalServer(c, err) - return - } - - if err := settingService.Update(c, "MFASecret", req.Secret); err != nil { - helper.InternalServer(c, err) - return - } - - helper.Success(c) -} - -// @Tags System Setting -// @Summary Begin passkey registration -// @Accept json -// @Param request body dto.PasskeyRegisterRequest true "request" -// @Success 200 {object} dto.PasskeyBeginResponse -// @Security ApiKeyAuth -// @Security Timestamp -// @Router /core/settings/passkey/register/begin [post] -func (b *BaseApi) PasskeyRegisterBegin(c *gin.Context) { - var req dto.PasskeyRegisterRequest - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - res, msgKey, err := authService.PasskeyBeginRegister(c, req.Name) - if msgKey != "" { - helper.ErrorWithDetail(c, http.StatusBadRequest, msgKey, err) - return - } - if err != nil { - helper.InternalServer(c, err) - return - } - helper.SuccessWithData(c, res) -} - -// @Tags System Setting -// @Summary Finish passkey registration -// @Accept json -// @Success 200 -// @Security ApiKeyAuth -// @Security Timestamp -// @Router /core/settings/passkey/register/finish [post] -func (b *BaseApi) PasskeyRegisterFinish(c *gin.Context) { - sessionID := c.GetHeader("Passkey-Session") - msgKey, err := authService.PasskeyFinishRegister(c, sessionID) - if msgKey != "" { - helper.ErrorWithDetail(c, http.StatusBadRequest, msgKey, err) - return - } - if err != nil { - helper.InternalServer(c, err) - return - } - helper.Success(c) -} - -// @Tags System Setting -// @Summary List passkeys -// @Success 200 {array} dto.PasskeyInfo -// @Security ApiKeyAuth -// @Security Timestamp -// @Router /core/settings/passkey/list [get] -func (b *BaseApi) PasskeyList(c *gin.Context) { - list, err := authService.PasskeyList() - if err != nil { - helper.InternalServer(c, err) - return - } - helper.SuccessWithData(c, list) -} - -// @Tags System Setting -// @Summary Delete passkey -// @Success 200 -// @Security ApiKeyAuth -// @Security Timestamp -// @Router /core/settings/passkey/{id} [delete] -func (b *BaseApi) PasskeyDelete(c *gin.Context) { - id := c.Param("id") - if id == "" { - helper.BadRequest(c, errors.New("passkey id is required")) - return - } - if err := authService.PasskeyDelete(id); err != nil { + if err := xpack.AuthProvider.HandlePasswordExpired(c, req.OldPassword, req.NewPassword); err != nil { helper.InternalServer(c, err) return } @@ -522,55 +373,6 @@ func (b *BaseApi) ReloadSSL(c *gin.Context) { helper.Success(c) } -// @Tags System Setting -// @Summary generate api key -// @Accept json -// @Success 200 {string} key -// @Security ApiKeyAuth -// @Security Timestamp -// @Router /core/settings/api/config/generate/key [post] -// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成 API 接口密钥","formatEN":"generate api key"} -func (b *BaseApi) GenerateApiKey(c *gin.Context) { - panelToken := c.GetHeader("1Panel-Token") - if panelToken != "" { - helper.BadAuth(c, "ErrApiConfigDisable", nil) - return - } - apiKey, err := settingService.GenerateApiKey() - if err != nil { - helper.InternalServer(c, err) - return - } - helper.SuccessWithData(c, apiKey) -} - -// @Tags System Setting -// @Summary Update api config -// @Accept json -// @Param request body dto.ApiInterfaceConfig true "request" -// @Success 200 -// @Security ApiKeyAuth -// @Security Timestamp -// @Router /core/settings/api/config/update [post] -// @x-panel-log {"bodyKeys":["ipWhiteList"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 API 接口配置 => IP 白名单: [ipWhiteList]","formatEN":"update api config => IP White List: [ipWhiteList]"} -func (b *BaseApi) UpdateApiConfig(c *gin.Context) { - panelToken := c.GetHeader("1Panel-Token") - if panelToken != "" { - helper.BadAuth(c, "ErrApiConfigDisable", nil) - return - } - var req dto.ApiInterfaceConfig - if err := helper.CheckBindAndValidate(&req, c); err != nil { - return - } - - if err := settingService.UpdateApiConfig(req); err != nil { - helper.InternalServer(c, err) - return - } - helper.Success(c) -} - // @Tags App // @Summary Update appstore config // @Accept json diff --git a/core/middleware/api_auth.go b/core/app/auth/api_auth.go similarity index 51% rename from core/middleware/api_auth.go rename to core/app/auth/api_auth.go index 0921c8ef2..9ff7aa1bf 100644 --- a/core/middleware/api_auth.go +++ b/core/app/auth/api_auth.go @@ -1,4 +1,4 @@ -package middleware +package auth import ( "crypto/md5" @@ -9,13 +9,24 @@ import ( "time" "github.com/1Panel-dev/1Panel/core/app/api/v2/helper" + "github.com/1Panel-dev/1Panel/core/app/repo" "github.com/1Panel-dev/1Panel/core/constant" "github.com/1Panel-dev/1Panel/core/global" "github.com/1Panel-dev/1Panel/core/utils/common" "github.com/gin-gonic/gin" ) -func ApiAuth() gin.HandlerFunc { +type APIAuthConfig struct { + ApiInterfaceStatus string + ApiKey string + IpWhiteList string + ApiKeyValidityTime string +} + +type APIAuthConfigLoader func(c *gin.Context) (APIAuthConfig, error) +type APIAuthSuccessHandler func(c *gin.Context, config APIAuthConfig) + +func APIAuthMiddleware(loadConfig APIAuthConfigLoader, onSuccess APIAuthSuccessHandler) gin.HandlerFunc { return func(c *gin.Context) { if strings.HasPrefix(c.Request.URL.Path, "/api/v2/core/auth") { c.Next() @@ -24,35 +35,61 @@ func ApiAuth() gin.HandlerFunc { panelToken := c.GetHeader("1Panel-Token") panelTimestamp := c.GetHeader("1Panel-Timestamp") - if panelToken != "" || panelTimestamp != "" { - if global.Api.ApiInterfaceStatus == constant.StatusEnable { - clientIP := c.ClientIP() - if !isValid1PanelTimestamp(panelTimestamp) { - helper.BadAuth(c, "ErrApiConfigKeyTimeInvalid", nil) - return - } - if !isValid1PanelToken(panelToken, panelTimestamp) { - helper.BadAuth(c, "ErrApiConfigKeyInvalid", nil) - return - } - - if !isIPInWhiteList(clientIP) { - helper.BadAuth(c, "ErrApiConfigIPInvalid", nil) - return - } - c.Set("API_AUTH", true) - c.Next() - return - } else { - helper.BadAuth(c, "ErrApiConfigStatusInvalid", nil) - return - } + if panelToken == "" && panelTimestamp == "" { + c.Next() + return } + + config, err := loadConfig(c) + if err != nil { + helper.InternalServer(c, err) + return + } + if config.ApiInterfaceStatus != constant.StatusEnable { + helper.BadAuth(c, "ErrApiConfigStatusInvalid", nil) + return + } + if !isValid1PanelTimestamp(panelTimestamp, config.ApiKeyValidityTime) { + helper.BadAuth(c, "ErrApiConfigKeyTimeInvalid", nil) + return + } + if !isValid1PanelToken(panelToken, panelTimestamp, config.ApiKey) { + helper.BadAuth(c, "ErrApiConfigKeyInvalid", nil) + return + } + if !isIPInWhiteList(c.ClientIP(), config.IpWhiteList) { + helper.BadAuth(c, "ErrApiConfigIPInvalid", nil) + return + } + + c.Set("API_AUTH", true) + if onSuccess != nil { + onSuccess(c, config) + } + c.Next() } } -func isValid1PanelTimestamp(panelTimestamp string) bool { - apiKeyValidityTime := global.Api.ApiKeyValidityTime +func LoadAPIAuthConfig(_ *gin.Context) (APIAuthConfig, error) { + settingRepo := repo.NewISettingRepo() + config := APIAuthConfig{} + var err error + if config.ApiInterfaceStatus, err = settingRepo.GetValueByKey("ApiInterfaceStatus"); err != nil { + return config, err + } + if config.ApiKey, err = settingRepo.GetValueByKey("ApiKey"); err != nil { + return config, err + } + if config.IpWhiteList, err = settingRepo.GetValueByKey("IpWhiteList"); err != nil { + return config, err + } + if config.ApiKeyValidityTime, err = settingRepo.GetValueByKey("ApiKeyValidityTime"); err != nil { + return config, err + } + return config, nil +} + +func isValid1PanelTimestamp(panelTimestamp string, apiKeyValidityTime string) bool { apiTime, err := strconv.Atoi(apiKeyValidityTime) if err != nil || apiTime < 0 { global.LOG.Errorf("apiTime %d, err: %v", apiTime, err) @@ -75,13 +112,11 @@ func isValid1PanelTimestamp(panelTimestamp string) bool { return nowTime-panelTime <= int64(apiTime)*60+tolerance } -func isValid1PanelToken(panelToken string, panelTimestamp string) bool { - system1PanelToken := global.Api.ApiKey - return panelToken == GenerateMD5("1panel"+system1PanelToken+panelTimestamp) +func isValid1PanelToken(panelToken string, panelTimestamp string, apiKey string) bool { + return panelToken == GenerateMD5("1panel"+apiKey+panelTimestamp) } -func isIPInWhiteList(clientIP string) bool { - ipWhiteString := global.Api.IpWhiteList +func isIPInWhiteList(clientIP string, ipWhiteString string) bool { if len(ipWhiteString) == 0 { global.LOG.Error("IP whitelist is empty") return false diff --git a/core/app/auth/auth.go b/core/app/auth/auth.go new file mode 100644 index 000000000..67c5bf6a1 --- /dev/null +++ b/core/app/auth/auth.go @@ -0,0 +1,372 @@ +package auth + +import ( + "crypto/hmac" + "encoding/base64" + "encoding/json" + "errors" + "strconv" + "time" + + "github.com/1Panel-dev/1Panel/core/app/dto" + "github.com/1Panel-dev/1Panel/core/app/repo" + "github.com/1Panel-dev/1Panel/core/buserr" + "github.com/1Panel-dev/1Panel/core/constant" + "github.com/1Panel-dev/1Panel/core/global" + initauth "github.com/1Panel-dev/1Panel/core/init/auth" + "github.com/1Panel-dev/1Panel/core/init/session/psession" + "github.com/1Panel-dev/1Panel/core/utils/common" + "github.com/1Panel-dev/1Panel/core/utils/encrypt" + "github.com/1Panel-dev/1Panel/core/utils/mfa" + "github.com/gin-gonic/gin" +) + +func Login(c *gin.Context, info dto.Login, entrance string) (*dto.UserLoginInfo, string, error) { + settingRepo := repo.NewISettingRepo() + nameSetting, err := settingRepo.Get(repo.WithByKey("UserName")) + if err != nil { + return nil, "", buserr.New("ErrRecordNotFound") + } + if info.Name != nameSetting.Value { + return nil, "ErrAuth", buserr.New("ErrAuth") + } + priKey, _ := settingRepo.Get(repo.WithByKey("PASSWORD_PRIVATE_KEY")) + passwordSetting, err := settingRepo.Get(repo.WithByKey("Password")) + if err != nil { + return nil, "", err + } + if err = CheckPassword(priKey.Value, info.Password, passwordSetting.Value); err != nil { + return nil, "ErrAuth", err + } + entranceSetting, err := settingRepo.Get(repo.WithByKey("SecurityEntrance")) + if err != nil { + return nil, "", err + } + if len(entranceSetting.Value) != 0 && entranceSetting.Value != entrance { + return nil, "ErrEntrance", buserr.New("ErrEntrance") + } + mfaSetting, err := settingRepo.Get(repo.WithByKey("MFAStatus")) + if err != nil { + return nil, "", err + } + if err = settingRepo.Update("Language", info.Language); err != nil { + return nil, "", err + } + if mfaSetting.Value == constant.StatusEnable { + return BeginMFALogin(c, nameSetting.Value, entrance, mfaSetting.Value), "", nil + } + + sessionUser := psession.SessionUser{ID: psession.SuperAdminSessionUserID, Name: nameSetting.Value, Role: "ADMIN"} + res, err := GenerateSession(c, sessionUser) + if err != nil { + return nil, "", err + } + if entrance != "" { + SetSecurityEntranceCookie(c, entrance) + } + return res, "", nil +} + +func MFALogin(c *gin.Context, info dto.MFALogin, entrance string) (*dto.UserLoginInfo, string, error) { + name, errCode, err := VerifyMFALogin(c, info.SessionID, info.Code, entrance) + if errCode != "" { + return nil, errCode, err + } + + sessionUser := psession.SessionUser{ID: psession.SuperAdminSessionUserID, Name: name, Role: "ADMIN"} + res, err := GenerateSession(c, sessionUser) + if err != nil { + return nil, "", err + } + if entrance != "" { + SetSecurityEntranceCookie(c, entrance) + } + return res, "", nil +} + +func BeginMFALogin(c *gin.Context, name, entrance, mfaStatus string) *dto.UserLoginInfo { + ip := common.GetRealClientIP(c) + mfaSession := initauth.GetMFASessionStore().Set(name, entrance, ip) + return &dto.UserLoginInfo{Name: name, MfaStatus: mfaStatus, MfaSession: mfaSession} +} + +func VerifyMFALogin(c *gin.Context, sessionID, code, entrance string) (string, string, error) { + settingRepo := repo.NewISettingRepo() + mfaSessions := initauth.GetMFASessionStore() + session, ok := mfaSessions.Get(sessionID) + if !ok { + return "", "ErrMFA", nil + } + if session.IP != common.GetRealClientIP(c) { + return "", "ErrMFA", nil + } + if session.Entrance != entrance { + return "", "", buserr.New("ErrEntrance") + } + mfaSecret, err := settingRepo.Get(repo.WithByKey("MFASecret")) + if err != nil { + return "", "", err + } + mfaInterval, err := settingRepo.Get(repo.WithByKey("MFAInterval")) + if err != nil { + return "", "", err + } + if !mfa.ValidCode(code, mfaInterval.Value, mfaSecret.Value) { + return "", "ErrMFA", nil + } + mfaSessions.Delete(sessionID) + return session.Name, "", nil +} + +func GenerateSession(c *gin.Context, sessionUser psession.SessionUser) (*dto.UserLoginInfo, error) { + settingRepo := repo.NewISettingRepo() + setting, err := settingRepo.Get(repo.WithByKey("SessionTimeout")) + if err != nil { + return nil, err + } + httpsSetting, err := settingRepo.Get(repo.WithByKey("SSL")) + if err != nil { + return nil, err + } + lifeTime, err := strconv.Atoi(setting.Value) + if err != nil { + return nil, err + } + + if err := global.SESSION.SetFresh(c, sessionUser, httpsSetting.Value == constant.StatusEnable, lifeTime); err != nil { + return nil, err + } + + return &dto.UserLoginInfo{Name: sessionUser.Name, Role: sessionUser.Role}, nil +} + +func SetSecurityEntranceCookie(c *gin.Context, entrance string) { + settingRepo := repo.NewISettingRepo() + entranceValue := base64.StdEncoding.EncodeToString([]byte(entrance)) + sslEnabled := false + if setting, err := settingRepo.Get(repo.WithByKey("SSL")); err == nil { + sslEnabled = setting.Value == constant.StatusEnable + } + c.SetCookie("SecurityEntrance", entranceValue, 0, "/", "", sslEnabled, true) +} + +func CheckEntrance(entrance string) error { + settingRepo := repo.NewISettingRepo() + entranceSetting, err := settingRepo.Get(repo.WithByKey("SecurityEntrance")) + if err != nil { + return err + } + if len(entranceSetting.Value) != 0 && entranceSetting.Value != entrance { + return buserr.New("ErrEntrance") + } + return nil +} + +func CheckPassword(priKey, password, passwordFromDB string) error { + privateKey, err := encrypt.ParseRSAPrivateKey(priKey) + if err != nil { + return err + } + loginPassword, err := encrypt.DecryptPassword(password, privateKey) + if err != nil { + return err + } + existPassword, err := encrypt.StringDecrypt(passwordFromDB) + if err != nil { + return err + } + if !hmac.Equal([]byte(loginPassword), []byte(existPassword)) { + return buserr.New("ErrAuth") + } + return nil +} + +func LoadMFA(req dto.MfaRequest) (mfa.Otp, error) { + settingRepo := repo.NewISettingRepo() + username, err := settingRepo.GetValueByKey("UserName") + if err != nil { + return mfa.Otp{}, err + } + otp, err := mfa.GetOtp(username, req.Title, req.Interval) + if err != nil { + return mfa.Otp{}, err + } + return otp, nil +} +func MFABind(req dto.MfaCredential) error { + success := mfa.ValidCode(req.Code, req.Interval, req.Secret) + if !success { + return errors.New("code is not valid") + } + + settingRepo := repo.NewISettingRepo() + if err := settingRepo.Update("MFAInterval", req.Interval); err != nil { + return err + } + if err := settingRepo.Update("MFAStatus", constant.StatusEnable); err != nil { + return err + } + if err := settingRepo.Update("MFASecret", req.Secret); err != nil { + return err + } + return nil +} +func GetCurrentUserInfo() (*dto.CurrentUserInfo, error) { + setting, err := repo.NewISettingRepo().List() + if err != nil { + return nil, buserr.New("ErrRecordNotFound") + } + settingMap := make(map[string]string) + for _, set := range setting { + settingMap[set.Key] = set.Value + } + var info dto.CurrentUserInfo + stringSettingMap := make(map[string]string, len(settingMap)) + for key, value := range settingMap { + stringSettingMap[key] = value + } + delete(stringSettingMap, "SessionTimeout") + delete(stringSettingMap, "ExpirationDays") + arr, err := json.Marshal(stringSettingMap) + if err != nil { + return nil, err + } + if err := json.Unmarshal(arr, &info); err != nil { + return nil, err + } + info.SessionTimeout, _ = strconv.Atoi(settingMap["SessionTimeout"]) + info.ExpirationDays, _ = strconv.Atoi(settingMap["ExpirationDays"]) + info.Name = settingMap["UserName"] + return &info, nil +} +func UpdateCurrentUserInfo(c *gin.Context, req dto.CurrentUserUpdate) error { + settingRepo := repo.NewISettingRepo() + if len(req.Password) != 0 { + if len(req.OldPassword) == 0 { + return buserr.New("ErrInitialPassword") + } + oldPassword, err := base64.StdEncoding.DecodeString(req.OldPassword) + if err != nil { + return err + } + newPassword, err := base64.StdEncoding.DecodeString(req.Password) + if err != nil { + return err + } + if err := HandlePasswordExpired(c, string(oldPassword), string(newPassword)); err != nil { + return err + } + } + if err := settingRepo.Update("UserName", req.Name); err != nil { + return err + } + if err := settingRepo.Update("SessionTimeout", strconv.Itoa(req.SessionTimeout)); err != nil { + return err + } + if err := settingRepo.Update("ExpirationDays", strconv.Itoa(req.ExpirationDays)); err != nil { + return err + } + if err := settingRepo.Update("ExpirationTime", time.Now().AddDate(0, 0, req.ExpirationDays).Format(constant.DateTimeLayout)); err != nil { + return err + } + deleteCurrentSession(c) + return nil +} + +func GenerateApiKey() (string, error) { + apiKey := common.RandStr(32) + if err := repo.NewISettingRepo().Update("ApiKey", apiKey); err != nil { + return "", err + } + return apiKey, nil +} +func UpdateApiConfig(req dto.ApiInterfaceConfig) error { + settingRepo := repo.NewISettingRepo() + if err := settingRepo.UpdateOrCreate("ApiInterfaceStatus", req.ApiInterfaceStatus); err != nil { + return err + } + if err := settingRepo.UpdateOrCreate("ApiKey", req.ApiKey); err != nil { + return err + } + if err := settingRepo.UpdateOrCreate("IpWhiteList", req.IpWhiteList); err != nil { + return err + } + if err := settingRepo.UpdateOrCreate("ApiKeyValidityTime", req.ApiKeyValidityTime); err != nil { + return err + } + return nil +} + +func HandlePasswordExpired(c *gin.Context, old, new string) error { + settingRepo := repo.NewISettingRepo() + setting, err := settingRepo.Get(repo.WithByKey("Password")) + if err != nil { + return err + } + passwordFromDB, err := encrypt.StringDecrypt(setting.Value) + if err != nil { + return err + } + if passwordFromDB == old { + newPassword, err := encrypt.StringEncrypt(new) + if err != nil { + return err + } + if err := settingRepo.Update("Password", newPassword); err != nil { + return err + } + + expiredSetting, err := settingRepo.Get(repo.WithByKey("ExpirationDays")) + if err != nil { + return err + } + timeout, _ := strconv.Atoi(expiredSetting.Value) + if err := settingRepo.Update("ExpirationTime", time.Now().AddDate(0, 0, timeout).Format(constant.DateTimeLayout)); err != nil { + return err + } + return nil + } + return buserr.New("ErrInitialPassword") +} + +func LoadSessionTimeout(sessionUser psession.SessionUser) (int, error) { + settingRepo := repo.NewISettingRepo() + sessionTimeout, err := settingRepo.GetValueByKey("SessionTimeout") + if err != nil { + return 0, err + } + lifeTime, _ := strconv.Atoi(sessionTimeout) + return lifeTime, nil +} +func LoadExpired(sessionUser psession.SessionUser) (bool, time.Time, error) { + settingRepo := repo.NewISettingRepo() + expirationDays, err := settingRepo.GetValueByKey("ExpirationDays") + if err != nil { + return true, time.Time{}, err + } + expiredDays, _ := strconv.Atoi(expirationDays) + if expiredDays == 0 { + return false, time.Time{}, nil + } + + expirationTime, err := settingRepo.GetValueByKey("ExpirationTime") + if err != nil { + return true, time.Time{}, err + } + expiredTime, err := time.ParseInLocation(constant.DateTimeLayout, expirationTime, common.LoadExpiredLocation()) + if err != nil { + return true, time.Time{}, err + } + return true, expiredTime, nil +} + +func deleteCurrentSession(c *gin.Context) { + if c == nil { + return + } + sessionUser, err := global.SESSION.Get(c) + if err != nil || sessionUser.ID == "" { + return + } + _ = global.SESSION.DeleteByID(sessionUser.ID) +} diff --git a/core/app/auth/passkey.go b/core/app/auth/passkey.go new file mode 100644 index 000000000..dfcc7a9ed --- /dev/null +++ b/core/app/auth/passkey.go @@ -0,0 +1,598 @@ +package auth + +import ( + "crypto/rand" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/core/app/dto" + "github.com/1Panel-dev/1Panel/core/app/repo" + "github.com/1Panel-dev/1Panel/core/buserr" + "github.com/1Panel-dev/1Panel/core/constant" + "github.com/1Panel-dev/1Panel/core/global" + "github.com/1Panel-dev/1Panel/core/init/session/psession" + "github.com/1Panel-dev/1Panel/core/utils/encrypt" + "github.com/1Panel-dev/1Panel/core/utils/passkey" + "github.com/gin-gonic/gin" + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" + "gorm.io/gorm" +) + +func EvaluatePasskeyStatus(c *gin.Context, configured func() (bool, error)) bool { + enabled, err := PasskeyEnabled(c) + if err != nil { + global.LOG.Errorf("passkey enabled check failed, err: %v", err) + enabled = false + } + configuredOK, err := configured() + if err != nil { + global.LOG.Errorf("passkey config check failed, err: %v", err) + configuredOK = false + } + return enabled && configuredOK +} + +func PasskeyStatus(c *gin.Context) bool { + return EvaluatePasskeyStatus(c, communityPasskeyConfigured) +} + +func PasskeyBeginLogin(c *gin.Context, entrance string) (*dto.PasskeyBeginResponse, string, error) { + if err := CheckEntrance(entrance); err != nil { + return nil, "ErrEntrance", err + } + config, msgKey, err := PasskeyConfig(c) + if err != nil { + return nil, msgKey, err + } + records, err := loadCommunityPasskeyCredentialRecords() + if err != nil { + return nil, "", err + } + if len(records) == 0 { + return nil, "ErrPasskeyNotConfigured", buserr.New("ErrPasskeyNotConfigured") + } + user, err := communityPasskeyUser(records, true) + if err != nil { + return nil, "", err + } + wa, err := webauthn.New(config) + if err != nil { + return nil, "", err + } + assertion, sessionData, err := wa.BeginLogin(user) + if err != nil { + return nil, "", err + } + sessionID := passkey.GetPasskeySessionStore().Set(passkey.PasskeySessionKindLogin, "", *sessionData) + return &dto.PasskeyBeginResponse{SessionID: sessionID, PublicKey: assertion.Response}, "", nil +} + +func PasskeyFinishLogin(c *gin.Context, sessionID, entrance string) (*dto.UserLoginInfo, string, error) { + if sessionID == "" { + return nil, "ErrPasskeySession", buserr.New("ErrPasskeySession") + } + if err := CheckEntrance(entrance); err != nil { + return nil, "ErrEntrance", err + } + config, msgKey, err := PasskeyConfig(c) + if err != nil { + return nil, msgKey, err + } + sessionStore := passkey.GetPasskeySessionStore() + session, ok := sessionStore.Get(sessionID) + if !ok || session.Kind != passkey.PasskeySessionKindLogin { + return nil, "ErrPasskeySession", buserr.New("ErrPasskeySession") + } + sessionStore.Delete(sessionID) + records, err := loadCommunityPasskeyCredentialRecords() + if err != nil { + return nil, "", err + } + if len(records) == 0 { + return nil, "ErrPasskeyNotConfigured", buserr.New("ErrPasskeyNotConfigured") + } + user, err := communityPasskeyUser(records, true) + if err != nil { + return nil, "", err + } + wa, err := webauthn.New(config) + if err != nil { + return nil, "", err + } + credential, err := wa.FinishLogin(user, session.Session, c.Request) + if err != nil { + return nil, "ErrAuth", err + } + if err := UpdatePasskeyCredentialRecord(records, credential); err != nil { + return nil, "ErrAuth", err + } + if err := saveCommunityPasskeyCredentialRecords(records); err != nil { + return nil, "", err + } + userSetting, err := repo.NewISettingRepo().Get(repo.WithByKey("UserName")) + if err != nil { + return nil, "", err + } + res, err := GenerateSession(c, psession.SessionUser{ID: psession.SuperAdminSessionUserID, Name: userSetting.Value, Role: "ADMIN"}) + if err != nil { + return nil, "", err + } + if entrance != "" { + SetSecurityEntranceCookie(c, entrance) + } + return res, "", nil +} + +func PasskeyBeginRegister(c *gin.Context, name string) (*dto.PasskeyBeginResponse, string, error) { + config, msgKey, err := PasskeyConfig(c) + if err != nil { + return nil, msgKey, err + } + records, err := loadCommunityPasskeyCredentialRecords() + if err != nil { + return nil, "", err + } + if len(records) >= passkey.PasskeyMaxCredentials { + return nil, "ErrPasskeyLimit", buserr.New("ErrPasskeyLimit") + } + user, err := communityPasskeyUser(records, true) + if err != nil { + return nil, "", err + } + wa, err := webauthn.New(config) + if err != nil { + return nil, "", err + } + exclusions := make([]protocol.CredentialDescriptor, len(user.Credentials)) + for i, credential := range user.Credentials { + exclusions[i] = credential.Descriptor() + } + creation, sessionData, err := wa.BeginRegistration(user, webauthn.WithExclusions(exclusions)) + if err != nil { + return nil, "", err + } + sessionID := passkey.GetPasskeySessionStore().Set(passkey.PasskeySessionKindRegister, strings.TrimSpace(name), *sessionData) + return &dto.PasskeyBeginResponse{SessionID: sessionID, PublicKey: creation.Response}, "", nil +} + +func PasskeyFinishRegister(c *gin.Context, sessionID string) (string, error) { + if sessionID == "" { + return "ErrPasskeySession", buserr.New("ErrPasskeySession") + } + config, msgKey, err := PasskeyConfig(c) + if err != nil { + return msgKey, err + } + sessionStore := passkey.GetPasskeySessionStore() + session, ok := sessionStore.Get(sessionID) + if !ok || session.Kind != passkey.PasskeySessionKindRegister { + return "ErrPasskeySession", buserr.New("ErrPasskeySession") + } + sessionStore.Delete(sessionID) + records, err := loadCommunityPasskeyCredentialRecords() + if err != nil { + return "", err + } + if len(records) >= passkey.PasskeyMaxCredentials { + return "ErrPasskeyLimit", buserr.New("ErrPasskeyLimit") + } + user, err := communityPasskeyUser(records, true) + if err != nil { + return "", err + } + wa, err := webauthn.New(config) + if err != nil { + return "", err + } + credential, err := wa.FinishRegistration(user, session.Session, c.Request) + if err != nil { + return "ErrPasskeyVerify", err + } + if PasskeyCredentialExists(records, credential.ID) { + return "ErrPasskeyDuplicate", buserr.New("ErrPasskeyDuplicate") + } + displayName := strings.TrimSpace(session.Name) + if displayName == "" { + displayName = fmt.Sprintf("%s-%s", passkey.PasskeyCredentialNameDefault, time.Now().Format("20060102150405")) + } + records = append(records, passkey.PasskeyCredentialRecord{ + ID: base64.RawURLEncoding.EncodeToString(credential.ID), + Name: displayName, + CreatedAt: time.Now().Format(constant.DateTimeLayout), + LastUsedAt: "", + FlagsValue: CredentialFlagsValue(credential.Flags), + Credential: *credential, + }) + if err := saveCommunityPasskeyCredentialRecords(records); err != nil { + return "", err + } + return "", nil +} + +func PasskeyList() ([]dto.PasskeyInfo, error) { + records, err := loadCommunityPasskeyCredentialRecords() + if err != nil { + return nil, err + } + list := make([]dto.PasskeyInfo, 0, len(records)) + for _, record := range records { + list = append(list, dto.PasskeyInfo{ID: record.ID, Name: record.Name, CreatedAt: record.CreatedAt, LastUsedAt: record.LastUsedAt}) + } + return list, nil +} + +func PasskeyDelete(id string) error { + records, err := loadCommunityPasskeyCredentialRecords() + if err != nil { + return err + } + index := -1 + for i, record := range records { + if record.ID == id { + index = i + break + } + } + if index == -1 { + return buserr.New("ErrRecordNotFound") + } + records = append(records[:index], records[index+1:]...) + return saveCommunityPasskeyCredentialRecords(records) +} + +func ClearPasskeys() error { + settingRepo := repo.NewISettingRepo() + if err := settingRepo.Update(passkey.PasskeyUserIDSettingKey, ""); err != nil { + return err + } + return settingRepo.Update(passkey.PasskeyCredentialSettingKey, "") +} + +func communityPasskeyConfigured() (bool, error) { + bindDomain, err := repo.NewISettingRepo().Get(repo.WithByKey("BindDomain")) + if err != nil { + return false, err + } + if strings.TrimSpace(bindDomain.Value) == "" { + return false, nil + } + records, err := loadCommunityPasskeyCredentialRecords() + if err != nil { + return false, err + } + return len(records) > 0, nil +} + +func communityPasskeyUser(records []passkey.PasskeyCredentialRecord, allowCreate bool) (*passkey.PasskeyUser, error) { + settingRepo := repo.NewISettingRepo() + storedUserID, err := settingRepo.Get(repo.WithByKey(passkey.PasskeyUserIDSettingKey)) + if err != nil { + return nil, err + } + rawUserID, encodedUserID, err := GeneratePasskeyUserID(storedUserID.Value, allowCreate) + if err != nil { + return nil, err + } + if storedUserID.Value == "" && encodedUserID != "" { + if err := settingRepo.Update(passkey.PasskeyUserIDSettingKey, encodedUserID); err != nil { + return nil, err + } + } + nameSetting, err := settingRepo.Get(repo.WithByKey("UserName")) + if err != nil { + return nil, err + } + return NewPasskeyUser(rawUserID, nameSetting.Value, records), nil +} + +func loadCommunityPasskeyCredentialRecords() ([]passkey.PasskeyCredentialRecord, error) { + setting, err := repo.NewISettingRepo().Get(repo.WithByKey(passkey.PasskeyCredentialSettingKey)) + if err != nil { + return nil, err + } + return LoadPasskeyCredentialRecords(setting.Value) +} + +func saveCommunityPasskeyCredentialRecords(records []passkey.PasskeyCredentialRecord) error { + encoded, err := SavePasskeyCredentialRecords(records) + if err != nil { + return err + } + return repo.NewISettingRepo().Update(passkey.PasskeyCredentialSettingKey, encoded) +} + +func PasskeyEnabled(c *gin.Context) (bool, error) { + return strings.EqualFold(PasskeyRequestScheme(c), "https"), nil +} + +func PasskeyConfig(c *gin.Context) (*webauthn.Config, string, error) { + enabled, err := PasskeyEnabled(c) + if err != nil { + return nil, "", err + } + if !enabled { + return nil, "ErrPasskeyDisabled", buserr.New("ErrPasskeyDisabled") + } + origin, rpID, err := PasskeyOriginAndRPID(c) + if err != nil { + return nil, "", err + } + panelName, err := repo.NewISettingRepo().Get(repo.WithByKey("PanelName")) + if err != nil { + return nil, "", err + } + return &webauthn.Config{ + RPID: rpID, + RPDisplayName: panelName.Value, + RPOrigins: []string{origin}, + AuthenticatorSelection: protocol.AuthenticatorSelection{ + UserVerification: protocol.VerificationRequired, + }, + }, "", nil +} + +func PasskeyOriginAndRPID(c *gin.Context) (string, string, error) { + host := passkeyRequestHost(c) + if host == "" { + return "", "", fmt.Errorf("missing request host") + } + scheme := PasskeyRequestScheme(c) + origin := fmt.Sprintf("%s://%s", scheme, host) + + bindDomain, err := repo.NewISettingRepo().Get(repo.WithByKey("BindDomain")) + if err != nil { + return "", "", err + } + bindDomainValue := strings.TrimSpace(bindDomain.Value) + if bindDomainValue == "" { + return "", "", buserr.New("ErrPasskeyNotConfigured") + } + hostDomain := stripHostPort(host) + bindDomainValue = stripHostPort(bindDomainValue) + if hostDomain == "" || !strings.EqualFold(hostDomain, bindDomainValue) { + return "", "", buserr.New("ErrPasskeyDisabled") + } + return origin, bindDomainValue, nil +} + +func NewPasskeyUser(userID []byte, name string, records []passkey.PasskeyCredentialRecord) *passkey.PasskeyUser { + credentials := make([]webauthn.Credential, len(records)) + for i, record := range records { + credentials[i] = record.Credential + } + return &passkey.PasskeyUser{ + ID: userID, + Name: name, + DisplayName: name, + Credentials: credentials, + } +} + +func GeneratePasskeyUserID(encoded string, allowCreate bool) ([]byte, string, error) { + if encoded == "" { + if !allowCreate { + return nil, "", buserr.New("ErrPasskeyNotConfigured") + } + raw := make([]byte, 32) + if _, err := rand.Read(raw); err != nil { + return nil, "", err + } + return raw, base64.RawURLEncoding.EncodeToString(raw), nil + } + raw, err := base64.RawURLEncoding.DecodeString(encoded) + if err != nil { + return nil, "", err + } + return raw, encoded, nil +} + +func LoadPasskeyCredentialRecords(encryptedValue string) ([]passkey.PasskeyCredentialRecord, error) { + if encryptedValue == "" { + return []passkey.PasskeyCredentialRecord{}, nil + } + decrypted, err := encrypt.StringDecrypt(encryptedValue) + if err != nil { + return nil, err + } + var records []passkey.PasskeyCredentialRecord + if err := json.Unmarshal([]byte(decrypted), &records); err != nil { + return nil, err + } + for i := range records { + records[i].Credential.Flags = webauthn.NewCredentialFlags(protocol.AuthenticatorFlags(records[i].FlagsValue)) + } + return records, nil +} + +func SavePasskeyCredentialRecords(records []passkey.PasskeyCredentialRecord) (string, error) { + if len(records) == 0 { + return "", nil + } + copyRecords := make([]passkey.PasskeyCredentialRecord, len(records)) + copy(copyRecords, records) + for i := range copyRecords { + copyRecords[i].FlagsValue = CredentialFlagsValue(copyRecords[i].Credential.Flags) + } + raw, err := json.Marshal(copyRecords) + if err != nil { + return "", err + } + return encrypt.StringEncrypt(string(raw)) +} + +func PasskeyCredentialExists(records []passkey.PasskeyCredentialRecord, credentialID []byte) bool { + encoded := base64.RawURLEncoding.EncodeToString(credentialID) + for _, record := range records { + if record.ID == encoded { + return true + } + } + return false +} + +func UpdatePasskeyCredentialRecord(records []passkey.PasskeyCredentialRecord, credential *webauthn.Credential) error { + encoded := base64.RawURLEncoding.EncodeToString(credential.ID) + for i := range records { + if records[i].ID == encoded { + records[i].Credential = *credential + records[i].FlagsValue = CredentialFlagsValue(credential.Flags) + records[i].LastUsedAt = time.Now().Format(constant.DateTimeLayout) + return nil + } + } + return buserr.New("ErrPasskeyNotConfigured") +} + +func CredentialFlagsValue(flags webauthn.CredentialFlags) uint8 { + var value protocol.AuthenticatorFlags + if flags.UserPresent { + value |= protocol.FlagUserPresent + } + if flags.UserVerified { + value |= protocol.FlagUserVerified + } + if flags.BackupEligible { + value |= protocol.FlagBackupEligible + } + if flags.BackupState { + value |= protocol.FlagBackupState + } + return uint8(value) +} + +func PasskeyRequestScheme(c *gin.Context) string { + if c.Request.TLS != nil { + return "https" + } + if !passkeyIsFromTrustedProxy(c) { + return "http" + } + if proto := passkeyForwardedProto(c.GetHeader("Forwarded")); proto != "" { + return proto + } + if proto := passkeyXForwardedProto(c.GetHeader("X-Forwarded-Proto")); proto != "" { + return proto + } + return "http" +} + +func passkeyRequestHost(c *gin.Context) string { + host := c.Request.Host + if strings.Contains(host, ",") { + host = strings.TrimSpace(strings.Split(host, ",")[0]) + } + return strings.TrimSpace(host) +} + +func passkeyIsFromTrustedProxy(c *gin.Context) bool { + remoteIP := passkeyRemoteIP(c.Request.RemoteAddr) + if remoteIP == nil { + return false + } + proxies, err := loadPasskeyTrustedProxies() + if err != nil { + global.LOG.Errorf("load passkey trusted proxies failed, err: %v", err) + return false + } + for _, cidr := range proxies { + if cidr.Contains(remoteIP) { + return true + } + } + return false +} + +func passkeyRemoteIP(remoteAddr string) net.IP { + if host, _, err := net.SplitHostPort(strings.TrimSpace(remoteAddr)); err == nil { + return net.ParseIP(host) + } + return net.ParseIP(strings.TrimSpace(remoteAddr)) +} + +func loadPasskeyTrustedProxies() ([]*net.IPNet, error) { + setting, err := repo.NewISettingRepo().Get(repo.WithByKey("PasskeyTrustedProxies")) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return parsePasskeyTrustedProxies("127.0.0.1\n::1") + } + return nil, err + } + return parsePasskeyTrustedProxies(setting.Value) +} + +func parsePasskeyTrustedProxies(value string) ([]*net.IPNet, error) { + lines := strings.Split(value, "\n") + proxies := make([]*net.IPNet, 0, len(lines)) + for _, line := range lines { + entry := strings.TrimSpace(line) + if entry == "" { + continue + } + if !strings.Contains(entry, "/") { + if strings.Contains(entry, ":") { + entry += "/128" + } else { + entry += "/32" + } + } + _, cidr, err := net.ParseCIDR(entry) + if err != nil { + return nil, err + } + proxies = append(proxies, cidr) + } + return proxies, nil +} + +func passkeyForwardedProto(forwarded string) string { + for _, part := range strings.Split(forwarded, ";") { + item := strings.TrimSpace(part) + if len(item) < 6 || !strings.EqualFold(item[:6], "proto=") { + continue + } + proto := strings.Trim(strings.TrimSpace(item[6:]), `"`) + if strings.EqualFold(proto, "https") { + return "https" + } + if strings.EqualFold(proto, "http") { + return "http" + } + } + return "" +} + +func passkeyXForwardedProto(forwarded string) string { + if forwarded == "" { + return "" + } + proto := strings.TrimSpace(strings.Split(forwarded, ",")[0]) + if strings.EqualFold(proto, "https") { + return "https" + } + if strings.EqualFold(proto, "http") { + return "http" + } + return "" +} + +func stripHostPort(host string) string { + host = strings.TrimSpace(host) + if host == "" { + return "" + } + if strings.HasPrefix(host, "[") { + if parsedHost, _, err := net.SplitHostPort(host); err == nil { + return strings.Trim(parsedHost, "[]") + } + } + if parsedHost, _, err := net.SplitHostPort(host); err == nil { + return parsedHost + } + return strings.Trim(host, "[]") +} diff --git a/core/app/dto/auth.go b/core/app/dto/auth.go index 29ce3f729..84772df16 100644 --- a/core/app/dto/auth.go +++ b/core/app/dto/auth.go @@ -47,3 +47,7 @@ type SystemSetting struct { Language string `json:"language"` IsIntl bool `json:"isIntl"` } + +type PasskeyID struct { + ID string `json:"id" validate:"required"` +} diff --git a/core/app/dto/setting.go b/core/app/dto/setting.go index bd3fb932e..175357ece 100644 --- a/core/app/dto/setting.go +++ b/core/app/dto/setting.go @@ -9,16 +9,15 @@ type SettingInfo struct { DeveloperMode string `json:"developerMode"` UpgradeBackupCopies string `json:"upgradeBackupCopies"` - SessionTimeout string `json:"sessionTimeout"` - Port string `json:"port"` - Ipv6 string `json:"ipv6"` - BindAddress string `json:"bindAddress"` - PanelName string `json:"panelName"` - Edition string `json:"edition"` - Theme string `json:"theme"` - MenuTabs string `json:"menuTabs"` - Language string `json:"language"` - DocSource string `json:"docSource"` + Port string `json:"port"` + Ipv6 string `json:"ipv6"` + BindAddress string `json:"bindAddress"` + PanelName string `json:"panelName"` + Edition string `json:"edition"` + Theme string `json:"theme"` + MenuTabs string `json:"menuTabs"` + Language string `json:"language"` + DocSource string `json:"docSource"` ServerPort string `json:"serverPort"` SSL string `json:"ssl"` @@ -32,8 +31,6 @@ type SettingInfo struct { ExpirationDays string `json:"expirationDays"` ExpirationTime string `json:"expirationTime"` ComplexityVerification string `json:"complexityVerification"` - MFAStatus string `json:"mfaStatus"` - MFAInterval string `json:"mfaInterval"` AppStoreVersion string `json:"appStoreVersion"` AppStoreLastModified string `json:"appStoreLastModified"` @@ -48,12 +45,56 @@ type SettingInfo struct { ProxyUser string `json:"proxyUser"` ProxyPasswd string `json:"proxyPasswd"` ProxyPasswdKeep string `json:"proxyPasswdKeep"` +} + +type SettingBaseInfo struct { + SystemVersion string `json:"systemVersion"` + DeveloperMode string `json:"developerMode"` + UpgradeBackupCopies string `json:"upgradeBackupCopies"` + + Port string `json:"port"` + Ipv6 string `json:"ipv6"` + BindAddress string `json:"bindAddress"` + PanelName string `json:"panelName"` + Edition string `json:"edition"` + Theme string `json:"theme"` + MenuTabs string `json:"menuTabs"` + Language string `json:"language"` + HideMenu string `json:"hideMenu"` + DocSource string `json:"docSource"` + + ServerPort string `json:"serverPort"` + SecurityEntrance string `json:"securityEntrance"` + ComplexityVerification string `json:"complexityVerification"` + NoAuthSetting string `json:"noAuthSetting"` + ProxyType string `json:"proxyType"` + + DashboardMemoVisible string `json:"dashboardMemoVisible"` + DashboardSimpleNodeVisible string `json:"dashboardSimpleNodeVisible"` +} + +type CurrentUserInfo struct { + Name string `json:"name"` + SessionTimeout int `json:"sessionTimeout"` + MFAStatus string `json:"mfaStatus"` + MFAInterval string `json:"mfaInterval"` + ExpirationDays int `json:"expirationDays"` + ExpirationTime string `json:"expirationTime"` + ComplexitySetting string `json:"complexitySetting"` ApiInterfaceStatus string `json:"apiInterfaceStatus"` ApiKey string `json:"apiKey"` IpWhiteList string `json:"ipWhiteList"` ApiKeyValidityTime string `json:"apiKeyValidityTime"` } +type CurrentUserUpdate struct { + Name string `json:"name" validate:"required"` + Password string `json:"password"` + OldPassword string `json:"oldPassword"` + SessionTimeout int `json:"sessionTimeout" validate:"required,min=300,max=864000"` + ExpirationDays int `json:"expirationDays" validate:"min=0,max=60"` + ExpirationTime string `json:"expirationTime"` +} type SettingKey struct { Key string `json:"key" validate:"required,oneof=ScriptSync"` diff --git a/core/app/service/auth.go b/core/app/service/auth.go index 55a1491b0..a17b913d7 100644 --- a/core/app/service/auth.go +++ b/core/app/service/auth.go @@ -8,22 +8,18 @@ import ( "errors" "fmt" "net" - "strconv" "strings" "time" + "github.com/1Panel-dev/1Panel/core/app/auth" "github.com/1Panel-dev/1Panel/core/app/dto" "github.com/1Panel-dev/1Panel/core/app/repo" "github.com/1Panel-dev/1Panel/core/buserr" "github.com/1Panel-dev/1Panel/core/constant" "github.com/1Panel-dev/1Panel/core/global" - initauth "github.com/1Panel-dev/1Panel/core/init/auth" "github.com/1Panel-dev/1Panel/core/init/session/psession" - "github.com/1Panel-dev/1Panel/core/utils/common" "github.com/1Panel-dev/1Panel/core/utils/encrypt" - "github.com/1Panel-dev/1Panel/core/utils/mfa" "github.com/1Panel-dev/1Panel/core/utils/passkey" - "github.com/1Panel-dev/1Panel/core/utils/xpack" "github.com/gin-gonic/gin" "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" @@ -35,9 +31,7 @@ type AuthService struct{} type IAuthService interface { GetResponsePage() (string, error) VerifyCode(code string) (bool, error) - Login(c *gin.Context, info dto.Login, entrance string) (*dto.UserLoginInfo, string, error) LogOut(c *gin.Context) error - MFALogin(c *gin.Context, info dto.MFALogin, entrance string) (*dto.UserLoginInfo, string, error) PasskeyBeginLogin(c *gin.Context, entrance string) (*dto.PasskeyBeginResponse, string, error) PasskeyFinishLogin(c *gin.Context, sessionID, entrance string) (*dto.UserLoginInfo, string, error) PasskeyBeginRegister(c *gin.Context, name string) (*dto.PasskeyBeginResponse, string, error) @@ -53,104 +47,6 @@ func NewIAuthService() IAuthService { return &AuthService{} } -func (u *AuthService) Login(c *gin.Context, info dto.Login, entrance string) (*dto.UserLoginInfo, string, error) { - nameSetting, err := settingRepo.Get(repo.WithByKey("UserName")) - if err != nil { - return nil, "", buserr.New("ErrRecordNotFound") - } - if nameSetting.Value != info.Name { - return xpack.Login(c, info, entrance) - } - if err = checkPassword(info.Password); err != nil { - return nil, "ErrAuth", err - } - entranceSetting, err := settingRepo.Get(repo.WithByKey("SecurityEntrance")) - if err != nil { - return nil, "", err - } - if len(entranceSetting.Value) != 0 && entranceSetting.Value != entrance { - return nil, "ErrEntrance", buserr.New("ErrEntrance") - } - mfa, err := settingRepo.Get(repo.WithByKey("MFAStatus")) - if err != nil { - return nil, "", err - } - if err = settingRepo.Update("Language", info.Language); err != nil { - return nil, "", err - } - if mfa.Value == constant.StatusEnable { - ip := common.GetRealClientIP(c) - mfaSession := initauth.GetMFASessionStore().Set(nameSetting.Value, entrance, ip) - return &dto.UserLoginInfo{Name: nameSetting.Value, MfaStatus: mfa.Value, MfaSession: mfaSession}, "", nil - } - res, err := u.generateSession(c, info.Name) - if err != nil { - return nil, "", err - } - if entrance != "" { - SetSecurityEntranceCookie(c, entrance) - } - return res, "", nil -} - -func (u *AuthService) MFALogin(c *gin.Context, info dto.MFALogin, entrance string) (*dto.UserLoginInfo, string, error) { - mfaSessions := initauth.GetMFASessionStore() - session, ok := mfaSessions.Get(info.SessionID) - if !ok { - return nil, "ErrMFA", nil - } - if session.IP != common.GetRealClientIP(c) { - return nil, "ErrMFA", nil - } - if session.Entrance != entrance { - return nil, "", buserr.New("ErrEntrance") - } - mfaSecret, err := settingRepo.Get(repo.WithByKey("MFASecret")) - if err != nil { - return nil, "", err - } - mfaInterval, err := settingRepo.Get(repo.WithByKey("MFAInterval")) - if err != nil { - return nil, "", err - } - success := mfa.ValidCode(info.Code, mfaInterval.Value, mfaSecret.Value) - if !success { - return nil, "ErrMFA", nil - } - res, err := u.generateSession(c, session.Name) - if err != nil { - return nil, "", err - } - mfaSessions.Delete(info.SessionID) - if entrance != "" { - SetSecurityEntranceCookie(c, entrance) - } - return res, "", nil -} - -func (u *AuthService) generateSession(c *gin.Context, name string) (*dto.UserLoginInfo, error) { - setting, err := settingRepo.Get(repo.WithByKey("SessionTimeout")) - if err != nil { - return nil, err - } - httpsSetting, err := settingRepo.Get(repo.WithByKey("SSL")) - if err != nil { - return nil, err - } - lifeTime, err := strconv.Atoi(setting.Value) - if err != nil { - return nil, err - } - - sessionUser := psession.SessionUser{ID: psession.SuperAdminSessionUserID, Name: name, Role: "ADMIN"} - lifeTime = xpack.LoadSessionTimeout(sessionUser, lifeTime) - if err := global.SESSION.SetFresh(c, sessionUser, httpsSetting.Value == constant.StatusEnable, lifeTime); err != nil { - return nil, err - } - - return &dto.UserLoginInfo{Name: name, Role: "ADMIN"}, nil -} - func (u *AuthService) LogOut(c *gin.Context) error { httpsSetting, err := settingRepo.Get(repo.WithByKey("SSL")) if err != nil { @@ -304,7 +200,8 @@ func (u *AuthService) PasskeyFinishLogin(c *gin.Context, sessionID, entrance str if err != nil { return nil, "", err } - res, err := u.generateSession(c, userSetting.Value) + sessionUser := psession.SessionUser{ID: psession.SuperAdminSessionUserID, Name: userSetting.Value, Role: "ADMIN"} + res, err := auth.GenerateSession(c, sessionUser) if err != nil { return nil, "", err } @@ -794,27 +691,3 @@ func stripHostPort(hostport string) string { } return strings.Trim(hostport, "[]") } -func checkPassword(password string) error { - priKey, _ := settingRepo.Get(repo.WithByKey("PASSWORD_PRIVATE_KEY")) - - privateKey, err := encrypt.ParseRSAPrivateKey(priKey.Value) - if err != nil { - return err - } - loginPassword, err := encrypt.DecryptPassword(password, privateKey) - if err != nil { - return err - } - passwordSetting, err := settingRepo.Get(repo.WithByKey("Password")) - if err != nil { - return err - } - existPassword, err := encrypt.StringDecrypt(passwordSetting.Value) - if err != nil { - return err - } - if !hmac.Equal([]byte(loginPassword), []byte(existPassword)) { - return buserr.New("ErrAuth") - } - return nil -} diff --git a/core/app/service/backup.go b/core/app/service/backup.go index ddd11a0af..a27aa9a53 100644 --- a/core/app/service/backup.go +++ b/core/app/service/backup.go @@ -98,7 +98,7 @@ func (u *BackupService) Create(req dto.BackupOperate) error { if err := backupRepo.Create(&backup); err != nil { return err } - if err := xpack.Sync(constant.SyncBackupAccounts); err != nil { + if err := xpack.MultiNodeProvider.Sync(constant.SyncBackupAccounts); err != nil { global.LOG.Errorf("sync backup account to node failed, err: %v", err) } return nil @@ -119,7 +119,7 @@ func (u *BackupService) Delete(name string) error { global.LOG.Errorf("check used of local cronjob failed, err: %v", err) return buserr.New("ErrBackupInUsed") } - if err := xpack.CheckBackupUsed(name); err != nil { + if err := xpack.MultiNodeProvider.CheckBackupUsed(name); err != nil { global.LOG.Errorf("check used of node cronjob failed, err: %v", err) return buserr.New("ErrBackupInUsed") } @@ -127,7 +127,7 @@ func (u *BackupService) Delete(name string) error { if err := backupRepo.Delete(repo.WithByName(name)); err != nil { return err } - if err := xpack.Sync(constant.SyncBackupAccounts); err != nil { + if err := xpack.MultiNodeProvider.Sync(constant.SyncBackupAccounts); err != nil { global.LOG.Errorf("sync backup account to node failed, err: %v", err) } return nil @@ -176,7 +176,7 @@ func (u *BackupService) Update(req dto.BackupOperate) error { if err := backupRepo.Save(&newBackup); err != nil { return err } - if err := xpack.Sync(constant.SyncBackupAccounts); err != nil { + if err := xpack.MultiNodeProvider.Sync(constant.SyncBackupAccounts); err != nil { global.LOG.Errorf("sync backup account to node failed, err: %v", err) } return nil @@ -218,7 +218,7 @@ func (u *BackupService) RefreshToken(req dto.OperateByName) error { if err := backupRepo.Save(&backup); err != nil { return err } - if err := xpack.Sync(constant.SyncBackupAccounts); err != nil { + if err := xpack.MultiNodeProvider.Sync(constant.SyncBackupAccounts); err != nil { global.LOG.Errorf("sync backup account to node failed, err: %v", err) } return nil diff --git a/core/app/service/group.go b/core/app/service/group.go index c23495c8b..acb6df4c5 100644 --- a/core/app/service/group.go +++ b/core/app/service/group.go @@ -105,13 +105,13 @@ func (u *GroupService) Delete(id uint) error { case "command": err = commandRepo.UpdateGroup(id, defaultGroup.ID) case "node": - err = xpack.UpdateGroup("node", id, defaultGroup.ID) + err = xpack.MultiNodeProvider.UpdateGroup("node", id, defaultGroup.ID) case "website": bodyItem := []byte(fmt.Sprintf(`{"Group":%v, "NewGroup":%v}`, id, defaultGroup.ID)) if _, err := proxy_local.NewLocalClient("/api/v2/websites/group/change", http.MethodPost, bytes.NewReader(bodyItem), nil); err != nil { return err } - if err := xpack.UpdateGroup("node", id, defaultGroup.ID); err != nil { + if err := xpack.MultiNodeProvider.UpdateGroup("node", id, defaultGroup.ID); err != nil { return err } } diff --git a/core/app/service/script_library.go b/core/app/service/script_library.go index 35fb32ebc..3fb3914d0 100644 --- a/core/app/service/script_library.go +++ b/core/app/service/script_library.go @@ -124,7 +124,7 @@ func (u *ScriptService) Create(req dto.ScriptOperate) error { if req.IsInteractive { return nil } - if err := xpack.Sync(constant.SyncScripts); err != nil { + if err := xpack.MultiNodeProvider.Sync(constant.SyncScripts); err != nil { global.LOG.Errorf("sync scripts to node failed, err: %v", err) } return nil @@ -140,7 +140,7 @@ func (u *ScriptService) Delete(req dto.OperateByIDs) error { return err } } - if err := xpack.Sync(constant.SyncScripts); err != nil { + if err := xpack.MultiNodeProvider.Sync(constant.SyncScripts); err != nil { global.LOG.Errorf("sync scripts to node failed, err: %v", err) } return nil @@ -160,7 +160,7 @@ func (u *ScriptService) Update(req dto.ScriptOperate) error { if err := scriptRepo.Update(req.ID, updateMap); err != nil { return err } - if err := xpack.Sync(constant.SyncScripts); err != nil { + if err := xpack.MultiNodeProvider.Sync(constant.SyncScripts); err != nil { global.LOG.Errorf("sync scripts to node failed, err: %v", err) } return nil @@ -276,7 +276,7 @@ func (u *ScriptService) Sync(req dto.OperateByTaskID) error { if err := global.DB.Model(&model.Setting{}).Where("key = ?", "ScriptVersion").Updates(map[string]interface{}{"value": string(versionRes)}).Error; err != nil { return fmt.Errorf("update script version in db failed, err: %v", err) } - if err := xpack.Sync(constant.SyncScripts); err != nil { + if err := xpack.MultiNodeProvider.Sync(constant.SyncScripts); err != nil { global.LOG.Errorf("sync scripts to node failed, err: %v", err) } return nil diff --git a/core/app/service/setting.go b/core/app/service/setting.go index f01cd3ece..8cc7577f8 100644 --- a/core/app/service/setting.go +++ b/core/app/service/setting.go @@ -43,16 +43,13 @@ type SettingService struct{} type ISettingService interface { GetSettingInfo() (*dto.SettingInfo, error) + GetSettingBaseInfo() (*dto.SettingBaseInfo, error) LoadInterfaceAddr() ([]string, error) Update(c *gin.Context, key, value string) error - UpdatePassword(c *gin.Context, old, new string) error UpdatePort(port uint) error UpdateBindInfo(req dto.BindInfo) error UpdateSSL(c *gin.Context, req dto.SSLUpdate) error LoadFromCert() (*dto.SSLInfo, error) - HandlePasswordExpired(c *gin.Context, old, new string) error - GenerateApiKey() (string, error) - UpdateApiConfig(req dto.ApiInterfaceConfig) error UpdateProxy(req dto.ProxyUpdate) error @@ -113,6 +110,40 @@ func (u *SettingService) GetSettingInfo() (*dto.SettingInfo, error) { return &info, err } +func (u *SettingService) GetSettingBaseInfo() (*dto.SettingBaseInfo, error) { + setting, err := settingRepo.List() + if err != nil { + return nil, buserr.New("ErrRecordNotFound") + } + settingMap := make(map[string]string) + for _, set := range setting { + settingMap[set.Key] = set.Value + } + if hideMenu, ok := settingMap["HideMenu"]; ok && len(hideMenu) > 0 { + var menus []dto.ShowMenu + if err := json.Unmarshal([]byte(hideMenu), &menus); err == nil { + sortShowMenus(menus) + if sortedBytes, err := json.Marshal(menus); err == nil { + settingMap["HideMenu"] = string(sortedBytes) + } + } + } + var info dto.SettingBaseInfo + arr, err := json.Marshal(settingMap) + if err != nil { + return nil, err + } + if err := json.Unmarshal(arr, &info); err != nil { + return nil, err + } + if info.Edition == "" { + info.Edition = "cn" + _ = settingRepo.UpdateOrCreate("Edition", info.Edition) + } + + return &info, err +} + func sortShowMenus(menus []dto.ShowMenu) { for i := range menus { if len(menus[i].Children) > 0 { @@ -164,14 +195,6 @@ func (u *SettingService) Update(c *gin.Context, key, value string) error { } switch key { - case "ExpirationDays": - timeout, err := strconv.Atoi(value) - if err != nil { - return err - } - if err := settingRepo.Update("ExpirationTime", time.Now().AddDate(0, 0, timeout).Format(constant.DateTimeLayout)); err != nil { - return err - } case "BindDomain": if len(value) != 0 { _ = global.SESSION.Clean() @@ -179,11 +202,9 @@ func (u *SettingService) Update(c *gin.Context, key, value string) error { if err := u.clearPasskeySettings(); err != nil { return err } - case "UserName", "Password": - u.deleteCurrentSession(c) case "Language": i18n.SetCachedDBLanguage(value) - if err := xpack.Sync(constant.SyncLanguage); err != nil { + if err := xpack.MultiNodeProvider.Sync(constant.SyncLanguage); err != nil { global.LOG.Errorf("sync language to node failed, err: %v", err) } case "UpgradeBackupCopies": @@ -196,7 +217,7 @@ func (u *SettingService) Update(c *gin.Context, key, value string) error { } case "Edition": global.CONF.Base.Edition = value - if err := xpack.Sync(constant.SyncEdition); err != nil { + if err := xpack.MultiNodeProvider.Sync(constant.SyncEdition); err != nil { global.LOG.Errorf("sync edition to node failed, err: %v", err) } } @@ -263,14 +284,14 @@ func (u *SettingService) UpdateProxy(req dto.ProxyUpdate) error { if err := settingRepo.Update("ProxyPasswdKeep", req.ProxyPasswdKeep); err != nil { return err } - if err := xpack.ProxyDocker(loadDockerProxy(req)); err != nil { + if err := xpack.MultiNodeProvider.ProxyDocker(loadDockerProxy(req)); err != nil { return err } syncScope := constant.SyncSystemProxy if req.WithDockerRestart { syncScope = constant.SyncSystemProxyWithRestartDocker } - if err := xpack.Sync(syncScope); err != nil { + if err := xpack.MultiNodeProvider.Sync(syncScope); err != nil { global.LOG.Errorf("sync proxy to node failed, err: %v", err) } return nil @@ -410,7 +431,7 @@ func (u *SettingService) UpdateSSL(c *gin.Context, req dto.SSLUpdate) error { if err := os.Rename(path.Join(secretDir, "server.key.tmp"), path.Join(secretDir, "server.key")); err != nil { return err } - status, _ := settingRepo.GetValueByKey("SSL") + status := global.CONF.Conn.SSL if req.SSL != status { go func() { time.Sleep(1 * time.Second) @@ -420,6 +441,7 @@ func (u *SettingService) UpdateSSL(c *gin.Context, req dto.SSLUpdate) error { if err := settingRepo.Update("SSL", req.SSL); err != nil { return err } + global.CONF.Conn.SSL = req.SSL return u.UpdateSystemSSL() } @@ -562,14 +584,6 @@ func (u *SettingService) UpdateTerminal(req dto.TerminalInfo) error { return nil } -func (u *SettingService) UpdatePassword(c *gin.Context, old, new string) error { - if err := u.HandlePasswordExpired(c, old, new); err != nil { - return err - } - u.deleteCurrentSession(c) - return nil -} - func (u *SettingService) deleteCurrentSession(c *gin.Context) { if c == nil { return @@ -588,7 +602,7 @@ func (u *SettingService) clearPasskeySettings() error { if err := settingRepo.Update(passkey.PasskeyCredentialSettingKey, ""); err != nil { return err } - return nil + return xpack.AuthProvider.ClearPasskeys() } func (u *SettingService) UpdateSystemSSL() error { @@ -610,35 +624,6 @@ func (u *SettingService) UpdateSystemSSL() error { return nil } -func (u *SettingService) GenerateApiKey() (string, error) { - apiKey := common.RandStr(32) - if err := settingRepo.Update("ApiKey", apiKey); err != nil { - return global.Api.ApiKey, err - } - global.Api.ApiKey = apiKey - return apiKey, nil -} - -func (u *SettingService) UpdateApiConfig(req dto.ApiInterfaceConfig) error { - if err := settingRepo.UpdateOrCreate("ApiInterfaceStatus", req.ApiInterfaceStatus); err != nil { - return err - } - global.Api.ApiInterfaceStatus = req.ApiInterfaceStatus - if err := settingRepo.UpdateOrCreate("ApiKey", req.ApiKey); err != nil { - return err - } - global.Api.ApiKey = req.ApiKey - if err := settingRepo.UpdateOrCreate("IpWhiteList", req.IpWhiteList); err != nil { - return err - } - global.Api.IpWhiteList = req.IpWhiteList - if err := settingRepo.UpdateOrCreate("ApiKeyValidityTime", req.ApiKeyValidityTime); err != nil { - return err - } - global.Api.ApiKeyValidityTime = req.ApiKeyValidityTime - return nil -} - func loadInfoFromCert() (dto.SSLInfo, error) { var info dto.SSLInfo certFile := path.Join(global.CONF.Base.InstallDir, "1panel/secret/server.crt") diff --git a/core/app/service/upgrade.go b/core/app/service/upgrade.go index 8e1365185..d2780c166 100644 --- a/core/app/service/upgrade.go +++ b/core/app/service/upgrade.go @@ -259,7 +259,7 @@ func (u *UpgradeService) Upgrade(req dto.Upgrade) error { global.LOG.Info("upgrade successful!") dropBackupCopies() - xpack.AutoUpgradeWithMaster() + xpack.MultiNodeProvider.AutoUpgradeWithMaster() go writeLogs(req.Version) _ = settingRepo.Update("SystemVersion", req.Version) _ = global.AgentDB.Model(&model.Setting{}).Where("key = ?", "SystemVersion").Updates(map[string]interface{}{"value": req.Version}).Error diff --git a/core/cmd/server/conf/app.yaml b/core/cmd/server/conf/app.yaml index 1da4fc8c7..5660c4dce 100644 --- a/core/cmd/server/conf/app.yaml +++ b/core/cmd/server/conf/app.yaml @@ -4,7 +4,7 @@ base: is_demo: false is_offline: false is_fxplay: false - is_xpackee: true + is_xpackee: false port: 9999 username: admin password: admin123 diff --git a/core/cmd/server/validator_linux_amd64 b/core/cmd/server/validator_linux_amd64 deleted file mode 100644 index 3e2a15dfb..000000000 Binary files a/core/cmd/server/validator_linux_amd64 and /dev/null differ diff --git a/core/global/config.go b/core/global/config.go index d8623401c..1e78384cc 100644 --- a/core/global/config.go +++ b/core/global/config.go @@ -30,13 +30,6 @@ type Conn struct { Entrance string `mapstructure:"entrance"` } -type ApiInterface struct { - ApiKey string `mapstructure:"api_key"` - ApiInterfaceStatus string `mapstructure:"api_interface_status"` - IpWhiteList string `mapstructure:"ip_white_list"` - ApiKeyValidityTime string `mapstructure:"api_key_validity_time"` -} - type LogConfig struct { Level string `mapstructure:"level"` TimeZone string `mapstructure:"timeZone"` diff --git a/core/global/global.go b/core/global/global.go index ec9f0fa1c..d4ec6a52f 100644 --- a/core/global/global.go +++ b/core/global/global.go @@ -18,7 +18,6 @@ var ( AgentDB *gorm.DB LOG *logrus.Logger CONF ServerConfig - Api ApiInterface VALID *validator.Validate SESSION *psession.PSession Viper *viper.Viper diff --git a/core/i18n/lang/en.yaml b/core/i18n/lang/en.yaml index e710652be..c716ace4f 100644 --- a/core/i18n/lang/en.yaml +++ b/core/i18n/lang/en.yaml @@ -55,8 +55,6 @@ AppInstallCheck: 'Check application installation environment' # backup ErrBackupInUsed: "This backup account is used in scheduled tasks and cannot be deleted" -ErrRolePresetCannotDelete: "System preset roles cannot be deleted" -ErrRoleBoundToUser: "This role is already bound to users and cannot be deleted" ErrBackupCheck: "Backup account connection test failed {{ .err }}" ErrBackupLocal: "Local server backup account does not support this operation!" ErrBackupPublic: "Detected that this backup account is not public, check and try again!" @@ -75,6 +73,7 @@ ErrLicenseExist: "This license record already exists. You can directly go to the ErrXpackNotFound: "This section requires Business Edition. Import a license in Panel Settings > License." ErrXpackExceptional: "This section requires Business Edition. Sync the license status in Panel Settings > License." ErrXpackLost: "The license retry limit has been reached. Go to Panel Settings > License and run a manual sync." +ErrXpackEELicenseRequired: "Enterprise Edition license is not imported. Import a license first." ErrDeviceLost: "Required files for license verification are missing, check and try again!" ErrDeviceErr: "Current environment does not match the license import environment. Edit the license and re-import." ErrXpackTimeout: "Request timeout, network connection might be unstable, try again later!" @@ -92,6 +91,19 @@ ErrNodeBind: "This node is already bound to a license, check and retry!" ErrNodeLocalRollback: "The primary node does not support direct rollback. Run '1pctl restore' manually to roll back." ErrIntlLicense: "The current version does not support importing international licenses yet. Stay tuned!" +# user +ErrNoSuchUser: "User information not found, check and try again!" +ErrToMaster: "This user does not have permission to perform this operation on the master node. Check and try again!" +ErrToNode: "This user does not have permission to perform this operation on this node. Check and try again!" +ErrRBAC: "Insufficient user permissions. This operation cannot be performed. Check and try again!" +ErrNoneNode: "This user has no node permissions. Contact an administrator or check and try again!" +ErrOnlySuperAdmin: "Only super admins can use this feature!" +ErrRolePresetCannotDelete: "System preset roles cannot be deleted" +ErrRolePresetCannotUpdate: "System preset roles cannot be updated" +ErrRoleBoundToUser: "This role is already bound to users and cannot be deleted" +ErrRolePresetCannotBind: "System preset roles cannot be assigned to users" +ErrSuperAdminCannotDelete: "Super admin users cannot be deleted" + InvalidRequestBodyType: "Invalid request body format. Ensure content matches the required format and retry." InvalidLicenseCodeType: "Invalid license code format provided, check and try again!" LicenseNotFoundType: "License not found, no matching record exists in the system for the provided license. Check and try again!" diff --git a/core/i18n/lang/es-ES.yaml b/core/i18n/lang/es-ES.yaml index 6ae891e5b..dce0d5363 100644 --- a/core/i18n/lang/es-ES.yaml +++ b/core/i18n/lang/es-ES.yaml @@ -54,8 +54,6 @@ AppInstallCheck: 'Verificar entorno de instalación de aplicación' # backup ErrBackupInUsed: 'Cuenta de respaldo en uso por tarea programada' -ErrRolePresetCannotDelete: "System preset roles cannot be deleted" -ErrRoleBoundToUser: "This role is already bound to users and cannot be deleted" ErrBackupCheck: 'Conexión de respaldo falló: {{ .err }}' ErrBackupLocal: "La cuenta de respaldo del servidor local no admite esta operación" ErrBackupPublic: "Se detectó que esta cuenta de respaldo no es pública, verifique e intente de nuevo" @@ -75,6 +73,7 @@ ErrXpackNotFound: "Esta sección requiere la edición Business. Importa una lice ErrXpackExceptional: "Esta sección requiere la edición Business. Sincroniza el estado de la licencia en Configuración del panel > Licencia." ErrXpackOutOfDate: "La licencia actual ha expirado, importe nuevamente la licencia en Panel > Ajustes > Licencia" ErrXpackLost: "La licencia alcanzó el límite de reintentos. Ve a Configuración del panel > Licencia y ejecuta una sincronización manual." +ErrXpackEELicenseRequired: "La licencia de la edición Enterprise no se ha importado. Importa primero una licencia." ErrDeviceLost: "Faltan archivos necesarios para la verificación de licencia, verifique e intente de nuevo" ErrDeviceErr: "El entorno actual no coincide con el entorno de importación de la licencia. Edite la licencia e impórtela de nuevo" ErrXpackTimeout: "Tiempo de espera de la solicitud, puede que la conexión de red sea inestable, intente más tarde" @@ -91,6 +90,20 @@ ErrNodeUnbind: "Este nodo no está dentro del rango de vinculación de la licenc ErrNodeBind: "Este nodo ya está vinculado a una licencia, verifique e intente de nuevo" ErrNodeLocalRollback: "El nodo principal no admite la reversión directa. Ejecuta manualmente '1pctl restore' para revertir." ErrIntlLicense: "La versión actual aún no admite importar licencias de la edición internacional. Próximamente" + +# user +ErrNoSuchUser: "No se encontró la información del usuario. Verifique e intente de nuevo." +ErrToMaster: "Este usuario no tiene permiso para realizar esta operación en el nodo principal. Verifique e intente de nuevo." +ErrToNode: "Este usuario no tiene permiso para realizar esta operación en este nodo. Verifique e intente de nuevo." +ErrRBAC: "Permisos de usuario insuficientes. No se puede realizar esta operación. Verifique e intente de nuevo." +ErrNoneNode: "Este usuario no tiene permisos sobre ningún nodo. Contacte al administrador o verifique e intente de nuevo." +ErrOnlySuperAdmin: "Solo los superadministradores pueden usar esta función." +ErrRolePresetCannotDelete: "No se pueden eliminar los roles predefinidos del sistema" +ErrRolePresetCannotUpdate: "No se pueden actualizar los roles predefinidos del sistema" +ErrRoleBoundToUser: "Este rol ya está vinculado a usuarios y no se puede eliminar" +ErrRolePresetCannotBind: "Los roles predefinidos del sistema no se pueden asignar a usuarios" +ErrSuperAdminCannotDelete: "No se pueden eliminar usuarios superadministradores" + InvalidRequestBodyType: "Formato del cuerpo de la solicitud no válido, por favor revisa y asegúrate de que el contenido cumpla con el formato requerido antes de reintentar." InvalidLicenseCodeType: "Formato de código de licencia no válido, por favor revisa e inténtalo de nuevo." LicenseNotFoundType: "Licencia no encontrada, no existe ningún registro coincidente en el sistema para la licencia proporcionada. Por favor revisa e inténtalo de nuevo." diff --git a/core/i18n/lang/ja.yaml b/core/i18n/lang/ja.yaml index 5eae9cc01..36b15a220 100644 --- a/core/i18n/lang/ja.yaml +++ b/core/i18n/lang/ja.yaml @@ -49,8 +49,6 @@ AppInstallCheck: 'アプリケーションインストール環境を確認' # backup ErrBackupInUsed: 'バックアップアカウントがスケジュールで使用中' -ErrRolePresetCannotDelete: "System preset roles cannot be deleted" -ErrRoleBoundToUser: "This role is already bound to users and cannot be deleted" ErrBackupCheck: '接続テストに失敗しました: {{ .err }}' ErrBackupLocal: "ローカルサーバーバックアップアカウントはこの操作をサポートしていません!" ErrBackupPublic: "このバックアップアカウントが公開されていないと検出されました。再確認してください!" @@ -70,6 +68,7 @@ ErrLicenseExist: "このライセンス記録は既に存在します。ライ ErrXpackNotFound: "この機能は Business Edition で利用できます。パネル設定 > ライセンス でライセンスをインポートしてください" ErrXpackExceptional: "この機能は Business Edition で利用できます。パネル設定 > ライセンス でライセンス状態を同期してください" ErrXpackLost: "ライセンスの再試行回数が上限に達しました。パネル設定 > ライセンス で手動同期を実行してください" +ErrXpackEELicenseRequired: "Enterprise Edition のライセンスがインポートされていません。先にライセンスをインポートしてください" ErrDeviceLost: "ライセンス検証に必要なファイルが失われました。確認して再試行してください!" ErrDeviceErr: "現在の環境とライセンスのインポート環境が一致しません。ライセンスを編集して再度インポートしてください!" ErrXpackTimeout: "リクエストタイムアウト、ネットワーク接続が不安定な可能性があります。後で再試行してください" @@ -87,6 +86,19 @@ ErrNodeBind: "そのノードはライセンスにバインドされています ErrNodeLocalRollback: "マスターノードでは直接ロールバックできません。'1pctl restore' を手動で実行してロールバックしてください" ErrIntlLicense: "現在のバージョンでは国際版ライセンスのインポートはまだサポートされていません。しばらくお待ちください!" +# user +ErrNoSuchUser: "ユーザー情報が見つかりません。確認して再試行してください!" +ErrToMaster: "このユーザーにはマスターノードでこの操作を実行する権限がありません。確認して再試行してください!" +ErrToNode: "このユーザーにはこのノードでこの操作を実行する権限がありません。確認して再試行してください!" +ErrRBAC: "ユーザー権限が不足しているため、この操作を実行できません。確認して再試行してください!" +ErrNoneNode: "このユーザーにはノード権限がありません。管理者に連絡するか、確認して再試行してください!" +ErrOnlySuperAdmin: "この機能はスーパー管理者のみ使用できます!" +ErrRolePresetCannotDelete: "システム既定ロールは削除できません" +ErrRolePresetCannotUpdate: "システム既定ロールは更新できません" +ErrRoleBoundToUser: "このロールはすでにユーザーに紐付いているため削除できません" +ErrRolePresetCannotBind: "システム既定ロールはユーザーに割り当てできません" +ErrSuperAdminCannotDelete: "スーパー管理者ユーザーは削除できません" + InvalidRequestBodyType: "リクエストボディの形式が無効です。内容が要求された形式に準拠しているか確認してから再試行してください!" InvalidLicenseCodeType: "提供されたライセンスコードの形式が無効です。確認してから再試行してください!" LicenseNotFoundType: "ライセンスが見つかりません。提供されたライセンスに一致する記録がシステム内に存在しません。確認してから再試行してください!" diff --git a/core/i18n/lang/ko.yaml b/core/i18n/lang/ko.yaml index d83e91c73..1c921f8af 100644 --- a/core/i18n/lang/ko.yaml +++ b/core/i18n/lang/ko.yaml @@ -49,8 +49,6 @@ AppInstallCheck: '애플리케이션 설치 환경 확인' # backup ErrBackupInUsed: '백업 계정이 예약에 사용 중' -ErrRolePresetCannotDelete: "System preset roles cannot be deleted" -ErrRoleBoundToUser: "This role is already bound to users and cannot be deleted" ErrBackupCheck: '연결 테스트 실패: {{ .err }}' ErrBackupLocal: "로컬 서버 백업 계정은 이 작업을 지원하지 않습니다" ErrBackupPublic: "이 백업 계정이 공개된 것으로 감지되지 않았습니다. 다시 확인하십시오" @@ -69,6 +67,7 @@ ErrLicenseExist: "해당 라이선스 기록이 이미 존재합니다. 라이 ErrXpackNotFound: "이 기능은 Business Edition에서 사용할 수 있습니다. 패널 설정 > 라이선스에서 라이선스를 가져오세요" ErrXpackExceptional: "이 기능은 Business Edition에서 사용할 수 있습니다. 패널 설정 > 라이선스에서 라이선스 상태를 동기화하세요" ErrXpackLost: "라이선스 재시도 한도에 도달했습니다. 패널 설정 > 라이선스에서 수동 동기화를 실행하세요" +ErrXpackEELicenseRequired: "Enterprise Edition 라이선스를 가져오지 않았습니다. 먼저 라이선스를 가져오세요" ErrDeviceLost: "라이센스 검증에 필요한 파일이 누락되었습니다. 확인 후 다시 시도해 주세요" ErrDeviceErr: "현재 환경이 라이선스 가져오기 환경과 일치하지 않습니다. 라이선스를 편집하고 다시 가져오십시오" ErrXpackTimeout: "요청 시간 초과, 네트워크 연결이 불안정할 수 있습니다. 나중에 다시 시도해 주세요" @@ -86,6 +85,19 @@ ErrNodeBind: "이 노드가 이미 라이선스에 바인딩되어 있음을 감 ErrNodeLocalRollback: "마스터 노드는 직접 롤백을 지원하지 않습니다. '1pctl restore'를 수동으로 실행해 롤백하세요" ErrIntlLicense: "현재 버전에서는 국제판 라이선스 가져오기를 아직 지원하지 않습니다. 곧 제공될 예정입니다" +# user +ErrNoSuchUser: "사용자 정보를 찾을 수 없습니다. 확인 후 다시 시도하세요" +ErrToMaster: "이 사용자는 마스터 노드에서 이 작업을 수행할 권한이 없습니다. 확인 후 다시 시도하세요" +ErrToNode: "이 사용자는 이 노드에서 이 작업을 수행할 권한이 없습니다. 확인 후 다시 시도하세요" +ErrRBAC: "사용자 권한이 부족하여 이 작업을 수행할 수 없습니다. 확인 후 다시 시도하세요" +ErrNoneNode: "이 사용자에게는 노드 권한이 없습니다. 관리자에게 문의하거나 확인 후 다시 시도하세요" +ErrOnlySuperAdmin: "슈퍼 관리자만 이 기능을 사용할 수 있습니다" +ErrRolePresetCannotDelete: "시스템 기본 역할은 삭제할 수 없습니다" +ErrRolePresetCannotUpdate: "시스템 기본 역할은 수정할 수 없습니다" +ErrRoleBoundToUser: "이 역할은 이미 사용자에게 연결되어 있어 삭제할 수 없습니다" +ErrRolePresetCannotBind: "시스템 기본 역할은 사용자에게 할당할 수 없습니다" +ErrSuperAdminCannotDelete: "슈퍼 관리자 사용자는 삭제할 수 없습니다" + InvalidRequestBodyType: "요청 본문 형식이 잘못되었습니다. 내용이 형식 요구 사항을 충족하는지 확인한 후 다시 시도하세요" InvalidLicenseCodeType: "제공된 라이선스 코드 형식이 잘못되었습니다. 확인 후 다시 시도하세요" LicenseNotFoundType: "라이선스가 존재하지 않습니다. 시스템에서 제공된 라이선스와 일치하는 기록을 찾을 수 없습니다. 확인 후 다시 시도하세요" diff --git a/core/i18n/lang/ms.yaml b/core/i18n/lang/ms.yaml index 2d2962f81..c167e15e4 100644 --- a/core/i18n/lang/ms.yaml +++ b/core/i18n/lang/ms.yaml @@ -44,8 +44,6 @@ ErrFileNotFound: "Fail {{ .name }} tidak wujud" # backup ErrBackupInUsed: 'Akaun sandaran sedang digunakan oleh tugas' -ErrRolePresetCannotDelete: "System preset roles cannot be deleted" -ErrRoleBoundToUser: "This role is already bound to users and cannot be deleted" ErrBackupCheck: 'Ujian sambungan gagal: {{ .err }}' ErrBackupLocal: "Akaun sandaran pelayan tempatan tidak menyokong operasi ini" ErrBackupPublic: "Akaun sandaran ini dikesan tidak awam, sila semak semula dan cuba lagi" @@ -68,6 +66,7 @@ ErrDeviceLost: "Fail yang diperlukan untuk pengesahan lesen hilang, sila semak d ErrDeviceErr: "Persekitaran semasa tidak sepadan dengan persekitaran import lesen. Sila edit lesen dan import semula" ErrXpackTimeout: "Permintaan tamat masa, sambungan rangkaian mungkin tidak stabil, sila cuba lagi kemudian" ErrUnbindMaster: "Terdapat nod dalam pengurusan nod, sila keluarkan dahulu dan cuba lagi" +ErrXpackEELicenseRequired: "Lesen Edisi Enterprise belum diimport. Sila import lesen terlebih dahulu" ErrFreeNodeLimit: "Had nod Edisi Community telah dicapai. Pergi ke www.lxware.cn/1panel untuk naik taraf dan cuba lagi" ErrNodeBound: "Lesen ini telah diikat dengan nod lain, sila semak dan cuba lagi" ErrNodeBoundDelete: "Lisensi ini telah diikat dan tidak menyokong operasi penghapusan. Sila semak dan cuba lagi" @@ -81,6 +80,19 @@ ErrNodeBind: "Nod ini telah diikat dengan lesen, sila semak dan cuba lagi" ErrNodeLocalRollback: "Nod utama tidak menyokong rollback secara langsung. Jalankan '1pctl restore' secara manual untuk rollback" ErrIntlLicense: "Versi semasa belum menyokong import lesen edisi antarabangsa. Nantikan" +# user +ErrNoSuchUser: "Maklumat pengguna tidak ditemui, sila semak dan cuba lagi" +ErrToMaster: "Pengguna ini tiada kebenaran untuk melakukan operasi ini pada nod utama, sila semak dan cuba lagi" +ErrToNode: "Pengguna ini tiada kebenaran untuk melakukan operasi ini pada nod ini, sila semak dan cuba lagi" +ErrRBAC: "Kebenaran pengguna tidak mencukupi, operasi ini tidak boleh dilakukan. Sila semak dan cuba lagi" +ErrNoneNode: "Pengguna ini tidak mempunyai sebarang kebenaran nod. Sila hubungi pentadbir atau semak dan cuba lagi" +ErrOnlySuperAdmin: "Hanya super admin boleh menggunakan fungsi ini" +ErrRolePresetCannotDelete: "Peranan pratetap sistem tidak boleh dipadam" +ErrRolePresetCannotUpdate: "Peranan pratetap sistem tidak boleh dikemas kini" +ErrRoleBoundToUser: "Peranan ini sudah terikat kepada pengguna dan tidak boleh dipadam" +ErrRolePresetCannotBind: "Peranan pratetap sistem tidak boleh diberikan kepada pengguna" +ErrSuperAdminCannotDelete: "Pengguna super admin tidak boleh dipadam" + InvalidRequestBodyType: "Format badan permintaan tidak sah, sila periksa dan pastikan kandungan memenuhi format yang diperlukan sebelum mencuba semula" InvalidLicenseCodeType: "Format kod lesen yang diberikan tidak sah, sila periksa dan cuba lagi" LicenseNotFoundType: "Lesen tidak dijumpai, tiada rekod yang sepadan dengan lesen yang diberikan dalam sistem. Sila periksa dan cuba lagi" diff --git a/core/i18n/lang/pt-BR.yaml b/core/i18n/lang/pt-BR.yaml index 3a8ae4849..07e86fd59 100644 --- a/core/i18n/lang/pt-BR.yaml +++ b/core/i18n/lang/pt-BR.yaml @@ -49,8 +49,6 @@ AppInstallCheck: 'Verificar ambiente de instalação da aplicação' # backup ErrBackupInUsed: 'Conta de backup em uso por tarefa' -ErrRolePresetCannotDelete: "System preset roles cannot be deleted" -ErrRoleBoundToUser: "This role is already bound to users and cannot be deleted" ErrBackupCheck: 'Teste de conexão falhou: {{ .err }}' ErrBackupLocal: "A conta de backup do servidor local não suporta essa operação" ErrBackupPublic: "A conta de backup detectada não é pública, por favor verifique e tente novamente" @@ -69,6 +67,7 @@ ErrLicenseExist: "Este registro de licença já existe. Você pode ir diretament ErrXpackNotFound: "Este recurso requer a edição Business. Importe uma licença em Configurações do Painel > Licença." ErrXpackExceptional: "Este recurso requer a edição Business. Sincronize o status da licença em Configurações do Painel > Licença." ErrXpackLost: "A licença atingiu o limite de tentativas. Acesse Configurações do Painel > Licença e execute uma sincronização manual." +ErrXpackEELicenseRequired: "A licença da edição Enterprise não foi importada. Importe uma licença primeiro." ErrDeviceLost: "Arquivos necessários para a verificação da licença estão faltando, por favor verifique e tente novamente" ErrDeviceErr: "O ambiente atual não corresponde ao ambiente de importação da licença. Por favor, edite a licença e reimporte" ErrXpackTimeout: "Requisição expirou, a conexão de rede pode estar instável, por favor tente novamente mais tarde" @@ -86,6 +85,19 @@ ErrNodeBind: "Este nó já está vinculado a uma licença, por favor verifique e ErrNodeLocalRollback: "O nó principal não suporta rollback direto. Por favor, execute manualmente o comando '1pctl restore' para fazer o rollback" ErrIntlLicense: "A versão atual ainda não oferece suporte à importação de licença da edição internacional. Em breve" +# user +ErrNoSuchUser: "Informações do usuário não encontradas. Verifique e tente novamente." +ErrToMaster: "Este usuário não tem permissão para executar esta operação no nó principal. Verifique e tente novamente." +ErrToNode: "Este usuário não tem permissão para executar esta operação neste nó. Verifique e tente novamente." +ErrRBAC: "Permissões de usuário insuficientes. Não é possível executar esta operação. Verifique e tente novamente." +ErrNoneNode: "Este usuário não tem permissões de nenhum nó. Contate o administrador ou verifique e tente novamente." +ErrOnlySuperAdmin: "Somente superadministradores podem usar este recurso." +ErrRolePresetCannotDelete: "Funções predefinidas do sistema não podem ser excluídas" +ErrRolePresetCannotUpdate: "Funções predefinidas do sistema não podem ser atualizadas" +ErrRoleBoundToUser: "Esta função já está vinculada a usuários e não pode ser excluída" +ErrRolePresetCannotBind: "Funções predefinidas do sistema não podem ser atribuídas a usuários" +ErrSuperAdminCannotDelete: "Usuários superadministradores não podem ser excluídos" + InvalidRequestBodyType: "Formato do corpo da requisição inválido. Verifique se o conteúdo está no formato correto e tente novamente" InvalidLicenseCodeType: "Formato do código de licença inválido. Verifique e tente novamente" LicenseNotFoundType: "Licença não encontrada. Não há registros correspondentes no sistema para a licença fornecida. Verifique e tente novamente" diff --git a/core/i18n/lang/ru.yaml b/core/i18n/lang/ru.yaml index d42547908..fa863a4b7 100644 --- a/core/i18n/lang/ru.yaml +++ b/core/i18n/lang/ru.yaml @@ -49,8 +49,6 @@ AppInstallCheck: 'Проверить среду установки прилож # backup ErrBackupInUsed: 'Аккаунт бэкапа занят задачей' -ErrRolePresetCannotDelete: "System preset roles cannot be deleted" -ErrRoleBoundToUser: "This role is already bound to users and cannot be deleted" ErrBackupCheck: 'Проверка подключения не удалась: {{ .err }}' ErrBackupLocal: "Локальная учетная запись резервного копирования не поддерживает эту операцию" ErrBackupPublic: "Обнаружено, что эта учетная запись резервного копирования не является публичной, проверьте и повторите попытку" @@ -69,6 +67,7 @@ ErrLicenseExist: "Данная лицензия уже существует. В ErrXpackNotFound: "Этот раздел доступен в Business Edition. Импортируйте лицензию в Настройки панели > Лицензия" ErrXpackExceptional: "Этот раздел доступен в Business Edition. Синхронизируйте статус лицензии в Настройки панели > Лицензия" ErrXpackLost: "Достигнут лимит повторных попыток по лицензии. Перейдите в Настройки панели > Лицензия и выполните ручную синхронизацию" +ErrXpackEELicenseRequired: "Лицензия Enterprise Edition не импортирована. Сначала импортируйте лицензию." ErrDeviceLost: "Необходимые файлы для проверки лицензии отсутствуют, пожалуйста, проверьте и попробуйте снова" ErrDeviceErr: "Текущая среда не соответствует среде импорта лицензии. Отредактируйте лицензию и повторите импорт" ErrXpackTimeout: "Время ожидания запроса истекло, возможно нестабильное сетевое соединение, повторите попытку позже" @@ -86,6 +85,19 @@ ErrNodeBind: "Этот узел уже связан с лицензией, пр ErrNodeLocalRollback: "Прямой откат на основном узле не поддерживается. Выполните '1pctl restore' вручную для отката" ErrIntlLicense: "Текущая версия пока не поддерживает импорт международной лицензии. Скоро будет доступно" +# user +ErrNoSuchUser: "Информация о пользователе не найдена. Проверьте и повторите попытку." +ErrToMaster: "У этого пользователя нет прав для выполнения этой операции на основном узле. Проверьте и повторите попытку." +ErrToNode: "У этого пользователя нет прав для выполнения этой операции на этом узле. Проверьте и повторите попытку." +ErrRBAC: "Недостаточно прав пользователя. Операция не может быть выполнена. Проверьте и повторите попытку." +ErrNoneNode: "У этого пользователя нет прав ни на один узел. Обратитесь к администратору или проверьте и повторите попытку." +ErrOnlySuperAdmin: "Эта функция доступна только супер администраторам." +ErrRolePresetCannotDelete: "Системные предустановленные роли нельзя удалять" +ErrRolePresetCannotUpdate: "Системные предустановленные роли нельзя обновлять" +ErrRoleBoundToUser: "Эта роль уже привязана к пользователям и не может быть удалена" +ErrRolePresetCannotBind: "Системные предустановленные роли нельзя назначать пользователям" +ErrSuperAdminCannotDelete: "Пользователей супер-администратора нельзя удалять" + InvalidRequestBodyType: "Неверный формат тела запроса. Проверьте, соответствует ли содержимое требуемому формату, и повторите попытку" InvalidLicenseCodeType: "Указан неверный формат лицензионного кода. Проверьте и повторите попытку" LicenseNotFoundType: "Лицензия не найдена. В системе нет записей, соответствующих указанной лицензии. Проверьте и повторите попытку" diff --git a/core/i18n/lang/tr.yaml b/core/i18n/lang/tr.yaml index edcc6c608..27c680a91 100644 --- a/core/i18n/lang/tr.yaml +++ b/core/i18n/lang/tr.yaml @@ -49,8 +49,6 @@ AppInstallCheck: 'Uygulama kurulum ortamını kontrol et' # backup ErrBackupInUsed: 'Yedek hesabı görevde kullanılıyor' -ErrRolePresetCannotDelete: "System preset roles cannot be deleted" -ErrRoleBoundToUser: "This role is already bound to users and cannot be deleted" ErrBackupCheck: 'Bağlantı testi başarısız: {{ .err }}' ErrBackupLocal: "Yerel sunucu yedekleme hesabı bu işlemi desteklemiyor" ErrBackupPublic: "Bu yedekleme hesabının herkese açık olmadığı tespit edildi, lütfen kontrol edip tekrar deneyin" @@ -69,6 +67,7 @@ ErrLicenseExist: "Bu lisans kaydı zaten mevcut. Düğüm bağlama için doğrud ErrXpackNotFound: "Bu bölüm Business Sürüm gerektirir. Lisansı Panel Ayarları > Lisans bölümünden içe aktarın" ErrXpackExceptional: "Bu bölüm Business Sürüm gerektirir. Lisans durumunu Panel Ayarları > Lisans bölümünden senkronize edin" ErrXpackLost: "Lisans yeniden deneme sınırına ulaştı. Panel Ayarları > Lisans bölümüne gidip manuel senkronizasyon çalıştırın" +ErrXpackEELicenseRequired: "Enterprise Edition lisansı içe aktarılmamış. Önce bir lisans içe aktarın." ErrDeviceLost: "Lisans doğrulama için gerekli dosyalar eksik, lütfen kontrol edip tekrar deneyin" ErrXpackTimeout: "İstek zaman aşımı, ağ bağlantısı kararsız olabilir, lütfen daha sonra tekrar deneyin" ErrUnbindMaster: "Düğüm yönetiminde düğümler tespit edildi, mevcut lisansın bağı çözülemiyor, lütfen önce düğümleri kaldırın ve tekrar deneyin" @@ -85,6 +84,19 @@ ErrNodeBind: "Bu düğüm zaten bir lisansa bağlı, lütfen kontrol edip tekrar ErrNodeLocalRollback: "Ana düğüm doğrudan rollback'i desteklemez. Rollback için '1pctl restore' komutunu manuel çalıştırın" ErrIntlLicense: "Mevcut sürüm uluslararası sürüm lisansının içe aktarılmasını henüz desteklemiyor. Yakında" +# user +ErrNoSuchUser: "Kullanıcı bilgisi bulunamadı, lütfen kontrol edip tekrar deneyin" +ErrToMaster: "Bu kullanıcının ana düğümde bu işlemi yapma izni yok, lütfen kontrol edip tekrar deneyin" +ErrToNode: "Bu kullanıcının bu düğümde bu işlemi yapma izni yok, lütfen kontrol edip tekrar deneyin" +ErrRBAC: "Kullanıcı izinleri yetersiz, bu işlem yapılamaz. Lütfen kontrol edip tekrar deneyin" +ErrNoneNode: "Bu kullanıcının hiçbir düğüm izni yok. Lütfen yöneticiyle iletişime geçin veya kontrol edip tekrar deneyin" +ErrOnlySuperAdmin: "Bu özellik yalnızca süper yöneticiler tarafından kullanılabilir" +ErrRolePresetCannotDelete: "Sistem ön tanımlı rolleri silinemez" +ErrRolePresetCannotUpdate: "Sistem ön tanımlı rolleri güncellenemez" +ErrRoleBoundToUser: "Bu rol zaten kullanıcılara bağlı ve silinemez" +ErrRolePresetCannotBind: "Sistem ön tanımlı rolleri kullanıcılara atanamaz" +ErrSuperAdminCannotDelete: "Süper yönetici kullanıcılar silinemez" + InvalidRequestBodyType: "Geçersiz istek gövdesi formatı, lütfen içeriğin format gereksinimlerine uygun olduğunu kontrol edip tekrar deneyin" InvalidLicenseCodeType: "Sağlanan lisans kodu formatı geçersiz, lütfen kontrol edip tekrar deneyin" LicenseNotFoundType: "Lisans bulunamadı, sistemde sağlanan lisansla eşleşen bir kayıt yok. Lütfen kontrol edip tekrar deneyin" diff --git a/core/i18n/lang/zh-Hant.yaml b/core/i18n/lang/zh-Hant.yaml index 7fc728fa1..f80ddd7a8 100644 --- a/core/i18n/lang/zh-Hant.yaml +++ b/core/i18n/lang/zh-Hant.yaml @@ -49,8 +49,6 @@ AppInstallCheck: '檢查應用安裝環境' #backup ErrBackupInUsed: "該備份帳號已在排程任務中使用,無法刪除" -ErrRolePresetCannotDelete: "系統預設角色,無法刪除" -ErrRoleBoundToUser: "角色已被使用者綁定,無法刪除" ErrBackupCheck: "備份帳號測試連線失敗 {{ .err }}" ErrBackupLocal: "本機伺服器備份帳號暫不支援該操作!" ErrBackupPublic: "偵測到該備份帳號為非公用,請檢查後再試。" @@ -69,6 +67,7 @@ ErrLicenseExist: "該許可證記錄已存在,您可直接前往許可證頁 ErrXpackNotFound: "此功能需要商業版。請在 面板設定 > 許可證 匯入許可證" ErrXpackExceptional: "此功能需要商業版。請在 面板設定 > 許可證 同步許可證狀態" ErrXpackLost: "許可證已達重試上限,請前往 面板設定 > 許可證 手動同步" +ErrXpackEELicenseRequired: "企業版許可證未匯入,請先匯入許可證" ErrDeviceLost: "許可證驗證必要檔案遺失,請檢查後重試。" ErrDeviceErr: "目前環境與許可證匯入環境不一致,請編輯許可證重新匯入!" ErrXpackTimeout: "請求逾時,網路連接可能不穩定,請稍後再試。" @@ -86,6 +85,19 @@ ErrNodeBind: "偵測到該節點已綁定許可證,請檢查後重試。" ErrNodeLocalRollback: "主節點暫不支援直接回滾,請手動執行 '1pctl restore' 進行回滾" ErrIntlLicense: "當前版本暫不支援匯入國際版許可證,敬請期待!" +# user +ErrNoSuchUser: "未能找到該使用者資訊,請檢查後重試!" +ErrToMaster: "該使用者無權限對主節點進行此操作,請檢查後重試!" +ErrToNode: "該使用者無權限對該節點進行此操作,請檢查後重試!" +ErrRBAC: "使用者權限不足,無法進行此操作,請檢查後重試!" +ErrNoneNode: "該使用者無任何節點權限,請聯絡管理員或檢查後重試!" +ErrOnlySuperAdmin: "僅超級管理員支援此功能!" +ErrRolePresetCannotDelete: "系統預設角色,無法刪除" +ErrRolePresetCannotUpdate: "系統預設角色,無法修改" +ErrRoleBoundToUser: "角色已被使用者綁定,無法刪除" +ErrRolePresetCannotBind: "系統預設角色,無法分配給使用者" +ErrSuperAdminCannotDelete: "超級管理員使用者無法刪除" + InvalidRequestBodyType: "請求內容格式錯誤,請檢查請求內容是否符合格式要求後重試!" InvalidLicenseCodeType: "提供的許可證格式錯誤,請檢查後重試。" LicenseNotFoundType: "許可證不存在,系統中未找到與提供許可證符合的紀錄,請檢查後重試。" diff --git a/core/i18n/lang/zh.yaml b/core/i18n/lang/zh.yaml index b388f2c7d..7ccd1c45e 100644 --- a/core/i18n/lang/zh.yaml +++ b/core/i18n/lang/zh.yaml @@ -55,8 +55,6 @@ AppInstallCheck: "检查应用安装环境" #backup ErrBackupInUsed: "该备份账号已在计划任务中使用,无法删除" -ErrRolePresetCannotDelete: "系统预设角色,无法删除" -ErrRoleBoundToUser: "角色已被用户绑定,无法删除" ErrBackupCheck: "备份账号测试连接失败 {{ .err }}" ErrBackupLocal: "本地服务器备份账号暂不支持该操作!" ErrBackupPublic: "检测到该备份账号为非公用,请检查后重试!" @@ -72,20 +70,20 @@ ErrXpackVersion: "许可证校验失败,该许可证受版本限制,无法 ErrLicenseSave: "许可证信息保存失败,错误 {{ .err }},请重试!" ErrLicenseSync: "许可证信息同步失败,数据库中未检测到许可证信息!" ErrLicenseExist: "该许可证记录已存在,您可直接前往许可证页面进行节点绑定。" -ErrXpackNotFound: "该部分为商业版功能,请先在 面板设置-许可证 界面导入许可证" -ErrXpackExceptional: "该部分为商业版功能,请先在 面板设置-许可证 界面同步许可证状态" -ErrXpackLost: "许可证已达到最大重试次数,请进入【面板设置】【许可证】页面手动点击同步按钮,以确保商业版功能正常使用" +ErrXpackNotFound: "该部分为专业版功能,请先在 面板设置-许可证 界面导入许可证" +ErrXpackExceptional: "该部分为专业版功能,请先在 面板设置-许可证 界面同步许可证状态" +ErrXpackLost: "许可证已达到最大重试次数,请进入【面板设置】【许可证】页面手动点击同步按钮,以确保专业版功能正常使用" +ErrXpackEELicenseRequired: "企业版许可证未导入,请先导入许可证" ErrDeviceLost: "许可证校验必要文件丢失,请检查后重试!" ErrDeviceErr: "当前环境与许可证导入环境不一致,请编辑许可证重新导入!" ErrXpackTimeout: "请求超时,网络连接可能不稳定,请稍后再试!" -ErrUnbindMaster: "检测到节点管理内存在商业版节点,无法解绑当前许可证,请先移除或解绑后重试!" +ErrUnbindMaster: "检测到节点管理内存在专业版节点,无法解绑当前许可证,请先移除或解绑后重试!" ErrFreeNodeLimit: "社区版节点数量已达到免费上限,请前往 www.lxware.cn/1panel 购买后重试!" ErrNodeBound: "该许可证已绑定到其他节点,请检查后重试!" ErrNodeBoundDelete: "该许可证已被绑定,不支持删除操作,请检查后重试!" ErrNodeBoundLimit: "当前免费节点已经达到上限,请检查后重试!" ErrLicenseFree: "仅当许可证正常绑定到节点后,才能使用其免费节点,请检查后重试!" ErrLicenseUnbind: "检测到该许可证存在社区版节点,请在 [ 面板设置 - 许可证 ] 中解绑后重试!" -ErrLicenseBindCount: "新许可证免费节点数量小于当前已绑定社区版节点数量,请检查后重试!" ErrNoSuchNode: "未能找到该节点信息,请检查后重试!" ErrNodeUnbind: "检测到该节点未在许可证绑定范围内,请检查后重试!" ErrNodeBind: "检测到该节点已绑定许可证,请检查后重试!" @@ -98,6 +96,12 @@ ErrToMaster: "该用户无权限对主节点进行该操作,请检查后重试 ErrToNode: "该用户无权限对该节点进行该操作,请检查后重试!" ErrRBAC: "用户权限不足,无法进行该操作,请检查后重试!" ErrNoneNode: "该用户无任何节点权限,请联系管理员或检查后重试!" +ErrOnlySuperAdmin: "仅超级管理员支持该功能!" +ErrRolePresetCannotDelete: "系统预设角色,无法删除" +ErrRolePresetCannotUpdate: "系统预设角色,无法修改" +ErrRoleBoundToUser: "角色已被用户绑定,无法删除" +ErrRolePresetCannotBind: "系统预设角色,无法分配给用户" +ErrSuperAdminCannotDelete: "超级管理员用户无法删除" InvalidRequestBodyType: "请求体格式错误,请检查请求内容是否符合格式要求后重试!" InvalidLicenseCodeType: "提供的许可证格式错误,请检查后重试!" diff --git a/core/init/auth/mfa_session.go b/core/init/auth/mfa_session.go index 3f5e23e4e..ec27df1c3 100644 --- a/core/init/auth/mfa_session.go +++ b/core/init/auth/mfa_session.go @@ -3,8 +3,9 @@ package auth import ( "crypto/rand" "encoding/base64" - "sync" "time" + + "github.com/1Panel-dev/1Panel/core/utils/ttlstore" ) const ( @@ -28,98 +29,42 @@ type mfaSession struct { } type mfaSessionStore struct { - mu sync.Mutex - items map[string]mfaSession + store *ttlstore.Store[mfaSession] } func newMFASessionStore() *mfaSessionStore { - return &mfaSessionStore{items: make(map[string]mfaSession)} + return &mfaSessionStore{ + store: ttlstore.New[mfaSession](MFASessionTTL, MFASessionStoreMaxEntries, generateMFASessionID), + } } func (s *mfaSessionStore) Set(name, entrance, ip string) string { - s.mu.Lock() - defer s.mu.Unlock() - - s.cleanupExpiredLocked() - if len(s.items) >= MFASessionStoreMaxEntries { - s.removeOldestLocked() - } - - sessionID := generateMFASessionID() - s.items[sessionID] = mfaSession{ - Name: name, - Entrance: entrance, - IP: ip, - ExpiresAt: time.Now().Add(MFASessionTTL), - } - return sessionID + return s.store.Set(mfaSession{ + Name: name, + Entrance: entrance, + IP: ip, + }) } func (s *mfaSessionStore) Get(sessionID string) (mfaSession, bool) { - s.mu.Lock() - defer s.mu.Unlock() - - item, ok := s.items[sessionID] - if !ok { - return mfaSession{}, false - } - if time.Now().After(item.ExpiresAt) { - delete(s.items, sessionID) - return mfaSession{}, false - } - return item, true + return s.store.Get(sessionID) } func (s *mfaSessionStore) Delete(sessionID string) { - s.mu.Lock() - defer s.mu.Unlock() - delete(s.items, sessionID) + s.store.Delete(sessionID) } func (s *mfaSessionStore) RecordFailure(sessionID string) int { - s.mu.Lock() - defer s.mu.Unlock() - - item, ok := s.items[sessionID] + failures := 0 + ok := s.store.Update(sessionID, func(item *mfaSession) bool { + item.Failures++ + failures = item.Failures + return item.Failures < MFASessionMaxFailures + }) if !ok { return 0 } - if time.Now().After(item.ExpiresAt) { - delete(s.items, sessionID) - return 0 - } - - item.Failures++ - if item.Failures >= MFASessionMaxFailures { - delete(s.items, sessionID) - return item.Failures - } - - s.items[sessionID] = item - return item.Failures -} - -func (s *mfaSessionStore) cleanupExpiredLocked() { - now := time.Now() - for id, item := range s.items { - if now.After(item.ExpiresAt) { - delete(s.items, id) - } - } -} - -func (s *mfaSessionStore) removeOldestLocked() { - var oldestID string - var oldestTime time.Time - for id, item := range s.items { - if oldestID == "" || item.ExpiresAt.Before(oldestTime) { - oldestID = id - oldestTime = item.ExpiresAt - } - } - if oldestID != "" { - delete(s.items, oldestID) - } + return failures } func generateMFASessionID() string { diff --git a/core/init/cron/job/backup.go b/core/init/cron/job/backup.go index edb9daf7d..75418bb28 100644 --- a/core/init/cron/job/backup.go +++ b/core/init/cron/job/backup.go @@ -56,7 +56,7 @@ func (b *backup) Run() { varsItem, _ := json.Marshal(varMap) _ = global.DB.Model(&model.BackupAccount{}).Where("id = ?", backupItem.ID).Updates(map[string]interface{}{"vars": string(varsItem)}).Error global.LOG.Infof("Refresh %s-%s access_token successful!", backupItem.Type, backupItem.Name) - if err := xpack.Sync(constant.SyncBackupAccounts); err != nil { + if err := xpack.MultiNodeProvider.Sync(constant.SyncBackupAccounts); err != nil { global.LOG.Errorf("sync backup account to node failed, err: %v", err) } } diff --git a/core/init/hook/hook.go b/core/init/hook/hook.go index 52b365be5..e347d1af9 100644 --- a/core/init/hook/hook.go +++ b/core/init/hook/hook.go @@ -10,6 +10,7 @@ import ( "github.com/1Panel-dev/1Panel/core/utils/cmd" "github.com/1Panel-dev/1Panel/core/utils/common" "github.com/1Panel-dev/1Panel/core/utils/encrypt" + "github.com/1Panel-dev/1Panel/core/utils/xpack" ) func Init() { @@ -17,12 +18,6 @@ func Init() { global.CONF.Conn.Port, _ = settingRepo.GetValueByKey("ServerPort") global.CONF.Conn.Ipv6, _ = settingRepo.GetValueByKey("Ipv6") global.CONF.Base.Edition, _ = settingRepo.GetValueByKey("Edition") - global.Api.ApiInterfaceStatus, _ = settingRepo.GetValueByKey("ApiInterfaceStatus") - if global.Api.ApiInterfaceStatus == constant.StatusEnable { - global.Api.ApiKey, _ = settingRepo.GetValueByKey("ApiKey") - global.Api.IpWhiteList, _ = settingRepo.GetValueByKey("IpWhiteList") - global.Api.ApiKeyValidityTime, _ = settingRepo.GetValueByKey("ApiKeyValidityTime") - } global.CONF.Conn.BindAddress, _ = settingRepo.GetValueByKey("BindAddress") global.CONF.Conn.SSL, _ = settingRepo.GetValueByKey("SSL") global.CONF.Base.Version, _ = settingRepo.GetValueByKey("SystemVersion") @@ -65,15 +60,24 @@ func handleUserInfo(tags string, settingRepo repo.ISettingRepo) { if strings.Contains(global.CONF.Base.ChangeUserInfo, "entrance") { settingMap["SecurityEntrance"] = common.RandStrAndNum(10) } - for key, val := range settingMap { - if len(val) == 0 { - continue + if global.CONF.Base.IsXpackEE { + if len(settingMap["UserName"]) != 0 || len(settingMap["Password"]) != 0 { + if err := xpack.AuthProvider.ResetSuperAdminUser(settingMap["UserName"], settingMap["Password"]); err != nil { + global.LOG.Fatalf("reset xpackee super admin failed, err: %v", err) + return + } } - if key == "Password" { - val, _ = encrypt.StringEncrypt(val) - } - if err := settingRepo.Update(key, val); err != nil { - global.LOG.Errorf("update %s before start failed, err: %v", key, err) + } else { + for key, val := range settingMap { + if len(val) == 0 { + continue + } + if key == "Password" { + val, _ = encrypt.StringEncrypt(val) + } + if err := settingRepo.Update(key, val); err != nil { + global.LOG.Errorf("update %s before start failed, err: %v", key, err) + } } } diff --git a/core/init/migration/helper/menu.go b/core/init/migration/helper/menu.go index 930ad1588..54b11cd56 100644 --- a/core/init/migration/helper/menu.go +++ b/core/init/migration/helper/menu.go @@ -7,9 +7,37 @@ import ( "github.com/1Panel-dev/1Panel/core/app/dto" "github.com/1Panel-dev/1Panel/core/app/model" + "github.com/1Panel-dev/1Panel/core/global" "gorm.io/gorm" ) +func UpsertMenuByLabel(children []dto.ShowMenu, newMenu dto.ShowMenu, afterLabel string) []dto.ShowMenu { + for i := range children { + if children[i].Label != newMenu.Label { + continue + } + children[i].Disabled = newMenu.Disabled + children[i].Title = newMenu.Title + children[i].Path = newMenu.Path + children[i].Sort = newMenu.Sort + children[i].IsShow = newMenu.IsShow + return children + } + + insertIndex := len(children) + for i := range children { + if children[i].Label == afterLabel { + insertIndex = i + 1 + break + } + } + + children = append(children, dto.ShowMenu{}) + copy(children[insertIndex+1:], children[insertIndex:]) + children[insertIndex] = newMenu + return children +} + func LoadMenus() string { item := []dto.ShowMenu{ {ID: "1", Disabled: true, Title: "menu.home", IsShow: true, Label: "Home-Menu", Path: "/", Sort: 100}, @@ -56,6 +84,23 @@ func LoadMenus() string { {ID: "12", Disabled: false, Title: "menu.logs", IsShow: true, Label: "Log-Menu", Path: "/logs", Sort: 1200}, {ID: "13", Disabled: true, Title: "menu.settings", IsShow: true, Label: "Setting-Menu", Path: "/settings", Sort: 1300}, } + if global.CONF.Base.IsXpackEE { + for i := range item { + if item[i].Label != "Xpack-Menu" { + continue + } + item[i].Children = UpsertMenuByLabel(item[i].Children, dto.ShowMenu{ + ID: "121", + Disabled: false, + Title: "xpack.user.userManage", + IsShow: true, + Label: "UserManagement", + Path: "/xpack-ee/users", + Sort: 350, + }, "NodeDashboard") + break + } + } menu, _ := json.Marshal(item) return string(menu) } @@ -89,6 +134,8 @@ func MenuSort() []dto.MenuLabelSort { {Label: "XApp", Sort: 100}, {Label: "Dashboard", Sort: 200}, {Label: "Node", Sort: 300}, + {Label: "NodeDashboard", Sort: 300}, + {Label: "UserManagement", Sort: 350}, {Label: "Upage", Sort: 400}, {Label: "MonitorDashboard", Sort: 500}, {Label: "Tamper", Sort: 600}, diff --git a/core/init/migration/migrate.go b/core/init/migration/migrate.go index 10f444628..86cb75c0a 100644 --- a/core/init/migration/migrate.go +++ b/core/init/migration/migrate.go @@ -38,6 +38,7 @@ func Init() { migrations.UpdateAiModelMenuStructure, migrations.AddDocSourceSetting, migrations.AddAppStoreInstallAllowPortSetting, + migrations.AddUserManagementMenu, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/core/init/migration/migrations/init.go b/core/init/migration/migrations/init.go index 6cf65ad8f..47511fa81 100644 --- a/core/init/migration/migrations/init.go +++ b/core/init/migration/migrations/init.go @@ -41,17 +41,19 @@ var InitSetting = &gormigrate.Migration{ ID: "20200908-add-table-setting", Migrate: func(tx *gorm.DB) error { encryptKey := common.RandStr(16) - if err := tx.Create(&model.Setting{Key: "UserName", Value: global.CONF.Base.Username}).Error; err != nil { - return err - } global.CONF.Base.EncryptKey = encryptKey - pass, _ := encrypt.StringEncrypt(global.CONF.Base.Password) language := "en" if global.CONF.Base.Language == "zh" { language = "zh" } - if err := tx.Create(&model.Setting{Key: "Password", Value: pass}).Error; err != nil { - return err + if !global.CONF.Base.IsXpackEE { + if err := tx.Create(&model.Setting{Key: "UserName", Value: global.CONF.Base.Username}).Error; err != nil { + return err + } + pass, _ := encrypt.StringEncrypt(global.CONF.Base.Password) + if err := tx.Create(&model.Setting{Key: "Password", Value: pass}).Error; err != nil { + return err + } } _, _ = cmd.RunDefaultWithStdoutBashCf("%s sed -i -e 's#ORIGINAL_PASSWORD=.*#ORIGINAL_PASSWORD=**********#g' /usr/local/bin/1pctl", cmd.SudoHandleCmd()) if err := tx.Create(&model.Setting{Key: "Theme", Value: "light"}).Error; err != nil { @@ -990,3 +992,52 @@ func normalizeAiMenuChild(children []dto.ShowMenu, fallback dto.ShowMenu, labels } return fallback } + +var AddUserManagementMenu = &gormigrate.Migration{ + ID: "20260414-add-user-management-menu", + Migrate: func(tx *gorm.DB) error { + if !global.CONF.Base.IsXpackEE { + return nil + } + var menuJSON string + if err := tx.Model(&model.Setting{}).Where("key = ?", "HideMenu").Pluck("value", &menuJSON).Error; err != nil { + return err + } + if menuJSON == "" { + menuJSON = helper.LoadMenus() + } + + var menus []dto.ShowMenu + if err := json.Unmarshal([]byte(menuJSON), &menus); err != nil { + return tx.Model(&model.Setting{}). + Where("key = ?", "HideMenu"). + Update("value", helper.LoadMenus()).Error + } + + newItem := dto.ShowMenu{ + ID: "121", + Disabled: false, + Title: "xpack.user.userManage", + IsShow: true, + Label: "UserManagement", + Path: "/xpack-ee/users", + Sort: 350, + } + + for i := range menus { + if menus[i].Label != "Xpack-Menu" { + continue + } + menus[i].Children = helper.UpsertMenuByLabel(menus[i].Children, newItem, "NodeDashboard") + break + } + + updatedJSON, err := json.Marshal(menus) + if err != nil { + return tx.Model(&model.Setting{}). + Where("key = ?", "HideMenu"). + Update("value", helper.LoadMenus()).Error + } + return tx.Model(&model.Setting{}).Where("key = ?", "HideMenu").Update("value", string(updatedJSON)).Error + }, +} diff --git a/core/init/router/proxy.go b/core/init/router/proxy.go index 7ed1214e3..030e3de7b 100644 --- a/core/init/router/proxy.go +++ b/core/init/router/proxy.go @@ -3,15 +3,14 @@ package router import ( "net/http" "net/url" - "strconv" "strings" "github.com/1Panel-dev/1Panel/core/app/api/v2/helper" - "github.com/1Panel-dev/1Panel/core/app/repo" "github.com/1Panel-dev/1Panel/core/cmd/server/res" "github.com/1Panel-dev/1Panel/core/constant" "github.com/1Panel-dev/1Panel/core/global" "github.com/1Panel-dev/1Panel/core/init/proxy" + psessionUtils "github.com/1Panel-dev/1Panel/core/init/session/psession" "github.com/1Panel-dev/1Panel/core/utils/xpack" "github.com/gin-gonic/gin" ) @@ -58,7 +57,7 @@ func Proxy() gin.HandlerFunc { proxyLocalAgent(c) return } - xpack.Proxy(c, currentNode) + xpack.MultiNodeProvider.Proxy(c, currentNode) c.Abort() } } @@ -78,18 +77,13 @@ func checkSession(c *gin.Context) bool { if err != nil { return false } - settingRepo := repo.NewISettingRepo() - sessionTimeout, err := settingRepo.GetValueByKey("SessionTimeout") + c.Set(psessionUtils.GinContextSessionUserKey, psession) + lifeTime, err := xpack.AuthProvider.LoadSessionTimeout(c, psession) if err != nil { + global.LOG.Errorf("get session timeout failed, err: %v", err) return false } - lifeTime, _ := strconv.Atoi(sessionTimeout) - lifeTime = xpack.LoadSessionTimeout(psession, lifeTime) - ssl, err := settingRepo.GetValueByKey("SSL") - if err != nil { - return false - } - if _, err := global.SESSION.RefreshIfNeeded(c, psession, ssl == constant.StatusEnable, lifeTime); err != nil { + if _, err := global.SESSION.RefreshIfNeeded(c, psession, global.CONF.Conn.SSL == constant.StatusEnable, lifeTime); err != nil { global.LOG.Warnf("proxy refresh session failed, path=%s, err=%v", c.Request.URL.Path, err) return false } diff --git a/core/init/router/router.go b/core/init/router/router.go index a5b9e9bda..6771b6bf0 100644 --- a/core/init/router/router.go +++ b/core/init/router/router.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" + appauth "github.com/1Panel-dev/1Panel/core/app/auth" "github.com/1Panel-dev/1Panel/core/app/service" "github.com/1Panel-dev/1Panel/core/cmd/server/docs" "github.com/1Panel-dev/1Panel/core/cmd/server/web" @@ -57,7 +58,7 @@ func setWebStatic(rootRouter *gin.RouterGroup) { } entrance = authService.GetSecurityEntrance() if entrance != "" { - service.SetSecurityEntranceCookie(c, entrance) + appauth.SetSecurityEntranceCookie(c, entrance) } staticServer := http.FileServer(http.FS(web.IndexHtml)) staticServer.ServeHTTP(c.Writer, c.Request) @@ -85,10 +86,10 @@ func Routers() *gin.Engine { Router.Use(middleware.OperationLog()) Router.Use(middleware.GlobalLoading()) + Router.Use(xpack.AuthProvider.CoreAPIAuthMiddleware()) Router.Use(middleware.PasswordExpired()) - Router.Use(middleware.ApiAuth()) Router.Use(middleware.CSRFTokenGuard()) - Router.Use(xpack.CoreRBACMiddlewares()...) + Router.Use(xpack.AuthProvider.CoreRBACMiddlewares()...) Router.Use(Proxy()) PrivateGroup := Router.Group("/api/v2/core") diff --git a/core/init/session/psession/psession.go b/core/init/session/psession/psession.go index 6cee79238..9e94ddca6 100644 --- a/core/init/session/psession/psession.go +++ b/core/init/session/psession/psession.go @@ -20,6 +20,7 @@ type SessionUser struct { } const SuperAdminSessionUserID = "__super_admin__" +const GinContextSessionUserKey = "session_user" type sessionItem struct { CreatedAt time.Time @@ -35,7 +36,7 @@ type PSession struct { lastFullCleanup time.Time } -const maxSessionEntries = 64 +const maxSessionEntries = 1024 func NewPSession(_ string) *PSession { return &PSession{ diff --git a/core/middleware/password_expired.go b/core/middleware/password_expired.go index 5808be107..dd146aa09 100644 --- a/core/middleware/password_expired.go +++ b/core/middleware/password_expired.go @@ -2,53 +2,60 @@ package middleware import ( "net/http" - "strconv" "strings" - "sync" "time" "github.com/1Panel-dev/1Panel/core/app/api/v2/helper" - "github.com/1Panel-dev/1Panel/core/app/repo" - "github.com/1Panel-dev/1Panel/core/constant" - "github.com/1Panel-dev/1Panel/core/utils/common" + "github.com/1Panel-dev/1Panel/core/buserr" + "github.com/1Panel-dev/1Panel/core/global" + psessionUtils "github.com/1Panel-dev/1Panel/core/init/session/psession" + "github.com/1Panel-dev/1Panel/core/utils/xpack" "github.com/gin-gonic/gin" ) -var ( - expiredLoc *time.Location - expiredLocOnce sync.Once -) - func PasswordExpired() gin.HandlerFunc { return func(c *gin.Context) { if strings.HasPrefix(c.Request.URL.Path, "/api/v2/core/auth") || c.Request.URL.Path == "/api/v2/core/settings/expired/handle" || - c.Request.URL.Path == "/api/v2/core/settings/search" { + c.Request.URL.Path == "/api/v2/core/settings/search" || + c.Request.URL.Path == "/api/v2/core/settings/search/base" { c.Next() return } - settingRepo := repo.NewISettingRepo() - expirationDays, err := settingRepo.GetValueByKey("ExpirationDays") + if c.GetBool("API_AUTH") { + c.Next() + return + } + var err error + sessionUser, ok := c.Get(psessionUtils.GinContextSessionUserKey) + psession, typeOK := sessionUser.(psessionUtils.SessionUser) + if !ok || !typeOK { + psession, err = global.SESSION.Get(c) + } + if err != nil { + errItem := err.Error() + if errItem == "ErrSessionDataFormat" || errItem == "ErrSessionDataNotFound" { + helper.BadAuth(c, "ErrNotLogin", buserr.New(errItem)) + return + } + helper.BadAuth(c, "ErrNotLogin", err) + return + } + c.Set(psessionUtils.GinContextSessionUserKey, psession) + if len(psession.Name) == 0 { + helper.BadAuth(c, "ErrNotLogin", err) + return + } + needCheck, expiredTime, err := xpack.AuthProvider.LoadExpired(c, psession) if err != nil { helper.ErrorWithDetail(c, http.StatusInternalServerError, "ErrPasswordExpired", err) return } - expiredDays, _ := strconv.Atoi(expirationDays) - if expiredDays == 0 { + if !needCheck { c.Next() return } - expirationTime, err := settingRepo.GetValueByKey("ExpirationTime") - if err != nil { - helper.ErrorWithDetail(c, http.StatusInternalServerError, "ErrPasswordExpired", err) - return - } - expiredTime, err := time.ParseInLocation(constant.DateTimeLayout, expirationTime, loadExpiredLocation()) - if err != nil { - helper.ErrorWithDetail(c, 313, "ErrPasswordExpired", err) - return - } if time.Now().After(expiredTime) { helper.ErrorWithDetail(c, 313, "ErrPasswordExpired", err) return @@ -56,18 +63,3 @@ func PasswordExpired() gin.HandlerFunc { c.Next() } } - -func loadExpiredLocation() *time.Location { - expiredLocOnce.Do(func() { - loc, err := time.LoadLocation(common.LoadTimeZoneByCmd()) - if err != nil { - expiredLoc = time.Local - return - } - expiredLoc = loc - }) - if expiredLoc == nil { - return time.Local - } - return expiredLoc -} diff --git a/core/middleware/session.go b/core/middleware/session.go index 5f5f8837a..574c4dbfa 100644 --- a/core/middleware/session.go +++ b/core/middleware/session.go @@ -1,14 +1,11 @@ package middleware import ( - "strconv" - "strings" - "github.com/1Panel-dev/1Panel/core/app/api/v2/helper" - "github.com/1Panel-dev/1Panel/core/app/repo" "github.com/1Panel-dev/1Panel/core/buserr" "github.com/1Panel-dev/1Panel/core/constant" "github.com/1Panel-dev/1Panel/core/global" + psessionUtils "github.com/1Panel-dev/1Panel/core/init/session/psession" "github.com/1Panel-dev/1Panel/core/utils/xpack" "github.com/gin-gonic/gin" ) @@ -16,7 +13,7 @@ import ( func SessionAuth() gin.HandlerFunc { return func(c *gin.Context) { apiReq := c.GetBool("API_AUTH") - if strings.HasPrefix(c.Request.URL.Path, "/api/v2/core/auth") || apiReq { + if isAnonymousAuthPath(c.Request.URL.Path) || apiReq { c.Next() return } @@ -31,24 +28,20 @@ func SessionAuth() gin.HandlerFunc { helper.BadAuth(c, "ErrNotLogin", err) return } - if len(psession.Name) == 0 { + if len(psession.Name) == 0 || len(psession.ID) == 0 { helper.BadAuth(c, "ErrNotLogin", err) return } - settingRepo := repo.NewISettingRepo() - sessionTimeout, err := settingRepo.GetValueByKey("SessionTimeout") + c.Set(psessionUtils.GinContextSessionUserKey, psession) + lifeTime, err := xpack.AuthProvider.LoadSessionTimeout(c, psession) if err != nil { - global.LOG.Errorf("create operation record failed, err: %v", err) + global.LOG.Errorf("get session timeout failed, err: %v", err) + helper.InternalServer(c, err) + c.Abort() return } - lifeTime, _ := strconv.Atoi(sessionTimeout) - lifeTime = xpack.LoadSessionTimeout(psession, lifeTime) - ssl, err := settingRepo.GetValueByKey("SSL") - if err != nil { - global.LOG.Errorf("create operation record failed, err: %v", err) - return - } - if _, err := global.SESSION.RefreshIfNeeded(c, psession, ssl == constant.StatusEnable, lifeTime); err != nil { + + if _, err := global.SESSION.RefreshIfNeeded(c, psession, global.CONF.Conn.SSL == constant.StatusEnable, lifeTime); err != nil { errItem := err.Error() if errItem == "ErrSessionDataFormat" || errItem == "ErrSessionDataNotFound" { helper.BadAuth(c, "ErrNotLogin", buserr.New(errItem)) @@ -61,3 +54,19 @@ func SessionAuth() gin.HandlerFunc { c.Next() } } + +func isAnonymousAuthPath(path string) bool { + switch path { + case "/api/v2/core/auth/captcha", + "/api/v2/core/auth/passkey/begin", + "/api/v2/core/auth/passkey/finish", + "/api/v2/core/auth/mfalogin", + "/api/v2/core/auth/login", + "/api/v2/core/auth/logout", + "/api/v2/core/auth/setting", + "/api/v2/core/auth/welcome": + return true + default: + return false + } +} diff --git a/core/router/entry_xpack.go b/core/router/entry_xpack.go deleted file mode 100644 index 8b4e49688..000000000 --- a/core/router/entry_xpack.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build xpack - -package router - -import ( - xpackRouter "github.com/1Panel-dev/1Panel/core/xpack/router" -) - -func RouterGroups() []CommonRouter { - baseRouter := commonGroups() - for _, ro := range xpackRouter.XpackGroups() { - if val, ok := ro.(CommonRouter); ok { - baseRouter = append(baseRouter, val) - } - } - return baseRouter -} - -var RouterGroupApp = RouterGroups() diff --git a/core/router/entry_xpackee.go b/core/router/entry_xpackee.go deleted file mode 100644 index 7d4d50190..000000000 --- a/core/router/entry_xpackee.go +++ /dev/null @@ -1,25 +0,0 @@ -//go:build xpackee - -package router - -import ( - xpackEERouter "github.com/1Panel-dev/1Panel/core/xpack-ee/router" - xpackRouter "github.com/1Panel-dev/1Panel/core/xpack/router" -) - -func RouterGroups() []CommonRouter { - baseRouter := commonGroups() - for _, ro := range xpackRouter.XpackGroups() { - if val, ok := ro.(CommonRouter); ok { - baseRouter = append(baseRouter, val) - } - } - for _, ro := range xpackEERouter.XpackEEGroups() { - if val, ok := ro.(CommonRouter); ok { - baseRouter = append(baseRouter, val) - } - } - return baseRouter -} - -var RouterGroupApp = RouterGroups() diff --git a/core/router/ro_base.go b/core/router/ro_base.go index 662c8cb17..4375983a4 100644 --- a/core/router/ro_base.go +++ b/core/router/ro_base.go @@ -2,6 +2,7 @@ package router import ( v2 "github.com/1Panel-dev/1Panel/core/app/api/v2" + "github.com/1Panel-dev/1Panel/core/middleware" "github.com/gin-gonic/gin" ) @@ -9,6 +10,9 @@ type BaseRouter struct{} func (s *BaseRouter) InitRouter(Router *gin.RouterGroup) { baseRouter := Router.Group("auth") + authRouter := Router.Group("auth"). + Use(middleware.SessionAuth()). + Use(middleware.PasswordExpired()) baseApi := v2.ApiGroupApp.BaseApi { baseRouter.GET("/captcha", baseApi.Captcha) @@ -19,5 +23,17 @@ func (s *BaseRouter) InitRouter(Router *gin.RouterGroup) { baseRouter.POST("/logout", baseApi.LogOut) baseRouter.GET("/setting", baseApi.GetLoginSetting) baseRouter.GET("/welcome", baseApi.GetWelcomePage) + + authRouter.POST("/mfa", baseApi.LoadMFA) + authRouter.POST("/mfa/bind", baseApi.MFABind) + authRouter.POST("/passkey/register/begin", baseApi.PasskeyRegisterBegin) + authRouter.POST("/passkey/register/finish", baseApi.PasskeyRegisterFinish) + authRouter.GET("/passkey/list", baseApi.PasskeyList) + authRouter.POST("/passkey/del", baseApi.PasskeyDelete) + authRouter.POST("/api/generate", baseApi.GenerateApiKey) + authRouter.POST("/api/update", baseApi.UpdateApiConfig) + + authRouter.GET("/current", baseApi.GetCurrentUser) + authRouter.POST("/current/update", baseApi.UpdateCurrentUser) } } diff --git a/core/router/ro_setting.go b/core/router/ro_setting.go index d26e827e7..c8886ea08 100644 --- a/core/router/ro_setting.go +++ b/core/router/ro_setting.go @@ -19,7 +19,8 @@ func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) { baseApi := v2.ApiGroupApp.BaseApi { router.POST("/search", baseApi.GetSettingInfo) - router.POST("/expired/handle", baseApi.HandlePasswordExpired) + router.POST("/search/base", baseApi.GetSettingBaseInfo) + settingRouter.POST("/by", baseApi.GetSettingByKey) settingRouter.POST("/terminal/search", baseApi.GetTerminalSettingInfo) settingRouter.GET("/search/available", baseApi.GetSystemAvailable) @@ -34,20 +35,10 @@ func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) { settingRouter.POST("/ssl/update", baseApi.UpdateSSL) settingRouter.GET("/ssl/info", baseApi.LoadFromCert) settingRouter.POST("/ssl/download", baseApi.DownloadSSL) - settingRouter.POST("/password/update", baseApi.UpdatePassword) - settingRouter.POST("/mfa", baseApi.LoadMFA) - settingRouter.POST("/mfa/bind", baseApi.MFABind) - settingRouter.POST("/passkey/register/begin", baseApi.PasskeyRegisterBegin) - settingRouter.POST("/passkey/register/finish", baseApi.PasskeyRegisterFinish) - settingRouter.GET("/passkey/list", baseApi.PasskeyList) - settingRouter.DELETE("/passkey/:id", baseApi.PasskeyDelete) - settingRouter.POST("/upgrade", baseApi.Upgrade) settingRouter.POST("/upgrade/notes", baseApi.GetNotesByVersion) settingRouter.GET("/upgrade/releases", baseApi.LoadRelease) settingRouter.GET("/upgrade", baseApi.GetUpgradeInfo) - settingRouter.POST("/api/config/generate/key", baseApi.GenerateApiKey) - settingRouter.POST("/api/config/update", baseApi.UpdateApiConfig) noAuthRouter.POST("/ssl/reload", baseApi.ReloadSSL) diff --git a/core/server/init_xpack.go b/core/server/init_xpack.go deleted file mode 100644 index 404bc6b5b..000000000 --- a/core/server/init_xpack.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build xpack - -package server - -import ( - xpack "github.com/1Panel-dev/1Panel/core/xpack" -) - -func InitOthers() { - xpack.Init() -} diff --git a/core/server/init_xpackee.go b/core/server/init_xpackee.go deleted file mode 100644 index b8932cccf..000000000 --- a/core/server/init_xpackee.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build xpackee - -package server - -import ( - xpack "github.com/1Panel-dev/1Panel/core/xpack" - xpackEE "github.com/1Panel-dev/1Panel/core/xpack-ee" -) - -func InitOthers() { - xpack.Init() - xpackEE.Init() -} diff --git a/core/server/server.go b/core/server/server.go index abb115d8a..c151203a6 100644 --- a/core/server/server.go +++ b/core/server/server.go @@ -83,7 +83,8 @@ func Start() { type tcpKeepAliveListener struct { *net.TCPListener } - if global.CONF.Conn.SSL == constant.StatusEnable { + switch global.CONF.Conn.SSL { + case constant.StatusEnable: constant.CertStore.Store(loadCert()) server.TLSConfig = &tls.Config{ @@ -96,8 +97,7 @@ func Start() { if err := server.ServeTLS(tcpKeepAliveListener{ln.(*net.TCPListener)}, "", ""); err != nil { panic(err) } - return - } else if global.CONF.Conn.SSL == constant.StatusMux { + case constant.StatusMux: constant.CertStore.Store(loadCert()) server.TLSConfig = &tls.Config{ @@ -144,13 +144,11 @@ func Start() { if err := m.Serve(); err != nil { panic(err) } - return - } else { + default: global.LOG.Infof("listen at http://%s:%s [%s]", global.CONF.Conn.BindAddress, global.CONF.Conn.Port, tcpItem) if err := server.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}); err != nil { panic(err) } - return } } @@ -216,5 +214,4 @@ func handleMuxHttpConn(conn net.Conn) { resp.Header.Set("Connection", "close") _ = resp.Write(conn) - return } diff --git a/core/utils/common/common.go b/core/utils/common/common.go index 85aea82e5..3376c07ff 100644 --- a/core/utils/common/common.go +++ b/core/utils/common/common.go @@ -9,6 +9,7 @@ import ( "net" "os" "path" + "sort" "strconv" "strings" "time" @@ -54,25 +55,6 @@ func Md5(val string) string { return hex.EncodeToString(hash.Sum(nil)) } -func LoadTimeZoneByCmd() string { - loc := time.Now().Location().String() - if _, err := time.LoadLocation(loc); err != nil { - loc = "Asia/Shanghai" - } - std, err := cmd.RunDefaultWithStdoutBashC("timedatectl | grep 'Time zone'") - if err != nil { - return loc - } - fields := strings.Fields(string(std)) - if len(fields) != 5 { - return loc - } - if _, err := time.LoadLocation(fields[2]); err != nil { - return loc - } - return fields[2] -} - func ScanPort(port int) bool { ln, err := net.Listen("tcp", ":"+strconv.Itoa(port)) if err != nil { @@ -145,6 +127,25 @@ func SplitStr(str string, spi ...string) []string { return results } +func UniqueUints(items []uint) []uint { + result := make([]uint, 0, len(items)) + seen := make(map[uint]struct{}, len(items)) + for _, item := range items { + if item == 0 { + continue + } + if _, ok := seen[item]; ok { + continue + } + seen[item] = struct{}{} + result = append(result, item) + } + sort.Slice(result, func(i, j int) bool { + return result[i] < result[j] + }) + return result +} + func LoadArch() (string, error) { std, err := cmd.RunDefaultWithStdoutBashC("uname -a") if err != nil { diff --git a/core/utils/common/time.go b/core/utils/common/time.go new file mode 100644 index 000000000..bc6fa6407 --- /dev/null +++ b/core/utils/common/time.go @@ -0,0 +1,47 @@ +package common + +import ( + "strings" + "sync" + "time" + + "github.com/1Panel-dev/1Panel/core/utils/cmd" +) + +func LoadTimeZoneByCmd() string { + loc := time.Now().Location().String() + if _, err := time.LoadLocation(loc); err != nil { + loc = "Asia/Shanghai" + } + std, err := cmd.RunDefaultWithStdoutBashC("timedatectl | grep 'Time zone'") + if err != nil { + return loc + } + fields := strings.Fields(string(std)) + if len(fields) != 5 { + return loc + } + if _, err := time.LoadLocation(fields[2]); err != nil { + return loc + } + return fields[2] +} + +func LoadExpiredLocation() *time.Location { + var ( + expiredLoc *time.Location + expiredLocOnce sync.Once + ) + expiredLocOnce.Do(func() { + loc, err := time.LoadLocation(LoadTimeZoneByCmd()) + if err != nil { + expiredLoc = time.Local + return + } + expiredLoc = loc + }) + if expiredLoc == nil { + return time.Local + } + return expiredLoc +} diff --git a/core/utils/passkey/passkey_store.go b/core/utils/passkey/passkey_store.go index caa1ad195..f783ba64e 100644 --- a/core/utils/passkey/passkey_store.go +++ b/core/utils/passkey/passkey_store.go @@ -3,22 +3,22 @@ package passkey import ( "crypto/rand" "encoding/base64" - "sync" "time" "github.com/1Panel-dev/1Panel/core/utils/common" + "github.com/1Panel-dev/1Panel/core/utils/ttlstore" "github.com/go-webauthn/webauthn/webauthn" ) const ( - PasskeyUserIDSettingKey = "PasskeyUserID" - PasskeyCredentialSettingKey = "PasskeyCredentials" - PasskeyMaxCredentials = 5 - PasskeySessionTTL = 5 * time.Minute - PasskeySessionKindLogin = "login" - PasskeySessionKindRegister = "register" - PasskeyCredentialNameDefault = "Passkey" - PasskeySessionStoreMaxEntries = 1024 + PasskeyUserIDSettingKey = "PasskeyUserID" + PasskeyCredentialSettingKey = "PasskeyCredentials" + PasskeyMaxCredentials = 5 + PasskeySessionTTL = 5 * time.Minute + PasskeySessionKindLogin = "login" + PasskeySessionKindRegister = "register" + PasskeyCredentialNameDefault = "Passkey" + PasskeySessionStoreMaxEntries = 1024 ) var passkeySessions = newPasskeySessionStore() @@ -35,75 +35,29 @@ type passkeySession struct { } type passkeySessionStore struct { - mu sync.Mutex - items map[string]passkeySession + store *ttlstore.Store[passkeySession] } func newPasskeySessionStore() *passkeySessionStore { - return &passkeySessionStore{items: make(map[string]passkeySession)} + return &passkeySessionStore{ + store: ttlstore.New[passkeySession](PasskeySessionTTL, PasskeySessionStoreMaxEntries, generatePasskeySessionID), + } } func (s *passkeySessionStore) Set(kind, name string, session webauthn.SessionData) string { - s.mu.Lock() - defer s.mu.Unlock() - - s.cleanupExpiredLocked() - if len(s.items) >= PasskeySessionStoreMaxEntries { - s.removeOldestLocked() - } - - sessionID := generatePasskeySessionID() - s.items[sessionID] = passkeySession{ - Kind: kind, - Name: name, - Session: session, - ExpiresAt: time.Now().Add(PasskeySessionTTL), - } - return sessionID + return s.store.Set(passkeySession{ + Kind: kind, + Name: name, + Session: session, + }) } func (s *passkeySessionStore) Get(sessionID string) (passkeySession, bool) { - s.mu.Lock() - defer s.mu.Unlock() - - item, ok := s.items[sessionID] - if !ok { - return passkeySession{}, false - } - if time.Now().After(item.ExpiresAt) { - delete(s.items, sessionID) - return passkeySession{}, false - } - return item, true + return s.store.Get(sessionID) } func (s *passkeySessionStore) Delete(sessionID string) { - s.mu.Lock() - defer s.mu.Unlock() - delete(s.items, sessionID) -} - -func (s *passkeySessionStore) cleanupExpiredLocked() { - now := time.Now() - for id, item := range s.items { - if now.After(item.ExpiresAt) { - delete(s.items, id) - } - } -} - -func (s *passkeySessionStore) removeOldestLocked() { - var oldestID string - var oldestTime time.Time - for id, item := range s.items { - if oldestID == "" || item.ExpiresAt.Before(oldestTime) { - oldestID = id - oldestTime = item.ExpiresAt - } - } - if oldestID != "" { - delete(s.items, oldestID) - } + s.store.Delete(sessionID) } func generatePasskeySessionID() string { diff --git a/core/utils/req_helper/requset.go b/core/utils/req_helper/requset.go index 776093dc1..21ccdb322 100644 --- a/core/utils/req_helper/requset.go +++ b/core/utils/req_helper/requset.go @@ -30,7 +30,7 @@ func HandleRequest(url, method string, timeout int) (int, []byte, error) { } func HandleRequestWithProxy(url, method string, timeout int) (int, []byte, error) { - transport := xpack.LoadRequestTransport() + transport := xpack.MultiNodeProvider.LoadRequestTransport() return handleRequestWithTransport(url, method, transport, timeout) } @@ -78,7 +78,7 @@ func HandleGet(url string) (*http.Response, error) { } func HandleGetWithProxy(url string) (*http.Response, error) { - transport := xpack.LoadRequestTransport() + transport := xpack.MultiNodeProvider.LoadRequestTransport() return handleGetWithTransport(url, transport) } diff --git a/core/utils/ttlstore/ttlstore.go b/core/utils/ttlstore/ttlstore.go new file mode 100644 index 000000000..3260ef1db --- /dev/null +++ b/core/utils/ttlstore/ttlstore.go @@ -0,0 +1,116 @@ +package ttlstore + +import ( + "sync" + "time" +) + +type Item[T any] struct { + Value T + ExpiresAt time.Time +} + +type Store[T any] struct { + mu sync.Mutex + items map[string]Item[T] + ttl time.Duration + maxEntries int + newID func() string + now func() time.Time +} + +func New[T any](ttl time.Duration, maxEntries int, newID func() string) *Store[T] { + return &Store[T]{ + items: make(map[string]Item[T]), + ttl: ttl, + maxEntries: maxEntries, + newID: newID, + now: time.Now, + } +} + +func (s *Store[T]) Set(value T) string { + s.mu.Lock() + defer s.mu.Unlock() + + s.cleanupExpiredLocked() + if len(s.items) >= s.maxEntries { + s.removeOldestLocked() + } + + itemID := s.newID() + s.items[itemID] = Item[T]{ + Value: value, + ExpiresAt: s.now().Add(s.ttl), + } + return itemID +} + +func (s *Store[T]) Get(itemID string) (T, bool) { + s.mu.Lock() + defer s.mu.Unlock() + + item, ok := s.items[itemID] + if !ok { + var zero T + return zero, false + } + if s.now().After(item.ExpiresAt) { + delete(s.items, itemID) + var zero T + return zero, false + } + return item.Value, true +} + +func (s *Store[T]) Update(itemID string, fn func(*T) bool) bool { + s.mu.Lock() + defer s.mu.Unlock() + + item, ok := s.items[itemID] + if !ok { + return false + } + if s.now().After(item.ExpiresAt) { + delete(s.items, itemID) + return false + } + + keep := fn(&item.Value) + if !keep { + delete(s.items, itemID) + return true + } + + s.items[itemID] = item + return true +} + +func (s *Store[T]) Delete(itemID string) { + s.mu.Lock() + defer s.mu.Unlock() + delete(s.items, itemID) +} + +func (s *Store[T]) cleanupExpiredLocked() { + now := s.now() + for id, item := range s.items { + if now.After(item.ExpiresAt) { + delete(s.items, id) + } + } +} + +func (s *Store[T]) removeOldestLocked() { + var oldestID string + var oldestTime time.Time + for id, item := range s.items { + if oldestID == "" || item.ExpiresAt.Before(oldestTime) { + oldestID = id + oldestTime = item.ExpiresAt + } + } + if oldestID != "" { + delete(s.items, oldestID) + } +} diff --git a/core/utils/xpack/community.go b/core/utils/xpack/community.go index 509e2510e..7e955347c 100644 --- a/core/utils/xpack/community.go +++ b/core/utils/xpack/community.go @@ -2,88 +2,8 @@ package xpack -import ( - "crypto/tls" - "errors" - "net" - "net/http" - "time" +import "github.com/1Panel-dev/1Panel/core/utils/xpack/helper" - baseDto "github.com/1Panel-dev/1Panel/core/app/dto" - "github.com/1Panel-dev/1Panel/core/global" - "github.com/1Panel-dev/1Panel/core/init/proxy" - "github.com/1Panel-dev/1Panel/core/init/session/psession" - "github.com/1Panel-dev/1Panel/core/utils/ssh" - "github.com/1Panel-dev/1Panel/core/xpack/app/model" - "github.com/gin-gonic/gin" -) +var AuthProvider = helper.NewIAuthProvider() -func Proxy(c *gin.Context, currentNode string) { - if currentNode != "local" && currentNode != "" { - c.Next() - return - } - defer func() { - if err := recover(); err != nil && err != http.ErrAbortHandler { - global.LOG.Debug(err) - } - }() - proxy.LocalAgentProxy.ServeHTTP(c.Writer, c.Request) - c.Abort() -} - -func CoreRBACMiddlewares() []gin.HandlerFunc { return nil } - -func ProxyDocker(proxyURL string) error { return nil } - -func UpdateGroup(name string, group, newGroup uint) error { return nil } - -func CheckBackupUsed(name string) error { return nil } - -func LoadRequestTransport() *http.Transport { - return &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - DialContext: (&net.Dialer{ - Timeout: 60 * time.Second, - KeepAlive: 60 * time.Second, - }).DialContext, - TLSHandshakeTimeout: 5 * time.Second, - ResponseHeaderTimeout: 10 * time.Second, - IdleConnTimeout: 15 * time.Second, - } -} - -func LoadNodeInfo(currentNode string) (*ssh.ConnInfo, string, error) { - return nil, "", nil -} - -func Sync(dataType string) error { return nil } - -func AutoUpgradeWithMaster() {} - -func Login(_ *gin.Context, _ baseDto.Login, _ string) (*baseDto.UserLoginInfo, string, error) { - return nil, "", errors.New("not xpackee build") -} - -func LoadSessionTimeout(sessionUser psession.SessionUser, defaultTTL int) int { return defaultTTL } - -func RemoveBindNode(nodeID uint) error { return nil } - -func CheckLicenseStatus(isXpack bool, licenseID, nodeID uint) error { - return nil -} -func LoadLicenseByNodeID(nodeID uint) (bool, model.License, error) { - return false, model.License{}, nil -} -func Bind(node model.Node, license *model.License, withInit, withSync, withDryRun, withDockerRestart bool) error { - return nil -} -func BindFree(node model.Node, licenseID uint) error { - return nil -} -func Unbind(node model.Node, license model.License, withReset, withSync, withDockerRestart bool) error { - return nil -} -func UnbindFree(node model.Node, licenseID uint) error { - return nil -} +var MultiNodeProvider = helper.NewIMultiNodeProvider() diff --git a/core/utils/xpack/helper/auth_helper.go b/core/utils/xpack/helper/auth_helper.go new file mode 100644 index 000000000..e2624cf04 --- /dev/null +++ b/core/utils/xpack/helper/auth_helper.go @@ -0,0 +1,91 @@ +package helper + +import ( + "time" + + "github.com/1Panel-dev/1Panel/core/app/auth" + baseDto "github.com/1Panel-dev/1Panel/core/app/dto" + "github.com/1Panel-dev/1Panel/core/init/session/psession" + "github.com/1Panel-dev/1Panel/core/utils/mfa" + "github.com/1Panel-dev/1Panel/core/utils/xpack/providers" + "github.com/gin-gonic/gin" +) + +type authHelper struct{} + +func NewIAuthProvider() providers.AuthProvider { + return &authHelper{} +} + +func (a *authHelper) Login(c *gin.Context, info baseDto.Login, entrance string) (*baseDto.UserLoginInfo, string, error) { + return auth.Login(c, info, entrance) +} + +func (a *authHelper) MFALogin(c *gin.Context, info baseDto.MFALogin, entrance string) (*baseDto.UserLoginInfo, string, error) { + return auth.MFALogin(c, info, entrance) +} + +func (a *authHelper) PasskeyBeginLogin(c *gin.Context, entrance string) (*baseDto.PasskeyBeginResponse, string, error) { + return auth.PasskeyBeginLogin(c, entrance) +} +func (a *authHelper) PasskeyFinishLogin(c *gin.Context, sessionID, entrance string) (*baseDto.UserLoginInfo, string, error) { + return auth.PasskeyFinishLogin(c, sessionID, entrance) +} +func (a *authHelper) PasskeyBeginRegister(c *gin.Context, name string) (*baseDto.PasskeyBeginResponse, string, error) { + return auth.PasskeyBeginRegister(c, name) +} +func (a *authHelper) PasskeyFinishRegister(c *gin.Context, sessionID string) (string, error) { + return auth.PasskeyFinishRegister(c, sessionID) +} +func (a *authHelper) PasskeyList(c *gin.Context) ([]baseDto.PasskeyInfo, error) { + return auth.PasskeyList() +} +func (a *authHelper) PasskeyDelete(c *gin.Context, id string) error { + return auth.PasskeyDelete(id) +} +func (a *authHelper) PasskeyStatus(c *gin.Context) bool { + return auth.PasskeyStatus(c) +} +func (a *authHelper) ClearPasskeys() error { + return nil +} + +func (a *authHelper) ResetSuperAdminUser(name, password string) error { + return nil +} + +func (a *authHelper) LoadSessionTimeout(_ *gin.Context, sessionUser psession.SessionUser) (int, error) { + return auth.LoadSessionTimeout(sessionUser) +} +func (a *authHelper) LoadExpired(_ *gin.Context, sessionUser psession.SessionUser) (bool, time.Time, error) { + return auth.LoadExpired(sessionUser) +} + +func (a *authHelper) CoreAPIAuthMiddleware() gin.HandlerFunc { + return auth.APIAuthMiddleware(auth.LoadAPIAuthConfig, nil) +} + +func (a *authHelper) CoreRBACMiddlewares() []gin.HandlerFunc { return nil } + +func (a *authHelper) LoadMFA(_ *gin.Context, req baseDto.MfaRequest) (mfa.Otp, error) { + return auth.LoadMFA(req) +} +func (a *authHelper) MFABind(_ *gin.Context, req baseDto.MfaCredential) error { + return auth.MFABind(req) +} +func (a *authHelper) GenerateApiKey(_ *gin.Context) (string, error) { + return auth.GenerateApiKey() +} +func (a *authHelper) UpdateApiConfig(c *gin.Context, req baseDto.ApiInterfaceConfig) error { + return auth.UpdateApiConfig(req) +} + +func (a *authHelper) GetCurrentUserInfo(_ *gin.Context) (*baseDto.CurrentUserInfo, error) { + return auth.GetCurrentUserInfo() +} +func (a *authHelper) UpdateCurrentUserInfo(c *gin.Context, req baseDto.CurrentUserUpdate) error { + return auth.UpdateCurrentUserInfo(c, req) +} +func (a *authHelper) HandlePasswordExpired(c *gin.Context, old, new string) error { + return auth.HandlePasswordExpired(c, old, new) +} diff --git a/core/utils/xpack/helper/multi_node_helper.go b/core/utils/xpack/helper/multi_node_helper.go new file mode 100644 index 000000000..743d6caee --- /dev/null +++ b/core/utils/xpack/helper/multi_node_helper.go @@ -0,0 +1,60 @@ +package helper + +import ( + "crypto/tls" + "net" + "net/http" + "time" + + "github.com/1Panel-dev/1Panel/core/global" + "github.com/1Panel-dev/1Panel/core/init/proxy" + "github.com/1Panel-dev/1Panel/core/utils/ssh" + "github.com/gin-gonic/gin" +) + +type multiNodeHelper struct{} + +func NewIMultiNodeProvider() *multiNodeHelper { + return &multiNodeHelper{} +} + +func (m *multiNodeHelper) Proxy(c *gin.Context, currentNode string) { + if currentNode != "local" && currentNode != "" { + c.Next() + return + } + defer func() { + if err := recover(); err != nil && err != http.ErrAbortHandler { + global.LOG.Debug(err) + } + }() + proxy.LocalAgentProxy.ServeHTTP(c.Writer, c.Request) + c.Abort() +} + +func (m *multiNodeHelper) ProxyDocker(proxyURL string) error { return nil } + +func (m *multiNodeHelper) UpdateGroup(name string, group, newGroup uint) error { return nil } + +func (m *multiNodeHelper) CheckBackupUsed(name string) error { return nil } + +func (m *multiNodeHelper) LoadRequestTransport() *http.Transport { + return &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + DialContext: (&net.Dialer{ + Timeout: 60 * time.Second, + KeepAlive: 60 * time.Second, + }).DialContext, + TLSHandshakeTimeout: 5 * time.Second, + ResponseHeaderTimeout: 10 * time.Second, + IdleConnTimeout: 15 * time.Second, + } +} + +func (m *multiNodeHelper) LoadNodeInfo(currentNode string) (*ssh.ConnInfo, string, error) { + return nil, "", nil +} + +func (m *multiNodeHelper) Sync(dataType string) error { return nil } + +func (m *multiNodeHelper) AutoUpgradeWithMaster() {} diff --git a/core/utils/xpack/providers/auth.go b/core/utils/xpack/providers/auth.go new file mode 100644 index 000000000..bf7220267 --- /dev/null +++ b/core/utils/xpack/providers/auth.go @@ -0,0 +1,42 @@ +package providers + +import ( + "time" + + "github.com/1Panel-dev/1Panel/core/app/dto" + "github.com/1Panel-dev/1Panel/core/init/session/psession" + "github.com/1Panel-dev/1Panel/core/utils/mfa" + "github.com/gin-gonic/gin" +) + +type AuthProvider interface { + Login(c *gin.Context, info dto.Login, entrance string) (*dto.UserLoginInfo, string, error) + MFALogin(c *gin.Context, info dto.MFALogin, entrance string) (*dto.UserLoginInfo, string, error) + + ResetSuperAdminUser(name, password string) error + + LoadSessionTimeout(c *gin.Context, sessionUser psession.SessionUser) (int, error) + LoadExpired(c *gin.Context, sessionUser psession.SessionUser) (bool, time.Time, error) + + LoadMFA(c *gin.Context, req dto.MfaRequest) (mfa.Otp, error) + MFABind(c *gin.Context, req dto.MfaCredential) error + + GenerateApiKey(c *gin.Context) (string, error) + UpdateApiConfig(c *gin.Context, req dto.ApiInterfaceConfig) error + + PasskeyBeginLogin(c *gin.Context, entrance string) (*dto.PasskeyBeginResponse, string, error) + PasskeyFinishLogin(c *gin.Context, sessionID, entrance string) (*dto.UserLoginInfo, string, error) + PasskeyBeginRegister(c *gin.Context, name string) (*dto.PasskeyBeginResponse, string, error) + PasskeyFinishRegister(c *gin.Context, sessionID string) (string, error) + PasskeyList(c *gin.Context) ([]dto.PasskeyInfo, error) + PasskeyDelete(c *gin.Context, id string) error + PasskeyStatus(c *gin.Context) bool + ClearPasskeys() error + + GetCurrentUserInfo(c *gin.Context) (*dto.CurrentUserInfo, error) + UpdateCurrentUserInfo(c *gin.Context, req dto.CurrentUserUpdate) error + HandlePasswordExpired(c *gin.Context, old, new string) error + + CoreAPIAuthMiddleware() gin.HandlerFunc + CoreRBACMiddlewares() []gin.HandlerFunc +} diff --git a/core/utils/xpack/providers/multi_node.go b/core/utils/xpack/providers/multi_node.go new file mode 100644 index 000000000..77f6ff679 --- /dev/null +++ b/core/utils/xpack/providers/multi_node.go @@ -0,0 +1,20 @@ +package providers + +import ( + "net/http" + + "github.com/1Panel-dev/1Panel/core/utils/ssh" + "github.com/gin-gonic/gin" +) + +type MultiNodeProvider interface { + Proxy(c *gin.Context, currentNode string) + ProxyDocker(proxyURL string) error + UpdateGroup(name string, group, newGroup uint) error + CheckBackupUsed(name string) error + LoadNodeInfo(currentNode string) (*ssh.ConnInfo, string, error) + Sync(dataType string) error + AutoUpgradeWithMaster() + + LoadRequestTransport() *http.Transport +} diff --git a/core/utils/xpack/xpack.go b/core/utils/xpack/xpack.go deleted file mode 100644 index 6d3319311..000000000 --- a/core/utils/xpack/xpack.go +++ /dev/null @@ -1,68 +0,0 @@ -//go:build xpack - -package xpack - -import ( - "errors" - "net/http" - - baseDto "github.com/1Panel-dev/1Panel/core/app/dto" - "github.com/1Panel-dev/1Panel/core/init/session/psession" - "github.com/1Panel-dev/1Panel/core/utils/ssh" - "github.com/1Panel-dev/1Panel/core/xpack/app/model" - edition "github.com/1Panel-dev/1Panel/core/xpack/edition" - "github.com/gin-gonic/gin" -) - -func Proxy(c *gin.Context, currentNode string) { - edition.Proxy(c, currentNode) -} - -func CoreRBACMiddlewares() []gin.HandlerFunc { return nil } - -func ProxyDocker(proxyURL string) error { return edition.ProxyDocker(proxyURL) } - -func UpdateGroup(name string, group, newGroup uint) error { - return edition.UpdateGroup(name, group, newGroup) -} - -func CheckBackupUsed(name string) error { - return edition.CheckBackupUsed(name) -} - -func LoadRequestTransport() *http.Transport { return edition.LoadRequestTransport() } - -func LoadNodeInfo(currentNode string) (*ssh.ConnInfo, string, error) { - return edition.LoadNodeInfo(currentNode) -} - -func Sync(dataType string) error { return edition.Sync(dataType) } - -func AutoUpgradeWithMaster() { edition.AutoUpgradeWithMaster() } - -func Login(_ *gin.Context, _ baseDto.Login, _ string) (*baseDto.UserLoginInfo, string, error) { - return nil, "", errors.New("not xpackee build") -} - -func LoadSessionTimeout(sessionUser psession.SessionUser, defaultTTL int) int { return defaultTTL } - -func RemoveBindNode(nodeID uint) error { return nil } - -func CheckLicenseStatus(isXpack bool, licenseID, nodeID uint) error { - return edition.CheckLicenseStatus(isXpack, licenseID, nodeID) -} -func LoadLicenseByNodeID(nodeID uint) (bool, model.License, error) { - return edition.LoadLicenseByNodeID(nodeID) -} -func Bind(node model.Node, license *model.License, withInit, withSync, withDryRun, withDockerRestart bool) error { - return edition.Bind(node, license, withInit, withSync, withDryRun, withDockerRestart) -} -func BindFree(node model.Node, licenseID uint) error { - return edition.BindFree(node, licenseID) -} -func Unbind(node model.Node, license model.License, withReset, withSync, withDockerRestart bool) error { - return edition.Unbind(node, license, withReset, withSync, withDockerRestart) -} -func UnbindFree(node model.Node, licenseID uint) error { - return edition.UnbindFree(node, licenseID) -} diff --git a/core/utils/xpack/xpackee.go b/core/utils/xpack/xpackee.go deleted file mode 100644 index c108a1d35..000000000 --- a/core/utils/xpack/xpackee.go +++ /dev/null @@ -1,72 +0,0 @@ -//go:build xpackee - -package xpack - -import ( - "net/http" - - "github.com/1Panel-dev/1Panel/core/app/dto" - "github.com/1Panel-dev/1Panel/core/init/session/psession" - "github.com/1Panel-dev/1Panel/core/utils/ssh" - edition "github.com/1Panel-dev/1Panel/core/xpack-ee/edition" - xeemiddleware "github.com/1Panel-dev/1Panel/core/xpack-ee/router/middleware" - "github.com/1Panel-dev/1Panel/core/xpack/app/model" - "github.com/gin-gonic/gin" -) - -func Proxy(c *gin.Context, currentNode string) { - edition.Proxy(c, currentNode) -} - -func CoreRBACMiddlewares() []gin.HandlerFunc { - return []gin.HandlerFunc{xeemiddleware.RequireRBAC()} -} - -func ProxyDocker(proxyURL string) error { return edition.ProxyDocker(proxyURL) } - -func UpdateGroup(name string, group, newGroup uint) error { - return edition.UpdateGroup(name, group, newGroup) -} - -func CheckBackupUsed(name string) error { - return edition.CheckBackupUsed(name) -} - -func LoadRequestTransport() *http.Transport { return edition.LoadRequestTransport() } - -func LoadNodeInfo(currentNode string) (*ssh.ConnInfo, string, error) { - return edition.LoadNodeInfo(currentNode) -} - -func Sync(dataType string) error { return edition.Sync(dataType) } - -func AutoUpgradeWithMaster() { edition.AutoUpgradeWithMaster() } - -func Login(c *gin.Context, info dto.Login, entrance string) (*dto.UserLoginInfo, string, error) { - return edition.Login(c, info, entrance) -} - -func LoadSessionTimeout(sessionUser psession.SessionUser, defaultTTL int) int { - return edition.LoadSessionTimeout(sessionUser, defaultTTL) -} - -func RemoveBindNode(nodeID uint) error { return nil } - -func CheckLicenseStatus(isXpack bool, licenseID, nodeID uint) error { - return edition.CheckLicenseStatus(isXpack, licenseID, nodeID) -} -func LoadLicenseByNodeID(nodeID uint) (bool, model.License, error) { - return edition.LoadLicenseByNodeID(nodeID) -} -func Bind(node model.Node, license *model.License, withInit, withSync, withDryRun, withDockerRestart bool) error { - return edition.Bind(node, license, withInit, withSync, withDryRun, withDockerRestart) -} -func BindFree(node model.Node, licenseID uint) error { - return edition.BindFree(node, licenseID) -} -func Unbind(node model.Node, license model.License, withReset, withSync, withDockerRestart bool) error { - return edition.Unbind(node, license, withReset, withSync, withDockerRestart) -} -func UnbindFree(node model.Node, licenseID uint) error { - return edition.UnbindFree(node, licenseID) -} diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index d6d7d3625..5afc61b74 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -92,6 +92,11 @@ class RequestHttp { window.location.reload(); return Promise.reject(data); } + if (data.code == ResultEnum.ERRXPACKEE) { + globalStore.isXpackEELicensed = false; + router.push({ name: 'XpackEELicenseRequired' }); + return Promise.reject(data); + } if (data.code == ResultEnum.NodeUnBind) { changeToLocal(); window.location.reload(); diff --git a/frontend/src/api/interface/ai.ts b/frontend/src/api/interface/ai.ts index f0ddec8ac..ae00c62c4 100644 --- a/frontend/src/api/interface/ai.ts +++ b/frontend/src/api/interface/ai.ts @@ -47,6 +47,44 @@ export namespace AI { migMode: string; processes: Process[]; } + export interface MonitorGPUSearch { + productName: string; + startTime: Date; + endTime: Date; + } + export interface MonitorGPUOptions { + gpuType: string; + options: Array; + chartHide: Array; + } + export interface ChartHide { + productName: string; + process: boolean; + gpu: boolean; + memory: boolean; + power: boolean; + temperature: boolean; + speed: boolean; + } + export interface MonitorGPUData { + date: Array; + gpuValue: Array; + temperatureValue: Array; + powerTotal: Array; + powerUsed: Array; + powerPercent: Array; + memoryTotal: Array; + memoryUsed: Array; + memoryPercent: Array; + speedValue: Array; + gpuProcesses: Array>; + } + export interface GPUProcess { + pid: string; + type: string; + processName: string; + usedMemory: string; + } export interface Process { pid: string; type: string; diff --git a/frontend/src/api/interface/auth.ts b/frontend/src/api/interface/auth.ts index 74d0a6e1b..306dabae9 100644 --- a/frontend/src/api/interface/auth.ts +++ b/frontend/src/api/interface/auth.ts @@ -48,15 +48,46 @@ export namespace Login { export interface AuthInfo { id: number; name: string; + sessionTimeout: number; + expirationDays: number; + expirationTime: string; + mfaStatus: string; + mfaInterval: number; role: string; permissions: string[]; - nodeScopes: number[]; nodeRoles: Array<{ nodeId: number; nodeName: string; roleId: number; roleName: string }>; + + apiInterfaceStatus: string; + apiKey: string; + ipWhiteList: string; + apiKeyValidityTime: number; } export interface AuthInfoUpdate { id: number; + name: string; + password: string; oldPassword: string; - newPassword: string; - retryPassword: string; + sessionTimeout: number; + expirationDays: number; + expirationTime: string; + } + export interface MFARequest { + title: string; + interval: number; + } + export interface MFAInfo { + secret: string; + qrImage: string; + } + export interface MFABind { + secret: string; + code: string; + interval: string; + } + export interface ApiConfig { + apiInterfaceStatus: string; + apiKey: string; + ipWhiteList: string; + apiKeyValidityTime: number; } } diff --git a/frontend/src/api/interface/host.ts b/frontend/src/api/interface/host.ts index f6ab0343c..cb2fdcf01 100644 --- a/frontend/src/api/interface/host.ts +++ b/frontend/src/api/interface/host.ts @@ -160,45 +160,6 @@ export namespace Host { endTime: Date; } - export interface MonitorGPUSearch { - productName: string; - startTime: Date; - endTime: Date; - } - export interface MonitorGPUOptions { - gpuType: string; - options: Array; - chartHide: Array; - } - export interface ChartHide { - productName: string; - process: boolean; - gpu: boolean; - memory: boolean; - power: boolean; - temperature: boolean; - speed: boolean; - } - export interface MonitorGPUData { - date: Array; - gpuValue: Array; - temperatureValue: Array; - powerTotal: Array; - powerUsed: Array; - powerPercent: Array; - memoryTotal: Array; - memoryUsed: Array; - memoryPercent: Array; - speedValue: Array; - gpuProcesses: Array>; - } - export interface GPUProcess { - pid: string; - type: string; - processName: string; - usedMemory: string; - } - export interface SSHInfo { autoStart: boolean; isActive: boolean; diff --git a/frontend/src/api/interface/setting.ts b/frontend/src/api/interface/setting.ts index 6055e3f65..bf09ab12c 100644 --- a/frontend/src/api/interface/setting.ts +++ b/frontend/src/api/interface/setting.ts @@ -1,32 +1,44 @@ import { DateTimeFormats } from '@intlify/core-base'; export namespace Setting { - export interface SettingInfo { - userName: string; - password: string; - email: string; - systemIP: string; - systemVersion: string; - upgradeBackupCopies: string; + export interface AgentSettingInfo { dockerSockPath: string; - developerMode: string; + systemVersion: string; + systemIP: string; - sessionTimeout: number; localTime: string; timeZone: string; ntpSite: string; + defaultNetwork: string; + defaultIO: string; + lastCleanTime: string; + lastCleanSize: string; + lastCleanData: string; + + monitorStatus: string; + monitorInterval: string; + monitorStoreDays: string; + + appStoreVersion: string; + appStoreLastModified: string; + appStoreSyncStatus: string; + + fileRecycleBin: string; + } + export interface SettingInfo { + systemVersion: string; + upgradeBackupCopies: string; + developerMode: string; + + sessionTimeout: number; + panelName: string; edition: string; theme: string; menuTabs: string; language: string; docSource: string; - defaultIO: string; - defaultNetwork: string; - lastCleanTime: string; - lastCleanSize: string; - lastCleanData: string; serverPort: number; ipv6: string; @@ -39,16 +51,7 @@ export namespace Setting { securityEntrance: string; dashboardMemoVisible: string; dashboardSimpleNodeVisible: string; - expirationDays: number; - expirationTime: string; complexityVerification: string; - mfaStatus: string; - mfaSecret: string; - mfaInterval: string; - - monitorStatus: string; - monitorInterval: number; - monitorStoreDays: number; messageType: string; emailVars: string; @@ -64,11 +67,31 @@ export namespace Setting { proxyUser: string; proxyPasswd: string; proxyPasswdKeep: string; + } + export interface SettingBaseInfo { + systemVersion: string; + developerMode: string; + upgradeBackupCopies: string; - apiInterfaceStatus: string; - apiKey: string; - ipWhiteList: string; - apiKeyValidityTime: number; + port: string; + ipv6: string; + bindAddress: string; + panelName: string; + edition: string; + theme: string; + menuTabs: string; + language: string; + hideMenu: string; + docSource: string; + + serverPort: string; + securityEntrance: string; + complexityVerification: string; + noAuthSetting: string; + proxyType: string; + + dashboardMemoVisible: string; + dashboardSimpleNodeVisible: string; } export interface TerminalInfo { lineHeight: string; @@ -113,12 +136,6 @@ export namespace Setting { proxyPasswdKeep: string; withDockerRestart: boolean; } - export interface ApiConfig { - apiInterfaceStatus: string; - apiKey: string; - ipWhiteList: string; - apiKeyValidityTime: number; - } export interface SSLUpdate { ssl: string; domain: string; @@ -142,19 +159,6 @@ export namespace Setting { export interface PortUpdate { serverPort: number; } - export interface MFARequest { - title: string; - interval: number; - } - export interface MFAInfo { - secret: string; - qrImage: string; - } - export interface MFABind { - secret: string; - code: string; - interval: string; - } export interface PasskeyRegisterRequest { name: string; } diff --git a/frontend/src/api/modules/ai.ts b/frontend/src/api/modules/ai.ts index af6505cbd..03b3aab30 100644 --- a/frontend/src/api/modules/ai.ts +++ b/frontend/src/api/modules/ai.ts @@ -26,10 +26,6 @@ export const closeOllamaModel = (name: string) => { return http.post(`/ai/ollama/close`, { name: name }); }; -export const loadGPUInfo = () => { - return http.get(`/ai/gpu/load`); -}; - export const bindDomain = (req: AI.BindDomain) => { return http.post(`/ai/domain/bind`, req); }; @@ -42,6 +38,16 @@ export const updateBindDomain = (req: AI.BindDomain) => { return http.post(`/ai/domain/update`, req); }; +export const loadGPUInfo = () => { + return http.get(`/ai/gpu/load`); +}; +export const getGPUOptions = () => { + return http.get(`/ai/gpu/options`); +}; +export const loadGPUMonitor = (param: AI.MonitorGPUSearch) => { + return http.post(`/ai/gpu/monitor/search`, param); +}; + export const pageMcpServer = (req: AI.McpServerSearch) => { return http.post>(`/ai/mcp/search`, req); }; diff --git a/frontend/src/api/modules/auth.ts b/frontend/src/api/modules/auth.ts index 33c10bc73..7a2b6e403 100644 --- a/frontend/src/api/modules/auth.ts +++ b/frontend/src/api/modules/auth.ts @@ -35,17 +35,44 @@ export const getWelcomePage = () => { return http.get('/core/auth/welcome'); }; -export const getAuthInfo = () => { - return http.get('/core/xpackee/users/info'); +export const getUserInfo = () => { + return http.get('/core/auth/current'); }; -export const updateAuthInfo = (params: Login.AuthInfoUpdate) => { +export const loadMFA = (params: Login.MFARequest) => { + return http.post(`/core/auth/mfa`, params); +}; +export const bindMFA = (params: Login.MFABind) => { + return http.post(`/core/auth/mfa/bind`, params); +}; +export const generateApiKey = () => { + return http.post(`/core/auth/api/generate`); +}; +export const updateApiConfig = (param: Login.ApiConfig) => { + return http.post(`/core/auth/api/update`, param); +}; + +export const passkeyRegisterBegin = (param: { name: string }) => { + return http.post(`/core/auth/passkey/register/begin`, param); +}; +export const passkeyRegisterFinish = (param: Record, sessionId: string) => { + return http.post(`/core/auth/passkey/register/finish`, param, undefined, { 'Passkey-Session': sessionId }); +}; +export const passkeyList = () => { + return http.get>( + `/core/auth/passkey/list`, + ); +}; +export const passkeyDelete = (id: string) => { + return http.post(`/core/auth/passkey/del`, { id }); +}; + +export const updateUserInfo = (params: Login.AuthInfoUpdate) => { let request = deepCopy(params) as Login.AuthInfoUpdate; if (request.oldPassword) { request.oldPassword = Base64.encode(request.oldPassword); } - if (request.newPassword) { - request.newPassword = Base64.encode(request.newPassword); + if (request.password) { + request.password = Base64.encode(request.password); } - request.retryPassword = ''; - return http.post('/core/xpackee/users/info/update', request); + return http.post('/core/auth/current/update', request); }; diff --git a/frontend/src/api/modules/host.ts b/frontend/src/api/modules/host.ts index 3357db136..ada5adad6 100644 --- a/frontend/src/api/modules/host.ts +++ b/frontend/src/api/modules/host.ts @@ -65,12 +65,6 @@ export const operateFilterChain = (name: string, op: string) => { export const loadMonitor = (param: Host.MonitorSearch) => { return http.post>(`/hosts/monitor/search`, param); }; -export const getGPUOptions = () => { - return http.get(`/hosts/monitor/gpuoptions`); -}; -export const loadGPUMonitor = (param: Host.MonitorGPUSearch) => { - return http.post(`/hosts/monitor/gpu/search`, param); -}; export const getNetworkOptions = () => { return http.get>(`/hosts/monitor/netoptions`); }; diff --git a/frontend/src/api/modules/setting.ts b/frontend/src/api/modules/setting.ts index 204040451..be6d9a978 100644 --- a/frontend/src/api/modules/setting.ts +++ b/frontend/src/api/modules/setting.ts @@ -81,8 +81,13 @@ export const loadDaemonJsonPath = () => { export const updateAgentSetting = (param: Setting.SettingUpdate) => { return http.post(`/settings/update`, param); }; -export const getAgentSettingInfo = () => { - return http.post(`/settings/search`); +export const getAgentSettingInfo = (currentNode?: string) => { + return http.post( + `/settings/search`, + {}, + undefined, + currentNode ? { CurrentNode: currentNode } : undefined, + ); }; export const getAgentTerminalAIInfo = () => { return http.post(`/settings/terminal/ai/search`); @@ -113,6 +118,9 @@ export const updateCommonDescription = (param: Setting.CommonDescription) => { export const getSettingInfo = () => { return http.post(`/core/settings/search`); }; +export const getSettingBaseInfo = () => { + return http.post(`/core/settings/search/base`); +}; export const getSettingBy = (key: string) => { return http.post(`/core/settings/by`, { key: key }); }; @@ -164,24 +172,6 @@ export const downloadSSL = () => { export const handleExpired = (param: Setting.PasswordUpdate) => { return http.post(`/core/settings/expired/handle`, param); }; -export const loadMFA = (param: Setting.MFARequest) => { - return http.post(`/core/settings/mfa`, param); -}; -export const bindMFA = (param: Setting.MFABind) => { - return http.post(`/core/settings/mfa/bind`, param); -}; -export const passkeyRegisterBegin = (param: Setting.PasskeyRegisterRequest) => { - return http.post(`/core/settings/passkey/register/begin`, param); -}; -export const passkeyRegisterFinish = (param: Record, sessionId: string) => { - return http.post(`/core/settings/passkey/register/finish`, param, undefined, { 'Passkey-Session': sessionId }); -}; -export const passkeyList = () => { - return http.get>(`/core/settings/passkey/list`); -}; -export const passkeyDelete = (id: string) => { - return http.delete(`/core/settings/passkey/${id}`); -}; export const getAppStoreConfig = (node?: string) => { const params = node ? `?operateNode=${node}` : ''; return http.get(`/core/settings/apps/store/config${params}`); @@ -233,14 +223,6 @@ export const upgrade = (version: string) => { return http.post(`/core/settings/upgrade`, { version: version }); }; -// api config -export const generateApiKey = () => { - return http.post(`/core/settings/api/config/generate/key`); -}; -export const updateApiConfig = (param: Setting.ApiConfig) => { - return http.post(`/core/settings/api/config/update`, param); -}; - // memo export const getMemo = () => { return http.get(`/core/settings/memo`); diff --git a/frontend/src/components/docker-proxy/dialog.vue b/frontend/src/components/docker-proxy/dialog.vue index d78843e25..1724b2c47 100644 --- a/frontend/src/components/docker-proxy/dialog.vue +++ b/frontend/src/components/docker-proxy/dialog.vue @@ -24,7 +24,7 @@ diff --git a/frontend/src/layout/components/Sidebar/components/user-info/index.vue b/frontend/src/layout/components/Sidebar/components/user-info/index.vue new file mode 100644 index 000000000..dabbd8736 --- /dev/null +++ b/frontend/src/layout/components/Sidebar/components/user-info/index.vue @@ -0,0 +1,1133 @@ + + + + + diff --git a/frontend/src/layout/components/Sidebar/index.vue b/frontend/src/layout/components/Sidebar/index.vue index e741e46e1..074b4ec7c 100644 --- a/frontend/src/layout/components/Sidebar/index.vue +++ b/frontend/src/layout/components/Sidebar/index.vue @@ -23,7 +23,7 @@ - + @@ -36,9 +36,9 @@ import Collapse from './components/Collapse.vue'; import SubItem from './components/SubItem.vue'; import { menuList } from '@/routers/router'; import { GlobalStore, MenuStore } from '@/store'; -import { getSettingInfo } from '@/api/modules/setting'; +import { getSettingBaseInfo } from '@/api/modules/setting'; import PrimaryMenu from '@/assets/images/menu-bg.svg?component'; -import { hasPermission } from '@/utils/rbac'; +import { hasPermission, hasRouteRoleAccess } from '@/utils/rbac'; const route = useRoute(); const menuStore = MenuStore(); @@ -87,10 +87,21 @@ const openTask = () => { }; const search = async () => { + let settingInfo: { systemVersion: string; hideMenu?: string } | null = null; try { - const res = await getSettingInfo(); + const res = await getSettingBaseInfo(); + settingInfo = res.data; version.value = res.data.systemVersion; - let hideMenu = JSON.parse(res.data.hideMenu); + } catch (error) { + version.value = ''; + } + + if (!globalStore.isAdmin) { + menuStore.setMenuList(buildAuthVisibleMenuList(menuList)); + return; + } + try { + const hideMenu = JSON.parse(settingInfo?.hideMenu || '[]'); const showSet = new Set(); getCheckedLabels(hideMenu, showSet); const rstMenuList: RouteRecordRaw[] = []; @@ -106,7 +117,7 @@ const search = async () => { } } catch (error) { if (!menuStore.menuList || menuStore.menuList.length === 0) { - menuStore.setMenuList(menuList); + menuStore.setMenuList(buildAuthVisibleMenuList(menuList)); } } }; @@ -116,6 +127,12 @@ function isSameMenuList(source: RouteRecordRaw[], target: RouteRecordRaw[]) { } function allowMenuItem(item: RouteRecordRaw) { + if (globalStore.isAdmin) { + return true; + } + if (!hasRouteRoleAccess(item.meta)) { + return false; + } const permission = item.meta?.permission as string | undefined; if (!permission) { return true; @@ -124,6 +141,38 @@ function allowMenuItem(item: RouteRecordRaw) { return allowed; } +function buildAuthVisibleMenuList(source: RouteRecordRaw[]) { + return source + .map((item) => { + if (!allowMenuItem(item)) { + return null; + } + const menuItem = JSON.parse(JSON.stringify(item)); + const children = Array.isArray(menuItem.children) ? menuItem.children : []; + if (children.length === 0) { + return menuItem; + } + menuItem.children = buildAuthVisibleMenuList(children).filter(Boolean); + if (menuItem.children.length === 0) { + return null; + } + if (menuItem.children.length === 1) { + const onlyChild = menuItem.children[0]; + if (onlyChild.meta?.icon) { + menuItem.meta.icon = onlyChild.meta.icon; + } + if (onlyChild.meta?.title) { + menuItem.meta.title = onlyChild.meta.title; + } + } + if (menuItem.name === 'Xpack-Menu') { + menuItem.meta.hideInSidebar = false; + } + return menuItem; + }) + .filter(Boolean) as RouteRecordRaw[]; +} + function buildVisibleMenu(menu: RouteRecordRaw, showSet: Set): RouteRecordRaw | null { const menuItem = JSON.parse(JSON.stringify(menu)); if (!menuItem?.name || !showSet.has(menuItem.name as string)) { @@ -153,8 +202,13 @@ function buildVisibleMenu(menu: RouteRecordRaw, showSet: Set): RouteReco } if (menuItem.children.length === 1) { - menuItem.meta.icon = menuItem.children[0].meta.icon; - menuItem.meta.title = menuItem.children[0].meta.title; + const onlyChild = menuItem.children[0]; + if (onlyChild.meta?.icon) { + menuItem.meta.icon = onlyChild.meta.icon; + } + if (onlyChild.meta?.title) { + menuItem.meta.title = onlyChild.meta.title; + } } if (menuItem.name === 'Xpack-Menu') { menuItem.meta.hideInSidebar = false; @@ -198,8 +252,13 @@ function adjustAndCleanMenu(menuItem, list) { const newMenu = buildTree(menuItem); for (const menu of newMenu) { if (menu.children?.length === 1) { - menu.meta.icon = menu.children[0].meta.icon; - menu.meta.title = menu.children[0].meta.title; + const onlyChild = menu.children[0]; + if (onlyChild.meta?.icon) { + menu.meta.icon = onlyChild.meta.icon; + } + if (onlyChild.meta?.title) { + menu.meta.title = onlyChild.meta.title; + } } } @@ -208,7 +267,7 @@ function adjustAndCleanMenu(menuItem, list) { onMounted(() => { if (!menuStore.menuList || menuStore.menuList.length === 0) { - menuStore.setMenuList(menuList); + menuStore.setMenuList(globalStore.isAdmin ? menuList : buildAuthVisibleMenuList(menuList)); } search(); }); diff --git a/frontend/src/routers/index.ts b/frontend/src/routers/index.ts index 99a5f6976..9c13a4e4c 100644 --- a/frontend/src/routers/index.ts +++ b/frontend/src/routers/index.ts @@ -2,24 +2,38 @@ import router from '@/routers/router'; import NProgress from '@/config/nprogress'; import { GlobalStore } from '@/store'; import { AxiosCanceler } from '@/api/helper/axios-cancel'; -import { hasPermission } from '@/utils/rbac'; +import { hasRouteAccess } from '@/utils/rbac'; import i18n from '@/lang'; import { MsgError } from '@/utils/message'; const axiosCanceler = new AxiosCanceler(); let isRedirecting = false; +let licenseStatusLoading: Promise | null = null; -router.beforeEach((to, from, next) => { +const loadXpackEELicenseStatus = async () => { + if (!licenseStatusLoading) { + licenseStatusLoading = fetch('/api/v2/core/xpackee/licenses/status', { + credentials: 'include', + headers: { + CurrentNode: encodeURIComponent(GlobalStore().currentNode), + }, + }) + .then((res) => res.json()) + .then((res) => res?.data?.status === 'Bound') + .catch(() => false) + .finally(() => { + licenseStatusLoading = null; + }); + } + return licenseStatusLoading; +}; + +router.beforeEach(async (to, from, next) => { NProgress.start(); axiosCanceler.removeAllPending(); const globalStore = GlobalStore(); - if (globalStore.isXpackEE && !globalStore.isAdmin) { - if (xpackEEJumper(to, next)) { - return; - } - } - if (to.name !== 'entrance' && !globalStore.isLogin) { + if (to.name !== 'entrance' && to.name !== 'XpackEELicenseRequired' && !globalStore.isLogin) { next({ name: 'entrance', params: to.params, @@ -39,6 +53,37 @@ router.beforeEach((to, from, next) => { NProgress.done(); return; } + if ( + globalStore.isLogin && + globalStore.isXpackEE && + to.name !== 'XpackEELicenseRequired' && + to.name !== 'entrance' && + to.name !== 'login' + ) { + const licensed = globalStore.isXpackEELicensed || (await loadXpackEELicenseStatus()); + globalStore.isXpackEELicensed = licensed; + if (!licensed) { + next({ name: 'XpackEELicenseRequired', query: { code: String(to.params.code || '') } }); + NProgress.done(); + return; + } + } + if (to.name === 'XpackEELicenseRequired') { + if (!globalStore.isLogin) { + next({ + name: 'entrance', + params: to.params, + }); + NProgress.done(); + return; + } + if (globalStore.isXpackEELicensed) { + next({ name: 'home' }); + NProgress.done(); + return; + } + return next(); + } if (to.path === '/apps/all' && to.query.install != undefined) { return next(); @@ -65,18 +110,12 @@ router.beforeEach((to, from, next) => { return; } - const requiredPermission = [...to.matched].reverse().find((record) => record.meta?.permission)?.meta?.permission as - | string - | undefined; - if (requiredPermission && !hasPermission(requiredPermission)) { + if (!hasRouteAccess(to)) { MsgError(i18n.global.t('commons.res.forbidden')); next(false); NProgress.done(); return; } - - if (!to.matched.some((record) => record.meta.requiresAuth)) return next(); - return next(); }); @@ -104,52 +143,3 @@ router.afterEach((to) => { }); export default router; - -const xpackEEJumper = (to: any, next: any) => { - switch (to.name) { - case 'Panel': - case 'Safe': - case 'Alert': - if (hasPermission('setting_view')) { - return false; - } - MsgError(i18n.global.t('commons.res.forbidden')); - next(false); - NProgress.done(); - return true; - case 'License': - if (hasPermission('setting_view')) { - return false; - } - MsgError(i18n.global.t('commons.res.forbidden')); - next(false); - NProgress.done(); - return true; - case 'Node': - case 'SimpleNode': - case 'NodeAppUpgrade': - if (hasPermission('node_view')) { - return false; - } - MsgError(i18n.global.t('commons.res.forbidden')); - next(false); - NProgress.done(); - return true; - case 'UserXpackEEUser': - if (hasPermission('setting_view')) { - return false; - } - MsgError(i18n.global.t('commons.res.forbidden')); - next(false); - NProgress.done(); - return true; - case 'XpackEERole': - if (hasPermission('setting_view')) { - return false; - } - MsgError(i18n.global.t('commons.res.forbidden')); - next(false); - NProgress.done(); - return true; - } -}; diff --git a/frontend/src/routers/modules/ai.ts b/frontend/src/routers/modules/ai.ts index 66616ed15..9fd582635 100644 --- a/frontend/src/routers/modules/ai.ts +++ b/frontend/src/routers/modules/ai.ts @@ -19,7 +19,6 @@ const aiRouter = { icon: 'p-jiqiren2', title: 'aiTools.agents.agent', permission: 'ai_agent_view', - requiresAuth: true, }, }, { @@ -30,7 +29,6 @@ const aiRouter = { icon: 'p-moxing-menu', title: 'aiTools.model.model', permission: 'ai_model_view', - requiresAuth: true, }, }, { @@ -42,7 +40,6 @@ const aiRouter = { title: 'aiTools.model.localModel', activeMenu: '/ai/model/account', permission: 'ai_model_view', - requiresAuth: true, }, }, { @@ -53,7 +50,6 @@ const aiRouter = { icon: 'p-mcp-menu', title: 'menu.mcp', permission: 'ai_mcp_view', - requiresAuth: true, }, }, { @@ -65,18 +61,17 @@ const aiRouter = { title: 'aiTools.gpu.gpu', activeMenu: '/ai/gpu/current', permission: 'ai_gpu_view', - requiresAuth: true, }, }, { path: '/ai/gpu/history', name: 'GPUHistory', component: () => import('@/views/ai/gpu/history/index.vue'), + hidden: true, meta: { title: 'aiTools.gpu.history', activeMenu: '/ai/gpu/current', permission: 'ai_gpu_view', - requiresAuth: true, }, }, ], diff --git a/frontend/src/routers/modules/app-store.ts b/frontend/src/routers/modules/app-store.ts index 29ecacefc..40582c17c 100644 --- a/frontend/src/routers/modules/app-store.ts +++ b/frontend/src/routers/modules/app-store.ts @@ -29,7 +29,6 @@ const appStoreRouter = { hidden: true, meta: { activeMenu: '/apps', - requiresAuth: false, parent: 'menu.app', title: 'app.all', permission: 'app_view', @@ -43,7 +42,6 @@ const appStoreRouter = { hidden: true, meta: { activeMenu: '/apps', - requiresAuth: false, parent: 'menu.app', title: 'app.installed', permission: 'app_view', @@ -57,7 +55,6 @@ const appStoreRouter = { hidden: true, meta: { activeMenu: '/apps', - requiresAuth: false, parent: 'menu.app', title: 'app.canUpgrade', permission: 'app_view', @@ -71,7 +68,6 @@ const appStoreRouter = { hidden: true, meta: { activeMenu: '/apps', - requiresAuth: false, parent: 'menu.app', title: 'commons.button.set', permission: 'app_view', diff --git a/frontend/src/routers/modules/container.ts b/frontend/src/routers/modules/container.ts index c16f620ed..547957570 100644 --- a/frontend/src/routers/modules/container.ts +++ b/frontend/src/routers/modules/container.ts @@ -26,9 +26,9 @@ const containerRouter = { hidden: true, meta: { activeMenu: '/containers', - requiresAuth: false, parent: 'menu.container', title: 'menu.home', + permission: 'container_view', }, }, { @@ -39,7 +39,6 @@ const containerRouter = { hidden: true, meta: { activeMenu: '/containers', - requiresAuth: false, parent: 'menu.container', title: 'menu.container', permission: 'container_view', @@ -53,8 +52,8 @@ const containerRouter = { hidden: true, meta: { activeMenu: '/containers', - requiresAuth: false, ignoreTab: true, + permission: 'container_view', }, }, { @@ -64,7 +63,6 @@ const containerRouter = { hidden: true, meta: { activeMenu: '/containers', - requiresAuth: false, parent: 'menu.container', title: 'container.image', permission: 'container_view', @@ -77,7 +75,6 @@ const containerRouter = { hidden: true, meta: { activeMenu: '/containers', - requiresAuth: false, parent: 'menu.container', title: 'container.network', permission: 'container_view', @@ -90,7 +87,6 @@ const containerRouter = { hidden: true, meta: { activeMenu: '/containers', - requiresAuth: false, parent: 'menu.container', title: 'container.volume', permission: 'container_view', @@ -103,7 +99,6 @@ const containerRouter = { hidden: true, meta: { activeMenu: '/containers', - requiresAuth: false, parent: 'menu.container', title: 'container.repo', permission: 'container_view', @@ -116,7 +111,6 @@ const containerRouter = { hidden: true, meta: { activeMenu: '/containers', - requiresAuth: false, parent: 'menu.container', title: 'container.compose', permission: 'container_view', @@ -129,7 +123,6 @@ const containerRouter = { hidden: true, meta: { activeMenu: '/containers', - requiresAuth: false, parent: 'menu.container', title: 'container.composeTemplate', permission: 'container_view', @@ -142,7 +135,6 @@ const containerRouter = { hidden: true, meta: { activeMenu: '/containers', - requiresAuth: false, parent: 'menu.container', title: 'container.setting', permission: 'container_view', diff --git a/frontend/src/routers/modules/cronjob.ts b/frontend/src/routers/modules/cronjob.ts index bd088d068..a4909b33e 100644 --- a/frontend/src/routers/modules/cronjob.ts +++ b/frontend/src/routers/modules/cronjob.ts @@ -25,9 +25,8 @@ const cronRouter = { hidden: true, meta: { activeMenu: '/cronjobs', - requiresAuth: false, title: 'menu.cronjob', - permission: 'cronjob_task_view', + permission: 'cronjob_view', }, }, { @@ -37,8 +36,8 @@ const cronRouter = { hidden: true, meta: { activeMenu: '/cronjobs', - requiresAuth: false, ignoreTab: true, + permission: 'cronjob_view', }, }, { @@ -48,9 +47,8 @@ const cronRouter = { hidden: true, meta: { activeMenu: '/cronjobs', - requiresAuth: false, title: 'cronjob.library.library', - permission: 'cronjob_script_view', + permission: 'cronjob_view', }, }, ], diff --git a/frontend/src/routers/modules/database.ts b/frontend/src/routers/modules/database.ts index 954ea2346..eef12edef 100644 --- a/frontend/src/routers/modules/database.ts +++ b/frontend/src/routers/modules/database.ts @@ -25,10 +25,9 @@ const databaseRouter = { hidden: true, meta: { activeMenu: '/databases', - requiresAuth: false, parent: 'menu.database', title: 'MySQL', - permission: 'database_mysql_view', + permission: 'database_view', }, }, { @@ -39,8 +38,8 @@ const databaseRouter = { hidden: true, meta: { activeMenu: '/databases', - requiresAuth: false, ignoreTab: true, + permission: 'database_view', }, }, { @@ -50,11 +49,10 @@ const databaseRouter = { hidden: true, meta: { activeMenu: '/databases', - requiresAuth: false, parent: 'menu.database', title: 'MySQL', detail: 'database.remote', - permission: 'database_mysql_view', + permission: 'database_view', }, }, { @@ -64,10 +62,9 @@ const databaseRouter = { hidden: true, meta: { activeMenu: '/databases', - requiresAuth: false, parent: 'menu.database', title: 'PostgreSQL', - permission: 'database_postgresql_view', + permission: 'database_view', }, }, { @@ -77,11 +74,10 @@ const databaseRouter = { hidden: true, meta: { activeMenu: '/databases', - requiresAuth: false, parent: 'menu.database', title: 'PostgreSQL', detail: 'database.remote', - permission: 'database_postgresql_view', + permission: 'database_view', }, }, { @@ -92,8 +88,8 @@ const databaseRouter = { hidden: true, meta: { activeMenu: '/databases', - requiresAuth: false, ignoreTab: true, + permission: 'database_view', }, }, { @@ -103,10 +99,9 @@ const databaseRouter = { hidden: true, meta: { activeMenu: '/databases', - requiresAuth: false, parent: 'menu.database', title: 'Redis', - permission: 'database_redis_view', + permission: 'database_view', }, }, { @@ -116,11 +111,10 @@ const databaseRouter = { hidden: true, meta: { activeMenu: '/databases', - requiresAuth: false, parent: 'menu.database', title: 'Redis', detail: 'database.remote', - permission: 'database_redis_view', + permission: 'database_view', }, }, { diff --git a/frontend/src/routers/modules/error.ts b/frontend/src/routers/modules/error.ts index 3fd4ff3b2..991e6a215 100644 --- a/frontend/src/routers/modules/error.ts +++ b/frontend/src/routers/modules/error.ts @@ -10,7 +10,6 @@ const errorRouter = { hidden: true, component: () => import('@/components/error-message/404.vue'), meta: { - requiresAuth: false, title: '404页面', key: '404', }, diff --git a/frontend/src/routers/modules/host.ts b/frontend/src/routers/modules/host.ts index 17c5a68a4..9163e5a30 100644 --- a/frontend/src/routers/modules/host.ts +++ b/frontend/src/routers/modules/host.ts @@ -19,7 +19,6 @@ const hostRouter = { meta: { icon: 'p-file-menu', title: 'menu.files', - requiresAuth: false, permission: 'host_file_view', }, }, @@ -30,7 +29,6 @@ const hostRouter = { meta: { icon: 'p-system-monitor-menu', title: 'menu.monitor', - requiresAuth: false, permission: 'host_monitor_view', }, }, @@ -43,7 +41,6 @@ const hostRouter = { activeMenu: '/hosts/monitor/monitor', title: 'menu.monitor', detail: 'commons.button.set', - requiresAuth: false, permission: 'host_monitor_view', }, }, @@ -56,7 +53,6 @@ const hostRouter = { activeMenu: '/hosts/firewall/port', title: 'menu.firewall', detail: 'firewall.portRule', - requiresAuth: false, permission: 'host_firewall_view', }, }, @@ -69,7 +65,7 @@ const hostRouter = { activeMenu: '/hosts/firewall/port', parent: 'menu.firewall', title: 'firewall.forwardRule', - requiresAuth: false, + permission: 'host_firewall_view', }, }, { @@ -81,7 +77,7 @@ const hostRouter = { activeMenu: '/hosts/firewall/port', parent: 'menu.firewall', title: 'firewall.ipRule', - requiresAuth: false, + permission: 'host_firewall_view', }, }, { @@ -93,7 +89,7 @@ const hostRouter = { activeMenu: '/hosts/firewall/port', parent: 'menu.firewall', title: 'firewall.advancedControl', - requiresAuth: false, + permission: 'host_firewall_view', }, }, { @@ -104,7 +100,6 @@ const hostRouter = { meta: { icon: 'p-disk-menu', title: 'menu.disk', - requiresAuth: false, permission: 'host_disk_view', }, }, @@ -118,7 +113,6 @@ const hostRouter = { detail: 'menu.process', activeMenu: '/hosts/process/process', keepAlive: true, - requiresAuth: false, permission: 'host_process_view', }, }, @@ -131,7 +125,7 @@ const hostRouter = { parent: 'menu.processManage', title: 'menu.network', activeMenu: '/hosts/process/process', - requiresAuth: false, + permission: 'host_process_view', }, }, { @@ -144,7 +138,6 @@ const hostRouter = { detail: 'menu.config', activeMenu: '/hosts/ssh/ssh', keepAlive: true, - requiresAuth: false, permission: 'host_ssh_view', }, }, @@ -157,7 +150,7 @@ const hostRouter = { parent: 'menu.ssh', title: 'ssh.loginLogs', activeMenu: '/hosts/ssh/ssh', - requiresAuth: false, + permission: 'host_ssh_view', }, }, { @@ -169,7 +162,7 @@ const hostRouter = { parent: 'menu.ssh', title: 'ssh.session', activeMenu: '/hosts/ssh/ssh', - requiresAuth: false, + permission: 'host_ssh_view', }, }, ], diff --git a/frontend/src/routers/modules/log.ts b/frontend/src/routers/modules/log.ts index 535ef64ec..cdab2fa4b 100644 --- a/frontend/src/routers/modules/log.ts +++ b/frontend/src/routers/modules/log.ts @@ -28,7 +28,6 @@ const logsRouter = { parent: 'menu.logs', title: 'logs.operation', activeMenu: '/logs', - requiresAuth: false, permission: 'log_view', }, }, @@ -41,7 +40,6 @@ const logsRouter = { parent: 'menu.logs', title: 'logs.login', activeMenu: '/logs', - requiresAuth: false, permission: 'log_view', }, }, @@ -54,7 +52,7 @@ const logsRouter = { parent: 'menu.logs', title: 'logs.websiteLog', activeMenu: '/logs', - requiresAuth: false, + permission: 'log_view', }, }, { @@ -66,7 +64,6 @@ const logsRouter = { parent: 'menu.logs', title: 'logs.system', activeMenu: '/logs', - requiresAuth: false, permission: 'log_view', }, }, @@ -79,7 +76,7 @@ const logsRouter = { parent: 'menu.logs', title: 'ssh.loginLogs', activeMenu: '/logs', - requiresAuth: false, + permission: 'log_view', }, }, { @@ -91,7 +88,7 @@ const logsRouter = { parent: 'menu.logs', title: 'logs.task', activeMenu: '/logs', - requiresAuth: false, + permission: 'log_view', }, }, ], diff --git a/frontend/src/routers/modules/setting.ts b/frontend/src/routers/modules/setting.ts index e13e83115..a5773445e 100644 --- a/frontend/src/routers/modules/setting.ts +++ b/frontend/src/routers/modules/setting.ts @@ -9,6 +9,7 @@ const settingRouter = { meta: { title: 'menu.settings', icon: 'p-config', + adminOnly: true, }, children: [ { @@ -26,9 +27,8 @@ const settingRouter = { meta: { parent: 'menu.settings', title: 'setting.panel', - requiresAuth: true, activeMenu: '/settings', - permission: 'setting_view', + adminOnly: true, }, }, { @@ -39,9 +39,8 @@ const settingRouter = { meta: { parent: 'menu.settings', title: 'xpack.alert.alertNotice', - requiresAuth: true, activeMenu: '/settings', - permission: 'setting_view', + adminOnly: true, }, }, { @@ -52,9 +51,8 @@ const settingRouter = { meta: { parent: 'menu.settings', title: 'setting.backupAccount', - requiresAuth: true, activeMenu: '/settings', - permission: 'setting_view', + adminOnly: true, }, }, { @@ -65,9 +63,8 @@ const settingRouter = { meta: { parent: 'menu.settings', title: 'setting.license', - requiresAuth: true, activeMenu: '/settings', - permission: 'setting_view', + adminOnly: true, }, }, { @@ -78,9 +75,8 @@ const settingRouter = { meta: { parent: 'menu.settings', title: 'setting.about', - requiresAuth: true, activeMenu: '/settings', - permission: 'setting_view', + adminOnly: true, }, }, { @@ -91,9 +87,8 @@ const settingRouter = { meta: { parent: 'menu.settings', title: 'setting.safe', - requiresAuth: true, activeMenu: '/settings', - permission: 'setting_view', + adminOnly: true, }, }, { @@ -104,9 +99,8 @@ const settingRouter = { meta: { parent: 'menu.settings', title: 'setting.snapshot', - requiresAuth: true, activeMenu: '/settings', - permission: 'setting_view', + adminOnly: true, }, }, { @@ -115,7 +109,6 @@ const settingRouter = { hidden: true, component: () => import('@/views/setting/expired.vue'), meta: { - requiresAuth: true, activeMenu: '/settings', ignoreTab: true, }, diff --git a/frontend/src/routers/modules/terminal.ts b/frontend/src/routers/modules/terminal.ts index d5214c85f..63fd1a75a 100644 --- a/frontend/src/routers/modules/terminal.ts +++ b/frontend/src/routers/modules/terminal.ts @@ -9,6 +9,7 @@ const terminalRouter = { meta: { title: 'menu.terminal', icon: 'p-terminal2', + protectedRoleOnly: true, }, children: [ { @@ -18,7 +19,7 @@ const terminalRouter = { component: () => import('@/views/terminal/index.vue'), meta: { keepAlive: true, - requiresAuth: false, + protectedRoleOnly: true, }, }, ], diff --git a/frontend/src/routers/modules/toolbox.ts b/frontend/src/routers/modules/toolbox.ts index 69939f47e..0af80193a 100644 --- a/frontend/src/routers/modules/toolbox.ts +++ b/frontend/src/routers/modules/toolbox.ts @@ -28,7 +28,6 @@ const toolboxRouter = { parent: 'menu.toolbox', title: 'toolbox.device.toolbox', activeMenu: '/toolbox', - requiresAuth: false, permission: 'toolbox_view', }, }, @@ -41,7 +40,6 @@ const toolboxRouter = { parent: 'menu.toolbox', title: 'menu.supervisor', activeMenu: '/toolbox', - requiresAuth: false, permission: 'toolbox_view', }, }, @@ -54,7 +52,6 @@ const toolboxRouter = { parent: 'menu.toolbox', title: 'toolbox.clam.clam', activeMenu: '/toolbox', - requiresAuth: false, permission: 'toolbox_view', }, }, @@ -66,7 +63,7 @@ const toolboxRouter = { meta: { ignoreTab: true, activeMenu: '/toolbox', - requiresAuth: false, + permission: 'toolbox_view', }, }, { @@ -78,7 +75,6 @@ const toolboxRouter = { parent: 'menu.toolbox', title: 'FTP', activeMenu: '/toolbox', - requiresAuth: false, permission: 'toolbox_view', }, }, @@ -91,7 +87,6 @@ const toolboxRouter = { parent: 'menu.toolbox', title: 'Fail2Ban', activeMenu: '/toolbox', - requiresAuth: false, permission: 'toolbox_view', }, }, @@ -104,7 +99,6 @@ const toolboxRouter = { parent: 'menu.toolbox', title: 'setting.diskClean', activeMenu: '/toolbox', - requiresAuth: false, permission: 'toolbox_view', }, }, diff --git a/frontend/src/routers/modules/website.ts b/frontend/src/routers/modules/website.ts index 83b947b70..ef061d71b 100644 --- a/frontend/src/routers/modules/website.ts +++ b/frontend/src/routers/modules/website.ts @@ -18,7 +18,6 @@ const webSiteRouter = { meta: { icon: 'p-website', title: 'menu.website', - requiresAuth: false, permission: 'website_view', }, }, @@ -30,8 +29,8 @@ const webSiteRouter = { props: true, meta: { activeMenu: '/websites', - requiresAuth: false, ignoreTab: true, + permission: 'website_view', }, }, { @@ -41,7 +40,6 @@ const webSiteRouter = { meta: { icon: 'p-ssl-menu', title: 'menu.ssl', - requiresAuth: false, permission: 'website_cert_view', }, }, @@ -54,7 +52,6 @@ const webSiteRouter = { title: 'menu.runtime', detail: 'PHP', activeMenu: '/websites/runtimes/php', - requiresAuth: false, permission: 'website_runtime_view', }, }, @@ -67,7 +64,6 @@ const webSiteRouter = { title: 'menu.runtime', detail: 'Node', activeMenu: '/websites/runtimes/php', - requiresAuth: false, permission: 'website_runtime_view', }, }, @@ -80,7 +76,6 @@ const webSiteRouter = { title: 'menu.runtime', detail: 'Java', activeMenu: '/websites/runtimes/php', - requiresAuth: false, permission: 'website_runtime_view', }, }, @@ -93,7 +88,6 @@ const webSiteRouter = { title: 'menu.runtime', detail: 'Go', activeMenu: '/websites/runtimes/php', - requiresAuth: false, permission: 'website_runtime_view', }, }, @@ -106,7 +100,6 @@ const webSiteRouter = { title: 'menu.runtime', detail: 'Python', activeMenu: '/websites/runtimes/php', - requiresAuth: false, permission: 'website_runtime_view', }, }, @@ -119,7 +112,6 @@ const webSiteRouter = { title: 'menu.runtime', detail: '.Net', activeMenu: '/websites/runtimes/php', - requiresAuth: false, permission: 'website_runtime_view', }, }, diff --git a/frontend/src/routers/router.ts b/frontend/src/routers/router.ts index c82210e4e..87c933a74 100644 --- a/frontend/src/routers/router.ts +++ b/frontend/src/routers/router.ts @@ -21,7 +21,6 @@ const homeRouter: RouteRecordRaw = { name: 'home', component: () => import('@/views/home/index.vue'), meta: { - requiresAuth: true, keepAlive: true, }, }, @@ -70,17 +69,22 @@ export const routes: RouteRecordRaw[] = [ props: true, component: () => import('@/views/login/index.vue'), meta: { - requiresAuth: false, key: 'login', }, }, + { + path: '/xpack-ee/license-required', + name: 'XpackEELicenseRequired', + component: () => import('@/views/setting/license-required/index.vue'), + meta: { + key: 'xpack-ee-license-required', + }, + }, { path: '/s/:code', name: 'file-share', component: () => import('@/views/share/index.vue'), - meta: { - requiresAuth: false, - }, + meta: {}, }, { path: '/:code?', diff --git a/frontend/src/store/interface/index.ts b/frontend/src/store/interface/index.ts index 4fc5b49f4..378df072c 100644 --- a/frontend/src/store/interface/index.ts +++ b/frontend/src/store/interface/index.ts @@ -57,7 +57,6 @@ export interface GlobalState { // tags isAdmin: boolean; permissions: string[]; - nodeScopes: number[]; nodeRoles: Array<{ nodeId: number; nodeName: string; roleId: number; roleName: string }>; isXpackEE: boolean; isIntl: boolean; diff --git a/frontend/src/store/modules/global.ts b/frontend/src/store/modules/global.ts index 25a03041f..d8a267d97 100644 --- a/frontend/src/store/modules/global.ts +++ b/frontend/src/store/modules/global.ts @@ -54,7 +54,6 @@ const GlobalStore = defineStore({ // tags isAdmin: false, permissions: [], - nodeScopes: [], nodeRoles: [], isXpackEE: false, isIntl: false, @@ -76,6 +75,8 @@ const GlobalStore = defineStore({ state.themeConfig.theme === 'dark' || (state.themeConfig.theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches), isDarkGoldTheme: (state) => state.themeConfig.primary === '#F0BE96' && state.isProductPro, + isNodeAdmin: (state) => + state.nodeRoles.some((item) => item.nodeName === state.currentNode && item.roleName === 'Node Admin'), docsUrl: (state) => { if (state.docWithRegion) { return state.isIntl ? INTL_DOCS_URL : CN_DOCS_URL; @@ -96,22 +97,30 @@ const GlobalStore = defineStore({ setAuthInfo(payload: { isAdmin: boolean; permissions: string[]; - nodeScopes: number[]; nodeRoles?: Array<{ nodeId: number; nodeName: string; roleId: number; roleName: string }>; }) { this.isAdmin = !!payload.isAdmin; this.permissions = payload.permissions || []; - this.nodeScopes = payload.nodeScopes || []; this.nodeRoles = payload.nodeRoles || []; }, clearAuthInfo() { this.permissions = []; - this.nodeScopes = []; this.nodeRoles = []; this.isAdmin = false; }, hasPermission(permission: string) { - return this.isAdmin || this.permissions.includes(permission); + if (this.isAdmin) { + return true; + } + const normalizedPermission = permission.trim(); + if (!normalizedPermission) { + return false; + } + if (this.permissions.includes(normalizedPermission)) { + return true; + } + const managePermission = getManagePermission(normalizedPermission); + return managePermission ? this.permissions.includes(managePermission) : false; }, setGlobalLoading(loading: boolean) { this.isLoading = loading; @@ -153,3 +162,10 @@ const GlobalStore = defineStore({ }); export default GlobalStore; + +function getManagePermission(permission: string) { + if (permission.endsWith('_view')) { + return permission.replace(/_view$/, '_manage'); + } + return ''; +} diff --git a/frontend/src/utils/rbac.ts b/frontend/src/utils/rbac.ts index 18b9aeb83..8d1f87f55 100644 --- a/frontend/src/utils/rbac.ts +++ b/frontend/src/utils/rbac.ts @@ -1,20 +1,68 @@ -import { getAuthInfo } from '@/api/modules/auth'; +import { getUserInfo } from '@/api/modules/auth'; +import { getXpackeeUserInfo } from '@/extensions/xpack'; import { GlobalStore } from '@/store'; +import type { RouteMeta } from 'vue-router'; -export const syncAuthInfo = async () => { +type RouteAccessMeta = { + adminOnly?: boolean; + protectedRoleOnly?: boolean; +}; + +type RouteAccessTarget = { + matched: Array<{ + meta?: RouteMeta & { + permission?: string; + }; + }>; +}; + +export const syncAuthInfo = async (currentNode?: string) => { const globalStore = GlobalStore(); if (!globalStore.isXpackEE) { - return; + const res = await getUserInfo(); + return res.data; } - const res = await getAuthInfo(); + const res = await getXpackeeUserInfo(currentNode ?? globalStore.currentNode); globalStore.setAuthInfo({ isAdmin: res.data.role === 'ADMIN', permissions: res.data.permissions || [], - nodeScopes: res.data.nodeScopes || [], nodeRoles: res.data.nodeRoles || [], }); + return res.data; }; export const hasPermission = (permission: string) => { return GlobalStore().hasPermission(permission); }; + +export const hasRouteRoleAccess = (meta?: RouteMeta & RouteAccessMeta) => { + const globalStore = GlobalStore(); + if (!meta) { + return true; + } + if (globalStore.isAdmin) { + return true; + } + if (meta.adminOnly && !globalStore.isAdmin) { + return false; + } + if (meta.protectedRoleOnly) { + return globalStore.isNodeAdmin; + } + return true; +}; + +export const hasRoutePermissionAccess = (route: RouteAccessTarget) => { + const requiredPermissions = [ + ...new Set( + route.matched + .map((record) => record.meta?.permission) + .filter((permission): permission is string => !!permission), + ), + ]; + return requiredPermissions.every((permission) => hasPermission(permission)); +}; + +export const hasRouteAccess = (route: RouteAccessTarget) => { + return route.matched.every((record) => hasRouteRoleAccess(record.meta)) && hasRoutePermissionAccess(route); +}; diff --git a/frontend/src/utils/router.ts b/frontend/src/utils/router.ts index 421194662..de082f4f3 100644 --- a/frontend/src/utils/router.ts +++ b/frontend/src/utils/router.ts @@ -1,29 +1,38 @@ import router from '@/routers'; import { TabsStore } from '@/store'; +import { hasRouteAccess } from '@/utils/rbac'; +import i18n from '@/lang'; +import { MsgError } from '@/utils/message'; +import type { RouteLocationRaw } from 'vue-router'; + +const pushWithAccessCheck = async (to: RouteLocationRaw) => { + const resolvedRoute = router.resolve(to); + if (!hasRouteAccess(resolvedRoute)) { + MsgError(i18n.global.t('commons.res.forbidden')); + return; + } + await router.push(to); + tabStoreMiddleWare(); +}; export const routerToName = async (name: string) => { - await router.push({ name: name }); - tabStoreMiddleWare(); + await pushWithAccessCheck({ name: name }); }; export const routerToPath = async (path: string) => { - await router.push({ path: path }); - tabStoreMiddleWare(); + await pushWithAccessCheck({ path: path }); }; export const routerToFileWithPath = async (pathItem: string) => { - await router.push({ name: 'File', query: { path: pathItem, uncached: 'true' } }); - tabStoreMiddleWare(); + await pushWithAccessCheck({ name: 'File', query: { path: pathItem, uncached: 'true' } }); }; export const routerToNameWithQuery = async (name: string, query: any) => { - await router.push({ name: name, query: query }); - tabStoreMiddleWare(); + await pushWithAccessCheck({ name: name, query: query }); }; export const routerToPathWithQuery = async (path: string, query: any) => { - await router.push({ path: path, query: query }); - tabStoreMiddleWare(); + await pushWithAccessCheck({ path: path, query: query }); }; export const jumpToPath = (router: any, path: string) => { @@ -31,8 +40,7 @@ export const jumpToPath = (router: any, path: string) => { }; export const routerToNameWithParams = async (name: string, params: any) => { - await router.push({ name: name, params: params }); - tabStoreMiddleWare(); + await pushWithAccessCheck({ name: name, params: params }); }; const tabStoreMiddleWare = () => { diff --git a/frontend/src/utils/xpack.ts b/frontend/src/utils/xpack.ts index 978c75c4b..d48b8c59e 100644 --- a/frontend/src/utils/xpack.ts +++ b/frontend/src/utils/xpack.ts @@ -1,7 +1,7 @@ import { getLicenseStatus, getMasterLicenseStatus, - getSettingInfo, + getSettingBaseInfo, getXpackEELicenseStatus, } from '@/api/modules/setting'; import { useTheme } from '@/global/use-theme'; @@ -75,7 +75,7 @@ export async function getXpackSetting() { const loadDataFromDB = async () => { const globalStore = getGlobalStore(); - const res = await getSettingInfo(); + const res = await getSettingBaseInfo(); document.title = res.data.panelName; globalStore.entrance = res.data.securityEntrance; globalStore.openMenuTabs = res.data.menuTabs === 'Enable'; @@ -95,7 +95,7 @@ export async function loadProductProFromDB() { } return; } - const res = await getLicenseStatus(); + const res = await getXpackEELicenseStatus(); if (!res || !res.data) { globalStore.isXpackEELicensed = false; } else { @@ -118,6 +118,7 @@ export async function loadMasterProductProFromDB() { globalStore.isXpackEELicensed = false; } else { globalStore.isXpackEELicensed = res.data.status === 'Bound'; + globalStore.isMasterProductPro = res.data.status === 'Bound'; } } switchTheme(); diff --git a/frontend/src/views/ai/agents/agent/components/hermes-chat.vue b/frontend/src/views/ai/agents/agent/components/hermes-chat.vue index 7f1ca8ec5..247f9887a 100644 --- a/frontend/src/views/ai/agents/agent/components/hermes-chat.vue +++ b/frontend/src/views/ai/agents/agent/components/hermes-chat.vue @@ -154,7 +154,7 @@ const connectTerminal = async (initCmd: string) => { args += `&operateNode=${node.value}`; } terminalRef.value?.acceptParams({ - endpoint: '/api/v2/containers/exec', + endpoint: '/api/v2/hosts/terminal/container', args, error: '', initCmd, diff --git a/frontend/src/views/ai/agents/agent/components/terminal.vue b/frontend/src/views/ai/agents/agent/components/terminal.vue index 93a060058..e8d455265 100644 --- a/frontend/src/views/ai/agents/agent/components/terminal.vue +++ b/frontend/src/views/ai/agents/agent/components/terminal.vue @@ -77,7 +77,7 @@ const initTerm = async () => { args += `&operateNode=${form.node}`; } terminalRef.value?.acceptParams({ - endpoint: '/api/v2/containers/exec', + endpoint: '/api/v2/hosts/terminal/container', args, error: '', initCmd: form.initCmd, diff --git a/frontend/src/views/ai/gpu/history/index.vue b/frontend/src/views/ai/gpu/history/index.vue index 3a17b23fc..7a00ea86e 100644 --- a/frontend/src/views/ai/gpu/history/index.vue +++ b/frontend/src/views/ai/gpu/history/index.vue @@ -167,14 +167,14 @@ + + diff --git a/frontend/src/views/setting/panel/api-interface/index.vue b/frontend/src/views/setting/panel/api-interface/index.vue deleted file mode 100644 index ffc5bd444..000000000 --- a/frontend/src/views/setting/panel/api-interface/index.vue +++ /dev/null @@ -1,198 +0,0 @@ - - - - diff --git a/frontend/src/views/setting/panel/index.vue b/frontend/src/views/setting/panel/index.vue index 0818972c3..199cc7ab6 100644 --- a/frontend/src/views/setting/panel/index.vue +++ b/frontend/src/views/setting/panel/index.vue @@ -6,26 +6,6 @@
- - - - - - - - - - - -