Browse Source

feat: 网站创建增加 FTP 选项 (#5076)

pull/5088/head
ssongliu 6 months ago committed by GitHub
parent
commit
d9f9f9a629
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      backend/app/api/v1/ftp.go
  2. 10
      backend/app/api/v1/website.go
  3. 3
      backend/app/dto/request/website.go
  4. 1
      backend/app/model/website.go
  5. 28
      backend/app/service/ftp.go
  6. 8
      backend/app/service/website.go
  7. 4
      backend/init/migration/migrations/v_1_10.go
  8. 29
      backend/utils/toolbox/pure-ftpd.go
  9. 12
      cmd/server/docs/docs.go
  10. 12
      cmd/server/docs/swagger.json
  11. 8
      cmd/server/docs/swagger.yaml
  12. 2
      frontend/src/api/interface/website.ts
  13. 8
      frontend/src/api/modules/website.ts
  14. 19
      frontend/src/global/form-rules.ts
  15. 7
      frontend/src/lang/modules/en.ts
  16. 6
      frontend/src/lang/modules/tw.ts
  17. 5
      frontend/src/lang/modules/zh.ts
  18. 2
      frontend/src/views/database/mysql/index.vue
  19. 2
      frontend/src/views/database/mysql/remote/index.vue
  20. 2
      frontend/src/views/database/postgresql/index.vue
  21. 2
      frontend/src/views/database/postgresql/remote/index.vue
  22. 2
      frontend/src/views/database/redis/remote/index.vue
  23. 18
      frontend/src/views/toolbox/ftp/index.vue
  24. 23
      frontend/src/views/toolbox/ftp/log/index.vue
  25. 21
      frontend/src/views/toolbox/ftp/operate/index.vue
  26. 43
      frontend/src/views/website/website/create/index.vue

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

@ -122,7 +122,7 @@ func (b *BaseApi) CreateFtp(c *gin.Context) {
} }
req.Password = string(pass) req.Password = string(pass)
} }
if err := ftpService.Create(req); err != nil { if _, err := ftpService.Create(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return return
} }

10
backend/app/api/v1/website.go

