Browse Source

feat: 工具箱支持 FTP (#5039)

pull/5053/head
ssongliu 6 months ago committed by GitHub
parent
commit
64ed07e4fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      backend/app/api/v1/backup.go
  2. 1
      backend/app/api/v1/entry.go
  3. 135
      backend/app/api/v1/ftp.go
  4. 30
      backend/app/dto/ftp.go
  5. 11
      backend/app/model/ftp.go
  6. 80
      backend/app/repo/ftp.go
  7. 1
      backend/app/service/entry.go
  8. 177
      backend/app/service/ftp.go
  9. 1
      backend/init/migration/migrate.go
  10. 11
      backend/init/migration/migrations/v_1_10.go
  11. 6
      backend/router/ro_toolbox.go
  12. 143
      backend/utils/toolbox/pure-ftpd.go
  13. 263
      cmd/server/docs/docs.go
  14. 263
      cmd/server/docs/swagger.json
  15. 172
      cmd/server/docs/swagger.yaml
  16. 22
      frontend/src/api/interface/toolbox.ts
  17. 32
      frontend/src/api/modules/toolbox.ts
  18. 8
      frontend/src/lang/modules/en.ts
  19. 6
      frontend/src/lang/modules/tw.ts
  20. 6
      frontend/src/lang/modules/zh.ts
  21. 10
      frontend/src/routers/modules/toolbox.ts
  22. 268
      frontend/src/views/toolbox/ftp/index.vue
  23. 137
      frontend/src/views/toolbox/ftp/operate/index.vue
  24. 4
      frontend/src/views/toolbox/index.vue

4
backend/app/api/v1/backup.go

@ -118,11 +118,11 @@ func (b *BaseApi) LoadOneDriveInfo(c *gin.Context) {
// @Summary Delete backup account
// @Description 删除备份账号
// @Accept json
// @Param request body dto.BatchDeleteReq true "request"
// @Param request body dto.OperateByID true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /settings/backup/del [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":true,"db":"backup_accounts","output_column":"type","output_value":"types"}],"formatZH":"删除备份账号 [types]","formatEN":"delete backup account [types]"}
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"backup_accounts","output_column":"type","output_value":"types"}],"formatZH":"删除备份账号 [types]","formatEN":"delete backup account [types]"}
func (b *BaseApi) DeleteBackup(c *gin.Context) {
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {

1
backend/app/api/v1/entry.go

@ -37,6 +37,7 @@ var (
deviceService = service.NewIDeviceService()
fail2banService = service.NewIFail2BanService()
ftpService = service.NewIFtpService()
settingService = service.NewISettingService()
backupService = service.NewIBackupService()

135
backend/app/api/v1/ftp.go

@ -0,0 +1,135 @@
package v1
import (
"encoding/base64"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/gin-gonic/gin"
)
// @Tags FTP
// @Summary Page FTP user
// @Description 获取 FTP 账户列表分页
// @Accept json
// @Param request body dto.SearchWithPage true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Router /toolbox/ftp/search [post]
func (b *BaseApi) SearchFtp(c *gin.Context) {
var req dto.SearchWithPage
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, list, err := ftpService.SearchWithPage(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
}
// @Tags FTP
// @Summary Create FTP user
// @Description 创建 FTP 账户
// @Accept json
// @Param request body dto.FtpCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /toolbox/ftp [post]
// @x-panel-log {"bodyKeys":["user", "path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 FTP 账户 [user][path]","formatEN":"create FTP [user][path]"}
func (b *BaseApi) CreateFtp(c *gin.Context) {
var req dto.FtpCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if len(req.Password) != 0 {
pass, err := base64.StdEncoding.DecodeString(req.Password)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
req.Password = string(pass)
}
if err := ftpService.Create(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags FTP
// @Summary Delete FTP user
// @Description 删除 FTP 账户
// @Accept json
// @Param request body dto.BatchDeleteReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /toolbox/ftp/del [post]
// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"ftps","output_column":"user","output_value":"users"}],"formatZH":"删除 FTP 账户 [users]","formatEN":"delete FTP users [users]"}
func (b *BaseApi) DeleteFtp(c *gin.Context) {
var req dto.BatchDeleteReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := ftpService.Delete(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags FTP
// @Summary Sync FTP user
// @Description 同步 FTP 账户
// @Accept json
// @Param request body dto.BatchDeleteReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /toolbox/ftp/sync [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 FTP 账户","formatEN":"sync FTP users"}
func (b *BaseApi) SyncFtp(c *gin.Context) {
if err := ftpService.Sync(); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags FTP
// @Summary Update FTP user
// @Description 修改 FTP 账户
// @Accept json
// @Param request body dto.FtpUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /toolbox/ftp/update [post]
// @x-panel-log {"bodyKeys":["user", "path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 FTP 账户 [user][path]","formatEN":"update FTP [user][path]"}
func (b *BaseApi) UpdateFtp(c *gin.Context) {
var req dto.FtpUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if len(req.Password) != 0 {
pass, err := base64.StdEncoding.DecodeString(req.Password)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
req.Password = string(pass)
}
if err := ftpService.Update(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}

30
backend/app/dto/ftp.go

@ -0,0 +1,30 @@
package dto
import "time"
type FtpInfo struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"createdAt"`
User string `json:"user"`
Password string `json:"password"`
Path string `json:"path"`
Status string `json:"status"`
Description string `json:"description"`
}
type FtpCreate struct {
User string `json:"user" validate:"required"`
Password string `json:"password" validate:"required"`
Path string `json:"path" validate:"required"`
Description string `json:"description"`
}
type FtpUpdate struct {
ID uint `json:"id"`
Password string `json:"password" validate:"required"`
Path string `json:"path" validate:"required"`
Status string `json:"status"`
Description string `json:"description"`
}

11
backend/app/model/ftp.go

@ -0,0 +1,11 @@
package model
type Ftp struct {
BaseModel
User string `gorm:"type:varchar(64);not null" json:"user"`
Password string `gorm:"type:varchar(64);not null" json:"password"`
Status string `gorm:"type:varchar(64);not null" json:"status"`
Path string `gorm:"type:varchar(64);not null" json:"path"`
Description string `gorm:"type:varchar(64);not null" json:"description"`
}

80
backend/app/repo/ftp.go

@ -0,0 +1,80 @@
package repo
import (
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/global"
"gorm.io/gorm"
)
type FtpRepo struct{}
type IFtpRepo interface {
Get(opts ...DBOption) (model.Ftp, error)
GetList(opts ...DBOption) ([]model.Ftp, error)
Page(limit, offset int, opts ...DBOption) (int64, []model.Ftp, error)
Create(ftp *model.Ftp) error
Update(id uint, vars map[string]interface{}) error
Delete(opts ...DBOption) error
WithByUser(user string) DBOption
}
func NewIFtpRepo() IFtpRepo {
return &FtpRepo{}
}
func (u *FtpRepo) Get(opts ...DBOption) (model.Ftp, error) {
var ftp model.Ftp
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&ftp).Error
return ftp, err
}
func (h *FtpRepo) WithByUser(user string) DBOption {
return func(g *gorm.DB) *gorm.DB {
if len(user) == 0 {
return g
}
return g.Where("user like ?", "%"+user+"%")
}
}
func (u *FtpRepo) GetList(opts ...DBOption) ([]model.Ftp, error) {
var ftps []model.Ftp
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&ftps).Error
return ftps, err
}
func (h *FtpRepo) Page(page, size int, opts ...DBOption) (int64, []model.Ftp, error) {
var users []model.Ftp
db := global.DB.Model(&model.Ftp{})
for _, opt := range opts {
db = opt(db)
}
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&users).Error
return count, users, err
}
func (h *FtpRepo) Create(ftp *model.Ftp) error {
return global.DB.Create(ftp).Error
}
func (h *FtpRepo) Update(id uint, vars map[string]interface{}) error {
return global.DB.Model(&model.Ftp{}).Where("id = ?", id).Updates(vars).Error
}
func (h *FtpRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.Ftp{}).Error
}

1
backend/app/service/entry.go

@ -24,6 +24,7 @@ var (
hostRepo = repo.NewIHostRepo()
groupRepo = repo.NewIGroupRepo()
commandRepo = repo.NewICommandRepo()
ftpRepo = repo.NewIFtpRepo()
settingRepo = repo.NewISettingRepo()
backupRepo = repo.NewIBackupRepo()

177
backend/app/service/ftp.go

@ -0,0 +1,177 @@
package service
import (
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/utils/encrypt"
"github.com/1Panel-dev/1Panel/backend/utils/toolbox"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
)
type FtpService struct{}
type IFtpService interface {
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error)
Create(req dto.FtpCreate) error
Delete(req dto.BatchDeleteReq) error
Update(req dto.FtpUpdate) error
Sync() error
}
func NewIFtpService() IFtpService {
return &FtpService{}
}
func (f *FtpService) SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) {
total, lists, err := ftpRepo.Page(req.Page, req.PageSize, ftpRepo.WithByUser(req.Info), commonRepo.WithOrderBy("created_at desc"))
if err != nil {
return 0, nil, err
}
var users []dto.FtpInfo
for _, user := range lists {
var item dto.FtpInfo
if err := copier.Copy(&item, &user); err != nil {
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
}
item.Password, _ = encrypt.StringDecrypt(item.Password)
users = append(users, item)
}
return total, users, err
}
func (f *FtpService) Sync() error {
client, err := toolbox.NewFtpClient()
if err != nil {
return err
}
lists, err := client.LoadList()
if err != nil {
return nil
}
listsInDB, err := ftpRepo.GetList()
if err != nil {
return err
}
sameData := make(map[string]struct{})
for _, item := range lists {
for _, itemInDB := range listsInDB {
if item.User == itemInDB.User {
sameData[item.User] = struct{}{}
if item.Path != itemInDB.Path {
_ = ftpRepo.Update(itemInDB.ID, map[string]interface{}{"path": item.Path, "status": constant.StatusDisable})
}
break
}
}
}
for _, item := range lists {
if _, ok := sameData[item.User]; !ok {
_ = ftpRepo.Create(&model.Ftp{User: item.User, Path: item.Path, Status: constant.StatusDisable})
}
}
for _, item := range listsInDB {
if _, ok := sameData[item.User]; !ok {
_ = ftpRepo.Update(item.ID, map[string]interface{}{"status": "deleted"})
}
}
return nil
}
func (f *FtpService) Create(req dto.FtpCreate) error {
pass, err := encrypt.StringEncrypt(req.Password)
if err != nil {
return err
}
userInDB, _ := ftpRepo.Get(hostRepo.WithByUser(req.User))
if userInDB.ID != 0 {
return constant.ErrRecordExist
}
client, err := toolbox.NewFtpClient()
if err != nil {
return err
}
if err := client.UserAdd(req.User, req.Password, req.Path); err != nil {
return err
}
var ftp model.Ftp
if err := copier.Copy(&ftp, &req); err != nil {
return errors.WithMessage(constant.ErrStructTransform, err.Error())
}
ftp.Status = constant.StatusEnable
ftp.Password = pass
if err := ftpRepo.Create(&ftp); err != nil {
return err
}
return nil
}
func (f *FtpService) Delete(req dto.BatchDeleteReq) error {
client, err := toolbox.NewFtpClient()
if err != nil {
return err
}
for _, id := range req.Ids {
ftpItem, err := ftpRepo.Get(commonRepo.WithByID(id))
if err != nil {
return err
}
_ = client.UserDel(ftpItem.User)
_ = ftpRepo.Delete(commonRepo.WithByID(id))
}
return nil
}
func (f *FtpService) Update(req dto.FtpUpdate) error {
pass, err := encrypt.StringEncrypt(req.Password)
if err != nil {
return err
}
ftpItem, _ := ftpRepo.Get(commonRepo.WithByID(req.ID))
if ftpItem.ID == 0 {
return constant.ErrRecordNotFound
}
passItem, err := encrypt.StringDecrypt(ftpItem.Password)
if err != nil {
return err
}
client, err := toolbox.NewFtpClient()
if err != nil {
return err
}
needReload := false
updates := make(map[string]interface{})
if req.Password != passItem {
if err := client.SetPasswd(ftpItem.User, req.Password); err != nil {
return err
}
updates["password"] = pass
needReload = true
}
if req.Status != ftpItem.Status {
if err := client.SetStatus(ftpItem.User, req.Status); err != nil {
return err
}
updates["status"] = req.Status
needReload = true
}
if req.Path != ftpItem.Path {
if err := client.SetPath(ftpItem.User, req.Path); err != nil {
return err
}
updates["path"] = req.Path
needReload = true
}
if req.Description != ftpItem.Description {
updates["description"] = req.Description
}
if needReload {
_ = client.Reload()
}
if len(updates) != 0 {
return ftpRepo.Update(ftpItem.ID, updates)
}
return nil
}

1
backend/init/migration/migrate.go

@ -86,6 +86,7 @@ func Init() {
migrations.AddWebsiteSSLColumn,
migrations.AddRedisCommand,
migrations.AddMonitorMenu,
migrations.AddFtp,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

11
backend/init/migration/migrations/v_1_10.go

@ -2,6 +2,7 @@ package migrations
import (
"encoding/json"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/global"
@ -202,3 +203,13 @@ var AddMonitorMenu = &gormigrate.Migration{
return tx.Model(&model.Setting{}).Where("key", "XpackHideMenu").Updates(map[string]interface{}{"value": string(data)}).Error
},
}
var AddFtp = &gormigrate.Migration{
ID: "20240517-add-ftp",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.Ftp{}); err != nil {
return err
}
return nil
},
}

6
backend/router/ro_toolbox.go

@ -36,5 +36,11 @@ func (s *ToolboxRouter) InitRouter(Router *gin.RouterGroup) {
toolboxRouter.POST("/fail2ban/operate/sshd", baseApi.OperateSSHD)
toolboxRouter.POST("/fail2ban/update", baseApi.UpdateFail2BanConf)
toolboxRouter.POST("/fail2ban/update/byconf", baseApi.UpdateFail2BanConfByFile)
toolboxRouter.POST("/ftp/search", baseApi.SearchFtp)
toolboxRouter.POST("/ftp", baseApi.CreateFtp)
toolboxRouter.POST("/ftp/update", baseApi.UpdateFtp)
toolboxRouter.POST("/ftp/del", baseApi.DeleteFtp)
toolboxRouter.POST("/ftp/sync", baseApi.SyncFtp)
}
}

143
backend/utils/toolbox/pure-ftpd.go

@ -0,0 +1,143 @@
package toolbox
import (
"errors"
"os/user"
"strings"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
)
type Ftp struct {
DefaultUser string
}
type FtpClient interface {
Status() (bool, error)
LoadList() ([]FtpList, error)
UserAdd(username, path, passwd string) error
UserDel(username string) error
SetPasswd(username, passwd string) error
Reload() error
}
func NewFtpClient() (*Ftp, error) {
userItem, err := user.LookupId("1000")
if err == nil {
return &Ftp{DefaultUser: userItem.Username}, err
}
if err.Error() != user.UnknownUserIdError(1000).Error() {
return nil, err
}
groupItem, err := user.LookupGroupId("1000")
if err == nil {
stdout2, err := cmd.Execf("useradd -u 1000 -g %s %s", groupItem.Name, "1panel")
if err != nil {
return nil, errors.New(stdout2)
}
return &Ftp{DefaultUser: "1panel"}, nil
}
if err.Error() != user.UnknownGroupIdError("1000").Error() {
return nil, err
}
stdout, err := cmd.Exec("groupadd -g 1000 1panel")
if err != nil {
return nil, errors.New(string(stdout))
}
stdout2, err := cmd.Execf("useradd -u 1000 -g %s %s", groupItem.Name, userItem.Username)
if err != nil {
return nil, errors.New(stdout2)
}
return &Ftp{DefaultUser: "1panel"}, nil
}
func (f *Ftp) Status() (bool, error) {
return systemctl.IsActive("pure-ftpd.service")
}
func (f *Ftp) UserAdd(username, passwd, path string) error {
std, err := cmd.Execf("pure-pw useradd %s -u %s -d %s <<EOF \n%s\n%s\nEOF", username, f.DefaultUser, path, passwd, passwd)
if err != nil {
return errors.New(std)
}
_ = f.Reload()
std2, err := cmd.Execf("chown %s %s", f.DefaultUser, path)
if err != nil {
return errors.New(std2)
}
return nil
}
func (f *Ftp) UserDel(username string) error {
std, err := cmd.Execf("pure-pw userdel %s", username)
if err != nil {
return errors.New(std)
}
_ = f.Reload()
return nil
}
func (f *Ftp) SetPasswd(username, passwd string) error {
std, err := cmd.Execf("pure-pw passwd %s <<EOF \n%s\n%s\nEOF", username, passwd, passwd)
if err != nil {
return errors.New(std)
}
return nil
}
func (f *Ftp) SetPath(username, path string) error {
std, err := cmd.Execf("pure-pw usermod %s -d %s", username, path)
if err != nil {
return errors.New(std)
}
std2, err := cmd.Execf("chown %s %s", f.DefaultUser, path)
if err != nil {
return errors.New(std2)
}
return nil
}
func (f *Ftp) SetStatus(username, status string) error {
statusItem := "''"
if status == constant.StatusDisable {
statusItem = "1"
}
std, err := cmd.Execf("pure-pw usermod %s -r %s", username, statusItem)
if err != nil {
return errors.New(std)
}
return nil
}
func (f *Ftp) LoadList() ([]FtpList, error) {
std, err := cmd.Exec("pure-pw list")
if err != nil {
return nil, errors.New(std)
}
var lists []FtpList
lines := strings.Split(std, "\n")
for _, line := range lines {
parts := strings.Fields(line)
if len(parts) < 2 {
continue
}
lists = append(lists, FtpList{User: parts[0], Path: strings.ReplaceAll(parts[1], "/./", "")})
}
return lists, nil
}
type FtpList struct {
User string
Path string
}
func (f *Ftp) Reload() error {
std, err := cmd.Exec("pure-pw mkdb")
if err != nil {
return errors.New(std)
}
return nil
}

263
cmd/server/docs/docs.go

@ -9561,7 +9561,7 @@ const docTemplate = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.BatchDeleteReq"
"$ref": "#/definitions/dto.OperateByID"
}
}
],
@ -9576,7 +9576,7 @@ const docTemplate = `{
"db": "backup_accounts",
"input_column": "id",
"input_value": "id",
"isList": true,
"isList": false,
"output_column": "type",
"output_value": "types"
}
@ -11512,6 +11512,219 @@ const docTemplate = `{
}
}
},
"/toolbox/ftp": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "创建 FTP 账户",
"consumes": [
"application/json"
],
"tags": [
"FTP"
],
"summary": "Create FTP user",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.FtpCreate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"user",
"path"
],
"formatEN": "create FTP [user][path]",
"formatZH": "创建 FTP 账户 [user][path]",
"paramKeys": []
}
}
},
"/toolbox/ftp/del": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "删除 FTP 账户",
"consumes": [
"application/json"
],
"tags": [
"FTP"
],
"summary": "Delete FTP user",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.BatchDeleteReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "ftps",
"input_column": "id",
"input_value": "ids",
"isList": true,
"output_column": "user",
"output_value": "users"
}
],
"bodyKeys": [
"ids"
],
"formatEN": "delete FTP users [users]",
"formatZH": "删除 FTP 账户 [users]",
"paramKeys": []
}
}
},
"/toolbox/ftp/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 FTP 账户列表分页",
"consumes": [
"application/json"
],
"tags": [
"FTP"
],
"summary": "Page FTP user",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SearchWithPage"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
}
},
"/toolbox/ftp/sync": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "同步 FTP 账户",
"consumes": [
"application/json"
],
"tags": [
"FTP"
],
"summary": "Sync FTP user",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.BatchDeleteReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [],
"formatEN": "sync FTP users",
"formatZH": "同步 FTP 账户",
"paramKeys": []
}
}
},
"/toolbox/ftp/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改 FTP 账户",
"consumes": [
"application/json"
],
"tags": [
"FTP"
],
"summary": "Update FTP user",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.FtpUpdate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"user",
"path"
],
"formatEN": "update FTP [user][path]",
"formatZH": "修改 FTP 账户 [user][path]",
"paramKeys": []
}
}
},
"/toolbox/scan": {
"post": {
"security": [
@ -15969,6 +16182,52 @@ const docTemplate = `{
}
}
},
"dto.FtpCreate": {
"type": "object",
"required": [
"password",
"path",
"user"
],
"properties": {
"description": {
"type": "string"
},
"password": {
"type": "string"
},
"path": {
"type": "string"
},
"user": {
"type": "string"
}
}
},
"dto.FtpUpdate": {
"type": "object",
"required": [
"password",
"path"
],
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"password": {
"type": "string"
},
"path": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"dto.GPUInfo": {
"type": "object",
"properties": {

263
cmd/server/docs/swagger.json

@ -9554,7 +9554,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.BatchDeleteReq"
"$ref": "#/definitions/dto.OperateByID"
}
}
],
@ -9569,7 +9569,7 @@
"db": "backup_accounts",
"input_column": "id",
"input_value": "id",
"isList": true,
"isList": false,
"output_column": "type",
"output_value": "types"
}
@ -11505,6 +11505,219 @@
}
}
},
"/toolbox/ftp": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "创建 FTP 账户",
"consumes": [
"application/json"
],
"tags": [
"FTP"
],
"summary": "Create FTP user",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.FtpCreate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"user",
"path"
],
"formatEN": "create FTP [user][path]",
"formatZH": "创建 FTP 账户 [user][path]",
"paramKeys": []
}
}
},
"/toolbox/ftp/del": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "删除 FTP 账户",
"consumes": [
"application/json"
],
"tags": [
"FTP"
],
"summary": "Delete FTP user",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.BatchDeleteReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [
{
"db": "ftps",
"input_column": "id",
"input_value": "ids",
"isList": true,
"output_column": "user",
"output_value": "users"
}
],
"bodyKeys": [
"ids"
],
"formatEN": "delete FTP users [users]",
"formatZH": "删除 FTP 账户 [users]",
"paramKeys": []
}
}
},
"/toolbox/ftp/search": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取 FTP 账户列表分页",
"consumes": [
"application/json"
],
"tags": [
"FTP"
],
"summary": "Page FTP user",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.SearchWithPage"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.PageResult"
}
}
}
}
},
"/toolbox/ftp/sync": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "同步 FTP 账户",
"consumes": [
"application/json"
],
"tags": [
"FTP"
],
"summary": "Sync FTP user",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.BatchDeleteReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [],
"formatEN": "sync FTP users",
"formatZH": "同步 FTP 账户",
"paramKeys": []
}
}
},
"/toolbox/ftp/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改 FTP 账户",
"consumes": [
"application/json"
],
"tags": [
"FTP"
],
"summary": "Update FTP user",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/dto.FtpUpdate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFunctions": [],
"bodyKeys": [
"user",
"path"
],
"formatEN": "update FTP [user][path]",
"formatZH": "修改 FTP 账户 [user][path]",
"paramKeys": []
}
}
},
"/toolbox/scan": {
"post": {
"security": [
@ -15962,6 +16175,52 @@
}
}
},
"dto.FtpCreate": {
"type": "object",
"required": [
"password",
"path",
"user"
],
"properties": {
"description": {
"type": "string"
},
"password": {
"type": "string"
},
"path": {
"type": "string"
},
"user": {
"type": "string"
}
}
},
"dto.FtpUpdate": {
"type": "object",
"required": [
"password",
"path"
],
"properties": {
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"password": {
"type": "string"
},
"path": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"dto.GPUInfo": {
"type": "object",
"properties": {

172
cmd/server/docs/swagger.yaml

@ -1202,6 +1202,37 @@ definitions:
- type
- vars
type: object
dto.FtpCreate:
properties:
description:
type: string
password:
type: string
path:
type: string
user:
type: string
required:
- password
- path
- user
type: object
dto.FtpUpdate:
properties:
description:
type: string
id:
type: integer
password:
type: string
path:
type: string
status:
type: string
required:
- password
- path
type: object
dto.GPUInfo:
properties:
fanSpeed:
@ -11162,7 +11193,7 @@ paths:
name: request
required: true
schema:
$ref: '#/definitions/dto.BatchDeleteReq'
$ref: '#/definitions/dto.OperateByID'
responses:
"200":
description: OK
@ -11176,7 +11207,7 @@ paths:
- db: backup_accounts
input_column: id
input_value: id
isList: true
isList: false
output_column: type
output_value: types
bodyKeys:
@ -12397,6 +12428,143 @@ paths:
summary: Update fail2ban conf by file
tags:
- Fail2ban
/toolbox/ftp:
post:
consumes:
- application/json
description: 创建 FTP 账户
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.FtpCreate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Create FTP user
tags:
- FTP
x-panel-log:
BeforeFunctions: []
bodyKeys:
- user
- path
formatEN: create FTP [user][path]
formatZH: 创建 FTP 账户 [user][path]
paramKeys: []
/toolbox/ftp/del:
post:
consumes:
- application/json
description: 删除 FTP 账户
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.BatchDeleteReq'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Delete FTP user
tags:
- FTP
x-panel-log:
BeforeFunctions:
- db: ftps
input_column: id
input_value: ids
isList: true
output_column: user
output_value: users
bodyKeys:
- ids
formatEN: delete FTP users [users]
formatZH: 删除 FTP 账户 [users]
paramKeys: []
/toolbox/ftp/search:
post:
consumes:
- application/json
description: 获取 FTP 账户列表分页
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.SearchWithPage'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.PageResult'
security:
- ApiKeyAuth: []
summary: Page FTP user
tags:
- FTP
/toolbox/ftp/sync:
post:
consumes:
- application/json
description: 同步 FTP 账户
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.BatchDeleteReq'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Sync FTP user
tags:
- FTP
x-panel-log:
BeforeFunctions: []
bodyKeys: []
formatEN: sync FTP users
formatZH: 同步 FTP 账户
paramKeys: []
/toolbox/ftp/update:
post:
consumes:
- application/json
description: 修改 FTP 账户
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/dto.FtpUpdate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Update FTP user
tags:
- FTP
x-panel-log:
BeforeFunctions: []
bodyKeys:
- user
- path
formatEN: update FTP [user][path]
formatZH: 修改 FTP 账户 [user][path]
paramKeys: []
/toolbox/scan:
post:
description: 扫描系统垃圾文件

22
frontend/src/api/interface/toolbox.ts

@ -76,4 +76,26 @@ export namespace Toolbox {
ips: Array<string>;
operate: string;
}
export interface FtpInfo {
id: number;
user: string;
password: string;
status: string;
path: string;
description: string;
}
export interface FtpCreate {
user: string;
password: string;
path: string;
description: string;
}
export interface FtpUpdate {
id: number;
password: string;
status: string;
path: string;
description: string;
}
}

32
frontend/src/api/modules/toolbox.ts

@ -1,8 +1,9 @@
import http from '@/api';
import { UpdateByFile } from '../interface';
import { ReqPage, ResPage, UpdateByFile } from '../interface';
import { Toolbox } from '../interface/toolbox';
import { Base64 } from 'js-base64';
import { TimeoutEnum } from '@/enums/http-enum';
import { deepCopy } from '@/utils/util';
// device
export const getDeviceBase = () => {
@ -68,3 +69,32 @@ export const updateFail2ban = (param: Toolbox.Fail2banUpdate) => {
export const updateFail2banByFile = (param: UpdateByFile) => {
return http.post(`/toolbox/fail2ban/update/byconf`, param, TimeoutEnum.T_5M);
};
// ftp
export const searchFtp = (param: ReqPage) => {
return http.post<ResPage<Toolbox.FtpInfo>>(`/toolbox/ftp/search`, param);
};
export const syncFtp = () => {
return http.post(`/toolbox/ftp/sync`);
};
export const createFtp = (params: Toolbox.FtpCreate) => {
let request = deepCopy(params) as Toolbox.FtpCreate;
if (request.password) {
request.password = Base64.encode(request.password);
}
return http.post(`/toolbox/ftp`, request);
};
export const updateFtp = (params: Toolbox.FtpUpdate) => {
let request = deepCopy(params) as Toolbox.FtpUpdate;
if (request.password) {
request.password = Base64.encode(request.password);
}
return http.post(`/toolbox/ftp/update`, request);
};
export const deleteFtp = (params: { ids: number[] }) => {
return http.post(`/toolbox/ftp/del`, params);
};

8
frontend/src/lang/modules/en.ts

@ -1029,6 +1029,14 @@ const message = {
logPath: 'Log Path',
logPathHelper: 'Default is /var/log/secure or /var/log/auth.log',
},
ftp: {
ftp: 'FTP Account',
enableHelper:
'Enabling the selected FTP account will restore its access permissions. Do you want to continue?',
disableHelper:
'Disabling the selected FTP account will revoke its access permissions. Do you want to continue?',
syncHelper: 'Sync FTP account data between server and database. Do you want to continue?',
},
},
logs: {
panelLog: 'Panel logs',

6
frontend/src/lang/modules/tw.ts

@ -976,6 +976,12 @@ const message = {
logPath: '日誌路徑',
logPathHelper: '預設為 /var/log/secure 或者 /var/log/auth.log',
},
ftp: {
ftp: 'FTP 帳戶',
enableHelper: '啟用選取的 FTP 帳號後 FTP 帳號將恢復訪問權限是否繼續操作',
disableHelper: '停用選取的 FTP 帳號後 FTP 帳號將失去訪問權限是否繼續操作',
syncHelper: '同步伺服器與資料庫中的 FTP 帳戶資料是否繼續操作',
},
},
logs: {
panelLog: '面板日誌',

6
frontend/src/lang/modules/zh.ts

@ -977,6 +977,12 @@ const message = {
logPath: '日志路径',
logPathHelper: '默认 /var/log/secure 或者 /var/log/auth.log',
},
ftp: {
ftp: 'FTP 账户',
enableHelper: '启用选中的 FTP 账号后 FTP 账号恢复访问权限是否继续操作',
disableHelper: '停用选中的 FTP 账号后 FTP 账号将失去访问权限是否继续操作',
syncHelper: '同步服务器与数据库中的 FTP 账户数据是否继续操作',
},
},
logs: {
panelLog: '面板日志',

10
frontend/src/routers/modules/toolbox.ts

@ -37,6 +37,16 @@ const toolboxRouter = {
requiresAuth: false,
},
},
{
path: 'ftp',
name: 'FTP',
component: () => import('@/views/toolbox/ftp/index.vue'),
hidden: true,
meta: {
activeMenu: '/toolbox',
requiresAuth: false,
},
},
{
path: 'fail2Ban',
name: 'Fail2ban',

268
frontend/src/views/toolbox/ftp/index.vue

@ -0,0 +1,268 @@
<template>
<div>
<LayoutContent v-loading="loading" title="FTP">
<template #toolbar>
<el-row>
<el-col :xs="24" :sm="16" :md="16" :lg="16" :xl="16">
<el-button type="primary" @click="onOpenDialog('add')">
{{ $t('commons.button.add') }} FTP
</el-button>
<el-button @click="onSync()">
{{ $t('commons.button.sync') }}
</el-button>
<el-button plain :disabled="selects.length === 0" @click="onDelete(null)">
{{ $t('commons.button.delete') }}
</el-button>
</el-col>
<el-col :xs="24" :sm="8" :md="8" :lg="8" :xl="8">
<TableSearch @search="search()" v-model:searchName="searchName" />
</el-col>
</el-row>
</template>
<template #main>
<ComplexTable
:pagination-config="paginationConfig"
v-model:selects="selects"
@sort-change="search"
@search="search"
:data="data"
>
<el-table-column type="selection" fix />
<el-table-column
:label="$t('commons.login.username')"
:min-width="60"
prop="user"
show-overflow-tooltip
/>
<el-table-column :label="$t('commons.login.password')" prop="password">
<template #default="{ row }">
<div v-if="row.password.length === 0">-</div>
<div v-else class="flex items-center">
<div class="star-center" v-if="!row.showPassword">
<span>**********</span>
</div>
<div>
<span v-if="row.showPassword">
{{ row.password }}
</span>
</div>
<el-button
v-if="!row.showPassword"
link
@click="row.showPassword = true"
icon="View"
class="ml-1.5"
></el-button>
<el-button
v-if="row.showPassword"
link
@click="row.showPassword = false"
icon="Hide"
class="ml-1.5"
></el-button>
<div>
<CopyButton :content="row.password" type="icon" />
</div>
</div>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.status')" :min-width="60" prop="status">
<template #default="{ row }">
<el-tag v-if="row.status === 'deleted'" type="info">{{ $t('database.isDelete') }}</el-tag>
<el-button
v-if="row.status === 'Enable'"
@click="onChangeStatus(row, 'disable')"
link
icon="VideoPlay"
type="success"
>
{{ $t('commons.status.enabled') }}
</el-button>
<el-button
v-if="row.status === 'Disable'"
icon="VideoPause"
@click="onChangeStatus(row, 'enable')"
link
type="danger"
>
{{ $t('commons.status.disabled') }}
</el-button>
</template>
</el-table-column>
<el-table-column :label="$t('file.root')" :min-width="120" prop="path" show-overflow-tooltip />
<el-table-column
:label="$t('commons.table.description')"
:min-width="80"
prop="description"
show-overflow-tooltip
/>
<el-table-column
:label="$t('commons.table.createdAt')"
:formatter="dateFormat"
:min-width="80"
prop="createdAt"
/>
<fu-table-operations
width="240px"
:buttons="buttons"
:ellipsis="10"
:label="$t('commons.table.operate')"
fix
/>
</ComplexTable>
</template>
</LayoutContent>
<OpDialog ref="opRef" @search="search" @submit="onSubmitDelete()" />
<OperateDialog @search="search" ref="dialogRef" />
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue';
import i18n from '@/lang';
import { dateFormat } from '@/utils/util';
import { MsgSuccess } from '@/utils/message';
import { deleteFtp, searchFtp, updateFtp, syncFtp } from '@/api/modules/toolbox';
import OperateDialog from '@/views/toolbox/ftp/operate/index.vue';
import { Toolbox } from '@/api/interface/toolbox';
const loading = ref();
const selects = ref<any>([]);
const data = ref();
const paginationConfig = reactive({
cacheSizeKey: 'ftp-page-size',
currentPage: 1,
pageSize: 10,
total: 0,
orderBy: 'created_at',
order: 'null',
});
const searchName = ref();
const opRef = ref();
const dialogRef = ref();
const operateIDs = ref();
const search = async (column?: any) => {
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
paginationConfig.order = column?.order ? column.order : paginationConfig.order;
let params = {
info: searchName.value,
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
};
loading.value = true;
await searchFtp(params)
.then((res) => {
loading.value = false;
data.value = res.data.items || [];
paginationConfig.total = res.data.total;
})
.catch(() => {
loading.value = false;
});
};
const onChangeStatus = async (row: Toolbox.FtpInfo, status: string) => {
ElMessageBox.confirm(i18n.global.t('toolbox.ftp.' + status + 'Helper'), i18n.global.t('cronjob.changeStatus'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
row.status = status === 'enable' ? 'Enable' : 'Disable';
await updateFtp(row);
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
search();
});
};
const onOpenDialog = async (title: string, rowData: Partial<Toolbox.FtpInfo> = {}) => {
let params = {
title,
rowData: { ...rowData },
};
dialogRef.value!.acceptParams(params);
};
const onSync = async () => {
ElMessageBox.confirm(i18n.global.t('toolbox.ftp.syncHelper'), i18n.global.t('commons.button.sync'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
}).then(async () => {
loading.value = true;
await syncFtp()
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('toolbox.ftp.operationSuccess'));
search();
})
.catch(() => {
loading.value = false;
});
});
};
const onDelete = async (row: Toolbox.FtpInfo | null) => {
let names = [];
let ids = [];
if (row) {
ids = [row.id];
names = [row.user];
} else {
for (const item of selects.value) {
names.push(item.user);
ids.push(item.id);
}
}
operateIDs.value = ids;
opRef.value.acceptParams({
title: i18n.global.t('commons.button.delete'),
names: names,
msg: i18n.global.t('commons.msg.operatorHelper', [
i18n.global.t('cronjob.cronTask'),
i18n.global.t('commons.button.delete'),
]),
api: null,
params: null,
});
};
const onSubmitDelete = async () => {
loading.value = true;
await deleteFtp({ ids: operateIDs.value })
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.deleteSuccess'));
search();
})
.catch(() => {
loading.value = false;
});
};
const buttons = [
{
label: i18n.global.t('commons.button.edit'),
disabled: (row: Toolbox.FtpInfo) => {
return row.status === 'deleted';
},
click: (row: Toolbox.FtpInfo) => {
onOpenDialog('edit', row);
},
},
{
label: i18n.global.t('commons.button.delete'),
disabled: (row: Toolbox.FtpInfo) => {
return row.status === 'deleted';
},
click: (row: Toolbox.FtpInfo) => {
onDelete(row);
},
},
];
onMounted(() => {
search();
});
</script>

