feat(system): add session check for agent api (#8258)

This commit is contained in:
zhengkunwang 2025-03-26 16:47:35 +08:00 committed by GitHub
parent 732bbd8951
commit 220b6223a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 356 additions and 278 deletions

View file

@ -2,10 +2,16 @@ package router
import (
"context"
"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/utils/security"
"net"
"net/http"
"net/http/httputil"
"os"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/core/app/api/v2/helper"
@ -38,6 +44,13 @@ func Proxy() gin.HandlerFunc {
currentNode = c.Request.Header.Get("CurrentNode")
}
if strings.HasPrefix(c.Request.URL.Path, "/api/v2/") && !checkSession(c) {
data, _ := res.ErrorMsg.ReadFile("html/401.html")
c.Data(401, "text/html; charset=utf-8", data)
c.Abort()
return
}
if !strings.HasPrefix(c.Request.URL.Path, "/api/v2/core") && (currentNode == "local" || len(currentNode) == 0 || currentNode == "127.0.0.1") {
sockPath := "/etc/1panel/agent.sock"
if _, err := os.Stat(sockPath); err != nil {
@ -58,6 +71,13 @@ func Proxy() gin.HandlerFunc {
req.URL.Host = "unix"
},
Transport: transport,
ModifyResponse: func(response *http.Response) error {
if response.StatusCode == 404 {
security.HandleNotSecurity(c, "")
c.Abort()
}
return nil
},
}
proxy.ServeHTTP(c.Writer, c.Request)
c.Abort()
@ -67,3 +87,22 @@ func Proxy() gin.HandlerFunc {
c.Abort()
}
}
func checkSession(c *gin.Context) bool {
psession, err := global.SESSION.Get(c)
if err != nil {
return false
}
settingRepo := repo.NewISettingRepo()
setting, err := settingRepo.Get(repo.WithByKey("SessionTimeout"))
if err != nil {
return false
}
lifeTime, _ := strconv.Atoi(setting.Value)
httpsSetting, err := settingRepo.Get(repo.WithByKey("SSL"))
if err != nil {
return false
}
_ = global.SESSION.Set(c, psession, httpsSetting.Value == constant.StatusEnable, lifeTime)
return true
}

View file

@ -3,157 +3,26 @@ package router
import (
"encoding/base64"
"fmt"
"net/http"
"path"
"regexp"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/core/app/repo"
"github.com/1Panel-dev/1Panel/core/utils/common"
"github.com/1Panel-dev/1Panel/core/app/service"
"github.com/1Panel-dev/1Panel/core/cmd/server/res"
"github.com/1Panel-dev/1Panel/core/constant"
"github.com/1Panel-dev/1Panel/core/cmd/server/docs"
"github.com/1Panel-dev/1Panel/core/cmd/server/web"
"github.com/1Panel-dev/1Panel/core/global"
"github.com/1Panel-dev/1Panel/core/i18n"
"github.com/1Panel-dev/1Panel/core/middleware"
rou "github.com/1Panel-dev/1Panel/core/router"
"github.com/1Panel-dev/1Panel/core/utils/security"
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
swaggerfiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"net/http"
"path"
)
var (
Router *gin.Engine
)
func toIndexHtml(c *gin.Context) {
c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
c.Writer.WriteHeader(http.StatusOK)
_, _ = c.Writer.Write(web.IndexByte)
c.Writer.Flush()
}
func isEntrancePath(c *gin.Context) bool {
entrance := service.NewIAuthService().GetSecurityEntrance()
if entrance != "" && strings.TrimSuffix(c.Request.URL.Path, "/") == "/"+entrance {
return true
}
return false
}
func checkEntrance(c *gin.Context) bool {
authService := service.NewIAuthService()
entrance := authService.GetSecurityEntrance()
if entrance == "" {
return true
}
cookieValue, err := c.Cookie("SecurityEntrance")
if err != nil {
return false
}
entranceValue, err := base64.StdEncoding.DecodeString(cookieValue)
if err != nil {
return false
}
return string(entranceValue) == entrance
}
func handleNoRoute(c *gin.Context, resType string) {
resPage, err := service.NewIAuthService().GetResponsePage()
if err != nil {
c.String(http.StatusInternalServerError, "Internal Server Error")
return
}
if resPage == "444" {
c.String(444, "")
return
}
file := fmt.Sprintf("html/%s.html", resPage)
if resPage == "200" && resType != "" {
file = fmt.Sprintf("html/200_%s.html", resType)
}
data, err := res.ErrorMsg.ReadFile(file)
if err != nil {
c.String(http.StatusInternalServerError, "Internal Server Error")
return
}
statusCode, err := strconv.Atoi(resPage)
if err != nil {
c.String(http.StatusInternalServerError, "Internal Server Error")
return
}
c.Data(statusCode, "text/html; charset=utf-8", data)
}
func isFrontendPath(c *gin.Context) bool {
reqUri := strings.TrimSuffix(c.Request.URL.Path, "/")
if _, ok := constant.WebUrlMap[reqUri]; ok {
return true
}
for _, route := range constant.DynamicRoutes {
if match, _ := regexp.MatchString(route, reqUri); match {
return true
}
}
return false
}
func checkFrontendPath(c *gin.Context) bool {
if !isFrontendPath(c) {
return false
}
authService := service.NewIAuthService()
if authService.GetSecurityEntrance() != "" {
return authService.IsLogin(c)
}
return true
}
func checkBindDomain(c *gin.Context) bool {
settingRepo := repo.NewISettingRepo()
status, _ := settingRepo.Get(repo.WithByKey("BindDomain"))
if len(status.Value) == 0 {
return true
}
domains := c.Request.Host
parts := strings.Split(c.Request.Host, ":")
if len(parts) > 0 {
domains = parts[0]
}
return domains == status.Value
}
func checkIPLimit(c *gin.Context) bool {
settingRepo := repo.NewISettingRepo()
status, _ := settingRepo.Get(repo.WithByKey("AllowIPs"))
if len(status.Value) == 0 {
return true
}
clientIP := c.ClientIP()
for _, ip := range strings.Split(status.Value, ",") {
if len(ip) == 0 {
continue
}
if ip == clientIP || (strings.Contains(ip, "/") && common.CheckIpInCidr(ip, clientIP)) {
return true
}
}
return false
}
func checkSession(c *gin.Context) bool {
_, err := global.SESSION.Get(c)
return err == nil
}
func setWebStatic(rootRouter *gin.RouterGroup) {
rootRouter.StaticFS("/public", http.FS(web.Favicon))
rootRouter.StaticFS("/favicon.ico", http.FS(web.Favicon))
@ -172,23 +41,14 @@ func setWebStatic(rootRouter *gin.RouterGroup) {
rootRouter.GET("/"+entrance, func(c *gin.Context) {
currentEntrance := authService.GetSecurityEntrance()
if currentEntrance != entrance {
handleNoRoute(c, "")
security.HandleNotSecurity(c, "")
return
}
toIndexHtml(c)
security.ToIndexHtml(c)
})
}
rootRouter.GET("/", func(c *gin.Context) {
if !checkEntrance(c) && !checkSession(c) {
handleNoRoute(c, "")
return
}
if !checkBindDomain(c) {
handleNoRoute(c, "err_domain")
return
}
if !checkIPLimit(c) {
handleNoRoute(c, "err_ip_limit")
if !security.CheckSecurity(c) {
return
}
entrance = authService.GetSecurityEntrance()
@ -229,26 +89,13 @@ func Routers() *gin.Engine {
router.InitRouter(PrivateGroup)
}
Router.Use(middleware.SessionAuth())
Router.Use(middleware.ApiAuth())
Router.Use(Proxy())
Router.NoRoute(func(c *gin.Context) {
if !checkBindDomain(c) {
handleNoRoute(c, "err_domain")
if !security.HandleNotRoute(c) {
return
}
if !checkIPLimit(c) {
handleNoRoute(c, "err_ip_limit")
return
}
if checkFrontendPath(c) {
toIndexHtml(c)
return
}
if isEntrancePath(c) {
toIndexHtml(c)
return
}
handleNoRoute(c, "")
security.HandleNotSecurity(c, "")
})
return Router

117
core/middleware/api_auth.go Normal file
View file

@ -0,0 +1,117 @@
package middleware
import (
"crypto/md5"
"encoding/hex"
"github.com/1Panel-dev/1Panel/core/app/api/v2/helper"
"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"
"net"
"strconv"
"strings"
"time"
)
func ApiAuth() gin.HandlerFunc {
return func(c *gin.Context) {
if strings.HasPrefix(c.Request.URL.Path, "/api/v2/core/auth") {
c.Next()
return
}
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.Next()
return
} else {
helper.BadAuth(c, "ErrApiConfigStatusInvalid", nil)
return
}
}
}
}
func isValid1PanelTimestamp(panelTimestamp string) bool {
apiKeyValidityTime := global.Api.ApiKeyValidityTime
apiTime, err := strconv.Atoi(apiKeyValidityTime)
if err != nil || apiTime < 0 {
global.LOG.Errorf("apiTime %d, err: %v", apiTime, err)
return false
}
if apiTime == 0 {
return true
}
panelTime, err := strconv.ParseInt(panelTimestamp, 10, 64)
if err != nil {
global.LOG.Errorf("panelTimestamp %s, panelTime %d, apiTime %d, err: %v", panelTimestamp, apiTime, panelTime, err)
return false
}
nowTime := time.Now().Unix()
tolerance := int64(60)
if panelTime > nowTime+tolerance {
global.LOG.Errorf("Valid Panel Timestamp, apiTime %d, panelTime %d, nowTime %d, err: %v", apiTime, panelTime, nowTime, err)
return false
}
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 isIPInWhiteList(clientIP string) bool {
ipWhiteString := global.Api.IpWhiteList
if len(ipWhiteString) == 0 {
global.LOG.Error("IP whitelist is empty")
return false
}
ipWhiteList, ipErr := common.HandleIPList(ipWhiteString)
if ipErr != nil {
global.LOG.Errorf("Failed to handle IP list: %v", ipErr)
return false
}
clientParsedIP := net.ParseIP(clientIP)
if clientParsedIP == nil {
return false
}
iPv4 := clientParsedIP.To4()
iPv6 := clientParsedIP.To16()
for _, cidr := range ipWhiteList {
if (iPv4 != nil && (cidr == "0.0.0.0" || cidr == "0.0.0.0/0" || iPv4.String() == cidr)) || (iPv6 != nil && (cidr == "::/0" || iPv6.String() == cidr)) {
return true
}
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
continue
}
if (iPv4 != nil && ipNet.Contains(iPv4)) || (iPv6 != nil && ipNet.Contains(iPv6)) {
return true
}
}
return false
}
func GenerateMD5(param string) string {
hash := md5.New()
hash.Write([]byte(param))
return hex.EncodeToString(hash.Sum(nil))
}

View file

@ -1,19 +1,13 @@
package middleware
import (
"crypto/md5"
"encoding/hex"
"net"
"strconv"
"strings"
"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"
"strconv"
"strings"
)
func SessionAuth() gin.HandlerFunc {
@ -23,32 +17,6 @@ func SessionAuth() gin.HandlerFunc {
return
}
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.Next()
return
} else {
helper.BadAuth(c, "ErrApiConfigStatusInvalid", nil)
return
}
}
psession, err := global.SESSION.Get(c)
if err != nil {
helper.BadAuth(c, "ErrNotLogin", err)
@ -70,70 +38,3 @@ func SessionAuth() gin.HandlerFunc {
c.Next()
}
}
func isValid1PanelTimestamp(panelTimestamp string) bool {
apiKeyValidityTime := global.Api.ApiKeyValidityTime
apiTime, err := strconv.Atoi(apiKeyValidityTime)
if err != nil || apiTime < 0 {
global.LOG.Errorf("apiTime %d, err: %v", apiTime, err)
return false
}
if apiTime == 0 {
return true
}
panelTime, err := strconv.ParseInt(panelTimestamp, 10, 64)
if err != nil {
global.LOG.Errorf("panelTimestamp %s, panelTime %d, apiTime %d, err: %v", panelTimestamp, apiTime, panelTime, err)
return false
}
nowTime := time.Now().Unix()
tolerance := int64(60)
if panelTime > nowTime+tolerance {
global.LOG.Errorf("Valid Panel Timestamp, apiTime %d, panelTime %d, nowTime %d, err: %v", apiTime, panelTime, nowTime, err)
return false
}
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 isIPInWhiteList(clientIP string) bool {
ipWhiteString := global.Api.IpWhiteList
if len(ipWhiteString) == 0 {
global.LOG.Error("IP whitelist is empty")
return false
}
ipWhiteList, ipErr := common.HandleIPList(ipWhiteString)
if ipErr != nil {
global.LOG.Errorf("Failed to handle IP list: %v", ipErr)
return false
}
clientParsedIP := net.ParseIP(clientIP)
if clientParsedIP == nil {
return false
}
iPv4 := clientParsedIP.To4()
iPv6 := clientParsedIP.To16()
for _, cidr := range ipWhiteList {
if (iPv4 != nil && (cidr == "0.0.0.0" || cidr == "0.0.0.0/0" || iPv4.String() == cidr)) || (iPv6 != nil && (cidr == "::/0" || iPv6.String() == cidr)) {
return true
}
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
continue
}
if (iPv4 != nil && ipNet.Contains(iPv4)) || (iPv6 != nil && ipNet.Contains(iPv6)) {
return true
}
}
return false
}
func GenerateMD5(param string) string {
hash := md5.New()
hash.Write([]byte(param))
return hex.EncodeToString(hash.Sum(nil))
}

View file

@ -0,0 +1,176 @@
package security
import (
"encoding/base64"
"fmt"
"github.com/1Panel-dev/1Panel/core/app/repo"
"github.com/1Panel-dev/1Panel/core/app/service"
"github.com/1Panel-dev/1Panel/core/cmd/server/res"
"github.com/1Panel-dev/1Panel/core/cmd/server/web"
"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"
"net/http"
"regexp"
"strconv"
"strings"
)
func HandleNotRoute(c *gin.Context) bool {
if !checkBindDomain(c) {
HandleNotSecurity(c, "err_domain")
return false
}
if !checkIPLimit(c) {
HandleNotSecurity(c, "err_ip_limit")
return false
}
if checkFrontendPath(c) {
ToIndexHtml(c)
return false
}
if isEntrancePath(c) {
ToIndexHtml(c)
return false
}
return true
}
func CheckSecurity(c *gin.Context) bool {
if !checkEntrance(c) && !checkSession(c) {
HandleNotSecurity(c, "")
return false
}
if !checkBindDomain(c) {
HandleNotSecurity(c, "err_domain")
return false
}
if !checkIPLimit(c) {
HandleNotSecurity(c, "err_ip_limit")
return false
}
return true
}
func ToIndexHtml(c *gin.Context) {
c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
c.Writer.WriteHeader(http.StatusOK)
_, _ = c.Writer.Write(web.IndexByte)
c.Writer.Flush()
}
func isEntrancePath(c *gin.Context) bool {
entrance := service.NewIAuthService().GetSecurityEntrance()
if entrance != "" && strings.TrimSuffix(c.Request.URL.Path, "/") == "/"+entrance {
return true
}
return false
}
func checkEntrance(c *gin.Context) bool {
authService := service.NewIAuthService()
entrance := authService.GetSecurityEntrance()
if entrance == "" {
return true
}
cookieValue, err := c.Cookie("SecurityEntrance")
if err != nil {
return false
}
entranceValue, err := base64.StdEncoding.DecodeString(cookieValue)
if err != nil {
return false
}
return string(entranceValue) == entrance
}
func HandleNotSecurity(c *gin.Context, resType string) {
resPage, err := service.NewIAuthService().GetResponsePage()
if err != nil {
c.String(http.StatusInternalServerError, "Internal Server Error")
return
}
if resPage == "444" {
c.String(444, "")
return
}
file := fmt.Sprintf("html/%s.html", resPage)
if resPage == "200" && resType != "" {
file = fmt.Sprintf("html/200_%s.html", resType)
}
data, err := res.ErrorMsg.ReadFile(file)
if err != nil {
c.String(http.StatusInternalServerError, "Internal Server Error")
return
}
statusCode, err := strconv.Atoi(resPage)
if err != nil {
c.String(http.StatusInternalServerError, "Internal Server Error")
return
}
c.Data(statusCode, "text/html; charset=utf-8", data)
}
func isFrontendPath(c *gin.Context) bool {
reqUri := strings.TrimSuffix(c.Request.URL.Path, "/")
if _, ok := constant.WebUrlMap[reqUri]; ok {
return true
}
for _, route := range constant.DynamicRoutes {
if match, _ := regexp.MatchString(route, reqUri); match {
return true
}
}
return false
}
func checkFrontendPath(c *gin.Context) bool {
if !isFrontendPath(c) {
return false
}
authService := service.NewIAuthService()
if authService.GetSecurityEntrance() != "" {
return authService.IsLogin(c)
}
return true
}
func checkBindDomain(c *gin.Context) bool {
settingRepo := repo.NewISettingRepo()
status, _ := settingRepo.Get(repo.WithByKey("BindDomain"))
if len(status.Value) == 0 {
return true
}
domains := c.Request.Host
parts := strings.Split(c.Request.Host, ":")
if len(parts) > 0 {
domains = parts[0]
}
return domains == status.Value
}
func checkIPLimit(c *gin.Context) bool {
settingRepo := repo.NewISettingRepo()
status, _ := settingRepo.Get(repo.WithByKey("AllowIPs"))
if len(status.Value) == 0 {
return true
}
clientIP := c.ClientIP()
for _, ip := range strings.Split(status.Value, ",") {
if len(ip) == 0 {
continue
}
if ip == clientIP || (strings.Contains(ip, "/") && common.CheckIpInCidr(ip, clientIP)) {
return true
}
}
return false
}
func checkSession(c *gin.Context) bool {
_, err := global.SESSION.Get(c)
return err == nil
}

View file

@ -44,6 +44,7 @@ const config = reactive({
});
const open = ref(false);
const showTail = ref(true);
const emit = defineEmits(['close']);
const openWithTaskID = (id: string, tail: boolean) => {
config.taskID = id;
@ -65,7 +66,7 @@ const openWithResourceID = (taskType: string, taskOperate: string, resourceID: n
const handleClose = () => {
open.value = false;
bus.emit('close', true);
emit('close', true);
bus.emit('refreshTask', true);
};

View file

@ -42,13 +42,14 @@ router.beforeEach((to, from, next) => {
if (to.path === '/apps/all' && to.query.install != undefined) {
return next();
}
const activeMenuKey = 'cachedRoute' + (to.meta.activeMenu || '');
if (to.query.uncached != undefined) {
const query = { ...to.query };
delete query.uncached;
localStorage.removeItem(activeMenuKey);
return next({ path: to.path, query });
}
const activeMenuKey = 'cachedRoute' + (to.meta.activeMenu || '');
const cachedRoute = localStorage.getItem(activeMenuKey);
if (
to.meta.activeMenu &&

View file

@ -185,7 +185,7 @@ import Install from '../detail/install/index.vue';
import router from '@/routers';
import { MsgSuccess } from '@/utils/message';
import { GlobalStore } from '@/store';
import { newUUID } from '@/utils/util';
import { newUUID, jumpToPath } from '@/utils/util';
import Detail from '../detail/index.vue';
import TaskLog from '@/components/log/task/index.vue';
import { storeToRefs } from 'pinia';
@ -254,7 +254,7 @@ const openInstall = (app: App.App) => {
case 'go':
case 'python':
case 'dotnet':
router.push({ path: '/websites/runtimes/' + app.type });
jumpToPath(router, '/websites/runtimes/' + app.type);
break;
default:
const params = {

View file

@ -72,7 +72,7 @@ import { ref } from 'vue';
import Install from './install/index.vue';
import router from '@/routers';
import { GlobalStore } from '@/store';
import { computeSizeFromMB } from '@/utils/util';
import { computeSizeFromMB, jumpToPath } from '@/utils/util';
import { storeToRefs } from 'pinia';
const globalStore = GlobalStore();
@ -133,16 +133,12 @@ const toLink = (link: string) => {
const openInstall = () => {
switch (app.value.type) {
case 'php':
router.push({ path: '/websites/runtimes/php' });
break;
case 'node':
router.push({ path: '/websites/runtimes/node' });
break;
case 'java':
router.push({ path: '/websites/runtimes/java' });
break;
case 'go':
router.push({ path: '/websites/runtimes/go' });
case 'python':
case 'dotnet':
jumpToPath(router, '/websites/runtimes/' + app.value.type);
break;
default:
const params = {

View file

@ -32,7 +32,7 @@
<el-form @submit.prevent ref="paramForm" :model="paramModel" label-position="top" :rules="rules">
<div v-for="(p, index) in params" :key="index">
<el-form-item
:prop="p.key"
:prop="'params.' + p.key"
:label="getLabel(p)"
v-if="p.showValue == undefined || p.showValue == ''"
>
@ -57,7 +57,7 @@
</el-select>
<el-input v-else v-model.trim="paramModel.params[p.key]" :disabled="!p.edit"></el-input>
</el-form-item>
<el-form-item :prop="p.key" :label="getLabel(p)" v-else>
<el-form-item :prop="'params.' + p.key" :label="getLabel(p)" v-else>
<el-input v-model.trim="p.showValue" :disabled="!p.edit"></el-input>
</el-form-item>
</div>

View file

@ -354,7 +354,7 @@
</div>
</template>
</LayoutContent>
<Backups ref="backupRef" @close="search" />
<Backups ref="backupRef" />
<Uploads ref="uploadRef" />
<AppResources ref="checkRef" @close="search" />
<AppDelete ref="deleteRef" @close="search" />