mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 04:22:33 +08:00
feat: 支持 webhook, close #695
This commit is contained in:
@@ -118,6 +118,8 @@ func initWeb() (*app.Web, error) {
|
||||
systemctlService := service.NewSystemctlService(locale)
|
||||
toolboxSystemService := service.NewToolboxSystemService(locale)
|
||||
toolboxBenchmarkService := service.NewToolboxBenchmarkService(locale)
|
||||
webHookRepo := data.NewWebHookRepo(locale, db)
|
||||
webHookService := service.NewWebHookService(webHookRepo)
|
||||
codeserverApp := codeserver.NewApp()
|
||||
dockerApp := docker.NewApp()
|
||||
fail2banApp := fail2ban.NewApp(locale, websiteRepo)
|
||||
@@ -139,7 +141,7 @@ func initWeb() (*app.Web, error) {
|
||||
s3fsApp := s3fs.NewApp(locale)
|
||||
supervisorApp := supervisor.NewApp(locale)
|
||||
loader := bootstrap.NewLoader(codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
|
||||
http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, environmentService, environmentPHPService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, loader)
|
||||
http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, environmentService, environmentPHPService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, webHookService, loader)
|
||||
wsService := service.NewWsService(locale, config, logger, sshRepo)
|
||||
ws := route.NewWs(wsService)
|
||||
mux, err := bootstrap.NewRouter(locale, middlewares, http, ws)
|
||||
|
||||
31
internal/biz/webhook.go
Normal file
31
internal/biz/webhook.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/panel/internal/http/request"
|
||||
)
|
||||
|
||||
type WebHook struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"not null;default:''" json:"name"` // 钩子名称
|
||||
Key string `gorm:"not null;uniqueIndex" json:"key"` // 唯一标识(用于 URL)
|
||||
Script string `gorm:"not null;default:''" json:"script"` // 脚本内容
|
||||
Raw bool `gorm:"not null;default:false" json:"raw"` // 是否以原始格式返回输出
|
||||
User string `gorm:"not null;default:''" json:"user"` // 以哪个用户身份执行脚本
|
||||
Status bool `gorm:"not null;default:true" json:"status"` // 启用状态
|
||||
CallCount uint `gorm:"not null;default:0" json:"call_count"` // 调用次数
|
||||
LastCallAt time.Time `json:"last_call_at"` // 上次调用时间
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type WebHookRepo interface {
|
||||
List(page, limit uint) ([]*WebHook, int64, error)
|
||||
Get(id uint) (*WebHook, error)
|
||||
GetByKey(key string) (*WebHook, error)
|
||||
Create(req *request.WebHookCreate) (*WebHook, error)
|
||||
Update(req *request.WebHookUpdate) error
|
||||
Delete(id uint) error
|
||||
Call(key string) (string, error)
|
||||
}
|
||||
@@ -27,5 +27,6 @@ var ProviderSet = wire.NewSet(
|
||||
NewTaskRepo,
|
||||
NewUserRepo,
|
||||
NewUserTokenRepo,
|
||||
NewWebHookRepo,
|
||||
NewWebsiteRepo,
|
||||
)
|
||||
|
||||
162
internal/data/webhook.go
Normal file
162
internal/data/webhook.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/utils/str"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/acepanel/panel/internal/app"
|
||||
"github.com/acepanel/panel/internal/biz"
|
||||
"github.com/acepanel/panel/internal/http/request"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
)
|
||||
|
||||
type webhookRepo struct {
|
||||
t *gotext.Locale
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewWebHookRepo(t *gotext.Locale, db *gorm.DB) biz.WebHookRepo {
|
||||
return &webhookRepo{
|
||||
t: t,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *webhookRepo) List(page, limit uint) ([]*biz.WebHook, int64, error) {
|
||||
webhooks := make([]*biz.WebHook, 0)
|
||||
var total int64
|
||||
err := r.db.Model(&biz.WebHook{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&webhooks).Error
|
||||
return webhooks, total, err
|
||||
}
|
||||
|
||||
func (r *webhookRepo) Get(id uint) (*biz.WebHook, error) {
|
||||
webhook := new(biz.WebHook)
|
||||
if err := r.db.Where("id = ?", id).First(webhook).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return webhook, nil
|
||||
}
|
||||
|
||||
func (r *webhookRepo) GetByKey(key string) (*biz.WebHook, error) {
|
||||
webhook := new(biz.WebHook)
|
||||
if err := r.db.Where("`key` = ?", key).First(webhook).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return webhook, nil
|
||||
}
|
||||
|
||||
func (r *webhookRepo) Create(req *request.WebHookCreate) (*biz.WebHook, error) {
|
||||
if err := os.MkdirAll(r.webhookDir(), 0755); err != nil {
|
||||
return nil, errors.New(r.t.Get("failed to create webhook directory: %v", err))
|
||||
}
|
||||
|
||||
key := str.Random(32)
|
||||
scriptFile := r.scriptPath(key)
|
||||
if err := io.Write(scriptFile, req.Script, 0755); err != nil {
|
||||
return nil, errors.New(r.t.Get("failed to write webhook script: %v", err))
|
||||
}
|
||||
|
||||
webhook := &biz.WebHook{
|
||||
Name: req.Name,
|
||||
Key: key,
|
||||
Script: req.Script,
|
||||
Raw: req.Raw,
|
||||
User: req.User,
|
||||
Status: true,
|
||||
}
|
||||
|
||||
if err := r.db.Create(webhook).Error; err != nil {
|
||||
_ = os.Remove(scriptFile)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return webhook, nil
|
||||
}
|
||||
|
||||
func (r *webhookRepo) Update(req *request.WebHookUpdate) error {
|
||||
webhook, err := r.Get(req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scriptFile := r.scriptPath(webhook.Key)
|
||||
if err = io.Write(scriptFile, req.Script, 0755); err != nil {
|
||||
return errors.New(r.t.Get("failed to write webhook script: %v", err))
|
||||
}
|
||||
|
||||
return r.db.Model(&biz.WebHook{}).Where("id = ?", req.ID).Updates(map[string]any{
|
||||
"name": req.Name,
|
||||
"script": req.Script,
|
||||
"raw": req.Raw,
|
||||
"user": req.User,
|
||||
"status": req.Status,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *webhookRepo) Delete(id uint) error {
|
||||
webhook, err := r.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scriptFile := r.scriptPath(webhook.Key)
|
||||
_ = os.Remove(scriptFile)
|
||||
|
||||
return r.db.Delete(&biz.WebHook{}, id).Error
|
||||
}
|
||||
|
||||
func (r *webhookRepo) Call(key string) (string, error) {
|
||||
webhook, err := r.GetByKey(key)
|
||||
if err != nil {
|
||||
return "", errors.New(r.t.Get("webhook not found"))
|
||||
}
|
||||
|
||||
if !webhook.Status {
|
||||
return "", errors.New(r.t.Get("webhook is disabled"))
|
||||
}
|
||||
|
||||
scriptFile := r.scriptPath(key)
|
||||
if !io.Exists(scriptFile) {
|
||||
return "", errors.New(r.t.Get("webhook script not found"))
|
||||
}
|
||||
|
||||
// 执行脚本
|
||||
var cmd *exec.Cmd
|
||||
if webhook.User == "" || webhook.User == "root" {
|
||||
cmd = exec.Command("bash", scriptFile)
|
||||
} else {
|
||||
cmd = exec.Command("su", "-s", "/bin/bash", "-c", fmt.Sprintf("bash %s", scriptFile), webhook.User)
|
||||
}
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
// 更新调用统计
|
||||
_ = r.db.Model(&biz.WebHook{}).Where("`key` = ?", key).Updates(map[string]any{
|
||||
"call_count": gorm.Expr("call_count + 1"),
|
||||
"last_call_at": time.Now(),
|
||||
}).Error
|
||||
|
||||
if err != nil {
|
||||
return string(output), fmt.Errorf("script execution failed: %w, output: %s", err, string(output))
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
// webhookDir 返回 webhook 脚本存储目录
|
||||
func (r *webhookRepo) webhookDir() string {
|
||||
return filepath.Join(app.Root, "server", "webhook")
|
||||
}
|
||||
|
||||
// scriptPath 返回指定 key 的脚本路径
|
||||
func (r *webhookRepo) scriptPath(key string) string {
|
||||
return filepath.Join(r.webhookDir(), key+".sh")
|
||||
}
|
||||
@@ -112,7 +112,13 @@ func Entrance(t *gotext.Locale, conf *config.Config, session *sessions.Manager)
|
||||
return
|
||||
}
|
||||
|
||||
// 情况四:非调试模式且未通过验证的请求,返回错误
|
||||
// 情况四:Webhook 访问,跳过验证
|
||||
if strings.HasPrefix(r.URL.Path, "/webhook/") {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// 情况五:非调试模式且未通过验证的请求,返回错误
|
||||
if !conf.App.Debug &&
|
||||
sess.Missing("verify_entrance") &&
|
||||
r.URL.Path != "/robots.txt" {
|
||||
|
||||
21
internal/http/request/webhook.go
Normal file
21
internal/http/request/webhook.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package request
|
||||
|
||||
type WebHookCreate struct {
|
||||
Name string `json:"name" form:"name" validate:"required"`
|
||||
Script string `json:"script" form:"script" validate:"required"`
|
||||
Raw bool `json:"raw" form:"raw"`
|
||||
User string `json:"user" form:"user"`
|
||||
}
|
||||
|
||||
type WebHookUpdate struct {
|
||||
ID uint `json:"id" form:"id" uri:"id" validate:"required|exists:web_hooks,id"`
|
||||
Name string `json:"name" form:"name" validate:"required"`
|
||||
Script string `json:"script" form:"script" validate:"required"`
|
||||
Raw bool `json:"raw" form:"raw"`
|
||||
User string `json:"user" form:"user" validate:"required"`
|
||||
Status bool `json:"status" form:"status"`
|
||||
}
|
||||
|
||||
type WebHookKey struct {
|
||||
Key string `json:"key" form:"key" uri:"key" validate:"required"`
|
||||
}
|
||||
@@ -9,107 +9,45 @@ import (
|
||||
|
||||
func init() {
|
||||
Migrations = append(Migrations, &gormigrate.Migration{
|
||||
ID: "20240812-init",
|
||||
ID: "20260101-init",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
&biz.App{},
|
||||
&biz.Cache{},
|
||||
&biz.Cert{},
|
||||
&biz.CertDNS{},
|
||||
&biz.CertAccount{},
|
||||
&biz.Cron{},
|
||||
&biz.Monitor{},
|
||||
&biz.App{},
|
||||
&biz.Setting{},
|
||||
&biz.Task{},
|
||||
&biz.User{},
|
||||
&biz.Website{},
|
||||
)
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Migrator().DropTable(
|
||||
&biz.Cert{},
|
||||
&biz.CertDNS{},
|
||||
&biz.CertAccount{},
|
||||
&biz.Cron{},
|
||||
&biz.Monitor{},
|
||||
&biz.App{},
|
||||
&biz.Setting{},
|
||||
&biz.Task{},
|
||||
&biz.User{},
|
||||
&biz.Website{},
|
||||
)
|
||||
},
|
||||
})
|
||||
Migrations = append(Migrations, &gormigrate.Migration{
|
||||
ID: "20241022-ssh",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
&biz.SSH{},
|
||||
)
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Migrator().DropTable(
|
||||
&biz.SSH{},
|
||||
)
|
||||
},
|
||||
})
|
||||
Migrations = append(Migrations, &gormigrate.Migration{
|
||||
ID: "20241124-database",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
_ = tx.Migrator().DropTable("databases")
|
||||
return tx.AutoMigrate(
|
||||
&biz.DatabaseServer{},
|
||||
&biz.DatabaseUser{},
|
||||
)
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Migrator().DropTable(
|
||||
&biz.DatabaseServer{},
|
||||
&biz.DatabaseUser{},
|
||||
)
|
||||
},
|
||||
})
|
||||
Migrations = append(Migrations, &gormigrate.Migration{
|
||||
ID: "20250318-cert-script",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
&biz.Cache{},
|
||||
&biz.Cert{},
|
||||
&biz.CertDNS{},
|
||||
&biz.CertAccount{},
|
||||
&biz.Cron{},
|
||||
&biz.Monitor{},
|
||||
&biz.App{},
|
||||
&biz.Setting{},
|
||||
&biz.SSH{},
|
||||
&biz.Task{},
|
||||
&biz.User{},
|
||||
&biz.Website{},
|
||||
&biz.SSH{},
|
||||
&biz.DatabaseServer{},
|
||||
&biz.DatabaseUser{},
|
||||
)
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Migrator().DropColumn(&biz.Cert{}, "script")
|
||||
},
|
||||
})
|
||||
Migrations = append(Migrations, &gormigrate.Migration{
|
||||
ID: "20250514-user-website",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
&biz.User{},
|
||||
&biz.Website{},
|
||||
&biz.UserToken{},
|
||||
&biz.WebHook{},
|
||||
&biz.Website{},
|
||||
)
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
if err := tx.Migrator().DropColumn(&biz.User{}, "two_fa"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Migrator().DropColumn(&biz.Website{}, "type"); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Migrator().DropTable(&biz.UserToken{})
|
||||
return tx.Migrator().DropTable(
|
||||
&biz.App{},
|
||||
&biz.Cert{},
|
||||
&biz.CertAccount{},
|
||||
&biz.CertDNS{},
|
||||
&biz.Cron{},
|
||||
&biz.DatabaseServer{},
|
||||
&biz.DatabaseUser{},
|
||||
&biz.Monitor{},
|
||||
&biz.Setting{},
|
||||
&biz.SSH{},
|
||||
&biz.Task{},
|
||||
&biz.User{},
|
||||
&biz.UserToken{},
|
||||
&biz.WebHook{},
|
||||
&biz.Website{},
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ type Http struct {
|
||||
systemctl *service.SystemctlService
|
||||
toolboxSystem *service.ToolboxSystemService
|
||||
toolboxBenchmark *service.ToolboxBenchmarkService
|
||||
webhook *service.WebHookService
|
||||
apps *apploader.Loader
|
||||
}
|
||||
|
||||
@@ -84,6 +85,7 @@ func NewHttp(
|
||||
systemctl *service.SystemctlService,
|
||||
toolboxSystem *service.ToolboxSystemService,
|
||||
toolboxBenchmark *service.ToolboxBenchmarkService,
|
||||
webhook *service.WebHookService,
|
||||
apps *apploader.Loader,
|
||||
) *Http {
|
||||
return &Http{
|
||||
@@ -119,6 +121,7 @@ func NewHttp(
|
||||
systemctl: systemctl,
|
||||
toolboxSystem: toolboxSystem,
|
||||
toolboxBenchmark: toolboxBenchmark,
|
||||
webhook: webhook,
|
||||
apps: apps,
|
||||
}
|
||||
}
|
||||
@@ -442,11 +445,23 @@ func (route *Http) Register(r *chi.Mux) {
|
||||
r.Post("/test", route.toolboxBenchmark.Test)
|
||||
})
|
||||
|
||||
r.Route("/webhook", func(r chi.Router) {
|
||||
r.Get("/", route.webhook.List)
|
||||
r.Post("/", route.webhook.Create)
|
||||
r.Put("/{id}", route.webhook.Update)
|
||||
r.Get("/{id}", route.webhook.Get)
|
||||
r.Delete("/{id}", route.webhook.Delete)
|
||||
})
|
||||
|
||||
r.Route("/apps", func(r chi.Router) {
|
||||
route.apps.Register(r)
|
||||
})
|
||||
})
|
||||
|
||||
// WebHook 调用接口
|
||||
r.Get("/webhook/{key}", route.webhook.Call)
|
||||
r.Post("/webhook/{key}", route.webhook.Call)
|
||||
|
||||
r.NotFound(func(writer http.ResponseWriter, request *http.Request) {
|
||||
// /api 开头的返回 404
|
||||
if strings.HasPrefix(request.URL.Path, "/api") {
|
||||
|
||||
@@ -33,6 +33,7 @@ var ProviderSet = wire.NewSet(
|
||||
NewTaskService,
|
||||
NewUserService,
|
||||
NewUserTokenService,
|
||||
NewWebHookService,
|
||||
NewWebsiteService,
|
||||
NewToolboxSystemService,
|
||||
NewToolboxBenchmarkService,
|
||||
|
||||
139
internal/service/webhook.go
Normal file
139
internal/service/webhook.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/libtnb/chix"
|
||||
|
||||
"github.com/acepanel/panel/internal/biz"
|
||||
"github.com/acepanel/panel/internal/http/request"
|
||||
)
|
||||
|
||||
type WebHookService struct {
|
||||
webhookRepo biz.WebHookRepo
|
||||
}
|
||||
|
||||
func NewWebHookService(webhook biz.WebHookRepo) *WebHookService {
|
||||
return &WebHookService{
|
||||
webhookRepo: webhook,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WebHookService) List(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.Paginate](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
webhooks, total, err := s.webhookRepo.List(req.Page, req.Limit)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": webhooks,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WebHookService) Get(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ID](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
webhook, err := s.webhookRepo.Get(req.ID)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, webhook)
|
||||
}
|
||||
|
||||
func (s *WebHookService) Create(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.WebHookCreate](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
webhook, err := s.webhookRepo.Create(req)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, webhook)
|
||||
}
|
||||
|
||||
func (s *WebHookService) Update(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.WebHookUpdate](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.webhookRepo.Update(req); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *WebHookService) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ID](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.webhookRepo.Delete(req.ID); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// Call 处理 webhook 调用请求
|
||||
func (s *WebHookService) Call(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.WebHookKey](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取 webhook 信息以判断返回格式
|
||||
webhook, err := s.webhookRepo.GetByKey(req.Key)
|
||||
if err != nil {
|
||||
Error(w, http.StatusNotFound, "webhook not found")
|
||||
return
|
||||
}
|
||||
|
||||
output, err := s.webhookRepo.Call(req.Key)
|
||||
if err != nil {
|
||||
if webhook.Raw {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte(output))
|
||||
return
|
||||
}
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if webhook.Raw {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
_, _ = w.Write([]byte(output))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, chix.M{
|
||||
"output": output,
|
||||
})
|
||||
}
|
||||
14
web/src/api/panel/webhook/index.ts
Normal file
14
web/src/api/panel/webhook/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { http } from '@/utils'
|
||||
|
||||
export default {
|
||||
// 获取 WebHook 列表
|
||||
list: (page: number, limit: number): any => http.Get('/webhook', { params: { page, limit } }),
|
||||
// 获取 WebHook 信息
|
||||
get: (id: number): any => http.Get(`/webhook/${id}`),
|
||||
// 创建 WebHook
|
||||
create: (req: any): any => http.Post('/webhook', req),
|
||||
// 修改 WebHook
|
||||
update: (id: number, req: any): any => http.Put(`/webhook/${id}`, req),
|
||||
// 删除 WebHook
|
||||
delete: (id: number): any => http.Delete(`/webhook/${id}`)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ defineOptions({
|
||||
import BenchmarkView from '@/views/toolbox/BenchmarkView.vue'
|
||||
import ProcessView from '@/views/toolbox/ProcessView.vue'
|
||||
import SystemView from '@/views/toolbox/SystemView.vue'
|
||||
import WebHookView from '@/views/toolbox/WebHookView.vue'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
@@ -18,12 +19,14 @@ const current = ref('process')
|
||||
<n-tabs v-model:value="current" animated>
|
||||
<n-tab name="process" :tab="$gettext('Process')" />
|
||||
<n-tab name="system" :tab="$gettext('System')" />
|
||||
<n-tab name="webhook" :tab="$gettext('WebHook')" />
|
||||
<n-tab name="benchmark" :tab="$gettext('Benchmark')" />
|
||||
</n-tabs>
|
||||
</template>
|
||||
<n-flex vertical>
|
||||
<process-view v-if="current === 'process'" />
|
||||
<system-view v-if="current === 'system'" />
|
||||
<web-hook-view v-if="current === 'webhook'" />
|
||||
<benchmark-view v-if="current === 'benchmark'" />
|
||||
</n-flex>
|
||||
</common-page>
|
||||
|
||||
394
web/src/views/toolbox/WebHookView.vue
Normal file
394
web/src/views/toolbox/WebHookView.vue
Normal file
@@ -0,0 +1,394 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NDataTable, NInput, NPopconfirm, NSwitch, NTag } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import webhook from '@/api/panel/webhook'
|
||||
import { formatDateTime } from '@/utils'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
// 创建弹窗
|
||||
const createModal = ref(false)
|
||||
const createModel = ref({
|
||||
name: '',
|
||||
script: '#!/bin/bash\n\n',
|
||||
raw: false,
|
||||
user: 'root'
|
||||
})
|
||||
|
||||
// 编辑弹窗
|
||||
const editModal = ref(false)
|
||||
const editModel = ref({
|
||||
id: 0,
|
||||
name: '',
|
||||
script: '',
|
||||
raw: false,
|
||||
user: '',
|
||||
status: true
|
||||
})
|
||||
|
||||
const columns: any = [
|
||||
{
|
||||
title: $gettext('Name'),
|
||||
key: 'name',
|
||||
minWidth: 150,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: 'Key',
|
||||
key: 'key',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any) {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
type: 'info',
|
||||
bordered: false
|
||||
},
|
||||
{
|
||||
default: () => row.key
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $gettext('Run As User'),
|
||||
key: 'user',
|
||||
width: 120,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any) {
|
||||
return row.user || 'root'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $gettext('Raw Output'),
|
||||
key: 'raw',
|
||||
width: 120,
|
||||
resizable: true,
|
||||
render(row: any) {
|
||||
return h(
|
||||
NTag,
|
||||
{
|
||||
type: row.raw ? 'success' : 'default',
|
||||
size: 'small'
|
||||
},
|
||||
{
|
||||
default: () => (row.raw ? $gettext('Yes') : $gettext('No'))
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $gettext('Enabled'),
|
||||
key: 'status',
|
||||
width: 100,
|
||||
resizable: true,
|
||||
render(row: any) {
|
||||
return h(NSwitch, {
|
||||
size: 'small',
|
||||
rubberBand: false,
|
||||
value: row.status,
|
||||
onUpdateValue: () => handleStatusChange(row)
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $gettext('Call Count'),
|
||||
key: 'call_count',
|
||||
width: 100,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: $gettext('Last Call'),
|
||||
key: 'last_call_at',
|
||||
width: 180,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any): string {
|
||||
if (!row.last_call_at || row.last_call_at === '0001-01-01T00:00:00Z') {
|
||||
return '-'
|
||||
}
|
||||
return formatDateTime(row.last_call_at)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $gettext('Creation Time'),
|
||||
key: 'created_at',
|
||||
width: 180,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any): string {
|
||||
return formatDateTime(row.created_at)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $gettext('Actions'),
|
||||
key: 'actions',
|
||||
width: 280,
|
||||
hideInExcel: true,
|
||||
render(row: any) {
|
||||
return [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'info',
|
||||
secondary: true,
|
||||
onClick: () => handleCopyUrl(row)
|
||||
},
|
||||
{
|
||||
default: () => $gettext('Copy URL')
|
||||
}
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
style: 'margin-left: 10px;',
|
||||
onClick: () => handleEdit(row)
|
||||
},
|
||||
{
|
||||
default: () => $gettext('Edit')
|
||||
}
|
||||
),
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleDelete(row.id)
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return $gettext('Are you sure you want to delete this WebHook?')
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
style: 'margin-left: 10px;'
|
||||
},
|
||||
{
|
||||
default: () => $gettext('Delete')
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const { loading, data, page, total, pageSize, pageCount, refresh } = usePagination(
|
||||
(page, pageSize) => webhook.list(page, pageSize),
|
||||
{
|
||||
initialData: { total: 0, list: [] },
|
||||
initialPageSize: 20,
|
||||
total: (res: any) => res.total,
|
||||
data: (res: any) => res.items
|
||||
}
|
||||
)
|
||||
|
||||
const handleStatusChange = (row: any) => {
|
||||
useRequest(
|
||||
webhook.update(row.id, {
|
||||
name: row.name,
|
||||
script: row.script,
|
||||
raw: row.raw,
|
||||
user: row.user,
|
||||
status: !row.status
|
||||
})
|
||||
).onSuccess(() => {
|
||||
row.status = !row.status
|
||||
window.$message.success($gettext('Modified successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleCopyUrl = (row: any) => {
|
||||
const url = `${window.location.origin}/webhook/${row.key}`
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
window.$message.success($gettext('URL copied to clipboard'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
editModel.value = {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
script: row.script,
|
||||
raw: row.raw,
|
||||
user: row.user || 'root',
|
||||
status: row.status
|
||||
}
|
||||
editModal.value = true
|
||||
}
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
useRequest(webhook.delete(id)).onSuccess(() => {
|
||||
window.$message.success($gettext('Deleted successfully'))
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
if (!createModel.value.name) {
|
||||
window.$message.warning($gettext('Please enter a name'))
|
||||
return
|
||||
}
|
||||
if (!createModel.value.script) {
|
||||
window.$message.warning($gettext('Please enter a script'))
|
||||
return
|
||||
}
|
||||
useRequest(webhook.create(createModel.value)).onSuccess(() => {
|
||||
createModal.value = false
|
||||
createModel.value = {
|
||||
name: '',
|
||||
script: '#!/bin/bash\n\n',
|
||||
raw: false,
|
||||
user: 'root'
|
||||
}
|
||||
window.$message.success($gettext('Created successfully'))
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
const handleUpdate = () => {
|
||||
if (!editModel.value.name) {
|
||||
window.$message.warning($gettext('Please enter a name'))
|
||||
return
|
||||
}
|
||||
if (!editModel.value.script) {
|
||||
window.$message.warning($gettext('Please enter a script'))
|
||||
return
|
||||
}
|
||||
useRequest(
|
||||
webhook.update(editModel.value.id, {
|
||||
name: editModel.value.name,
|
||||
script: editModel.value.script,
|
||||
raw: editModel.value.raw,
|
||||
user: editModel.value.user,
|
||||
status: editModel.value.status
|
||||
})
|
||||
).onSuccess(() => {
|
||||
editModal.value = false
|
||||
window.$message.success($gettext('Modified successfully'))
|
||||
refresh()
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refresh()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex vertical>
|
||||
<n-flex justify="end">
|
||||
<n-button type="primary" @click="createModal = true">
|
||||
{{ $gettext('Create WebHook') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="1400"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:row-key="(row: any) => row.id"
|
||||
v-model:page="page"
|
||||
v-model:pageSize="pageSize"
|
||||
:pagination="{
|
||||
page: page,
|
||||
pageCount: pageCount,
|
||||
pageSize: pageSize,
|
||||
itemCount: total,
|
||||
showQuickJumper: true,
|
||||
showSizePicker: true,
|
||||
pageSizes: [20, 50, 100, 200]
|
||||
}"
|
||||
/>
|
||||
</n-flex>
|
||||
|
||||
<!-- 创建弹窗 -->
|
||||
<n-modal
|
||||
v-model:show="createModal"
|
||||
preset="card"
|
||||
:title="$gettext('Create WebHook')"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
>
|
||||
<n-form :model="createModel">
|
||||
<n-form-item :label="$gettext('Name')">
|
||||
<n-input v-model:value="createModel.name" :placeholder="$gettext('Enter WebHook name')" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('User')">
|
||||
<n-input
|
||||
v-model:value="createModel.user"
|
||||
:placeholder="$gettext('User to run the script (default: root)')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Raw Output')">
|
||||
<n-switch v-model:value="createModel.raw" />
|
||||
<span ml-10 text-gray>
|
||||
{{ $gettext('Return script output as raw text instead of JSON') }}
|
||||
</span>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Script')">
|
||||
<common-editor v-model:value="createModel.script" lang="sh" height="40vh" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-button type="info" @click="handleCreate" block>
|
||||
{{ $gettext('Create') }}
|
||||
</n-button>
|
||||
</n-modal>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<n-modal
|
||||
v-model:show="editModal"
|
||||
preset="card"
|
||||
:title="$gettext('Edit WebHook')"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
>
|
||||
<n-form :model="editModel">
|
||||
<n-form-item :label="$gettext('Name')">
|
||||
<n-input v-model:value="editModel.name" :placeholder="$gettext('Enter WebHook name')" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('User')">
|
||||
<n-input
|
||||
v-model:value="editModel.user"
|
||||
:placeholder="$gettext('User to run the script (default: root)')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Raw Output')">
|
||||
<n-switch v-model:value="editModel.raw" />
|
||||
<span ml-10 text-gray>
|
||||
{{ $gettext('Return script output as raw text instead of JSON') }}
|
||||
</span>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Enabled')">
|
||||
<n-switch v-model:value="editModel.status" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Script')">
|
||||
<common-editor v-model:value="editModel.script" lang="sh" height="40vh" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-button type="info" @click="handleUpdate" block>
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
Reference in New Issue
Block a user