137
frontend/src/views/toolbox/ftp/operate/index.vue

@ -0,0 +1,137 @@
<template>
<el-drawer
v-model="drawerVisible"
:destroy-on-close="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
size="50%"
>
<template #header>
<DrawerHeader
:header="title"
:hideResource="dialogData.title === 'add'"
:resource="dialogData.rowData?.user"
:back="handleClose"
/>
</template>
<el-form ref="formRef" label-position="top" :model="dialogData.rowData" :rules="rules">
<el-row type="flex" justify="center">
<el-col :span="22">
<el-form-item :label="$t('commons.login.username')" prop="user">
<el-input
:disabled="dialogData.title === 'edit'"
clearable
v-model.trim="dialogData.rowData!.user"
/>
</el-form-item>
<el-form-item :label="$t('commons.login.password')" prop="password">
<el-input clearable v-model="dialogData.rowData!.password" />
</el-form-item>
<el-form-item :label="$t('file.root')" prop="path">
<el-input v-model="dialogData.rowData!.path">
<template #prepend>
<FileList @choose="loadDir" :dir="true"></FileList>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('commons.table.description')" prop="description">
<el-input type="textarea" :rows="3" clearable v-model="dialogData.rowData!.description" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="onSubmit(formRef)">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { Rules } from '@/global/form-rules';
import FileList from '@/components/file-list/index.vue';
import i18n from '@/lang';
import { ElForm } from 'element-plus';
import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message';
import { Toolbox } from '@/api/interface/toolbox';
import { createFtp, updateFtp } from '@/api/modules/toolbox';
interface DialogProps {
title: string;
rowData?: Toolbox.FtpInfo;
getTableList?: () => Promise<any>;
}
const loading = ref();
const title = ref<string>('');
const drawerVisible = ref(false);
const dialogData = ref<DialogProps>({
title: '',
});
const acceptParams = (params: DialogProps): void => {
dialogData.value = params;
title.value = i18n.global.t('commons.button.' + dialogData.value.title);
drawerVisible.value = true;
};
const emit = defineEmits<{ (e: 'search'): void }>();
const handleClose = () => {
drawerVisible.value = false;
};
const rules = reactive({
user: [Rules.requiredInput, Rules.noSpace],
password: [Rules.requiredInput],
path: [Rules.requiredInput],
});
type FormInstance = InstanceType<typeof ElForm>;
const formRef = ref<FormInstance>();
const loadDir = async (path: string) => {
dialogData.value.rowData!.path = path;
};
const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid) => {
if (!valid) return;
if (dialogData.value.title === 'edit') {
loading.value = true;
await updateFtp(dialogData.value.rowData)
.then(() => {
loading.value = false;
drawerVisible.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
})
.catch(() => {
loading.value = false;
});
return;
}
await createFtp(dialogData.value.rowData)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
drawerVisible.value = false;
})
.catch(() => {
loading.value = false;
});
});
};
defineExpose({
acceptParams,
});
</script>

4
frontend/src/views/toolbox/index.vue

@ -61,6 +61,10 @@ const buttons = [
label: i18n.global.t('menu.supervisor'),
path: '/toolbox/supervisor',
},
{
label: 'FTP',
path: '/toolbox/ftp',
},
{
label: 'Fail2ban',
path: '/toolbox/fail2ban',

Loading…
Cancel
Save