@ -1,6 +1,8 @@
package v1 package v1
import ( import (
"encoding/base64"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/dto/request" "github.com/1Panel-dev/1Panel/backend/app/dto/request"
@ -76,6 +78,14 @@ func (b *BaseApi) CreateWebsite(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
if len(req.FtpPassword) != 0 {
pass, err := base64.StdEncoding.DecodeString(req.FtpPassword)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
req.FtpPassword = string(pass)
}
err := websiteService.CreateWebsite(req) err := websiteService.CreateWebsite(req)
if err != nil { if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)

3
backend/app/dto/request/website.go

@ -27,6 +27,9 @@ type WebsiteCreate struct {
AppID uint `json:"appID"` AppID uint `json:"appID"`
AppInstallID uint `json:"appInstallID"` AppInstallID uint `json:"appInstallID"`
FtpUser string `json:"ftpUser"`
FtpPassword string `json:"ftpPassword"`
RuntimeID uint `json:"runtimeID"` RuntimeID uint `json:"runtimeID"`
RuntimeConfig RuntimeConfig
} }

1
backend/app/model/website.go

@ -26,6 +26,7 @@ type Website struct {
WebsiteSSLID uint `gorm:"type:integer" json:"webSiteSSLId"` WebsiteSSLID uint `gorm:"type:integer" json:"webSiteSSLId"`
RuntimeID uint `gorm:"type:integer" json:"runtimeID"` RuntimeID uint `gorm:"type:integer" json:"runtimeID"`
AppInstallID uint `gorm:"type:integer" json:"appInstallId"` AppInstallID uint `gorm:"type:integer" json:"appInstallId"`
FtpID uint `gorm:"type:integer" json:"ftpId"`
User string `gorm:"type:varchar;" json:"user"` User string `gorm:"type:varchar;" json:"user"`
Group string `gorm:"type:varchar;" json:"group"` Group string `gorm:"type:varchar;" json:"group"`

28
backend/app/service/ftp.go

@ -1,6 +1,9 @@
package service package service
import ( import (
"fmt"
"os"
"github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model" "github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
@ -16,7 +19,7 @@ type IFtpService interface {
LoadBaseInfo() (dto.FtpBaseInfo, error) LoadBaseInfo() (dto.FtpBaseInfo, error)
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error) SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error)
Operate(operation string) error Operate(operation string) error
Create(req dto.FtpCreate) error Create(req dto.FtpCreate) (uint, error)
Delete(req dto.BatchDeleteReq) error Delete(req dto.BatchDeleteReq) error
Update(req dto.FtpUpdate) error Update(req dto.FtpUpdate) error
Sync() error Sync() error
@ -122,32 +125,39 @@ func (f *FtpService) Sync() error {
return nil return nil
} }
func (f *FtpService) Create(req dto.FtpCreate) error { func (f *FtpService) Create(req dto.FtpCreate) (uint, error) {
if _, err := os.Stat(req.Path); err != nil {
if os.IsNotExist(err) {
fmt.Println(os.MkdirAll(req.Path, os.ModePerm))
} else {
return 0, err
}
}
pass, err := encrypt.StringEncrypt(req.Password) pass, err := encrypt.StringEncrypt(req.Password)
if err != nil { if err != nil {
return err return 0, err
} }
userInDB, _ := ftpRepo.Get(hostRepo.WithByUser(req.User)) userInDB, _ := ftpRepo.Get(hostRepo.WithByUser(req.User))
if userInDB.ID != 0 { if userInDB.ID != 0 {
return constant.ErrRecordExist return 0, constant.ErrRecordExist
} }
client, err := toolbox.NewFtpClient() client, err := toolbox.NewFtpClient()
if err != nil { if err != nil {
return err return 0, err
} }
if err := client.UserAdd(req.User, req.Password, req.Path); err != nil { if err := client.UserAdd(req.User, req.Password, req.Path); err != nil {
return err return 0, err
} }
var ftp model.Ftp var ftp model.Ftp
if err := copier.Copy(&ftp, &req); err != nil { if err := copier.Copy(&ftp, &req); err != nil {
return errors.WithMessage(constant.ErrStructTransform, err.Error()) return 0, errors.WithMessage(constant.ErrStructTransform, err.Error())
} }
ftp.Status = constant.StatusEnable ftp.Status = constant.StatusEnable
ftp.Password = pass ftp.Password = pass
if err := ftpRepo.Create(&ftp); err != nil { if err := ftpRepo.Create(&ftp); err != nil {
return err return 0, err
} }
return nil return ftp.ID, nil
} }
func (f *FtpService) Delete(req dto.BatchDeleteReq) error { func (f *FtpService) Delete(req dto.BatchDeleteReq) error {

8
backend/app/service/website.go

@ -331,6 +331,14 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
return err return err
} }
if len(create.FtpUser) != 0 && len(create.FtpPassword) != 0 {
itemID, err := NewIFtpService().Create(dto.FtpCreate{User: create.FtpUser, Password: create.FtpPassword, Path: path.Join(global.CONF.System.BaseDir, "1panel/apps/openresty/openresty/www/sites", create.Alias, "index")})
if err != nil {
global.LOG.Errorf("create ftp for website failed, err: %v", err)
}
website.FtpID = itemID
}
if err = createWafConfig(website, domains); err != nil { if err = createWafConfig(website, domains); err != nil {
return err return err
} }

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

@ -205,9 +205,9 @@ var AddMonitorMenu = &gormigrate.Migration{
} }
var AddFtp = &gormigrate.Migration{ var AddFtp = &gormigrate.Migration{
ID: "20240517-add-ftp", ID: "20240521-add-ftp",
Migrate: func(tx *gorm.DB) error { Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.Ftp{}); err != nil { if err := tx.AutoMigrate(&model.Ftp{}, model.Website{}); err != nil {
return err return err
} }
return nil return nil

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

@ -16,7 +16,8 @@ import (
) )
type Ftp struct { type Ftp struct {
DefaultUser string DefaultUser string
DefaultGroup string
} }
type FtpClient interface { type FtpClient interface {
@ -33,7 +34,11 @@ type FtpClient interface {
func NewFtpClient() (*Ftp, error) { func NewFtpClient() (*Ftp, error) {
userItem, err := user.LookupId("1000") userItem, err := user.LookupId("1000")
if err == nil { if err == nil {
return &Ftp{DefaultUser: userItem.Username}, err groupItem, err := user.LookupGroupId(userItem.Gid)
if err != nil {
return nil, err
}
return &Ftp{DefaultUser: userItem.Username, DefaultGroup: groupItem.Name}, err
} }
if err.Error() != user.UnknownUserIdError(1000).Error() { if err.Error() != user.UnknownUserIdError(1000).Error() {
return nil, err return nil, err
@ -45,7 +50,7 @@ func NewFtpClient() (*Ftp, error) {
if err != nil { if err != nil {
return nil, errors.New(stdout2) return nil, errors.New(stdout2)
} }
return &Ftp{DefaultUser: "1panel"}, nil return &Ftp{DefaultUser: "1panel", DefaultGroup: groupItem.Name}, nil
} }
if err.Error() != user.UnknownGroupIdError("1000").Error() { if err.Error() != user.UnknownGroupIdError("1000").Error() {
return nil, err return nil, err
@ -54,11 +59,11 @@ func NewFtpClient() (*Ftp, error) {
if err != nil { if err != nil {
return nil, errors.New(string(stdout)) return nil, errors.New(string(stdout))
} }
stdout2, err := cmd.Execf("useradd -u 1000 -g %s %s", groupItem.Name, userItem.Username) stdout2, err := cmd.Execf("useradd -u 1000 -g 1panel %s", userItem.Username)
if err != nil { if err != nil {
return nil, errors.New(stdout2) return nil, errors.New(stdout2)
} }
return &Ftp{DefaultUser: "1panel"}, nil return &Ftp{DefaultUser: "1panel", DefaultGroup: "1panel"}, nil
} }
func (f *Ftp) Status() (bool, bool) { func (f *Ftp) Status() (bool, bool) {
@ -87,7 +92,7 @@ func (f *Ftp) UserAdd(username, passwd, path string) error {
return errors.New(std) return errors.New(std)
} }
_ = f.Reload() _ = f.Reload()
std2, err := cmd.Execf("chown %s %s", f.DefaultUser, path) std2, err := cmd.Execf("chown -R %s:%s %s", f.DefaultUser, f.DefaultGroup, path)
if err != nil { if err != nil {
return errors.New(std2) return errors.New(std2)
} }
@ -170,19 +175,19 @@ func (f *Ftp) LoadLogs(user, operation string) ([]FtpLog, error) {
logItem := "" logItem := ""
if _, err := os.Stat("/etc/pure-ftpd/conf"); err != nil && os.IsNotExist(err) { if _, err := os.Stat("/etc/pure-ftpd/conf"); err != nil && os.IsNotExist(err) {
std, err := cmd.Exec("cat /etc/pure-ftpd/pure-ftpd.conf | grep AltLog | grep clf:") std, err := cmd.Exec("cat /etc/pure-ftpd/pure-ftpd.conf | grep AltLog | grep clf:")
if err != nil { logItem = "/var/log/pureftpd.log"
return logs, err if err == nil && !strings.HasPrefix(logItem, "#") {
logItem = std
} }
logItem = std
} else { } else {
if err != nil { if err != nil {
return logs, err return logs, err
} }
std, err := cmd.Exec("cat /etc/pure-ftpd/conf/AltLog") std, err := cmd.Exec("cat /etc/pure-ftpd/conf/AltLog")
if err != nil { logItem = "/var/log/pure-ftpd/transfer.log"
return nil, err if err != nil && !strings.HasPrefix(logItem, "#") {
logItem = std
} }
logItem = std
} }
logItem = strings.ReplaceAll(logItem, "AltLog", "") logItem = strings.ReplaceAll(logItem, "AltLog", "")

12
cmd/server/docs/docs.go

@ -19029,6 +19029,9 @@ const docTemplate = `{
"expireDate": { "expireDate": {
"type": "string" "type": "string"
}, },
"ftpId": {
"type": "integer"
},
"group": { "group": {
"type": "string" "type": "string"
}, },
@ -20820,6 +20823,12 @@ const docTemplate = `{
"installed" "installed"
] ]
}, },
"ftpPassword": {
"type": "string"
},
"ftpUser": {
"type": "string"
},
"otherDomains": { "otherDomains": {
"type": "string" "type": "string"
}, },
@ -22062,6 +22071,9 @@ const docTemplate = `{
"expireDate": { "expireDate": {
"type": "string" "type": "string"
}, },
"ftpId": {
"type": "integer"
},
"group": { "group": {
"type": "string" "type": "string"
}, },

12
cmd/server/docs/swagger.json

@ -19022,6 +19022,9 @@
"expireDate": { "expireDate": {
"type": "string" "type": "string"
}, },
"ftpId": {
"type": "integer"
},
"group": { "group": {
"type": "string" "type": "string"
}, },
@ -20813,6 +20816,12 @@
"installed" "installed"
] ]
}, },
"ftpPassword": {
"type": "string"
},
"ftpUser": {
"type": "string"
},
"otherDomains": { "otherDomains": {
"type": "string" "type": "string"
}, },
@ -22055,6 +22064,9 @@
"expireDate": { "expireDate": {
"type": "string" "type": "string"
}, },
"ftpId": {
"type": "integer"
},
"group": { "group": {
"type": "string" "type": "string"
}, },

8
cmd/server/docs/swagger.yaml

@ -3050,6 +3050,8 @@ definitions:
type: boolean type: boolean
expireDate: expireDate:
type: string type: string
ftpId:
type: integer
group: group:
type: string type: string
httpConfig: httpConfig:
@ -4248,6 +4250,10 @@ definitions:
- new - new
- installed - installed
type: string type: string
ftpPassword:
type: string
ftpUser:
type: string
otherDomains: otherDomains:
type: string type: string
port: port:
@ -5080,6 +5086,8 @@ definitions:
type: string type: string
expireDate: expireDate:
type: string type: string
ftpId:
type: integer
group: group:
type: string type: string
httpConfig: httpConfig:

2
frontend/src/api/interface/website.ts

@ -64,6 +64,8 @@ export namespace Website {
otherDomains: string; otherDomains: string;
proxy: string; proxy: string;
proxyType: string; proxyType: string;
ftpUser: string;
ftpPassword: string;
} }
export interface WebSiteUpdateReq { export interface WebSiteUpdateReq {

8
frontend/src/api/modules/website.ts

@ -3,6 +3,8 @@ import { ReqPage, ResPage } from '../interface';
import { Website } from '../interface/website'; import { Website } from '../interface/website';
import { File } from '../interface/file'; import { File } from '../interface/file';
import { TimeoutEnum } from '@/enums/http-enum'; import { TimeoutEnum } from '@/enums/http-enum';
import { deepCopy } from '@/utils/util';
import { Base64 } from 'js-base64';
export const SearchWebsites = (req: Website.WebSiteSearch) => { export const SearchWebsites = (req: Website.WebSiteSearch) => {
return http.post<ResPage<Website.WebsiteDTO>>(`/websites/search`, req); return http.post<ResPage<Website.WebsiteDTO>>(`/websites/search`, req);
@ -13,7 +15,11 @@ export const ListWebsites = () => {
}; };
export const CreateWebsite = (req: Website.WebSiteCreateReq) => { export const CreateWebsite = (req: Website.WebSiteCreateReq) => {
return http.post<any>(`/websites`, req); let request = deepCopy(req) as Website.WebSiteCreateReq;
if (request.ftpPassword) {
request.ftpPassword = Base64.encode(request.ftpPassword);
}
return http.post<any>(`/websites`, request);
}; };
export const OpWebsite = (req: Website.WebSiteOp) => { export const OpWebsite = (req: Website.WebSiteOp) => {

19
frontend/src/global/form-rules.ts

@ -181,6 +181,19 @@ const checkSimpleName = (rule: any, value: any, callback: any) => {
} }
}; };
const checkSimplePassword = (rule: any, value: any, callback: any) => {
if (value === '' || typeof value === 'undefined' || value == null) {
callback(new Error(i18n.global.t('commons.rule.simplePassword')));
} else {
const reg = /^[a-zA-Z0-9]{1}[a-zA-Z0-9_]{5,29}$/;
if (!reg.test(value) && value !== '') {
callback(new Error(i18n.global.t('commons.rule.simplePassword')));
} else {
callback();
}
}
};
const checkDBName = (rule: any, value: any, callback: any) => { const checkDBName = (rule: any, value: any, callback: any) => {
if (value === '' || typeof value === 'undefined' || value == null) { if (value === '' || typeof value === 'undefined' || value == null) {
callback(new Error(i18n.global.t('commons.rule.dbName'))); callback(new Error(i18n.global.t('commons.rule.dbName')));
@ -535,6 +548,7 @@ interface CommonRule {
name: FormItemRule; name: FormItemRule;
userName: FormItemRule; userName: FormItemRule;
simpleName: FormItemRule; simpleName: FormItemRule;
simplePassword: FormItemRule;
dbName: FormItemRule; dbName: FormItemRule;
imageName: FormItemRule; imageName: FormItemRule;
volumeName: FormItemRule; volumeName: FormItemRule;
@ -601,6 +615,11 @@ export const Rules: CommonRule = {
validator: checkSimpleName, validator: checkSimpleName,
trigger: 'blur', trigger: 'blur',
}, },
simplePassword: {
required: true,
validator: checkSimplePassword,
trigger: 'blur',
},
dbName: { dbName: {
required: true, required: true,
validator: checkDBName, validator: checkDBName,

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

@ -170,7 +170,8 @@ const message = {
commonName: commonName:
'Supports non-special characters starting with English, Chinese, numbers, .- and _, length 1-128', 'Supports non-special characters starting with English, Chinese, numbers, .- and _, length 1-128',
userName: 'Support English, Chinese, numbers and _ length 3-30', userName: 'Support English, Chinese, numbers and _ length 3-30',
simpleName: 'Supports non-underscore starting, English, numbers, _, length 1-30', simpleName: 'Supports non-underscore starting, English, numbers, _, length 3-30',
simplePassword: 'Supports non-underscore starting, English, numbers, _, length 6-30',
dbName: 'Supports non-special character starting, including English, Chinese, numbers, .-_, with a length of 1-64', dbName: 'Supports non-special character starting, including English, Chinese, numbers, .-_, with a length of 1-64',
imageName: 'Support English, numbers, :/.-_, length 1-150', imageName: 'Support English, numbers, :/.-_, length 1-150',
volumeName: 'Support English, numbers, .-_, length 2-30', volumeName: 'Support English, numbers, .-_, length 2-30',
@ -1736,6 +1737,10 @@ const message = {
zipFormat: '.tar.gz compressed package structure: test.tar.gz compressed package must contain {0} file', zipFormat: '.tar.gz compressed package structure: test.tar.gz compressed package must contain {0} file',
proxy: 'Reverse Proxy', proxy: 'Reverse Proxy',
alias: 'Path Name', alias: 'Path Name',
ftpUser: 'FTP Account',
ftpPassword: 'FTP Password',
ftpHelper:
'Create an FTP account corresponding to the site while creating the site, and the FTP directory points to the directory where the site is located.',
remark: 'Remark', remark: 'Remark',
group: 'Group', group: 'Group',
groupSetting: 'Group Management', groupSetting: 'Group Management',

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

@ -170,7 +170,8 @@ const message = {
illegalInput: '輸入框中存在不合法字符', illegalInput: '輸入框中存在不合法字符',
commonName: '支持非特殊字元開頭,英文中文數字.-和_,長度1-128', commonName: '支持非特殊字元開頭,英文中文數字.-和_,長度1-128',
userName: '支持英文中文數字和_,長度3-30', userName: '支持英文中文數字和_,長度3-30',
simpleName: '支持非底線開頭英文數字_,長度1-30', simpleName: '支持非底線開頭英文數字_,長度3-30',
simplePassword: '支持非底線開頭英文數字_,長度6-30',
dbName: '支持非特殊字符開頭英文中文數字.-_長度1-64', dbName: '支持非特殊字符開頭英文中文數字.-_長度1-64',
imageName: '支持英文數字:/.-_,長度1-150', imageName: '支持英文數字:/.-_,長度1-150',
volumeName: '支持英文數字.-和_,長度2-30', volumeName: '支持英文數字.-和_,長度2-30',
@ -1617,6 +1618,9 @@ const message = {
zipFormat: '.tar.gz 壓縮包結構test.tar.gz 壓縮包內必需包含 {0} 文件', zipFormat: '.tar.gz 壓縮包結構test.tar.gz 壓縮包內必需包含 {0} 文件',
proxy: '反向代理', proxy: '反向代理',
alias: '代號', alias: '代號',
ftpUser: 'FTP 帳號',
ftpPassword: 'FTP 密碼',
ftpHelper: '建立站點的同時為站點建立一個對應 FTP 帳戶並且 FTP 目錄指向站點所在目錄',
remark: '備註', remark: '備註',
group: '分組', group: '分組',
groupSetting: '分組管理', groupSetting: '分組管理',

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

@ -171,6 +171,7 @@ const message = {
commonName: '支持非特殊字符开头,英文中文数字.-和_,长度1-128', commonName: '支持非特殊字符开头,英文中文数字.-和_,长度1-128',
userName: '支持英文中文数字和_,长度3-30', userName: '支持英文中文数字和_,长度3-30',
simpleName: '支持非下划线开头英文数字_,长度3-30', simpleName: '支持非下划线开头英文数字_,长度3-30',
simplePassword: '支持非下划线开头英文数字_,长度6-30',
dbName: '支持非特殊字符开头英文中文数字.-_,长度1-64', dbName: '支持非特殊字符开头英文中文数字.-_,长度1-64',
imageName: '支持英文数字:/.-_,长度1-150', imageName: '支持英文数字:/.-_,长度1-150',
volumeName: '支持英文数字.-和_,长度2-30', volumeName: '支持英文数字.-和_,长度2-30',
@ -1617,6 +1618,10 @@ const message = {
zipFormat: '.tar.gz 压缩包结构test.tar.gz 压缩包内必需包含 {0} 文件', zipFormat: '.tar.gz 压缩包结构test.tar.gz 压缩包内必需包含 {0} 文件',
proxy: '反向代理', proxy: '反向代理',
alias: '代号', alias: '代号',
enableFtp: '创建 FTP',
ftpUser: 'FTP 账号',
ftpPassword: 'FTP 密码',
ftpHelper: '创建站点的同时为站点创建一个对应 FTP 帐户并且 FTP 目录指向站点所在目录',
remark: '备注', remark: '备注',
group: '分组', group: '分组',
groupSetting: '分组管理', groupSetting: '分组管理',

2
frontend/src/views/database/mysql/index.vue

@ -154,7 +154,7 @@
<el-table-column :label="$t('commons.login.password')" prop="password"> <el-table-column :label="$t('commons.login.password')" prop="password">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="row.username === ''">-</span> <span v-if="row.username === ''">-</span>
<div class="flex items-center" v-if="row.password && row.username"> <div class="flex items-center flex-wrap" v-if="row.password && row.username">
<div class="star-center" v-if="!row.showPassword"> <div class="star-center" v-if="!row.showPassword">
<span>**********</span> <span>**********</span>
</div> </div>

2
frontend/src/views/database/mysql/remote/index.vue

@ -23,7 +23,7 @@
<el-table-column :label="$t('commons.login.username')" prop="username" /> <el-table-column :label="$t('commons.login.username')" prop="username" />
<el-table-column :label="$t('commons.login.password')" prop="password"> <el-table-column :label="$t('commons.login.password')" prop="password">
<template #default="{ row }"> <template #default="{ row }">
<div class="flex items-center"> <div class="flex items-center flex-wrap">
<div class="star-center"> <div class="star-center">
<span v-if="!row.showPassword">**********</span> <span v-if="!row.showPassword">**********</span>
</div> </div>

2
frontend/src/views/database/postgresql/index.vue

@ -131,7 +131,7 @@
<el-table-column :label="$t('commons.login.password')" prop="password"> <el-table-column :label="$t('commons.login.password')" prop="password">
<template #default="{ row }"> <template #default="{ row }">
<span v-if="row.username === '' || row.password === ''">-</span> <span v-if="row.username === '' || row.password === ''">-</span>
<div class="flex items-center" v-else> <div class="flex items-center flex-wrap" v-else>
<div class="star-center" v-if="!row.showPassword"> <div class="star-center" v-if="!row.showPassword">
<span>**********</span> <span>**********</span>
</div> </div>

2
frontend/src/views/database/postgresql/remote/index.vue

@ -23,7 +23,7 @@
<el-table-column :label="$t('commons.login.username')" prop="username" /> <el-table-column :label="$t('commons.login.username')" prop="username" />
<el-table-column :label="$t('commons.login.password')" prop="password"> <el-table-column :label="$t('commons.login.password')" prop="password">
<template #default="{ row }"> <template #default="{ row }">
<div class="flex items-center"> <div class="flex items-center flex-wrap">
<div class="star-center"> <div class="star-center">
<span v-if="!row.showPassword">**********</span> <span v-if="!row.showPassword">**********</span>
</div> </div>

2
frontend/src/views/database/redis/remote/index.vue

@ -22,7 +22,7 @@
<el-table-column show-overflow-tooltip :label="$t('database.address')" prop="address" /> <el-table-column show-overflow-tooltip :label="$t('database.address')" prop="address" />
<el-table-column :label="$t('commons.login.password')" prop="password"> <el-table-column :label="$t('commons.login.password')" prop="password">
<template #default="{ row }"> <template #default="{ row }">
<div class="flex items-center"> <div class="flex items-center flex-wrap">
<div class="star-center"> <div class="star-center">
<span v-if="!row.showPassword">**********</span> <span v-if="!row.showPassword">**********</span>
</div> </div>

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

@ -63,7 +63,7 @@
<el-table-column :label="$t('commons.login.password')" prop="password"> <el-table-column :label="$t('commons.login.password')" prop="password">
<template #default="{ row }"> <template #default="{ row }">
<div v-if="row.password.length === 0">-</div> <div v-if="row.password.length === 0">-</div>
<div v-else class="flex items-center"> <div v-else class="flex items-center flex-wrap">
<div class="star-center" v-if="!row.showPassword"> <div class="star-center" v-if="!row.showPassword">
<span>**********</span> <span>**********</span>
</div> </div>
@ -117,7 +117,11 @@
</el-button> </el-button>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('file.root')" :min-width="120" prop="path" show-overflow-tooltip /> <el-table-column :label="$t('file.root')" :min-width="120" prop="path">
<template #default="{ row }">
<Tooltip @click="toFolder(row.path)" :text="row.path" />
</template>
</el-table-column>
<el-table-column <el-table-column
:label="$t('commons.table.description')" :label="$t('commons.table.description')"
:min-width="80" :min-width="80"
@ -126,12 +130,13 @@
/> />
<el-table-column <el-table-column
:label="$t('commons.table.createdAt')" :label="$t('commons.table.createdAt')"
show-overflow-tooltip
:formatter="dateFormat" :formatter="dateFormat"
:min-width="80" :min-width="80"
prop="createdAt" prop="createdAt"
/> />
<fu-table-operations <fu-table-operations
width="240px" width="200px"
:buttons="buttons" :buttons="buttons"
:ellipsis="10" :ellipsis="10"
:label="$t('commons.table.operate')" :label="$t('commons.table.operate')"
@ -175,6 +180,7 @@ import { deleteFtp, searchFtp, updateFtp, syncFtp, operateFtp, getFtpBase } from
import OperateDialog from '@/views/toolbox/ftp/operate/index.vue'; import OperateDialog from '@/views/toolbox/ftp/operate/index.vue';
import LogDialog from '@/views/toolbox/ftp/log/index.vue'; import LogDialog from '@/views/toolbox/ftp/log/index.vue';
import { Toolbox } from '@/api/interface/toolbox'; import { Toolbox } from '@/api/interface/toolbox';
import router from '@/routers';
const loading = ref(); const loading = ref();
const selects = ref<any>([]); const selects = ref<any>([]);
@ -232,6 +238,10 @@ const toDoc = () => {
window.open('https://1panel.cn/docs/user_manual/toolbox/ftp/', '_blank', 'noopener,noreferrer'); window.open('https://1panel.cn/docs/user_manual/toolbox/ftp/', '_blank', 'noopener,noreferrer');
}; };
const toFolder = (folder: string) => {
router.push({ path: '/hosts/files', query: { path: folder } });
};
const onOperate = async (operation: string) => { const onOperate = async (operation: string) => {
let msg = operation === 'enable' || operation === 'disable' ? 'ssh.' : 'commons.button.'; let msg = operation === 'enable' || operation === 'disable' ? 'ssh.' : 'commons.button.';
ElMessageBox.confirm(i18n.global.t('toolbox.ftp.operation', [i18n.global.t(msg + operation)]), 'FTP', { ElMessageBox.confirm(i18n.global.t('toolbox.ftp.operation', [i18n.global.t(msg + operation)]), 'FTP', {
@ -348,7 +358,7 @@ const buttons = [
return row.status === 'deleted'; return row.status === 'deleted';
}, },
click: (row: Toolbox.FtpInfo) => { click: (row: Toolbox.FtpInfo) => {
dialogLogRef.value!.acceptParams({ user: row.user }); dialogLogRef.value!.acceptParams({ user: row.user, path: row.path });
}, },
}, },
{ {

23
frontend/src/views/toolbox/ftp/log/index.vue

@ -15,9 +15,8 @@
<el-option value="GET" :label="$t('file.download')" /> <el-option value="GET" :label="$t('file.download')" />
</el-select> </el-select>
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search"> <ComplexTable class="mt-2" :pagination-config="paginationConfig" :data="data" @search="search">
<el-table-column label="ip" prop="ip" /> <el-table-column label="ip" prop="ip" show-overflow-tooltip />
<el-table-column :label="$t('commons.login.username')" prop="user" />
<el-table-column :label="$t('commons.table.status')" show-overflow-tooltip prop="status"> <el-table-column :label="$t('commons.table.status')" show-overflow-tooltip prop="status">
<template #default="{ row }"> <template #default="{ row }">
<el-tag v-if="row.status === '200'">{{ $t('commons.status.success') }}</el-tag> <el-tag v-if="row.status === '200'">{{ $t('commons.status.success') }}</el-tag>
@ -26,12 +25,12 @@
</el-table-column> </el-table-column>
<el-table-column :label="$t('commons.table.operate')" show-overflow-tooltip> <el-table-column :label="$t('commons.table.operate')" show-overflow-tooltip>
<template #default="{ row }"> <template #default="{ row }">
{{ loadFileName(row.operation) }} {{ loadOperation(row.operation) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('file.file')" show-overflow-tooltip> <el-table-column :label="$t('file.file')" show-overflow-tooltip>
<template #default="{ row }"> <template #default="{ row }">
{{ loadOperation(row.operation) }} {{ loadFileName(row.operation) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('file.size')" show-overflow-tooltip prop="size"> <el-table-column :label="$t('file.size')" show-overflow-tooltip prop="size">
@ -66,14 +65,18 @@ const paginationConfig = reactive({
}); });
const data = ref(); const data = ref();
const itemPath = ref();
interface DialogProps { interface DialogProps {
user: string; user: string;
path: string;
} }
const loading = ref(); const loading = ref();
const drawerVisible = ref(false); const drawerVisible = ref(false);
const acceptParams = (params: DialogProps): void => { const acceptParams = (params: DialogProps): void => {
paginationConfig.user = params.user; paginationConfig.user = params.user;
paginationConfig.operation = '';
itemPath.value = params.path;
search(); search();
drawerVisible.value = true; drawerVisible.value = true;
}; };
@ -101,7 +104,7 @@ const search = async () => {
}); });
}; };
const loadFileName = (operation: string) => { const loadOperation = (operation: string) => {
if (operation.startsWith('"PUT')) { if (operation.startsWith('"PUT')) {
return i18n.global.t('file.upload'); return i18n.global.t('file.upload');
} }
@ -109,8 +112,12 @@ const loadFileName = (operation: string) => {
return i18n.global.t('file.download'); return i18n.global.t('file.download');
} }
}; };
const loadOperation = (operation: string) => { const loadFileName = (operation: string) => {
return operation.replaceAll('"', '').replaceAll('PUT', '').replaceAll('GET', ''); return operation
.replaceAll('"', '')
.replaceAll('PUT', '')
.replaceAll('GET', '')
.replaceAll(itemPath.value + '/', '');
}; };
defineExpose({ defineExpose({

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

@ -14,7 +14,7 @@
:back="handleClose" :back="handleClose"
/> />
</template> </template>
<el-form ref="formRef" label-position="top" :model="dialogData.rowData" :rules="rules"> <el-form ref="formRef" label-position="top" :model="dialogData.rowData" :rules="rules" v-loading="loading">
<el-row type="flex" justify="center"> <el-row type="flex" justify="center">
<el-col :span="22"> <el-col :span="22">
<el-form-item :label="$t('commons.login.username')" prop="user"> <el-form-item :label="$t('commons.login.username')" prop="user">
@ -25,7 +25,11 @@
/> />
</el-form-item> </el-form-item>
<el-form-item :label="$t('commons.login.password')" prop="password"> <el-form-item :label="$t('commons.login.password')" prop="password">
<el-input clearable v-model="dialogData.rowData!.password" /> <el-input type="password" clearable v-model="dialogData.rowData!.password" show-password>
<template #append>
<el-button @click="random">{{ $t('commons.button.random') }}</el-button>
</template>
</el-input>
</el-form-item> </el-form-item>
<el-form-item :label="$t('file.root')" prop="path"> <el-form-item :label="$t('file.root')" prop="path">
<el-input v-model="dialogData.rowData!.path"> <el-input v-model="dialogData.rowData!.path">
@ -43,7 +47,7 @@
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button> <el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="onSubmit(formRef)"> <el-button :disabled="loading" type="primary" @click="onSubmit(formRef)">
{{ $t('commons.button.confirm') }} {{ $t('commons.button.confirm') }}
</el-button> </el-button>
</span> </span>
@ -61,6 +65,7 @@ import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
import { Toolbox } from '@/api/interface/toolbox'; import { Toolbox } from '@/api/interface/toolbox';
import { createFtp, updateFtp } from '@/api/modules/toolbox'; import { createFtp, updateFtp } from '@/api/modules/toolbox';
import { getRandomStr } from '@/utils/util';
interface DialogProps { interface DialogProps {
title: string; title: string;
@ -81,14 +86,18 @@ const acceptParams = (params: DialogProps): void => {
}; };
const emit = defineEmits<{ (e: 'search'): void }>(); const emit = defineEmits<{ (e: 'search'): void }>();
const random = async () => {
dialogData.value.rowData.password = getRandomStr(16);
};
const handleClose = () => { const handleClose = () => {
drawerVisible.value = false; drawerVisible.value = false;
}; };
const rules = reactive({ const rules = reactive({
user: [Rules.requiredInput, Rules.noSpace], user: [Rules.simpleName],
password: [Rules.requiredInput], password: [Rules.simplePassword],
path: [Rules.requiredInput], path: [Rules.requiredInput, Rules.noSpace],
}); });
type FormInstance = InstanceType<typeof ElForm>; type FormInstance = InstanceType<typeof ElForm>;

43
frontend/src/views/website/website/create/index.vue

@ -300,6 +300,33 @@
</span> </span>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item prop="enableFtp" v-if="website.type === 'static' || website.type === 'runtime'">
<el-checkbox
@change="random"
v-model="website.enableFtp"
:label="$t('website.enableFtp')"
size="large"
/>
<span class="input-help">{{ $t('website.ftpHelper') }}</span>
</el-form-item>
<el-row :gutter="20" v-if="website.enableFtp">
<el-col :span="12">
<el-form-item prop="ftpUser" :label="$t('website.ftpUser')">
<el-input v-model="website.ftpUser" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="ftpPassword" :label="$t('website.ftpPassword')">
<el-input type="password" clearable v-model="website.ftpPassword" show-password>
<template #append>
<el-button @click="random">{{ $t('commons.button.random') }}</el-button>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-form-item <el-form-item
v-if="website.type === 'proxy'" v-if="website.type === 'proxy'"
:label="$t('website.proxyAddress')" :label="$t('website.proxyAddress')"
@ -316,7 +343,7 @@
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item :label="$t('website.remark')" prop="remark"> <el-form-item :label="$t('website.remark')" prop="remark">
<el-input v-model="website.remark"></el-input> <el-input type="textarea" :rows="3" clearable v-model="website.remark" />
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-col> </el-col>
@ -354,6 +381,7 @@ import { GetGroupList } from '@/api/modules/group';
import { Group } from '@/api/interface/group'; import { Group } from '@/api/interface/group';
import { SearchRuntimes } from '@/api/modules/runtime'; import { SearchRuntimes } from '@/api/modules/runtime';
import { Runtime } from '@/api/interface/runtime'; import { Runtime } from '@/api/interface/runtime';
import { getRandomStr } from '@/utils/util';
const websiteForm = ref<FormInstance>(); const websiteForm = ref<FormInstance>();
const website = ref({ const website = ref({
@ -383,6 +411,9 @@ const website = ref({
allowPort: false, allowPort: false,
}, },
IPV6: false, IPV6: false,
enableFtp: false,
ftpUser: '',
ftpPassword: '',
proxyType: 'tcp', proxyType: 'tcp',
port: 9000, port: 9000,
proxyProtocol: 'http://', proxyProtocol: 'http://',
@ -406,6 +437,8 @@ const rules = ref<any>({
memoryLimit: [Rules.requiredInput, checkNumberRange(0, 9999999999)], memoryLimit: [Rules.requiredInput, checkNumberRange(0, 9999999999)],
containerName: [Rules.containerName], containerName: [Rules.containerName],
}, },
ftpUser: [Rules.simpleName],
ftpPassword: [Rules.simplePassword],
proxyType: [Rules.requiredSelect], proxyType: [Rules.requiredSelect],
port: [Rules.port], port: [Rules.port],
runtimeType: [Rules.requiredInput], runtimeType: [Rules.requiredInput],
@ -443,6 +476,10 @@ const handleClose = () => {
em('close', false); em('close', false);
}; };
const random = async () => {
website.value.ftpPassword = getRandomStr(16);
};
const changeType = (type: string) => { const changeType = (type: string) => {
switch (type) { switch (type) {
case 'deployment': case 'deployment':
@ -614,6 +651,10 @@ const submit = async (formEl: FormInstance | undefined) => {
if (website.value.type === 'proxy') { if (website.value.type === 'proxy') {
website.value.proxy = website.value.proxyProtocol + website.value.proxyAddress; website.value.proxy = website.value.proxyProtocol + website.value.proxyAddress;
} }
if (!website.value.enableFtp) {
website.value.ftpUser = '';
website.value.ftpPassword = '';
}
CreateWebsite(website.value) CreateWebsite(website.value)
.then(() => { .then(() => {
MsgSuccess(i18n.global.t('commons.msg.createSuccess')); MsgSuccess(i18n.global.t('commons.msg.createSuccess'));

Loading…
Cancel
Save