zhengkunwang 1 year ago committed by GitHub
parent
commit
c14e192bee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 67
      backend/app/api/v1/website.go
  2. 20
      backend/app/dto/request/nginx.go
  3. 4
      backend/app/dto/request/website.go
  4. 14
      backend/app/dto/response/nginx.go
  5. 324
      backend/app/service/website.go
  6. 4
      backend/router/ro_website.go
  7. 205
      cmd/server/docs/docs.go
  8. 205
      cmd/server/docs/swagger.json
  9. 135
      cmd/server/docs/swagger.yaml
  10. 26
      frontend/src/api/interface/website.ts
  11. 12
      frontend/src/api/modules/website.ts
  12. 11
      frontend/src/lang/modules/en.ts
  13. 11
      frontend/src/lang/modules/tw.ts
  14. 11
      frontend/src/lang/modules/zh.ts
  15. 6
      frontend/src/views/website/website/config/basic/index.vue
  16. 198
      frontend/src/views/website/website/config/basic/redirect/create/index.vue
  17. 93
      frontend/src/views/website/website/config/basic/redirect/file/index.vue
  18. 194
      frontend/src/views/website/website/config/basic/redirect/index.vue

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

@ -801,3 +801,70 @@ func (b *BaseApi) UpdateAntiLeech(c *gin.Context) {
}
helper.SuccessWithOutData(c)
}
// @Tags Website
// @Summary Update redirect conf
// @Description 修改重定向配置
// @Accept json
// @Param request body request.NginxRedirectReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /websites/redirect/update [post]
// @x-panel-log {"bodyKeys":["websiteID"],"paramKeys":[],"BeforeFuntions":[{"input_column":"id","input_value":"websiteID","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"修改网站 [domain] 重定向理配置 ","formatEN":"Update domain [domain] redirect config"}
func (b *BaseApi) UpdateRedirectConfig(c *gin.Context) {
var req request.NginxRedirectReq
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
err := websiteService.OperateRedirect(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// @Tags Website
// @Summary Get redirect conf
// @Description 获取重定向配置
// @Accept json
// @Param request body request.WebsiteProxyReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /websites/redirect [post]
func (b *BaseApi) GetRedirectConfig(c *gin.Context) {
var req request.WebsiteRedirectReq
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
res, err := websiteService.GetRedirect(req.WebsiteID)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags Website
// @Summary Update redirect file
// @Description 更新重定向文件
// @Accept json
// @Param request body request.NginxRedirectUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /websites/redirect/file [post]
// @x-panel-log {"bodyKeys":["websiteID"],"paramKeys":[],"BeforeFuntions":[{"input_column":"id","input_value":"websiteID","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"更新重定向文件 [domain]","formatEN":"Nginx conf redirect file update [domain]"}
func (b *BaseApi) UpdateRedirectConfigFile(c *gin.Context) {
var req request.NginxRedirectUpdate
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := websiteService.UpdateRedirectFile(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}

20
backend/app/dto/request/nginx.go

@ -66,3 +66,23 @@ type NginxAntiLeechUpdate struct {
LogEnable bool `json:"logEnable"`
Blocked bool `json:"blocked"`
}
type NginxRedirectReq struct {
Name string `json:"name" validate:"required"`
WebsiteID uint `json:"websiteID" validate:"required"`
Domains []string `json:"domains"`
KeepPath bool `json:"keepPath" validate:"required"`
Enable bool `json:"enable" validate:"required"`
Type string `json:"type" validate:"required"`
Redirect string `json:"redirect" validate:"required"`
Path string `json:"path"`
Target string `json:"target" validate:"required"`
Operate string `json:"operate" validate:"required"`
RedirectRoot bool `json:"redirectRoot"`
}
type NginxRedirectUpdate struct {
WebsiteID uint `json:"websiteID" validate:"required"`
Content string `json:"content" validate:"required"`
Name string `json:"name" validate:"required"`
}

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

@ -189,3 +189,7 @@ type WebsiteProxyConfig struct {
type WebsiteProxyReq struct {
ID uint `json:"id" validate:"required"`
}
type WebsiteRedirectReq struct {
WebsiteID uint `json:"websiteId" validate:"required"`
}

14
backend/app/dto/response/nginx.go

@ -34,3 +34,17 @@ type NginxAntiLeechRes struct {
LogEnable bool `json:"logEnable"`
Blocked bool `json:"blocked"`
}
type NginxRedirectConfig struct {
WebsiteID uint `json:"websiteID"`
Name string `json:"name"`
Domains []string `json:"domains"`
KeepPath bool `json:"keepPath"`
Enable bool `json:"enable"`
Type string `json:"type"`
Redirect string `json:"redirect"`
Path string `json:"path"`
Target string `json:"target"`
FilePath string `json:"filePath"`
Content string `json:"content"`
}

324
backend/app/service/website.go

@ -79,6 +79,9 @@ type IWebsiteService interface {
UpdateAuthBasic(req request.NginxAuthUpdate) (err error)
GetAntiLeech(id uint) (*response.NginxAntiLeechRes, error)
UpdateAntiLeech(req request.NginxAntiLeechUpdate) (err error)
OperateRedirect(req request.NginxRedirectReq) (err error)
GetRedirect(id uint) (res []response.NginxRedirectConfig, err error)
UpdateRedirectFile(req request.NginxRedirectUpdate) (err error)
}
func NewIWebsiteService() IWebsiteService {
@ -1690,7 +1693,7 @@ func (w WebsiteService) UpdateAntiLeech(req request.NginxAntiLeechUpdate) (err e
return
}
fileOp := files.NewFileOp()
backpContent, err := fileOp.GetContent(nginxFull.SiteConfig.Config.FilePath)
backupContent, err := fileOp.GetContent(nginxFull.SiteConfig.Config.FilePath)
if err != nil {
return
}
@ -1761,7 +1764,7 @@ func (w WebsiteService) UpdateAntiLeech(req request.NginxAntiLeechUpdate) (err e
return
}
if err = updateNginxConfig(constant.NginxScopeServer, nil, &website); err != nil {
_ = fileOp.WriteFile(nginxFull.SiteConfig.Config.FilePath, bytes.NewReader(backpContent), 0755)
_ = fileOp.WriteFile(nginxFull.SiteConfig.Config.FilePath, bytes.NewReader(backupContent), 0755)
return
}
return
@ -1844,3 +1847,320 @@ func (w WebsiteService) GetAntiLeech(id uint) (*response.NginxAntiLeechRes, erro
}
return res, nil
}
func (w WebsiteService) OperateRedirect(req request.NginxRedirectReq) (err error) {
var (
website model.Website
nginxInstall model.AppInstall
oldContent []byte
)
website, err = websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID))
if err != nil {
return err
}
nginxInstall, err = getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return
}
includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "redirect")
fileOp := files.NewFileOp()
if !fileOp.Stat(includeDir) {
_ = fileOp.CreateDir(includeDir, 0755)
}
fileName := fmt.Sprintf("%s.conf", req.Name)
includePath := path.Join(includeDir, fileName)
backName := fmt.Sprintf("%s.bak", req.Name)
backPath := path.Join(includeDir, backName)
if req.Operate == "create" && (fileOp.Stat(includePath) || fileOp.Stat(backPath)) {
err = buserr.New(constant.ErrNameIsExist)
return
}
defer func() {
if err != nil {
switch req.Operate {
case "create":
_ = fileOp.DeleteFile(includePath)
case "edit":
_ = fileOp.WriteFile(includePath, bytes.NewReader(oldContent), 0755)
}
}
}()
var (
config *components.Config
oldPar *parser.Parser
)
switch req.Operate {
case "create":
config = &components.Config{}
case "edit":
oldPar, err = parser.NewParser(includePath)
if err != nil {
return
}
config = oldPar.Parse()
oldContent, err = fileOp.GetContent(includePath)
if err != nil {
return
}
case "delete":
_ = fileOp.DeleteFile(includePath)
_ = fileOp.DeleteFile(backPath)
return updateNginxConfig(constant.NginxScopeServer, nil, &website)
case "disable":
_ = fileOp.Rename(includePath, backPath)
return updateNginxConfig(constant.NginxScopeServer, nil, &website)
case "enable":
_ = fileOp.Rename(backPath, includePath)
return updateNginxConfig(constant.NginxScopeServer, nil, &website)
}
target := req.Target
block := &components.Block{}
switch req.Type {
case "path":
if req.KeepPath {
target = req.Target + "$1"
}
redirectKey := "permanent"
if req.Redirect == "302" {
redirectKey = "redirect"
}
block = &components.Block{
Directives: []components.IDirective{
&components.Directive{
Name: "rewrite",
Parameters: []string{fmt.Sprintf("^%s(.*)", req.Path), target, redirectKey},
},
},
}
case "domain":
if req.KeepPath {
target = req.Target + "$request_uri"
}
returnBlock := &components.Block{
Directives: []components.IDirective{
&components.Directive{
Name: "return",
Parameters: []string{req.Redirect, target},
},
},
}
for _, domain := range req.Domains {
block.Directives = append(block.Directives, &components.Directive{
Name: "if",
Parameters: []string{"($host", "~", fmt.Sprintf("'^%s')", domain)},
Block: returnBlock,
})
}
case "404":
if req.KeepPath && !req.RedirectRoot {
target = req.Target + "$request_uri"
}
if req.RedirectRoot {
target = "/"
} else {
if req.KeepPath {
target = req.Target + "$request_uri"
}
}
block = &components.Block{
Directives: []components.IDirective{
&components.Directive{
Name: "error_page",
Parameters: []string{"404", "=", "@notfound"},
},
&components.Directive{
Name: "location",
Parameters: []string{"@notfound"},
Block: &components.Block{
Directives: []components.IDirective{
&components.Directive{
Name: "return",
Parameters: []string{"301", target},
},
},
},
},
},
}
}
config.FilePath = includePath
config.Block = block
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
return buserr.WithErr(constant.ErrUpdateBuWebsite, err)
}
nginxInclude := fmt.Sprintf("/www/sites/%s/redirect/*.conf", website.Alias)
if err = updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); err != nil {
return
}
return
}
func (w WebsiteService) GetRedirect(id uint) (res []response.NginxRedirectConfig, err error) {
var (
website model.Website
nginxInstall model.AppInstall
fileList response.FileInfo
)
website, err = websiteRepo.GetFirst(commonRepo.WithByID(id))
if err != nil {
return
}
nginxInstall, err = getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return
}
includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "redirect")
fileOp := files.NewFileOp()
if !fileOp.Stat(includeDir) {
return
}
fileList, err = NewIFileService().GetFileList(request.FileOption{FileOption: files.FileOption{Path: includeDir, Expand: true, Page: 1, PageSize: 100}})
if len(fileList.Items) == 0 {
return
}
var (
content []byte
config *components.Config
)
for _, configFile := range fileList.Items {
redirectConfig := response.NginxRedirectConfig{
WebsiteID: website.ID,
}
parts := strings.Split(configFile.Name, ".")
redirectConfig.Name = parts[0]
if parts[1] == "conf" {
redirectConfig.Enable = true
} else {
redirectConfig.Enable = false
}
redirectConfig.FilePath = configFile.Path
content, err = fileOp.GetContent(configFile.Path)
if err != nil {
return
}
redirectConfig.Content = string(content)
config = parser.NewStringParser(string(content)).Parse()
dirs := config.GetDirectives()
if len(dirs) > 0 {
firstName := dirs[0].GetName()
switch firstName {
case "if":
for _, ifDir := range dirs {
params := ifDir.GetParameters()
if len(params) > 2 && params[0] == "($host" {
domain := strings.Trim(strings.Trim(params[2], "'"), "^")
redirectConfig.Domains = append(redirectConfig.Domains, domain)
if len(redirectConfig.Domains) > 1 {
continue
}
redirectConfig.Type = "domain"
}
childDirs := ifDir.GetBlock().GetDirectives()
for _, dir := range childDirs {
if dir.GetName() == "return" {
dirParams := dir.GetParameters()
if len(dirParams) > 1 {
redirectConfig.Redirect = dirParams[0]
if strings.HasSuffix(dirParams[1], "$request_uri") {
redirectConfig.KeepPath = true
redirectConfig.Target = strings.TrimSuffix(dirParams[1], "$request_uri")
} else {
redirectConfig.KeepPath = false
redirectConfig.Target = dirParams[1]
}
}
}
}
}
case "rewrite":
redirectConfig.Type = "path"
for _, pathDir := range dirs {
if pathDir.GetName() == "rewrite" {
params := pathDir.GetParameters()
if len(params) > 2 {
redirectConfig.Path = strings.Trim(strings.Trim(params[0], "^"), "(.*)")
if strings.HasSuffix(params[1], "$1") {
redirectConfig.KeepPath = true
redirectConfig.Target = strings.TrimSuffix(params[1], "$1")
} else {
redirectConfig.KeepPath = false
redirectConfig.Target = params[1]
}
if params[2] == "permanent" {
redirectConfig.Redirect = "301"
} else {
redirectConfig.Redirect = "302"
}
}
}
}
case "error_page":
redirectConfig.Type = "404"
for _, errDir := range dirs {
if errDir.GetName() == "location" {
childDirs := errDir.GetBlock().GetDirectives()
for _, dir := range childDirs {
if dir.GetName() == "return" {
dirParams := dir.GetParameters()
if len(dirParams) > 1 {
redirectConfig.Redirect = dirParams[0]
if strings.HasSuffix(dirParams[1], "$request_uri") {
redirectConfig.KeepPath = true
redirectConfig.Target = strings.TrimSuffix(dirParams[1], "$request_uri")
} else {
redirectConfig.KeepPath = false
redirectConfig.Target = dirParams[1]
}
}
}
}
}
}
}
}
res = append(res, redirectConfig)
}
return
}
func (w WebsiteService) UpdateRedirectFile(req request.NginxRedirectUpdate) (err error) {
var (
website model.Website
nginxFull dto.NginxFull
oldRewriteContent []byte
)
website, err = websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID))
if err != nil {
return err
}
nginxFull, err = getNginxFull(&website)
if err != nil {
return err
}
includePath := fmt.Sprintf("/www/sites/%s/redirect/%s.conf", website.Alias, req.Name)
absolutePath := path.Join(nginxFull.Install.GetPath(), includePath)
fileOp := files.NewFileOp()
oldRewriteContent, err = fileOp.GetContent(absolutePath)
if err != nil {
return err
}
if err = fileOp.WriteFile(absolutePath, strings.NewReader(req.Content), 0755); err != nil {
return err
}
defer func() {
if err != nil {
_ = fileOp.WriteFile(absolutePath, bytes.NewReader(oldRewriteContent), 0755)
}
}()
return updateNginxConfig(constant.NginxScopeServer, nil, &website)
}

