From 64ed07e4fa47f17c06c5b3dc5bdf0162dd691534 Mon Sep 17 00:00:00 2001 From: ssongliu <73214554+ssongliu@users.noreply.github.com> Date: Fri, 17 May 2024 22:36:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B7=A5=E5=85=B7=E7=AE=B1=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20FTP=20(#5039)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/backup.go | 4 +- backend/app/api/v1/entry.go | 1 + backend/app/api/v1/ftp.go | 135 +++++++++ backend/app/dto/ftp.go | 30 ++ backend/app/model/ftp.go | 11 + backend/app/repo/ftp.go | 80 ++++++ backend/app/service/entry.go | 1 + backend/app/service/ftp.go | 177 ++++++++++++ backend/init/migration/migrate.go | 1 + backend/init/migration/migrations/v_1_10.go | 11 + backend/router/ro_toolbox.go | 6 + backend/utils/toolbox/pure-ftpd.go | 143 ++++++++++ cmd/server/docs/docs.go | 263 ++++++++++++++++- cmd/server/docs/swagger.json | 263 ++++++++++++++++- cmd/server/docs/swagger.yaml | 172 ++++++++++- frontend/src/api/interface/toolbox.ts | 22 ++ frontend/src/api/modules/toolbox.ts | 32 ++- frontend/src/lang/modules/en.ts | 8 + frontend/src/lang/modules/tw.ts | 6 + frontend/src/lang/modules/zh.ts | 6 + frontend/src/routers/modules/toolbox.ts | 10 + frontend/src/views/toolbox/ftp/index.vue | 268 ++++++++++++++++++ .../src/views/toolbox/ftp/operate/index.vue | 137 +++++++++ frontend/src/views/toolbox/index.vue | 4 + 24 files changed, 1782 insertions(+), 9 deletions(-) create mode 100644 backend/app/api/v1/ftp.go create mode 100644 backend/app/dto/ftp.go create mode 100644 backend/app/model/ftp.go create mode 100644 backend/app/repo/ftp.go create mode 100644 backend/app/service/ftp.go create mode 100644 backend/utils/toolbox/pure-ftpd.go create mode 100644 frontend/src/views/toolbox/ftp/index.vue create mode 100644 frontend/src/views/toolbox/ftp/operate/index.vue diff --git a/backend/app/api/v1/backup.go b/backend/app/api/v1/backup.go index 93a671b6d..1849ba568 100644 --- a/backend/app/api/v1/backup.go +++ b/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 { diff --git a/backend/app/api/v1/entry.go b/backend/app/api/v1/entry.go index 611cc343d..481a6917c 100644 --- a/backend/app/api/v1/entry.go +++ b/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() diff --git a/backend/app/api/v1/ftp.go b/backend/app/api/v1/ftp.go new file mode 100644 index 000000000..15630ead1 --- /dev/null +++ b/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) +} diff --git a/backend/app/dto/ftp.go b/backend/app/dto/ftp.go new file mode 100644 index 000000000..862e66aa9 --- /dev/null +++ b/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"` +} diff --git a/backend/app/model/ftp.go b/backend/app/model/ftp.go new file mode 100644 index 000000000..291e765c6 --- /dev/null +++ b/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"` +} diff --git a/backend/app/repo/ftp.go b/backend/app/repo/ftp.go new file mode 100644 index 000000000..c9732ec13 --- /dev/null +++ b/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 +} diff --git a/backend/app/service/entry.go b/backend/app/service/entry.go index bb97578a3..fea70a57d 100644 --- a/backend/app/service/entry.go +++ b/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() diff --git a/backend/app/service/ftp.go b/backend/app/service/ftp.go new file mode 100644 index 000000000..1b8ac2bf3 --- /dev/null +++ b/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 +} diff --git a/backend/init/migration/migrate.go b/backend/init/migration/migrate.go index 5f8b6b2ef..069a202f6 100644 --- a/backend/init/migration/migrate.go +++ b/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) diff --git a/backend/init/migration/migrations/v_1_10.go b/backend/init/migration/migrations/v_1_10.go index b245144ed..2aef6c04c 100644 --- a/backend/init/migration/migrations/v_1_10.go +++ b/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 + }, +} diff --git a/backend/router/ro_toolbox.go b/backend/router/ro_toolbox.go index 7638c4650..4c70a8619 100644 --- a/backend/router/ro_toolbox.go +++ b/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) } } diff --git a/backend/utils/toolbox/pure-ftpd.go b/backend/utils/toolbox/pure-ftpd.go new file mode 100644 index 000000000..44b46d31d --- /dev/null +++ b/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 <; 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; + } } diff --git a/frontend/src/api/modules/toolbox.ts b/frontend/src/api/modules/toolbox.ts index 80cb1c556..237f926f5 100644 --- a/frontend/src/api/modules/toolbox.ts +++ b/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>(`/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); +}; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 4a44ef4f1..8e7f25fa1 100644 --- a/frontend/src/lang/modules/en.ts +++ b/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', diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index e31c9600a..bd40de3f5 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/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: '面板日誌', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index ff49bb5b7..958aec56d 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/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: '面板日志', diff --git a/frontend/src/routers/modules/toolbox.ts b/frontend/src/routers/modules/toolbox.ts index 25a78241f..563987d2b 100644 --- a/frontend/src/routers/modules/toolbox.ts +++ b/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', diff --git a/frontend/src/views/toolbox/ftp/index.vue b/frontend/src/views/toolbox/ftp/index.vue new file mode 100644 index 000000000..06d14e0c8 --- /dev/null +++ b/frontend/src/views/toolbox/ftp/index.vue @@ -0,0 +1,268 @@ + + + diff --git a/frontend/src/views/toolbox/ftp/operate/index.vue b/frontend/src/views/toolbox/ftp/operate/index.vue new file mode 100644 index 000000000..0534b4841 --- /dev/null +++ b/frontend/src/views/toolbox/ftp/operate/index.vue @@ -0,0 +1,137 @@ + + + diff --git a/frontend/src/views/toolbox/index.vue b/frontend/src/views/toolbox/index.vue index 6cfed353a..cd9eddb77 100644 --- a/frontend/src/views/toolbox/index.vue +++ b/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',