chore: sync local updates
This commit is contained in:
@@ -2,14 +2,15 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -23,12 +24,12 @@ import (
|
||||
// ─── 持久化数据类型 ───────────────────────────────────────────────
|
||||
|
||||
type SSHProfile struct {
|
||||
Name string `json:"name,omitempty"` // 文件名(不含 .json)
|
||||
Alias string `json:"alias"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Alias string `json:"alias"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password,omitempty"`
|
||||
PrivateKey string `json:"privateKey,omitempty"`
|
||||
Passphrase string `json:"passphrase,omitempty"`
|
||||
}
|
||||
@@ -43,7 +44,7 @@ type ScriptInfo struct {
|
||||
Content string `json:"content,omitempty"`
|
||||
}
|
||||
|
||||
// 配置与数据目录辅助函数见 config.go
|
||||
// 配置与数据库辅助函数见 config.go / db.go
|
||||
|
||||
type wsMessage struct {
|
||||
Type string `json:"type"`
|
||||
@@ -71,7 +72,84 @@ func (w *wsWriter) send(msg wsMessage) {
|
||||
_ = w.conn.WriteJSON(msg)
|
||||
}
|
||||
|
||||
// ─── 会话令牌(服务启动时随机生成一次)────────────────────────────────
|
||||
|
||||
var sessionToken string
|
||||
|
||||
func initSessionToken() {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
log.Fatalf("failed to generate session token: %v", err)
|
||||
}
|
||||
sessionToken = hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// authMiddleware 校验请求中携带的令牌。
|
||||
// WebSocket 不支持自定义 Header,因此同时接受 ?token= 查询参数。
|
||||
func authMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
token := ""
|
||||
if auth := c.GetHeader("Authorization"); strings.HasPrefix(auth, "Bearer ") {
|
||||
token = strings.TrimPrefix(auth, "Bearer ")
|
||||
}
|
||||
if token == "" {
|
||||
token = c.Query("token")
|
||||
}
|
||||
if token != sessionToken {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/auth/login — 用密码换取令牌
|
||||
func handleLogin(c *gin.Context) {
|
||||
var body struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
||||
return
|
||||
}
|
||||
if body.Password != accessPassword() {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "密码错误"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": gin.H{"token": sessionToken}})
|
||||
}
|
||||
|
||||
// GET /api/auth/verify — 校验令牌是否有效(受 authMiddleware 保护)
|
||||
func handleVerify(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"data": gin.H{"valid": true}})
|
||||
}
|
||||
|
||||
// GET / — 服务说明(无需鉴权,便于网关 / 探活)
|
||||
func handleAPIRoot(c *gin.Context) {
|
||||
loc := time.FixedZone("CST", 8*3600)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"description": "提供 Web SSH 终端与跳板连接(SSH 由服务端发起);浏览器访问需先 POST /api/auth/login 换取令牌,后续 API 与 WebSocket 携带 Bearer 或 ?token=",
|
||||
"endpoints": gin.H{
|
||||
"health": "/health",
|
||||
"auth": "/api/auth/login (POST,公开), /api/auth/verify (GET,需令牌)",
|
||||
"websocket_ssh": "/api/ws/ssh (GET 升级 WebSocket,需令牌;查询参数 token= 或 Header)",
|
||||
"ssh_profiles": "/api/ssh, /api/ssh/:name (CRUD,需令牌)",
|
||||
"commands": "/api/commands, /api/commands/:index (CRUD,需令牌)",
|
||||
"scripts": "/api/scripts, /api/scripts/:name (CRUD,需令牌)",
|
||||
},
|
||||
"message": "萌芽 SSH MengyaConnect 后端 API 服务运行中",
|
||||
"timestamp": time.Now().In(loc).Format(time.RFC3339),
|
||||
"version": apiVersion(),
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
initSessionToken()
|
||||
if err := initDB(); err != nil {
|
||||
log.Fatalf("db init: %v", err)
|
||||
}
|
||||
migrateFromFiles()
|
||||
|
||||
if mode := os.Getenv("GIN_MODE"); mode != "" {
|
||||
gin.SetMode(mode)
|
||||
}
|
||||
@@ -88,36 +166,43 @@ func main() {
|
||||
},
|
||||
}
|
||||
|
||||
// ─── 基本配置 CRUD ──────────────────────────────────────────
|
||||
// ─── 不需要鉴权的路由 ────────────────────────────────────────
|
||||
router.GET("/", handleAPIRoot)
|
||||
router.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ok",
|
||||
"time": time.Now().Format(time.RFC3339),
|
||||
})
|
||||
})
|
||||
router.POST("/api/auth/login", handleLogin)
|
||||
|
||||
router.GET("/api/ws/ssh", func(c *gin.Context) {
|
||||
// ─── 需要鉴权的路由 ──────────────────────────────────────────
|
||||
auth := router.Group("/api", authMiddleware())
|
||||
|
||||
auth.GET("/auth/verify", handleVerify)
|
||||
|
||||
auth.GET("/ws/ssh", func(c *gin.Context) {
|
||||
handleSSHWebSocket(c, upgrader)
|
||||
})
|
||||
|
||||
// ─── SSH 配置 CRUD ──────────────────────────────────────────
|
||||
router.GET("/api/ssh", handleListSSH)
|
||||
router.POST("/api/ssh", handleCreateSSH)
|
||||
router.PUT("/api/ssh/:name", handleUpdateSSH)
|
||||
router.DELETE("/api/ssh/:name", handleDeleteSSH)
|
||||
// SSH 配置 CRUD
|
||||
auth.GET("/ssh", handleListSSH)
|
||||
auth.POST("/ssh", handleCreateSSH)
|
||||
auth.PUT("/ssh/:name", handleUpdateSSH)
|
||||
auth.DELETE("/ssh/:name", handleDeleteSSH)
|
||||
|
||||
// ─── 快捷命令 CRUD ─────────────────────────────────────────
|
||||
router.GET("/api/commands", handleListCommands)
|
||||
router.POST("/api/commands", handleCreateCommand)
|
||||
router.PUT("/api/commands/:index", handleUpdateCommand)
|
||||
router.DELETE("/api/commands/:index", handleDeleteCommand)
|
||||
// 快捷命令 CRUD
|
||||
auth.GET("/commands", handleListCommands)
|
||||
auth.POST("/commands", handleCreateCommand)
|
||||
auth.PUT("/commands/:index", handleUpdateCommand)
|
||||
auth.DELETE("/commands/:index", handleDeleteCommand)
|
||||
|
||||
// ─── 脚本 CRUD ─────────────────────────────────────────────
|
||||
router.GET("/api/scripts", handleListScripts)
|
||||
router.GET("/api/scripts/:name", handleGetScript)
|
||||
router.POST("/api/scripts", handleCreateScript)
|
||||
router.PUT("/api/scripts/:name", handleUpdateScript)
|
||||
router.DELETE("/api/scripts/:name", handleDeleteScript)
|
||||
// 脚本 CRUD
|
||||
auth.GET("/scripts", handleListScripts)
|
||||
auth.GET("/scripts/:name", handleGetScript)
|
||||
auth.POST("/scripts", handleCreateScript)
|
||||
auth.PUT("/scripts/:name", handleUpdateScript)
|
||||
auth.DELETE("/scripts/:name", handleDeleteScript)
|
||||
|
||||
addr := getEnv("ADDR", ":"+getEnv("PORT", "8080"))
|
||||
server := &http.Server{
|
||||
@@ -127,6 +212,7 @@ func main() {
|
||||
}
|
||||
|
||||
log.Printf("SSH WebSocket server listening on %s", addr)
|
||||
log.Printf("Access password configured (use ACCESS_PASSWORD env to override)")
|
||||
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatalf("server error: %v", err)
|
||||
}
|
||||
@@ -365,41 +451,33 @@ func streamToWebSocket(ctx context.Context, writer *wsWriter, reader io.Reader)
|
||||
}
|
||||
}
|
||||
|
||||
// CORS、中间件与环境变量工具函数见 config.go
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SSH 配置 CRUD
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
// GET /api/ssh — 列出所有 SSH 配置
|
||||
// GET /api/ssh
|
||||
func handleListSSH(c *gin.Context) {
|
||||
entries, err := os.ReadDir(sshDir())
|
||||
rows, err := DB.Query(
|
||||
`SELECT name,alias,host,port,username,password,private_key,passphrase
|
||||
FROM ssh_profiles ORDER BY id`)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"data": []SSHProfile{}})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db query failed"})
|
||||
return
|
||||
}
|
||||
var profiles []SSHProfile
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") {
|
||||
continue
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(sshDir(), e.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer rows.Close()
|
||||
profiles := []SSHProfile{}
|
||||
for rows.Next() {
|
||||
var p SSHProfile
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
if err := rows.Scan(&p.Name, &p.Alias, &p.Host, &p.Port,
|
||||
&p.Username, &p.Password, &p.PrivateKey, &p.Passphrase); err != nil {
|
||||
continue
|
||||
}
|
||||
p.Name = strings.TrimSuffix(e.Name(), ".json")
|
||||
profiles = append(profiles, p)
|
||||
}
|
||||
if profiles == nil {
|
||||
profiles = []SSHProfile{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": profiles})
|
||||
}
|
||||
|
||||
// POST /api/ssh — 新建 SSH 配置
|
||||
// POST /api/ssh
|
||||
func handleCreateSSH(c *gin.Context) {
|
||||
var p SSHProfile
|
||||
if err := c.ShouldBindJSON(&p); err != nil {
|
||||
@@ -410,33 +488,39 @@ func handleCreateSSH(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "alias、host 和 username 为必填项"})
|
||||
return
|
||||
}
|
||||
name := p.Name
|
||||
name := strings.TrimSpace(p.Name)
|
||||
if name == "" {
|
||||
name = p.Alias
|
||||
}
|
||||
safe, err := sanitizeName(strings.ReplaceAll(name, " ", "-"))
|
||||
if err != nil {
|
||||
name = strings.ReplaceAll(name, " ", "-")
|
||||
if err := validateName(name); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
|
||||
return
|
||||
}
|
||||
p.Name = ""
|
||||
raw, _ := json.MarshalIndent(p, "", " ")
|
||||
if err := os.MkdirAll(sshDir(), 0o750); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create dir"})
|
||||
if p.Port == 0 {
|
||||
p.Port = 22
|
||||
}
|
||||
_, err := DB.Exec(
|
||||
`INSERT INTO ssh_profiles (name,alias,host,port,username,password,private_key,passphrase)
|
||||
VALUES (?,?,?,?,?,?,?,?)`,
|
||||
name, p.Alias, p.Host, p.Port, p.Username, p.Password, p.PrivateKey, p.Passphrase,
|
||||
)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE") {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "name already exists"})
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db insert failed"})
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(sshDir(), safe+".json"), raw, 0o600); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to write file"})
|
||||
return
|
||||
}
|
||||
p.Name = safe
|
||||
p.Name = name
|
||||
c.JSON(http.StatusOK, gin.H{"data": p})
|
||||
}
|
||||
|
||||
// PUT /api/ssh/:name — 更新 SSH 配置
|
||||
// PUT /api/ssh/:name
|
||||
func handleUpdateSSH(c *gin.Context) {
|
||||
name, err := sanitizeName(c.Param("name"))
|
||||
if err != nil {
|
||||
name := c.Param("name")
|
||||
if err := validateName(name); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
|
||||
return
|
||||
}
|
||||
@@ -449,34 +533,42 @@ func handleUpdateSSH(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "alias、host 和 username 为必填项"})
|
||||
return
|
||||
}
|
||||
p.Name = ""
|
||||
raw, _ := json.MarshalIndent(p, "", " ")
|
||||
filePath := filepath.Join(sshDir(), name+".json")
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
if p.Port == 0 {
|
||||
p.Port = 22
|
||||
}
|
||||
res, err := DB.Exec(
|
||||
`UPDATE ssh_profiles
|
||||
SET alias=?,host=?,port=?,username=?,password=?,private_key=?,passphrase=?,
|
||||
updated_at=CURRENT_TIMESTAMP
|
||||
WHERE name=?`,
|
||||
p.Alias, p.Host, p.Port, p.Username, p.Password, p.PrivateKey, p.Passphrase, name,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db update failed"})
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(filePath, raw, 0o600); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to write file"})
|
||||
if n, _ := res.RowsAffected(); n == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
p.Name = name
|
||||
c.JSON(http.StatusOK, gin.H{"data": p})
|
||||
}
|
||||
|
||||
// DELETE /api/ssh/:name — 删除 SSH 配置
|
||||
// DELETE /api/ssh/:name
|
||||
func handleDeleteSSH(c *gin.Context) {
|
||||
name, err := sanitizeName(c.Param("name"))
|
||||
if err != nil {
|
||||
name := c.Param("name")
|
||||
if err := validateName(name); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
|
||||
return
|
||||
}
|
||||
if err := os.Remove(filepath.Join(sshDir(), name+".json")); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete"})
|
||||
}
|
||||
res, err := DB.Exec(`DELETE FROM ssh_profiles WHERE name=?`, name)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db delete failed"})
|
||||
return
|
||||
}
|
||||
if n, _ := res.RowsAffected(); n == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
|
||||
@@ -486,37 +578,49 @@ func handleDeleteSSH(c *gin.Context) {
|
||||
// 快捷命令 CRUD
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
func readCommands() ([]Command, error) {
|
||||
raw, err := os.ReadFile(cmdFilePath())
|
||||
func queryAllCommands() ([]Command, error) {
|
||||
rows, err := DB.Query(`SELECT alias,command FROM commands ORDER BY sort_order, id`)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []Command{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var cmds []Command
|
||||
if err := json.Unmarshal(raw, &cmds); err != nil {
|
||||
return nil, err
|
||||
defer rows.Close()
|
||||
cmds := []Command{}
|
||||
for rows.Next() {
|
||||
var cmd Command
|
||||
if err := rows.Scan(&cmd.Alias, &cmd.Command); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
return cmds, nil
|
||||
}
|
||||
|
||||
func writeCommands(cmds []Command) error {
|
||||
raw, err := json.MarshalIndent(cmds, "", " ")
|
||||
// commandIDAtIndex returns the primary-key id of the command at 0-based position idx.
|
||||
func commandIDAtIndex(idx int) (int64, error) {
|
||||
rows, err := DB.Query(`SELECT id FROM commands ORDER BY sort_order, id`)
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(cmdFilePath()), 0o750); err != nil {
|
||||
return err
|
||||
defer rows.Close()
|
||||
pos := 0
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if pos == idx {
|
||||
return id, nil
|
||||
}
|
||||
pos++
|
||||
}
|
||||
return os.WriteFile(cmdFilePath(), raw, 0o600)
|
||||
return 0, sql.ErrNoRows
|
||||
}
|
||||
|
||||
// GET /api/commands
|
||||
func handleListCommands(c *gin.Context) {
|
||||
cmds, err := readCommands()
|
||||
cmds, err := queryAllCommands()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read commands"})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db query failed"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": cmds})
|
||||
@@ -533,16 +637,16 @@ func handleCreateCommand(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "alias 和 command 为必填项"})
|
||||
return
|
||||
}
|
||||
cmds, err := readCommands()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read commands"})
|
||||
return
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
if err := writeCommands(cmds); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save commands"})
|
||||
var maxOrder int
|
||||
_ = DB.QueryRow(`SELECT COALESCE(MAX(sort_order),0) FROM commands`).Scan(&maxOrder)
|
||||
if _, err := DB.Exec(
|
||||
`INSERT INTO commands (alias,command,sort_order) VALUES (?,?,?)`,
|
||||
cmd.Alias, cmd.Command, maxOrder+1,
|
||||
); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db insert failed"})
|
||||
return
|
||||
}
|
||||
cmds, _ := queryAllCommands()
|
||||
c.JSON(http.StatusOK, gin.H{"data": cmds})
|
||||
}
|
||||
|
||||
@@ -562,20 +666,16 @@ func handleUpdateCommand(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "alias 和 command 为必填项"})
|
||||
return
|
||||
}
|
||||
cmds, err := readCommands()
|
||||
id, err := commandIDAtIndex(idx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read commands"})
|
||||
return
|
||||
}
|
||||
if idx >= len(cmds) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "index out of range"})
|
||||
return
|
||||
}
|
||||
cmds[idx] = cmd
|
||||
if err := writeCommands(cmds); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save commands"})
|
||||
if _, err := DB.Exec(`UPDATE commands SET alias=?,command=? WHERE id=?`, cmd.Alias, cmd.Command, id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db update failed"})
|
||||
return
|
||||
}
|
||||
cmds, _ := queryAllCommands()
|
||||
c.JSON(http.StatusOK, gin.H{"data": cmds})
|
||||
}
|
||||
|
||||
@@ -586,20 +686,16 @@ func handleDeleteCommand(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid index"})
|
||||
return
|
||||
}
|
||||
cmds, err := readCommands()
|
||||
id, err := commandIDAtIndex(idx)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read commands"})
|
||||
return
|
||||
}
|
||||
if idx >= len(cmds) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "index out of range"})
|
||||
return
|
||||
}
|
||||
cmds = append(cmds[:idx], cmds[idx+1:]...)
|
||||
if err := writeCommands(cmds); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save commands"})
|
||||
if _, err := DB.Exec(`DELETE FROM commands WHERE id=?`, id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db delete failed"})
|
||||
return
|
||||
}
|
||||
cmds, _ := queryAllCommands()
|
||||
c.JSON(http.StatusOK, gin.H{"data": cmds})
|
||||
}
|
||||
|
||||
@@ -607,45 +703,46 @@ func handleDeleteCommand(c *gin.Context) {
|
||||
// 脚本 CRUD
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
// GET /api/scripts — 列出所有脚本名称
|
||||
// GET /api/scripts
|
||||
func handleListScripts(c *gin.Context) {
|
||||
entries, err := os.ReadDir(scriptDir())
|
||||
rows, err := DB.Query(`SELECT name FROM scripts ORDER BY name`)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"data": []ScriptInfo{}})
|
||||
return
|
||||
}
|
||||
var scripts []ScriptInfo
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
scripts = append(scripts, ScriptInfo{Name: e.Name()})
|
||||
defer rows.Close()
|
||||
scripts := []ScriptInfo{}
|
||||
for rows.Next() {
|
||||
var s ScriptInfo
|
||||
if err := rows.Scan(&s.Name); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if scripts == nil {
|
||||
scripts = []ScriptInfo{}
|
||||
scripts = append(scripts, s)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": scripts})
|
||||
}
|
||||
|
||||
// GET /api/scripts/:name — 获取脚本内容
|
||||
// GET /api/scripts/:name
|
||||
func handleGetScript(c *gin.Context) {
|
||||
name, err := sanitizeName(c.Param("name"))
|
||||
if err != nil {
|
||||
name := c.Param("name")
|
||||
if err := validateName(name); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
|
||||
return
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(scriptDir(), name))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read"})
|
||||
}
|
||||
var s ScriptInfo
|
||||
err := DB.QueryRow(`SELECT name,content FROM scripts WHERE name=?`, name).
|
||||
Scan(&s.Name, &s.Content)
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
} else if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db query failed"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": ScriptInfo{Name: name, Content: string(raw)}})
|
||||
c.JSON(http.StatusOK, gin.H{"data": s})
|
||||
}
|
||||
|
||||
// POST /api/scripts — 新建脚本
|
||||
// POST /api/scripts
|
||||
func handleCreateScript(c *gin.Context) {
|
||||
var s ScriptInfo
|
||||
if err := c.ShouldBindJSON(&s); err != nil {
|
||||
@@ -656,59 +753,63 @@ func handleCreateScript(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "name 为必填项"})
|
||||
return
|
||||
}
|
||||
name, err := sanitizeName(s.Name)
|
||||
if err != nil {
|
||||
if err := validateName(s.Name); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
|
||||
return
|
||||
}
|
||||
if err := os.MkdirAll(scriptDir(), 0o750); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create dir"})
|
||||
_, err := DB.Exec(`INSERT INTO scripts (name,content) VALUES (?,?)`, s.Name, s.Content)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE") {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "name already exists"})
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db insert failed"})
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(scriptDir(), name), []byte(s.Content), 0o640); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to write"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": ScriptInfo{Name: name, Content: s.Content}})
|
||||
c.JSON(http.StatusOK, gin.H{"data": s})
|
||||
}
|
||||
|
||||
// PUT /api/scripts/:name — 更新脚本内容
|
||||
// PUT /api/scripts/:name
|
||||
func handleUpdateScript(c *gin.Context) {
|
||||
name, err := sanitizeName(c.Param("name"))
|
||||
if err != nil {
|
||||
name := c.Param("name")
|
||||
if err := validateName(name); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
|
||||
return
|
||||
}
|
||||
filePath := filepath.Join(scriptDir(), name)
|
||||
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
var s ScriptInfo
|
||||
if err := c.ShouldBindJSON(&s); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile(filePath, []byte(s.Content), 0o640); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to write"})
|
||||
res, err := DB.Exec(
|
||||
`UPDATE scripts SET content=?,updated_at=CURRENT_TIMESTAMP WHERE name=?`,
|
||||
s.Content, name,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db update failed"})
|
||||
return
|
||||
}
|
||||
if n, _ := res.RowsAffected(); n == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"data": ScriptInfo{Name: name, Content: s.Content}})
|
||||
}
|
||||
|
||||
// DELETE /api/scripts/:name — 删除脚本
|
||||
// DELETE /api/scripts/:name
|
||||
func handleDeleteScript(c *gin.Context) {
|
||||
name, err := sanitizeName(c.Param("name"))
|
||||
if err != nil {
|
||||
name := c.Param("name")
|
||||
if err := validateName(name); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid name"})
|
||||
return
|
||||
}
|
||||
if err := os.Remove(filepath.Join(scriptDir(), name)); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
} else {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete"})
|
||||
}
|
||||
res, err := DB.Exec(`DELETE FROM scripts WHERE name=?`, name)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db delete failed"})
|
||||
return
|
||||
}
|
||||
if n, _ := res.RowsAffected(); n == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
|
||||
|
||||
Reference in New Issue
Block a user