4
backend/router/ro_website.go

@ -61,5 +61,9 @@ func (a *WebsiteRouter) InitWebsiteRouter(Router *gin.RouterGroup) {
groupRouter.POST("/leech", baseApi.GetAntiLeech)
groupRouter.POST("/leech/update", baseApi.UpdateAntiLeech)
groupRouter.POST("/redirect/update", baseApi.UpdateRedirectConfig)
groupRouter.POST("/redirect", baseApi.GetRedirectConfig)
groupRouter.POST("/redirect/file", baseApi.UpdateRedirectConfigFile)
}
}

205
cmd/server/docs/docs.go

@ -9997,6 +9997,141 @@ const docTemplate = `{
}
}
},
"/websites/redirect": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取重定向配置",
"consumes": [
"application/json"
],
"tags": [
"Website"
],
"summary": "Get proxy conf",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteProxyReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/websites/redirect/file": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "更新重定向文件",
"consumes": [
"application/json"
],
"tags": [
"Website"
],
"summary": "Update redirect file",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.NginxRedirectUpdate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFuntions": [
{
"db": "websites",
"input_column": "id",
"input_value": "websiteID",
"isList": false,
"output_column": "primary_domain",
"output_value": "domain"
}
],
"bodyKeys": [
"websiteID"
],
"formatEN": "Nginx conf redirect file update [domain]",
"formatZH": "更新重定向文件 [domain]",
"paramKeys": []
}
}
},
"/websites/redirect/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改重定向配置",
"consumes": [
"application/json"
],
"tags": [
"Website"
],
"summary": "Update redirect conf",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.NginxRedirectReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFuntions": [
{
"db": "websites",
"input_column": "id",
"input_value": "websiteID",
"isList": false,
"output_column": "primary_domain",
"output_value": "domain"
}
],
"bodyKeys": [
"websiteID"
],
"formatEN": "Update domain [domain] redirect config",
"formatZH": "修改网站 [domain] 重定向理配置 ",
"paramKeys": []
}
}
},
"/websites/rewrite": {
"post": {
"security": [
@ -14494,6 +14629,76 @@ const docTemplate = `{
}
}
},
"request.NginxRedirectReq": {
"type": "object",
"required": [
"enable",
"keepPath",
"name",
"operate",
"redirect",
"target",
"type",
"websiteID"
],
"properties": {
"domains": {
"type": "array",
"items": {
"type": "string"
}
},
"enable": {
"type": "boolean"
},
"keepPath": {
"type": "boolean"
},
"name": {
"type": "string"
},
"operate": {
"type": "string"
},
"path": {
"type": "string"
},
"redirect": {
"type": "string"
},
"redirectRoot": {
"type": "boolean"
},
"target": {
"type": "string"
},
"type": {
"type": "string"
},
"websiteID": {
"type": "integer"
}
}
},
"request.NginxRedirectUpdate": {
"type": "object",
"required": [
"content",
"name",
"websiteID"
],
"properties": {
"content": {
"type": "string"
},
"name": {
"type": "string"
},
"websiteID": {
"type": "integer"
}
}
},
"request.NginxRewriteReq": {
"type": "object",
"required": [

205
cmd/server/docs/swagger.json

@ -9990,6 +9990,141 @@
}
}
},
"/websites/redirect": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取重定向配置",
"consumes": [
"application/json"
],
"tags": [
"Website"
],
"summary": "Get proxy conf",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.WebsiteProxyReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/websites/redirect/file": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "更新重定向文件",
"consumes": [
"application/json"
],
"tags": [
"Website"
],
"summary": "Update redirect file",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.NginxRedirectUpdate"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFuntions": [
{
"db": "websites",
"input_column": "id",
"input_value": "websiteID",
"isList": false,
"output_column": "primary_domain",
"output_value": "domain"
}
],
"bodyKeys": [
"websiteID"
],
"formatEN": "Nginx conf redirect file update [domain]",
"formatZH": "更新重定向文件 [domain]",
"paramKeys": []
}
}
},
"/websites/redirect/update": {
"post": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "修改重定向配置",
"consumes": [
"application/json"
],
"tags": [
"Website"
],
"summary": "Update redirect conf",
"parameters": [
{
"description": "request",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/request.NginxRedirectReq"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"x-panel-log": {
"BeforeFuntions": [
{
"db": "websites",
"input_column": "id",
"input_value": "websiteID",
"isList": false,
"output_column": "primary_domain",
"output_value": "domain"
}
],
"bodyKeys": [
"websiteID"
],
"formatEN": "Update domain [domain] redirect config",
"formatZH": "修改网站 [domain] 重定向理配置 ",
"paramKeys": []
}
}
},
"/websites/rewrite": {
"post": {
"security": [
@ -14487,6 +14622,76 @@
}
}
},
"request.NginxRedirectReq": {
"type": "object",
"required": [
"enable",
"keepPath",
"name",
"operate",
"redirect",
"target",
"type",
"websiteID"
],
"properties": {
"domains": {
"type": "array",
"items": {
"type": "string"
}
},
"enable": {
"type": "boolean"
},
"keepPath": {
"type": "boolean"
},
"name": {
"type": "string"
},
"operate": {
"type": "string"
},
"path": {
"type": "string"
},
"redirect": {
"type": "string"
},
"redirectRoot": {
"type": "boolean"
},
"target": {
"type": "string"
},
"type": {
"type": "string"
},
"websiteID": {
"type": "integer"
}
}
},
"request.NginxRedirectUpdate": {
"type": "object",
"required": [
"content",
"name",
"websiteID"
],
"properties": {
"content": {
"type": "string"
},
"name": {
"type": "string"
},
"websiteID": {
"type": "integer"
}
}
},
"request.NginxRewriteReq": {
"type": "object",
"required": [

135
cmd/server/docs/swagger.yaml

@ -2618,6 +2618,55 @@ definitions:
- name
- websiteID
type: object
request.NginxRedirectReq:
properties:
domains:
items:
type: string
type: array
enable:
type: boolean
keepPath:
type: boolean
name:
type: string
operate:
type: string
path:
type: string
redirect:
type: string
redirectRoot:
type: boolean
target:
type: string
type:
type: string
websiteID:
type: integer
required:
- enable
- keepPath
- name
- operate
- redirect
- target
- type
- websiteID
type: object
request.NginxRedirectUpdate:
properties:
content:
type: string
name:
type: string
websiteID:
type: integer
required:
- content
- name
- websiteID
type: object
request.NginxRewriteReq:
properties:
name:
@ -9911,6 +9960,92 @@ paths:
formatEN: Nginx conf proxy file update [domain]
formatZH: 更新反向代理文件 [domain]
paramKeys: []
/websites/redirect:
post:
consumes:
- application/json
description: 获取重定向配置
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.WebsiteProxyReq'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Get proxy conf
tags:
- Website
/websites/redirect/file:
post:
consumes:
- application/json
description: 更新重定向文件
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.NginxRedirectUpdate'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Update redirect file
tags:
- Website
x-panel-log:
BeforeFuntions:
- db: websites
input_column: id
input_value: websiteID
isList: false
output_column: primary_domain
output_value: domain
bodyKeys:
- websiteID
formatEN: Nginx conf redirect file update [domain]
formatZH: 更新重定向文件 [domain]
paramKeys: []
/websites/redirect/update:
post:
consumes:
- application/json
description: 修改重定向配置
parameters:
- description: request
in: body
name: request
required: true
schema:
$ref: '#/definitions/request.NginxRedirectReq'
responses:
"200":
description: OK
security:
- ApiKeyAuth: []
summary: Update redirect conf
tags:
- Website
x-panel-log:
BeforeFuntions:
- db: websites
input_column: id
input_value: websiteID
isList: false
output_column: primary_domain
output_value: domain
bodyKeys:
- websiteID
formatEN: Update domain [domain] redirect config
formatZH: '修改网站 [domain] 重定向理配置 '
paramKeys: []
/websites/rewrite:
post:
consumes:

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

@ -389,4 +389,30 @@ export namespace Website {
export interface LeechReq {
websiteID: number;
}
export interface WebsiteReq {
websiteID: number;
}
export interface RedirectConfig {
operate: string;
websiteID: number;
domains?: string[];
enable: boolean;
name: string;
keepPath: boolean;
type: string;
redirect: string;
path?: string;
target: string;
redirectRoot?: boolean;
filePath?: string;
content?: string;
}
export interface RedirectFileUpdate {
websiteID: number;
name: string;
content: string;
}
}

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

@ -214,3 +214,15 @@ export const GetAntiLeech = (req: Website.LeechReq) => {
export const UpdateAntiLeech = (req: Website.LeechConfig) => {
return http.post<any>(`/websites/leech/update`, req);
};
export const GetRedirectConfig = (req: Website.WebsiteReq) => {
return http.post<Website.RedirectConfig[]>(`/websites/redirect`, req);
};
export const OperateRedirectConfig = (req: Website.WebsiteReq) => {
return http.post<any>(`/websites/redirect/update`, req);
};
export const UpdateRedirectConfigFile = (req: Website.RedirectFileUpdate) => {
return http.post<any>(`/websites/redirect/file`, req);
};

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

@ -1438,6 +1438,17 @@ const message = {
privateKeyPath: 'Private key file',
certificatePath: 'Certificate file',
ipWhiteListHelper: 'The role of IP whitelist: all rules are invalid for IP whitelist',
redirect: 'redirect',
sourceDomain: 'source domain name/path',
targetURL: 'Target URL address',
keepPath: 'Keep URI parameters',
path: 'path',
redirectType: 'redirection type',
redirectWay: 'way',
keep: 'keep',
notKeep: 'Do not keep',
redirectRoot: 'Redirect to the homepage',
redirectHelper: '301 permanent redirection, 302 temporary redirection',
},
php: {
short_open_tag: 'Short tag support',

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

@ -1373,6 +1373,17 @@ const message = {
privateKeyPath: '私鑰文件',
certificatePath: '證書文件',
ipWhiteListHelper: 'IP白名單的作用所有規則對IP白名單無效',
redirect: '重定向',
sourceDomain: '源域名/路徑',
targetURL: '目標URL地址',
keepPath: '保留URI參數',
path: '路徑',
redirectType: '重定向類型',
redirectWay: '方式',
keep: '保留',
notKeep: '不保留',
redirectRoot: '重定向到首頁',
redirectHelper: '301永久重定向302臨時重定向',
},
php: {
short_open_tag: '短標簽支持',

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

@ -1373,6 +1373,17 @@ const message = {
privateKeyPath: '私钥文件',
certificatePath: '证书文件',
ipWhiteListHelper: 'IP 白名单的作用所有规则对IP白名单无效',
redirect: '重定向',
sourceDomain: '源域名/路径',
targetURL: '目标URL地址',
keepPath: '保留URI参数',
path: '路径',
redirectType: '重定向类型',
redirectWay: '方式',
keep: '保留',
notKeep: '不保留',
redirectRoot: '重定向到首页',
redirectHelper: '301永久重定向302临时重定向',
},
php: {
short_open_tag: '短标签支持',

6
frontend/src/views/website/website/config/basic/index.vue

@ -27,8 +27,11 @@
<el-tab-pane :label="$t('website.antiLeech')">
<AntiLeech :id="id" v-if="tabIndex == '8'"></AntiLeech>
</el-tab-pane>
<el-tab-pane :label="$t('website.redirect')">
<Redirect :id="id" v-if="tabIndex == '9'"></Redirect>
</el-tab-pane>
<el-tab-pane :label="$t('website.other')">
<Other :id="id" v-if="tabIndex == '9'"></Other>
<Other :id="id" v-if="tabIndex == '10'"></Other>
</el-tab-pane>
</el-tabs>
</template>
@ -46,6 +49,7 @@ import Rewrite from './rewrite/index.vue';
import Proxy from './proxy/index.vue';
import AuthBasic from './auth-basic/index.vue';
import AntiLeech from './anti-Leech/index.vue';
import Redirect from './redirect/index.vue';
const props = defineProps({
id: {

198
frontend/src/views/website/website/config/basic/redirect/create/index.vue

@ -0,0 +1,198 @@
<template>
<el-drawer v-model="open" :close-on-click-modal="false" size="40%" :before-close="handleClose">
<template #header>
<DrawerHeader
:header="$t('commons.button.' + redirect.operate) + $t('website.redirect')"
:back="handleClose"
/>
</template>
<el-row v-loading="loading">
<el-col :span="22" :offset="1">
<el-form ref="redirectForm" label-position="top" :model="redirect" :rules="rules">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input
v-model.trim="redirect.name"
:disabled="redirect.operate === 'edit' || redirect.type == '404'"
></el-input>
</el-form-item>
<el-form-item :label="$t('commons.table.type')" prop="type">
<el-select
v-model="redirect.type"
@change="changeType(redirect.type)"
:disabled="redirect.operate === 'edit'"
>
<el-option :label="$t('website.domain')" :value="'domain'"></el-option>
<el-option :label="$t('website.path')" :value="'path'"></el-option>
<el-option :label="'404'" :value="'404'"></el-option>
</el-select>
</el-form-item>
<el-form-item :label="$t('website.redirectWay')" prop="redirect">
<el-select v-model="redirect.redirect">
<el-option :label="'301'" :value="'301'"></el-option>
<el-option :label="'302'" :value="'302'"></el-option>
</el-select>
<span class="input-help">
{{ $t('website.redirectHelper') }}
</span>
</el-form-item>
<el-form-item :label="$t('website.path')" prop="path" v-if="redirect.type == 'path'">
<el-input v-model.trim="redirect.path"></el-input>
</el-form-item>
<el-form-item :label="$t('website.domain')" prop="domains" v-if="redirect.type == 'domain'">
<el-select v-model="redirect.domains" multiple>
<el-option
v-for="(item, index) in domains"
:key="index"
:value="item.domain"
:label="item.domain"
/>
</el-select>
</el-form-item>
<el-form-item :label="$t('website.redirectRoot')" prop="redirectRoot" v-if="redirect.type == '404'">
<el-switch v-model="redirect.redirectRoot"></el-switch>
</el-form-item>
<div v-if="!redirect.redirectRoot">
<el-form-item :label="$t('website.targetURL')" prop="target">
<el-input v-model.trim="redirect.target"></el-input>
</el-form-item>
<el-form-item :label="$t('website.keepPath')" prop="keepPath">
<el-switch v-model="redirect.keepPath"></el-switch>
</el-form-item>
</div>
</el-form>
</el-col>
</el-row>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit(redirectForm)" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
import DrawerHeader from '@/components/drawer-header/index.vue';
import { ListDomains, OperateRedirectConfig, GetRedirectConfig } from '@/api/modules/website';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { FormInstance } from 'element-plus';
import { ref } from 'vue';
import { MsgSuccess } from '@/utils/message';
import { Website } from '@/api/interface/website';
const redirectForm = ref<FormInstance>();
const rules = ref({
name: [Rules.requiredInput, Rules.appName],
type: [Rules.requiredSelect],
redirect: [Rules.requiredSelect],
domains: [Rules.requiredSelect],
target: [Rules.requiredInput],
path: [Rules.requiredInput],
});
const open = ref(false);
const loading = ref(false);
const initData = (): Website.RedirectConfig => ({
websiteID: 0,
operate: 'create',
enable: true,
name: '',
domains: [],
keepPath: true,
type: 'domain',
redirect: '301',
target: 'http://',
redirectRoot: false,
});
let redirect = ref(initData());
const em = defineEmits(['close']);
const handleClose = () => {
redirectForm.value?.resetFields();
open.value = false;
em('close', false);
};
const domains = ref([]);
const acceptParams = (redirectParam: Website.RedirectConfig) => {
if (redirectParam.operate == 'edit') {
redirect.value = redirectParam;
} else {
redirect.value = initData();
redirect.value.websiteID = redirectParam.websiteID;
}
domains.value = [];
getDomains();
open.value = true;
};
const changeType = (type: string) => {
if (type != '404') {
redirect.value.redirectRoot = false;
} else {
redirect.value.name = '404';
}
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
}
loading.value = true;
OperateRedirectConfig(redirect.value)
.then(() => {
if (redirect.value.operate == 'create') {
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
} else {
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
}
handleClose();
})
.finally(() => {
loading.value = false;
});
});
};
const getDomains = async () => {
try {
loading.value = true;
const res = await GetRedirectConfig({ websiteID: redirect.value.websiteID });
let oldDomains = [];
if (res.data) {
for (const old of res.data) {
if (old.type == 'domain') {
oldDomains = oldDomains.concat(old.domains);
}
}
}
ListDomains(redirect.value.websiteID)
.then((domainRes) => {
if (domainRes.data) {
if (oldDomains.length > 0) {
for (const data of domainRes.data) {
if (oldDomains.indexOf(data.domain) > -1) {
continue;
}
domains.value.push(data);
}
} else {
domains.value = domainRes.data || [];
}
}
})
.finally(() => {});
} catch (error) {
} finally {
loading.value = false;
}
};
defineExpose({
acceptParams,
});
</script>

93
frontend/src/views/website/website/config/basic/redirect/file/index.vue

@ -0,0 +1,93 @@
<template>
<el-drawer v-model="open" :close-on-click-modal="false" size="40%" :before-close="handleClose">
<template #header>
<DrawerHeader :header="$t('website.proxyFile')" :back="handleClose" />
</template>
<el-row v-loading="loading">
<el-col :span="22" :offset="1">
<div class="redirect-editor">
<codemirror
:autofocus="true"
placeholder=""
:indent-with-tab="true"
:tabSize="4"
:lineWrapping="true"
:matchBrackets="true"
style="height: 600px"
theme="cobalt"
:styleActiveLine="true"
:extensions="extensions"
v-model="req.content"
/>
</div>
</el-col>
</el-row>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit()" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-drawer>
</template>
<script lang="ts" setup>
import DrawerHeader from '@/components/drawer-header/index.vue';
import i18n from '@/lang';
import { FormInstance } from 'element-plus';
import { reactive, ref } from 'vue';
import { MsgSuccess } from '@/utils/message';
import { Codemirror } from 'vue-codemirror';
import { UpdateRedirectConfigFile } from '@/api/modules/website';
import { StreamLanguage } from '@codemirror/language';
import { nginx } from '@codemirror/legacy-modes/mode/nginx';
import { oneDark } from '@codemirror/theme-one-dark';
const extensions = [StreamLanguage.define(nginx), oneDark];
const proxyForm = ref<FormInstance>();
const open = ref(false);
const loading = ref(false);
const em = defineEmits(['close']);
const handleClose = () => {
proxyForm.value?.resetFields();
open.value = false;
em('close', false);
};
const req = reactive({
name: '',
websiteID: 0,
content: '',
});
const acceptParams = async (proxyreq: any) => {
req.name = proxyreq.name;
req.websiteID = proxyreq.websiteID;
req.content = proxyreq.content;
open.value = true;
};
const submit = async () => {
loading.value = true;
UpdateRedirectConfigFile(req)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
handleClose();
})
.finally(() => {
loading.value = false;
});
};
defineExpose({
acceptParams,
});
</script>
<style scoped>
.redirect-editor {
margin-top: 10px;
width: 100%;
}
</style>

194
frontend/src/views/website/website/config/basic/redirect/index.vue

@ -0,0 +1,194 @@
<template>
<ComplexTable :data="data" @search="search" v-loading="loading">
<template #toolbar>
<el-button type="primary" plain @click="openCreate">
{{ $t('commons.button.create') + $t('website.redirect') }}
</el-button>
</template>
<el-table-column :label="$t('commons.table.name')" prop="name"></el-table-column>
<el-table-column :label="$t('website.sourceDomain')" prop="domain" min-width="150px">
<template #default="{ row }">
<span v-if="row.type === 'domain'">{{ row.domains.join(',') }}</span>
<span v-else>{{ row.path }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.type')" prop="type" width="80px">
<template #default="{ row }">
<span v-if="row.type != 404">{{ $t('website.' + row.type) }}</span>
<span v-else>{{ 404 }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('website.redirectWay')" prop="redirect" width="80px"></el-table-column>
<el-table-column :label="$t('website.targetURL')" prop="target"></el-table-column>
<el-table-column :label="$t('website.keepPath')" prop="keepPath">
<template #default="{ row }">
<span v-if="row.type != '404'">{{ row.keepPath ? $t('website.keep') : $t('website.notKeep') }}</span>
<span v-else></span>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.status')" prop="enable">
<template #default="{ row }">
<el-button v-if="row.enable" link type="success" :icon="VideoPlay" @click="opProxy(row)">
{{ $t('commons.status.running') }}
</el-button>
<el-button v-else link type="danger" :icon="VideoPause" @click="opProxy(row)">
{{ $t('commons.status.stopped') }}
</el-button>
</template>
</el-table-column>
<fu-table-operations
:ellipsis="10"
width="260px"
:buttons="buttons"
:label="$t('commons.table.operate')"
:fixed="mobile ? false : 'right'"
fix
/>
</ComplexTable>
<Create ref="createRef" @close="search()" />
<File ref="fileRef" @close="search()" />
</template>
<script lang="ts" setup name="proxy">
import { Website } from '@/api/interface/website';
import { OperateRedirectConfig, GetRedirectConfig } from '@/api/modules/website';
import { computed, onMounted, ref } from 'vue';
import Create from './create/index.vue';
import File from './file/index.vue';
import { VideoPlay, VideoPause } from '@element-plus/icons-vue';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { useDeleteData } from '@/hooks/use-delete-data';
import { ElMessageBox } from 'element-plus';
import { GlobalStore } from '@/store';
const globalStore = GlobalStore();
const props = defineProps({
id: {
type: Number,
default: 0,
},
});
const mobile = computed(() => {
return globalStore.isMobile();
});
const id = computed(() => {
return props.id;
});
const loading = ref(false);
const data = ref();
const createRef = ref();
const fileRef = ref();
const buttons = [
{
label: i18n.global.t('website.proxyFile'),
click: function (row: Website.RedirectConfig) {
openEditFile(row);
},
disabled: (row: Website.RedirectConfig) => {
return !row.enable;
},
},
{
label: i18n.global.t('commons.button.edit'),
click: function (row: Website.RedirectConfig) {
openEdit(row);
},
disabled: (row: Website.ProxyConfig) => {
return !row.enable;
},
},
{
label: i18n.global.t('commons.button.delete'),
click: function (row: Website.RedirectConfig) {
deleteProxy(row);
},
},
];
const initData = (id: number): Website.RedirectConfig => ({
websiteID: id,
operate: 'create',
enable: true,
name: '',
domains: [],
keepPath: true,
type: '',
redirect: '',
target: '',
});
const openCreate = () => {
createRef.value.acceptParams(initData(id.value));
};
const openEdit = (proxyConfig: Website.RedirectConfig) => {
let proxy = JSON.parse(JSON.stringify(proxyConfig));
proxy.operate = 'edit';
createRef.value.acceptParams(proxy);
};
const openEditFile = (proxyConfig: Website.RedirectConfig) => {
fileRef.value.acceptParams({
name: proxyConfig.name,
content: proxyConfig.content,
websiteID: proxyConfig.websiteID,
});
};
const deleteProxy = async (redirectConfig: Website.RedirectConfig) => {
redirectConfig.operate = 'delete';
await useDeleteData(OperateRedirectConfig, redirectConfig, 'commons.msg.delete');
search();
};
const submit = async (redirectConfig: Website.RedirectConfig) => {
loading.value = true;
await OperateRedirectConfig(redirectConfig)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
search();
})
.finally(() => {
loading.value = false;
});
};
const opProxy = (redirectConfig: Website.RedirectConfig) => {
let proxy = JSON.parse(JSON.stringify(redirectConfig));
proxy.enable = !redirectConfig.enable;
let message = '';
if (proxy.enable) {
proxy.operate = 'enable';
message = i18n.global.t('commons.button.start') + i18n.global.t('website.redirect');
} else {
proxy.operate = 'disable';
message = i18n.global.t('commons.button.stop') + i18n.global.t('website.redirect');
}
ElMessageBox.confirm(message, i18n.global.t('cronjob.changeStatus'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
})
.then(async () => {
await submit(proxy);
})
.catch(() => {});
};
const search = async () => {
try {
loading.value = true;
const res = await GetRedirectConfig({ websiteID: id.value });
data.value = res.data || [];
} catch (error) {
} finally {
loading.value = false;
}
};
onMounted(() => {
search();
});
</script>
Loading…
Cancel
Save